Files
EventHubBack/src/handlers/admin/admin_handler_banned_words.erl
T

177 lines
6.3 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @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">>
}.