289 lines
12 KiB
Erlang
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)
|
|
}. |