%%%------------------------------------------------------------------- %%% @doc Административный обработчик бан-слов. %%% GET – список всех слов с пагинацией. %%% POST – добавить новое слово. %%% DELETE – удалить слово по :word. %%% @end %%%------------------------------------------------------------------- -module(admin_handler_banned_words). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> list_words(Req); <<"POST">> -> add_word(Req); <<"DELETE">> -> delete_word(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. -spec trails() -> [map()]. trails() -> [ #{ % GET list path => <<"/v1/admin/banned-words">>, method => <<"GET">>, description => <<"List all banned words (admin)">>, tags => [<<"Banned Words">>], parameters => [ #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], responses => #{ 200 => #{ description => <<"Array of banned words">>, content => #{<<"application/json">> => #{schema => #{ type => array, items => banned_word_schema() }}} } } }, #{ % POST add path => <<"/v1/admin/banned-words">>, method => <<"POST">>, description => <<"Add a new banned word">>, tags => [<<"Banned Words">>], requestBody => #{ required => true, content => #{<<"application/json">> => #{schema => #{ type => object, required => [<<"word">>], properties => #{ word => #{type => string} } }}} }, responses => #{ 201 => #{description => <<"Word added">>}, 409 => #{description => <<"Word already exists">>} } }, #{ % DELETE by word path => <<"/v1/admin/banned-words/:word">>, method => <<"DELETE">>, description => <<"Remove a banned word">>, tags => [<<"Banned Words">>], parameters => [ #{ name => <<"word">>, in => <<"path">>, description => <<"The word to remove">>, required => true, schema => #{type => string} } ], responses => #{ 200 => #{description => <<"Word removed">>}, 404 => #{description => <<"Word not found">>} } } ]. banned_word_schema() -> #{ type => object, properties => #{ id => #{type => string}, word => #{type => string}, added_by => #{type => string, nullable => true}, added_at => #{type => string, format => <<"date-time">>, nullable => true} } }. %%% Internal functions list_words(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> Pagination = handler_utils:parse_pagination_params(Req1), %% core_banned_words:list_banned_words() возвращает список, а не {ok, List} AllWords = core_banned_words:list_banned_words(), BannedWords = lists:sort(AllWords), Total = length(BannedWords), Page = lists:sublist(BannedWords, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), Json = [word_to_map(W) || W <- Page], ExtraHeaders = pagination_headers(Pagination, Total), handler_utils:send_json(Req1, 200, Json, ExtraHeaders); {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end. add_word(Req) -> case handler_utils:auth_admin(Req) of {ok, AdminId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of #{<<"word">> := Word} -> case core_banned_words:add_banned_word(Word, AdminId) of {ok, _} -> handler_utils:send_json(Req2, 201, #{status => <<"added">>}); {error, already_exists} -> handler_utils:send_error(Req2, 409, <<"Word already exists">>); {error, _} -> handler_utils:send_error(Req2, 500, <<"Internal server error">>) end; _ -> handler_utils:send_error(Req2, 400, <<"Missing 'word' field">>) catch _:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON">>) end; {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end. delete_word(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> Word = cowboy_req:binding(word, Req1), case core_banned_words:remove_banned_word(Word) of {ok, _} -> handler_utils:send_json(Req1, 200, #{status => <<"deleted">>}); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Word not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end. %% @private Преобразование записи banned_word в JSON-совместимую карту. word_to_map(W) -> #{ id => W#banned_word.id, word => W#banned_word.word, added_by => W#banned_word.added_by, added_at => datetime_to_iso8601(W#banned_word.added_at) }. %% @private Форматирование datetime в ISO8601 строку. datetime_to_iso8601(undefined) -> undefined; datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Year, Month, Day, Hour, Minute, Second])). pagination_headers(#{limit := Limit, offset := Offset}, Total) -> RangeEnd = min(Offset + Limit - 1, Total - 1), #{ <<"content-range">> => iolist_to_binary(io_lib:format("items ~B-~B/~B", [Offset, RangeEnd, Total])), <<"x-total-count">> => integer_to_binary(Total), <<"access-control-expose-headers">> => <<"Content-Range, X-Total-Count">> }.