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

289 lines
12 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @doc Административный обработчик бан-слов.
%%% GET – список всех слов с пагинацией, фильтрацией и сортировкой.
%%% POST – добавить новое слово.
%%% PUT – редактировать существующее слово.
%%% DELETE – удалить слово по :word.
%%% POST /batch – загрузить список слов (добавляются только новые).
%%% @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">> ->
case cowboy_req:path(Req) of
<<"/v1/admin/banned-words/batch">> -> batch_add_words(Req);
_ -> add_word(Req)
end;
<<"PUT">> -> update_word(Req);
<<"DELETE">> -> delete_word(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
-spec trails() -> [map()].
trails() ->
[ % GET list
#{ path => <<"/v1/admin/banned-words">>,
method => <<"GET">>,
description => <<"List all banned words (admin)">>,
tags => [<<"Banned Words">>],
parameters => [
#{name => <<"q">>, in => <<"query">>, schema => #{type => string}, description => <<"Search by word">>},
#{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"word">>, <<"added_at">>]}, description => <<"Sort field">>},
#{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort direction">>},
#{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">>}
}
},
% PUT update
#{ path => <<"/v1/admin/banned-words/{word}">>,
method => <<"PUT">>,
description => <<"Update an existing banned word">>,
tags => [<<"Banned Words">>],
parameters => [
#{name => <<"word">>, in => <<"path">>, description => <<"Current word">>, required => true, schema => #{type => string}}
],
requestBody => #{
required => true,
content => #{<<"application/json">> => #{schema => #{
type => object,
required => [<<"word">>],
properties => #{word => #{type => string}}
}}}
},
responses => #{
200 => #{description => <<"Word updated">>},
404 => #{description => <<"Word not found">>},
409 => #{description => <<"New 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">>}
}
},
% POST batch
#{ path => <<"/v1/admin/banned-words/batch">>,
method => <<"POST">>,
description => <<"Batch add banned words (only new words are added)">>,
tags => [<<"Banned Words">>],
requestBody => #{
required => true,
content => #{<<"application/json">> => #{schema => #{
type => object,
required => [<<"words">>],
properties => #{words => #{type => array, items => #{type => string}}}
}}}
},
responses => #{
200 => #{description => <<"Batch operation completed">>},
400 => #{description => <<"Invalid request body">>}
}
}
].
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}
}
}.
%%%===================================================================
%%% HTTP-методы
%%%===================================================================
list_words(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
Qs = cowboy_req:parse_qs(Req1),
Q = proplists:get_value(<<"q">>, Qs),
SortBy = proplists:get_value(<<"sort">>, Qs, <<"word">>),
SortOrder = proplists:get_value(<<"order">>, Qs, <<"asc">>),
Pagination = handler_utils:parse_pagination_params(Req1),
AllWords = core_banned_words:list_banned_words(),
% Фильтрация
Filtered = case Q of
undefined -> AllWords;
_ -> [W || W <- AllWords, string:str(string:lowercase(binary_to_list(W#banned_word.word)), string:lowercase(binary_to_list(Q))) > 0]
end,
% Сортировка
Sorted = case {SortBy, SortOrder} of
{<<"word">>, <<"asc">>} -> lists:keysort(#banned_word.word, Filtered);
{<<"word">>, <<"desc">>} -> lists:reverse(lists:keysort(#banned_word.word, Filtered));
{<<"added_at">>, <<"asc">>} -> lists:keysort(#banned_word.added_at, Filtered);
{<<"added_at">>, <<"desc">>} -> lists:reverse(lists:keysort(#banned_word.added_at, Filtered));
_ -> Filtered
end,
Total = length(Sorted),
Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)),
Json = [word_to_map(W) || W <- Page],
ExtraHeaders = handler_utils: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, _} ->
admin_utils:log_admin_action(AdminId, <<"add">>, <<"banned_word">>, Word, Req2),
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.
update_word(Req) ->
case handler_utils:auth_admin(Req) of
{ok, AdminId, Req1} ->
OldWord = cowboy_req:binding(word, Req1),
{ok, Body, Req2} = cowboy_req:read_body(Req1),
try jsx:decode(Body, [return_maps]) of
#{<<"word">> := NewWord} ->
case core_banned_words:update_banned_word(OldWord, NewWord) of
{ok, _Updated} ->
admin_utils:log_admin_action(AdminId, <<"update">>, <<"banned_word">>,
#{old_word => OldWord, new_word => NewWord}, Req2),
handler_utils:send_json(Req2, 200, #{status => <<"updated">>});
{error, not_found} ->
handler_utils:send_error(Req2, 404, <<"Word not found">>);
{error, already_exists} ->
handler_utils:send_error(Req2, 409, <<"New 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, _} ->
admin_utils:log_admin_action(AdminId, <<"delete">>, <<"banned_word">>, Word, Req1),
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.
batch_add_words(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
#{<<"words">> := Words} when is_list(Words) ->
ValidWords = [W || W <- Words, is_binary(W)],
InvalidCount = length(Words) - length(ValidWords),
Results = lists:map(
fun(Word) ->
case core_banned_words:add_banned_word(Word, AdminId) of
{ok, _} -> #{word => Word, status => <<"added">>};
{error, already_exists} -> #{word => Word, status => <<"skipped">>};
{error, _} -> #{word => Word, status => <<"error">>}
end
end,
ValidWords
),
Added = length([R || R <- Results, maps:get(status, R) =:= <<"added">>]),
Skipped = length([R || R <- Results, maps:get(status, R) =:= <<"skipped">>]),
admin_utils:log_admin_action(AdminId, <<"batch_add">>, <<"banned_words">>,
#{count => length(Words), added => Added, skipped => Skipped}, Req2),
handler_utils:send_json(Req2, 200, #{
results => Results,
added => Added,
skipped => Skipped,
invalid_count => InvalidCount
});
_ -> handler_utils:send_error(Req2, 400, <<"Missing 'words' array">>)
catch
_:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON">>)
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 => handler_utils:datetime_to_iso8601(W#banned_word.added_at)
}.