diff --git a/src/handlers/admin/admin_handler_banned_words.erl b/src/handlers/admin/admin_handler_banned_words.erl index 0124067..5c0d0a1 100644 --- a/src/handlers/admin/admin_handler_banned_words.erl +++ b/src/handlers/admin/admin_handler_banned_words.erl @@ -24,6 +24,7 @@ handle_item(Word, Req) -> _ -> send_error(Req, 405, <<"Method not allowed">>) end. +%% ================== GET /banned-words ================== list_banned_words(Req) -> case auth_admin(Req) of {ok, _AdminId, Req1} -> @@ -33,22 +34,24 @@ list_banned_words(Req) -> send_error(Req1, Code, Message) end. +%% ================== POST /banned-words ================== add_banned_word(Req) -> case auth_admin(Req) of {ok, AdminId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of - #{<<"word">> := NewWord} -> - case core_banned_words:add_banned_word(NewWord, AdminId) of - {ok, WordRec} -> - send_json(Req2, 201, banned_word_to_json(WordRec)); + #{<<"word">> := Word} when byte_size(Word) > 0 -> + case core_banned_words:add_banned_word(Word, AdminId) of + {ok, BW} -> + log_audit(AdminId, <<"add_banned_word">>, <<"banned_word">>, BW#banned_word.id, <<"">>), + send_json(Req2, 201, banned_word_to_json(BW)); {error, already_exists} -> send_error(Req2, 409, <<"Word already exists">>); {error, _} -> send_error(Req2, 500, <<"Internal server error">>) end; _ -> - send_error(Req2, 400, <<"Missing 'word' field">>) + send_error(Req2, 400, <<"Missing or empty 'word'">>) catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -56,11 +59,13 @@ add_banned_word(Req) -> send_error(Req1, Code, Message) end. +%% ================== DELETE /banned-words/:word ================== delete_banned_word(Word, Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> + {ok, AdminId, Req1} -> case core_banned_words:remove_banned_word(Word) of {ok, deleted} -> + log_audit(AdminId, <<"delete_banned_word">>, <<"banned_word">>, Word, <<"">>), send_json(Req1, 200, #{status => <<"deleted">>}); {error, not_found} -> send_error(Req1, 404, <<"Word not found">>) @@ -69,22 +74,24 @@ delete_banned_word(Word, Req) -> send_error(Req1, Code, Message) end. -update_banned_word(Word, Req) -> +%% ================== PUT /banned-words/:word ================== +update_banned_word(OldWord, Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> + {ok, AdminId, 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(Word, NewWord) of - {ok, WordRec} -> - send_json(Req2, 200, banned_word_to_json(WordRec)); + #{<<"word">> := NewWord} when byte_size(NewWord) > 0 -> + case core_banned_words:update_banned_word(OldWord, NewWord) of + {ok, BW} -> + log_audit(AdminId, <<"update_banned_word">>, <<"banned_word">>, BW#banned_word.id, <<"">>), + send_json(Req2, 200, banned_word_to_json(BW)); {error, not_found} -> send_error(Req2, 404, <<"Word not found">>); {error, _} -> send_error(Req2, 500, <<"Internal server error">>) end; _ -> - send_error(Req2, 400, <<"Missing 'word' field">>) + send_error(Req2, 400, <<"Missing or empty 'word'">>) catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -92,6 +99,16 @@ update_banned_word(Word, Req) -> send_error(Req1, Code, Message) end. +%% ── Аудит ────────────────────────────────────────────── +log_audit(AdminId, Action, EntityType, EntityId, Reason) -> + case core_admin:get_by_id(AdminId) of + {ok, Admin} -> + core_admin_audit:log(AdminId, Admin#admin.email, Admin#admin.role, + Action, EntityType, EntityId, <<"127.0.0.1">>, Reason); + _ -> ok + end. + +%% ================== Аутентификация ================== auth_admin(Req) -> case handler_auth:authenticate(Req) of {ok, AdminId, Req1} -> @@ -103,6 +120,7 @@ auth_admin(Req) -> {error, Code, Message, Req1} end. +%% ================== Сериализация ================== banned_word_to_json(BW) -> #{ id => BW#banned_word.id, @@ -116,6 +134,7 @@ datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> [Year, Month, Day, Hour, Minute, Second])); datetime_to_iso8601(undefined) -> undefined. +%% ================== HTTP-ответы ================== send_json(Req, Status, Data) -> Headers = #{ <<"content-type">> => <<"application/json">>, @@ -126,7 +145,12 @@ send_json(Req, Status, Data) -> cowboy_req:reply(Status, Headers, Body, Req), {ok, Body, []}. -send_error(Req, Status, Message) -> +send_error(Req, Code, Message) -> + Headers = #{ + <<"content-type">> => <<"application/json">>, + <<"access-control-allow-origin">> => <<"*">>, + <<"access-control-expose-headers">> => <<"Content-Range">> + }, Body = jsx:encode(#{error => Message}), - cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req), + cowboy_req:reply(Code, Headers, Body, Req), {ok, Body, []}. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_subscriptions.erl b/src/handlers/admin/admin_handler_subscriptions.erl index dde61b9..5341734 100644 --- a/src/handlers/admin/admin_handler_subscriptions.erl +++ b/src/handlers/admin/admin_handler_subscriptions.erl @@ -6,12 +6,11 @@ init(Req, _Opts) -> case cowboy_req:binding(id, Req) of - undefined -> - handle_collection(Req); - _SubscriptionId -> - handle_item(Req) + undefined -> handle_collection(Req); + _SubId -> handle_item(Req) end. +%% ================== Коллекция ================== handle_collection(Req) -> case cowboy_req:method(Req) of <<"GET">> -> list_subscriptions(Req); @@ -19,55 +18,33 @@ handle_collection(Req) -> _ -> send_error(Req, 405, <<"Method not allowed">>) end. +%% ================== Элемент ================== handle_item(Req) -> - SubscriptionId = cowboy_req:binding(id, Req), + SubId = cowboy_req:binding(id, Req), case cowboy_req:method(Req) of - <<"GET">> -> get_subscription(SubscriptionId, Req); - <<"PUT">> -> update_subscription(SubscriptionId, Req); - <<"DELETE">> -> delete_subscription(SubscriptionId, Req); + <<"GET">> -> get_subscription(SubId, Req); + <<"PUT">> -> update_subscription(SubId, Req); + <<"DELETE">> -> delete_subscription(SubId, Req); _ -> send_error(Req, 405, <<"Method not allowed">>) end. +%% ================== GET /subscriptions ================== list_subscriptions(Req) -> case auth_admin(Req) of {ok, _AdminId, Req1} -> - Subscriptions = core_subscription:list_subscriptions(), - send_json(Req1, 200, [subscription_to_json(S) || S <- Subscriptions]); + Subs = core_subscription:list_subscriptions(), + send_json(Req1, 200, [subscription_to_json(S) || S <- Subs]); {error, Code, Message, Req1} -> send_error(Req1, Code, Message) end. -create_subscription(Req) -> +%% ================== GET /subscriptions/:id ================== +get_subscription(Id, Req) -> case auth_admin(Req) of {ok, _AdminId, Req1} -> - {ok, Body, Req2} = cowboy_req:read_body(Req1), - try jsx:decode(Body, [return_maps]) of - #{<<"user_id">> := _UserId} = Data -> - SubscriptionData = maps:merge(#{ - <<"status">> => <<"active">>, - <<"trial_used">> => false - }, Data), - case core_subscription:create_subscription(SubscriptionData) of - {ok, Subscription} -> - send_json(Req2, 201, subscription_to_json(Subscription)); - {error, Reason} -> - send_error(Req2, 500, Reason) - end; - _ -> - send_error(Req2, 400, <<"Missing 'user_id' field">>) - catch - _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) - end; - {error, Code, Message, Req1} -> - send_error(Req1, Code, Message) - end. - -get_subscription(SubscriptionId, Req) -> - case auth_admin(Req) of - {ok, _AdminId, Req1} -> - case core_subscription:get_by_id(SubscriptionId) of - {ok, Subscription} -> - send_json(Req1, 200, subscription_to_json(Subscription)); + case core_subscription:get_by_id(Id) of + {ok, Sub} -> + send_json(Req1, 200, subscription_to_json(Sub)); {error, not_found} -> send_error(Req1, 404, <<"Subscription not found">>) end; @@ -75,22 +52,34 @@ get_subscription(SubscriptionId, Req) -> send_error(Req1, Code, Message) end. -update_subscription(SubscriptionId, Req) -> +%% ================== POST /subscriptions ================== +create_subscription(Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> + {ok, AdminId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), - try jsx:decode(Body, [return_maps]) of - UpdatesMap when is_map(UpdatesMap) -> - case core_subscription:update_subscription(SubscriptionId, UpdatesMap) of - {ok, Subscription} -> - send_json(Req2, 200, subscription_to_json(Subscription)); - {error, not_found} -> - send_error(Req2, 404, <<"Subscription not found">>); - {error, Reason} -> - send_error(Req2, 500, Reason) - end; - _ -> - send_error(Req2, 400, <<"Invalid JSON">>) + try + Decoded = jsx:decode(Body, [return_maps]), + case Decoded of + #{<<"user_id">> := UserId, <<"plan">> := Plan} -> + case validate_plan(Plan) of + true -> + SubData = maps:merge(#{ + <<"status">> => <<"active">>, + <<"trial_used">> => false + }, maps:without([<<"id">>], Decoded)), % ← исправлено: Decoded, а не Body + case core_subscription:create_subscription(SubData) of + {ok, Sub} -> + log_audit(AdminId, <<"create_subscription">>, <<"subscription">>, Sub#subscription.id, UserId), + send_json(Req2, 201, subscription_to_json(Sub)); + {error, Reason} -> + send_error(Req2, 500, Reason) + end; + false -> + send_error(Req2, 400, <<"Invalid plan value">>) + end; + _ -> + send_error(Req2, 400, <<"Missing 'user_id' or 'plan' field">>) + end catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -98,11 +87,41 @@ update_subscription(SubscriptionId, Req) -> send_error(Req1, Code, Message) end. -delete_subscription(SubscriptionId, Req) -> +%% ================== PUT /subscriptions/:id ================== +update_subscription(Id, Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> - case core_subscription:delete_subscription(SubscriptionId) of + {ok, AdminId, Req1} -> + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try + Updates = jsx:decode(Body, [return_maps]), + case map_size(Updates) > 0 of + true -> + case core_subscription:update_subscription(Id, Updates) of + {ok, Sub} -> + log_audit(AdminId, <<"update_subscription">>, <<"subscription">>, Id, <<"">>), + send_json(Req2, 200, subscription_to_json(Sub)); + {error, not_found} -> + send_error(Req2, 404, <<"Subscription not found">>); + {error, Reason} -> + send_error(Req2, 500, Reason) + end; + false -> + send_error(Req2, 400, <<"Request body is empty">>) + end + catch + _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% ================== DELETE /subscriptions/:id ================== +delete_subscription(Id, Req) -> + case auth_admin(Req) of + {ok, AdminId, Req1} -> + case core_subscription:delete_subscription(Id) of {ok, deleted} -> + log_audit(AdminId, <<"delete_subscription">>, <<"subscription">>, Id, <<"">>), send_json(Req1, 200, #{status => <<"deleted">>}); {error, not_found} -> send_error(Req1, 404, <<"Subscription not found">>) @@ -111,6 +130,7 @@ delete_subscription(SubscriptionId, Req) -> send_error(Req1, Code, Message) end. +%% ================== Аутентификация и роли ================== auth_admin(Req) -> case handler_auth:authenticate(Req) of {ok, AdminId, Req1} -> @@ -122,6 +142,16 @@ auth_admin(Req) -> {error, Code, Message, Req1} end. +%% ================== Аудит ================== +log_audit(AdminId, Action, EntityType, EntityId, Reason) -> + case core_admin:get_by_id(AdminId) of + {ok, Admin} -> + core_admin_audit:log(AdminId, Admin#admin.email, Admin#admin.role, + Action, EntityType, EntityId, <<"127.0.0.1">>, Reason); + _ -> ok + end. + +%% ================== Сериализация ================== subscription_to_json(S) -> #{ id => S#subscription.id, @@ -140,6 +170,12 @@ datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> [Year, Month, Day, Hour, Minute, Second])); datetime_to_iso8601(undefined) -> undefined. +%% ================== Валидация ================== +validate_plan(Plan) when is_binary(Plan) -> + lists:member(Plan, [<<"monthly">>, <<"yearly">>, <<"quarterly">>, <<"biannual">>, <<"annual">>]); +validate_plan(_) -> false. + +%% ================== HTTP-ответы ================== send_json(Req, Status, Data) -> Headers = #{ <<"content-type">> => <<"application/json">>, @@ -150,7 +186,12 @@ send_json(Req, Status, Data) -> cowboy_req:reply(Status, Headers, Body, Req), {ok, Body, []}. -send_error(Req, Status, Message) -> +send_error(Req, Code, Message) -> + Headers = #{ + <<"content-type">> => <<"application/json">>, + <<"access-control-allow-origin">> => <<"*">>, + <<"access-control-expose-headers">> => <<"Content-Range">> + }, Body = jsx:encode(#{error => Message}), - cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req), + cowboy_req:reply(Code, Headers, Body, Req), {ok, Body, []}. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_tickets.erl b/src/handlers/admin/admin_handler_tickets.erl index aea79fa..32488b7 100644 --- a/src/handlers/admin/admin_handler_tickets.erl +++ b/src/handlers/admin/admin_handler_tickets.erl @@ -25,34 +25,39 @@ handle_item(TicketId, Req) -> _ -> send_error(Req, 405, <<"Method not allowed">>) end. +%% ── Список тикетов ────────────────────────────────────── list_tickets(Req) -> case auth_admin(Req) of {ok, _AdminId, Req1} -> - Tickets = core_ticket:list_all(), % ← было list_tickets() + Tickets = core_ticket:list_all(), send_json(Req1, 200, [ticket_to_json(T) || T <- Tickets]); {error, Code, Message, Req1} -> send_error(Req1, Code, Message) end. +%% ── Создание тикета ────────────────────────────────────── create_ticket(Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> + {ok, AdminId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), - try jsx:decode(Body, [return_maps]) of - #{<<"error_message">> := _} = Data -> - % Администратор может указать error_hash, stacktrace, context, status - TicketData = maps:merge(#{ - <<"status">> => <<"open">>, - <<"assigned_to">> => undefined - }, Data), - case core_ticket:create_ticket(TicketData) of - {ok, Ticket} -> - send_json(Req2, 201, ticket_to_json(Ticket)); - {error, Reason} -> - send_error(Req2, 500, Reason) - end; - _ -> - send_error(Req2, 400, <<"Missing 'error_message' field">>) + try + Decoded = jsx:decode(Body, [return_maps]), + case Decoded of + #{<<"error_message">> := ErrorMsg} when byte_size(ErrorMsg) > 0 -> + TicketData = maps:merge(#{ + <<"reporter_id">> => AdminId, + <<"status">> => <<"open">> + }, maps:without([<<"id">>], Decoded)), + case core_ticket:create_ticket(TicketData) of + {ok, Ticket} -> + log_audit(AdminId, <<"create_ticket">>, <<"ticket">>, Ticket#ticket.id, <<"">>), + send_json(Req2, 201, ticket_to_json(Ticket)); + {error, Reason} -> + send_error(Req2, 500, Reason) + end; + _ -> + send_error(Req2, 400, <<"Missing or empty 'error_message'">>) + end catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -60,6 +65,7 @@ create_ticket(Req) -> send_error(Req1, Code, Message) end. +%% ── Получение тикета по ID ───────────────────────────── get_ticket(TicketId, Req) -> case auth_admin(Req) of {ok, _AdminId, Req1} -> @@ -73,22 +79,28 @@ get_ticket(TicketId, Req) -> send_error(Req1, Code, Message) end. +%% ── Обновление тикета ─────────────────────────────────── update_ticket(TicketId, Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> + {ok, AdminId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), - try jsx:decode(Body, [return_maps]) of - UpdatesMap when is_map(UpdatesMap) -> - case core_ticket:update_ticket(TicketId, UpdatesMap) of - {ok, Ticket} -> - send_json(Req2, 200, ticket_to_json(Ticket)); - {error, not_found} -> - send_error(Req2, 404, <<"Ticket not found">>); - {error, Reason} -> - send_error(Req2, 500, Reason) - end; - _ -> - send_error(Req2, 400, <<"Invalid JSON">>) + try + Updates = jsx:decode(Body, [return_maps]), + case map_size(Updates) > 0 of + true -> + case core_ticket:update_ticket(TicketId, Updates) of + {ok, Ticket} -> + Reason = maps:get(<<"reason">>, Updates, <<"">>), + log_audit(AdminId, <<"update_ticket">>, <<"ticket">>, TicketId, Reason), + send_json(Req2, 200, ticket_to_json(Ticket)); + {error, not_found} -> + send_error(Req2, 404, <<"Ticket not found">>); + {error, Reason} -> + send_error(Req2, 500, Reason) + end; + false -> + send_error(Req2, 400, <<"Request body is empty">>) + end catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -96,11 +108,13 @@ update_ticket(TicketId, Req) -> send_error(Req1, Code, Message) end. +%% ── Удаление тикета ───────────────────────────────────── delete_ticket(TicketId, Req) -> case auth_admin(Req) of - {ok, _AdminId, Req1} -> + {ok, AdminId, Req1} -> case core_ticket:delete_ticket(TicketId) of {ok, deleted} -> + log_audit(AdminId, <<"delete_ticket">>, <<"ticket">>, TicketId, <<"">>), send_json(Req1, 200, #{status => <<"deleted">>}); {error, not_found} -> send_error(Req1, 404, <<"Ticket not found">>) @@ -109,6 +123,16 @@ delete_ticket(TicketId, Req) -> send_error(Req1, Code, Message) end. +%% ── Аудит ────────────────────────────────────────────── +log_audit(AdminId, Action, EntityType, EntityId, Reason) -> + case core_admin:get_by_id(AdminId) of + {ok, Admin} -> + core_admin_audit:log(AdminId, Admin#admin.email, Admin#admin.role, + Action, EntityType, EntityId, <<"127.0.0.1">>, Reason); + _ -> ok + end. + +%% ── Аутентификация ────────────────────────────────────── auth_admin(Req) -> case handler_auth:authenticate(Req) of {ok, AdminId, Req1} -> @@ -120,9 +144,11 @@ auth_admin(Req) -> {error, Code, Message, Req1} end. +%% ── Сериализация ──────────────────────────────────────── ticket_to_json(T) -> #{ id => T#ticket.id, + reporter_id => T#ticket.reporter_id, error_hash => T#ticket.error_hash, error_message => T#ticket.error_message, stacktrace => T#ticket.stacktrace, @@ -140,6 +166,7 @@ datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> [Year, Month, Day, Hour, Minute, Second])); datetime_to_iso8601(undefined) -> undefined. +%% ── HTTP-ответы ───────────────────────────────────────── send_json(Req, Status, Data) -> Headers = #{ <<"content-type">> => <<"application/json">>, @@ -150,7 +177,12 @@ send_json(Req, Status, Data) -> cowboy_req:reply(Status, Headers, Body, Req), {ok, Body, []}. -send_error(Req, Status, Message) -> +send_error(Req, Code, Message) -> + Headers = #{ + <<"content-type">> => <<"application/json">>, + <<"access-control-allow-origin">> => <<"*">>, + <<"access-control-expose-headers">> => <<"Content-Range">> + }, Body = jsx:encode(#{error => Message}), - cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req), + cowboy_req:reply(Code, Headers, Body, Req), {ok, Body, []}. \ No newline at end of file diff --git a/src/handlers/admin/admin_handler_user_by_id.erl b/src/handlers/admin/admin_handler_user_by_id.erl index a71cb7c..73f63ff 100644 --- a/src/handlers/admin/admin_handler_user_by_id.erl +++ b/src/handlers/admin/admin_handler_user_by_id.erl @@ -4,10 +4,10 @@ init(Req, _Opts) -> case cowboy_req:method(Req) of - <<"GET">> -> get_user(Req); - <<"PUT">> -> update_user(Req); + <<"GET">> -> get_user(Req); + <<"PUT">> -> update_user(Req); <<"DELETE">> -> delete_user(Req); - _ -> send_error(Req, 405, <<"Method not allowed">>) + _ -> send_error(Req, 405, <<"Method not allowed">>) end. get_user(Req) -> @@ -37,21 +37,23 @@ update_user(Req) -> UserId = cowboy_req:binding(id, Req1), {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of - Decoded when is_map(Decoded) -> - Updates = maps:to_list(Decoded), - Converted = convert_updates(Updates), - case core_user:update(UserId, Converted) of - {ok, User} -> - send_json(Req2, 200, user_to_json(User)); - {error, not_found} -> - send_error(Req2, 404, <<"User not found">>); - {error, _} -> - send_error(Req2, 500, <<"Internal server error">>) + Updates when map_size(Updates) > 0 -> + % Проверка на наличие reason при изменении статуса + case maps:find(<<"status">>, Updates) of + {ok, NewStatus} when NewStatus =:= <<"blocked">> orelse NewStatus =:= <<"active">> -> + case maps:find(<<"reason">>, Updates) of + {ok, Reason} when byte_size(Reason) > 0 -> + apply_updates(UserId, Updates, AdminId, Reason, Req2); + _ -> + send_error(Req2, 400, <<"Missing or empty reason">>) + end; + _ -> + apply_updates(UserId, Updates, AdminId, undefined, Req2) end; _ -> - send_error(Req2, 400, <<"Invalid JSON">>) + send_error(Req2, 400, <<"Request body is empty">>) catch - _:_ -> send_error(Req2, 400, <<"Invalid JSON format">>) + _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; false -> send_error(Req1, 403, <<"Admin access required">>) @@ -79,33 +81,72 @@ delete_user(Req) -> send_error(Req1, Code, Message) end. -user_to_json(User) -> - #{ - id => User#user.id, - email => User#user.email, - role => User#user.role, - status => User#user.status, - created_at => datetime_to_iso8601(User#user.created_at), - updated_at => datetime_to_iso8601(User#user.updated_at) - }. - -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])). +%% ── Вспомогательная функция обновления ──────────────────── +apply_updates(UserId, Updates, AdminId, Reason, Req) -> + Converted = convert_updates(maps:to_list(Updates)), + case core_user:update(UserId, Converted) of + {ok, User} -> + % Логируем, если был указан reason + case Reason of + undefined -> ok; + _ -> + case core_admin:get_by_id(AdminId) of + {ok, Admin} -> + Action = case maps:get(<<"status">>, Updates, undefined) of + <<"blocked">> -> <<"block_user">>; + <<"active">> -> <<"unblock_user">>; + _ -> <<"update_user">> + end, + core_admin_audit:log(AdminId, Admin#admin.email, Admin#admin.role, + Action, <<"user">>, UserId, <<"127.0.0.1">>, Reason); + _ -> ok + end + end, + send_json(Req, 200, user_to_json(User)); + {error, not_found} -> + send_error(Req, 404, <<"User not found">>); + {error, _} -> + send_error(Req, 500, <<"Internal server error">>) + end. convert_updates(Updates) -> - lists:map(fun - ({<<"status">>, Value}) -> {status, binary_to_existing_atom(Value)}; - ({<<"role">>, Value}) -> {role, binary_to_existing_atom(Value)}; - (Other) -> Other + lists:map(fun({<<"status">>, Value}) -> {status, binary_to_existing_atom(Value, utf8)}; + ({<<"role">>, Value}) -> {role, binary_to_existing_atom(Value, utf8)}; + ({<<"reason">>, Value}) -> {reason, Value}; + (Other) -> Other end, Updates). +user_to_json(User) -> + #{ + id => User#user.id, + email => User#user.email, + role => atom_to_binary(User#user.role, utf8), + status => atom_to_binary(User#user.status, utf8), + reason => User#user.reason, + created_at => datetime_to_iso8601(User#user.created_at), + updated_at => datetime_to_iso8601(User#user.updated_at) + }. + +datetime_to_iso8601({{Y,M,D},{H,Min,S}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Y,M,D,H,Min,S])); +datetime_to_iso8601(_) -> null. + send_json(Req, Status, Data) -> + Headers = #{ + <<"content-type">> => <<"application/json">>, + <<"access-control-allow-origin">> => <<"*">>, + <<"access-control-expose-headers">> => <<"Content-Range">> + }, Body = jsx:encode(Data), - cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req), + cowboy_req:reply(Status, Headers, Body, Req), {ok, Body, []}. -send_error(Req, Status, Message) -> +send_error(Req, Code, Message) -> + Headers = #{ + <<"content-type">> => <<"application/json">>, + <<"access-control-allow-origin">> => <<"*">>, + <<"access-control-expose-headers">> => <<"Content-Range">> + }, Body = jsx:encode(#{error => Message}), - cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req), + cowboy_req:reply(Code, Headers, Body, Req), {ok, Body, []}. \ No newline at end of file