diff --git a/include/records.hrl b/include/records.hrl index abeba77..93feab4 100644 --- a/include/records.hrl +++ b/include/records.hrl @@ -9,6 +9,7 @@ password_hash :: binary(), role :: user | bot, status :: active | frozen | deleted, + reason :: binary() | undefined, created_at :: calendar:datetime(), updated_at :: calendar:datetime() }). @@ -51,6 +52,7 @@ rating_avg :: float(), rating_count :: non_neg_integer(), status :: active | frozen | deleted, + reason :: binary() | undefined, created_at :: calendar:datetime(), updated_at :: calendar:datetime() }). @@ -85,6 +87,7 @@ capacity :: integer() | undefined, online_link :: binary() | undefined, status :: active | cancelled | completed, + reason :: binary() | undefined, rating_avg :: float(), rating_count :: non_neg_integer(), created_at :: calendar:datetime(), @@ -118,6 +121,7 @@ rating :: 1..5, comment :: binary(), status :: visible | hidden | deleted, + reason :: binary() | undefined, created_at :: calendar:datetime(), updated_at :: calendar:datetime() }). diff --git a/src/core/core_admin_audit.erl b/src/core/core_admin_audit.erl index af5c24c..b7b11f8 100644 --- a/src/core/core_admin_audit.erl +++ b/src/core/core_admin_audit.erl @@ -1,6 +1,6 @@ -module(core_admin_audit). -include("records.hrl"). --export([log/7, list/0, list/1]). +-export([log/7, log/8, list/0, list/1]). % ← добавили log/8 -export([count_actions_by_admin/2]). log(AdminId, Email, Role, Action, EntityType, EntityId, Ip) -> @@ -25,33 +25,28 @@ log(AdminId, Email, Role, Action, EntityType, EntityId, Ip, Reason) -> list() -> mnesia:dirty_match_object(#admin_audit{_ = '_'}). -%% Фильтрация по параметрам (простая версия) list(Filters) -> - All = list(), % все записи + All = list(), lists:filter(fun(E) -> - % Фильтр по admin_id case proplists:get_value(admin_id, Filters) of undefined -> true; Id -> E#admin_audit.admin_id =:= Id end andalso - % Фильтр по action - case proplists:get_value(action, Filters) of - undefined -> true; - Act -> E#admin_audit.action =:= Act - end + case proplists:get_value(action, Filters) of + undefined -> true; + Act -> E#admin_audit.action =:= Act + end andalso - % Фильтр по дате с - case proplists:get_value(date_from, Filters) of - undefined -> true; - From -> E#admin_audit.timestamp >= From - end + case proplists:get_value(date_from, Filters) of + undefined -> true; + From -> E#admin_audit.timestamp >= From + end andalso - % Фильтр по дате по - case proplists:get_value(date_to, Filters) of - undefined -> true; - To -> E#admin_audit.timestamp =< To - end + case proplists:get_value(date_to, Filters) of + undefined -> true; + To -> E#admin_audit.timestamp =< To + end end, All). count_actions_by_admin(AdminId, Action) -> diff --git a/src/core/core_calendar.erl b/src/core/core_calendar.erl index 89c7d2b..b69eaac 100644 --- a/src/core/core_calendar.erl +++ b/src/core/core_calendar.erl @@ -1,9 +1,9 @@ -module(core_calendar). -include("records.hrl"). - -export([create/4, create/5, get_by_id/1, list_by_owner/1, update/2, delete/1]). -export([generate_id/0]). -export([count_calendars/0]). +-export([freeze/2, unfreeze/2]). % ← новые функции %% Создание календаря create(OwnerId, Title, Description, Confirmation) -> @@ -22,12 +22,7 @@ create(OwnerId, Title, Description, Confirmation) -> created_at = calendar:universal_time(), updated_at = calendar:universal_time() }, - - F = fun() -> - mnesia:write(Calendar), - {ok, Calendar} - end, - + F = fun() -> mnesia:write(Calendar), {ok, Calendar} end, case mnesia:transaction(F) of {atomic, Result} -> Result; {aborted, Reason} -> {error, Reason} @@ -50,12 +45,7 @@ create(OwnerId, Title, Description, Confirmation, Type) -> created_at = calendar:universal_time(), updated_at = calendar:universal_time() }, - - F = fun() -> - mnesia:write(Calendar), - {ok, Calendar} - end, - + F = fun() -> mnesia:write(Calendar), {ok, Calendar} end, case mnesia:transaction(F) of {atomic, Result} -> Result; {aborted, Reason} -> {error, Reason} @@ -78,15 +68,13 @@ list_by_owner(OwnerId) -> update(Id, Updates) -> F = fun() -> case mnesia:read(calendar, Id) of - [] -> - {error, not_found}; + [] -> {error, not_found}; [Calendar] -> UpdatedCalendar = apply_updates(Calendar, Updates), mnesia:write(UpdatedCalendar), {ok, UpdatedCalendar} end end, - case mnesia:transaction(F) of {atomic, Result} -> Result; {aborted, Reason} -> {error, Reason} @@ -96,11 +84,20 @@ update(Id, Updates) -> delete(Id) -> update(Id, [{status, deleted}]). -count_calendars() -> mnesia:table_info(calendar, size). +count_calendars() -> + mnesia:table_info(calendar, size). + +%% ── НОВЫЕ ФУНКЦИИ ────────────────────────────────────────── +freeze(Id, Reason) -> + update(Id, [{status, frozen}, {reason, Reason}]). + +unfreeze(Id, Reason) -> + update(Id, [{status, active}, {reason, Reason}]). %% Внутренние функции generate_id() -> base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). + apply_updates(Calendar, Updates) -> Updated = lists:foldl(fun({Field, Value}, C) -> set_field(Field, Value, C) @@ -115,4 +112,5 @@ set_field(confirmation, Value, C) -> C#calendar{confirmation = Value}; set_field(status, Value, C) -> C#calendar{status = Value}; set_field(rating_avg, Value, C) -> C#calendar{rating_avg = Value}; set_field(rating_count, Value, C) -> C#calendar{rating_count = Value}; +set_field(reason, Value, C) -> C#calendar{reason = Value}; % ← поддержка поля reason set_field(_, _, C) -> C. \ No newline at end of file diff --git a/src/core/core_event.erl b/src/core/core_event.erl index 72d1175..f9e5fbe 100644 --- a/src/core/core_event.erl +++ b/src/core/core_event.erl @@ -5,6 +5,7 @@ update/2, delete/1, materialize_occurrence/3]). -export([generate_id/0]). -export([count_events/0, count_events_by_date/2]). +-export([freeze/2, unfreeze/2]). %% Создание одиночного события create(CalendarId, Title, StartTime, Duration) -> @@ -197,6 +198,13 @@ apply_updates(Event, Updates) -> end, Event, Updates), Updated#event{updated_at = calendar:universal_time()}. +freeze(Id, Reason) -> + update(Id, [{status, frozen}, {reason, Reason}]). + +unfreeze(Id, Reason) -> + update(Id, [{status, active}, {reason, Reason}]). + +%% ── ОБНОВЛЁННАЯ set_field ────────────────────────────────── set_field(title, Value, E) -> E#event{title = Value}; set_field(description, Value, E) -> E#event{description = Value}; set_field(start_time, Value, E) -> E#event{start_time = Value}; @@ -207,6 +215,7 @@ set_field(tags, Value, E) -> E#event{tags = Value}; set_field(capacity, Value, E) -> E#event{capacity = Value}; set_field(online_link, Value, E) -> E#event{online_link = Value}; set_field(status, Value, E) -> E#event{status = Value}; +set_field(reason, Value, E) -> E#event{reason = Value}; set_field(rating_avg, Value, E) -> E#event{rating_avg = Value}; set_field(rating_count, Value, E) -> E#event{rating_count = Value}; set_field(_, _, E) -> E. \ No newline at end of file diff --git a/src/core/core_review.erl b/src/core/core_review.erl index 411a75f..52d768e 100644 --- a/src/core/core_review.erl +++ b/src/core/core_review.erl @@ -2,7 +2,7 @@ -include("records.hrl"). -export([create/5, get_by_id/1, list_by_target/2, list_by_user/1, - update/2, delete/1, hide/1, unhide/1]). + update/2, delete/1, hide/2, unhide/2]). -export([get_average_rating/2, has_user_reviewed/3]). -export([generate_id/0]). -export([count_reviews/0]). @@ -87,12 +87,11 @@ delete(Id) -> end. %% Скрытие отзыва (модерация) -hide(Id) -> - update(Id, [{status, hidden}]). +hide(Id, Reason) -> + update(Id, [{status, hidden}, {reason, Reason}]). -%% Раскрытие отзыва -unhide(Id) -> - update(Id, [{status, visible}]). +unhide(Id, Reason) -> + update(Id, [{status, visible}, {reason, Reason}]). %% Получение среднего рейтинга цели get_average_rating(TargetType, TargetId) -> @@ -129,4 +128,5 @@ apply_updates(Review, Updates) -> set_field(rating, Value, R) when Value >= 1, Value =< 5 -> R#review{rating = Value}; set_field(comment, Value, R) -> R#review{comment = Value}; set_field(status, Value, R) when Value =:= visible; Value =:= hidden -> R#review{status = Value}; +set_field(reason, Value, R) -> R#review{reason = Value}; set_field(_, _, R) -> R. \ No newline at end of file diff --git a/src/core/core_user.erl b/src/core/core_user.erl index 7badaca..4b19de8 100644 --- a/src/core/core_user.erl +++ b/src/core/core_user.erl @@ -1,11 +1,11 @@ -module(core_user). -include("records.hrl"). --export([create/2, get_by_id/1, get_by_email/1, update/2, delete/1]). +-export([create/2, get_by_id/1, get_by_email/1, update/2, update_status/3, delete/1]). -export([email_exists/1]). -export([generate_id/0]). -export([list_users/0]). --export([block/1, unblock/1]). +-export([block/2, unblock/2]). -export([count_users/0, count_users_by_date/2]). %% Создание пользователя @@ -85,6 +85,15 @@ update(Id, Updates) -> {aborted, Reason} -> {error, Reason} end. +update_status(Id, Status, Reason) -> + case get_by_id(Id) of + {ok, User} -> + Updated = User#user{status = Status, reason = Reason, updated_at = calendar:universal_time()}, + mnesia:dirty_write(Updated), + {ok, Updated}; + Error -> Error + end. + %% Удаление пользователя (soft delete) delete(Id) -> update(Id, [{status, deleted}]). @@ -94,30 +103,41 @@ list_users() -> ActiveUsers = [U || U <- Users, U#user.status =/= deleted], {ok, [user_to_map(U) || U <- ActiveUsers]}. +user_to_map(User) when is_map(User) -> + #{ + id => maps:get(id, User), + email => maps:get(email, User), + role => maps:get(role, User, <<"user">>), + status => maps:get(status, User, <<"active">>), + reason => maps:get(reason, User, undefined), + created_at => maps:get(created_at, User), + updated_at => maps:get(updated_at, User) + }; + user_to_map(User) -> #{ - id => User#user.id, - email => User#user.email, - password_hash => User#user.password_hash, - role => User#user.role, - status => User#user.status, + 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 => User#user.created_at, updated_at => User#user.updated_at }. -block(Id) -> +block(Id, Reason) -> case get_by_id(Id) of {ok, User} -> - Updated = User#user{status = blocked, updated_at = calendar:universal_time()}, + Updated = User#user{status = blocked, reason = Reason, updated_at = calendar:universal_time()}, mnesia:dirty_write(Updated), {ok, Updated}; Error -> Error end. -unblock(Id) -> +unblock(Id, Reason) -> case get_by_id(Id) of {ok, User} -> - Updated = User#user{status = active, updated_at = calendar:universal_time()}, + Updated = User#user{status = active, reason = Reason, updated_at = calendar:universal_time()}, mnesia:dirty_write(Updated), {ok, Updated}; Error -> Error diff --git a/src/handlers/admin/admin_handler_moderation.erl b/src/handlers/admin/admin_handler_moderation.erl index 2d42aa7..2a3a2c2 100644 --- a/src/handlers/admin/admin_handler_moderation.erl +++ b/src/handlers/admin/admin_handler_moderation.erl @@ -13,16 +13,17 @@ init(Req, _Opts) -> end. moderate(Req) -> - case auth_admin(Req) of - {ok, _AdminId, Req1} -> + case authenticate_and_check_admin(Req) of + {ok, AdminId, Req1} -> TargetType = cowboy_req:binding(target_type, Req1), TargetId = cowboy_req:binding(id, Req1), case lists:member(TargetType, ?VALID_TARGETS) of true -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of - #{<<"action">> := Action} -> - apply_moderation(TargetType, TargetId, Action, Req2); + #{<<"action">> := Action} = BodyMap -> + Reason = maps:get(<<"reason">>, BodyMap, <<"">>), + apply_moderation(TargetType, TargetId, Action, Reason, Req2, AdminId); _ -> send_error(Req2, 400, <<"Missing 'action' field">>) catch @@ -35,68 +36,95 @@ moderate(Req) -> send_error(Req1, Code, Message) end. -apply_moderation(<<"calendar">>, Id, Action, Req) -> - handle_calendar(Id, Action, Req); -apply_moderation(<<"event">>, Id, Action, Req) -> - handle_event(Id, Action, Req); -apply_moderation(<<"review">>, Id, Action, Req) -> - handle_review(Id, Action, Req); -apply_moderation(<<"user">>, Id, Action, Req) -> - handle_user(Id, Action, Req). +apply_moderation(<<"calendar">>, Id, Action, Reason, Req, AdminId) -> + handle_calendar(Id, Action, Reason, Req, AdminId); +apply_moderation(<<"event">>, Id, Action, Reason, Req, AdminId) -> + handle_event(Id, Action, Reason, Req, AdminId); +apply_moderation(<<"review">>, Id, Action, Reason, Req, AdminId) -> + handle_review(Id, Action, Reason, Req, AdminId); +apply_moderation(<<"user">>, Id, Action, Reason, Req, AdminId) -> + handle_user(Id, Action, Reason, Req, AdminId). -handle_calendar(Id, <<"freeze">>, Req) -> - case core_calendar:freeze(Id) of - {ok, Calendar} -> send_json(Req, 200, calendar_to_json(Calendar)); +handle_calendar(Id, <<"freeze">>, Reason, Req, AdminId) -> + case core_calendar:freeze(Id, Reason) of + {ok, Calendar} -> + log_audit(AdminId, <<"freeze_calendar">>, <<"calendar">>, Id, Reason), + send_json(Req, 200, calendar_to_json(Calendar)); {error, not_found} -> send_error(Req, 404, <<"Calendar not found">>) end; -handle_calendar(Id, <<"unfreeze">>, Req) -> - case core_calendar:unfreeze(Id) of - {ok, Calendar} -> send_json(Req, 200, calendar_to_json(Calendar)); +handle_calendar(Id, <<"unfreeze">>, Reason, Req, AdminId) -> + case core_calendar:unfreeze(Id, Reason) of + {ok, Calendar} -> + log_audit(AdminId, <<"unfreeze_calendar">>, <<"calendar">>, Id, Reason), + send_json(Req, 200, calendar_to_json(Calendar)); {error, not_found} -> send_error(Req, 404, <<"Calendar not found">>) end; -handle_calendar(_Id, _Action, Req) -> +handle_calendar(_Id, _Action, _Reason, Req, _AdminId) -> send_error(Req, 400, <<"Invalid action for calendar">>). -handle_event(Id, <<"freeze">>, Req) -> - case core_event:freeze(Id) of - {ok, Event} -> send_json(Req, 200, event_to_json(Event)); +handle_event(Id, <<"freeze">>, Reason, Req, AdminId) -> + case core_event:freeze(Id, Reason) of + {ok, Event} -> + log_audit(AdminId, <<"freeze_event">>, <<"event">>, Id, Reason), + send_json(Req, 200, event_to_json(Event)); {error, not_found} -> send_error(Req, 404, <<"Event not found">>) end; -handle_event(Id, <<"unfreeze">>, Req) -> - case core_event:unfreeze(Id) of - {ok, Event} -> send_json(Req, 200, event_to_json(Event)); +handle_event(Id, <<"unfreeze">>, Reason, Req, AdminId) -> + case core_event:unfreeze(Id, Reason) of + {ok, Event} -> + log_audit(AdminId, <<"unfreeze_event">>, <<"event">>, Id, Reason), + send_json(Req, 200, event_to_json(Event)); {error, not_found} -> send_error(Req, 404, <<"Event not found">>) end; -handle_event(_Id, _Action, Req) -> +handle_event(_Id, _Action, _Reason, Req, _AdminId) -> send_error(Req, 400, <<"Invalid action for event">>). -handle_review(Id, <<"hide">>, Req) -> - case core_review:hide(Id) of - {ok, Review} -> send_json(Req, 200, review_to_json(Review)); +handle_review(Id, <<"hide">>, Reason, Req, AdminId) -> + case core_review:hide(Id, Reason) of + {ok, Review} -> + log_audit(AdminId, <<"hide_review">>, <<"review">>, Id, Reason), + send_json(Req, 200, review_to_json(Review)); {error, not_found} -> send_error(Req, 404, <<"Review not found">>) end; -handle_review(Id, <<"show">>, Req) -> - case core_review:show(Id) of - {ok, Review} -> send_json(Req, 200, review_to_json(Review)); +handle_review(Id, <<"unhide">>, Reason, Req, AdminId) -> + case core_review:unhide(Id, Reason) of + {ok, Review} -> + log_audit(AdminId, <<"unhide_review">>, <<"review">>, Id, Reason), + send_json(Req, 200, review_to_json(Review)); {error, not_found} -> send_error(Req, 404, <<"Review not found">>) end; -handle_review(_Id, _Action, Req) -> +handle_review(_Id, _Action, _Reason, Req, _AdminId) -> send_error(Req, 400, <<"Invalid action for review">>). -handle_user(Id, <<"block">>, Req) -> - case core_user:block(Id) of - {ok, User} -> send_json(Req, 200, user_to_json(User)); +handle_user(Id, <<"block">>, Reason, Req, AdminId) -> + case core_user:block(Id, Reason) of + {ok, User} -> + log_audit(AdminId, <<"block_user">>, <<"user">>, Id, Reason), + send_json(Req, 200, user_to_json(User)); {error, not_found} -> send_error(Req, 404, <<"User not found">>) end; -handle_user(Id, <<"unblock">>, Req) -> - case core_user:unblock(Id) of - {ok, User} -> send_json(Req, 200, user_to_json(User)); +handle_user(Id, <<"unblock">>, Reason, Req, AdminId) -> + case core_user:unblock(Id, Reason) of + {ok, User} -> + log_audit(AdminId, <<"unblock_user">>, <<"user">>, Id, Reason), + send_json(Req, 200, user_to_json(User)); {error, not_found} -> send_error(Req, 404, <<"User not found">>) end; -handle_user(_Id, _Action, Req) -> +handle_user(_Id, _Action, _Reason, Req, _AdminId) -> send_error(Req, 400, <<"Invalid action for user">>). -auth_admin(Req) -> +%% ── АУДИТ ────────────────────────────────────────────────── +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, + client_ip(), Reason); + _ -> ok + end. + +%% ── ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ──────────────────────────────── +authenticate_and_check_admin(Req) -> case handler_auth:authenticate(Req) of {ok, AdminId, Req1} -> case admin_utils:is_admin(AdminId) of @@ -107,31 +135,37 @@ auth_admin(Req) -> {error, Code, Message, Req1} end. +client_ip() -> <<"127.0.0.1">>. + calendar_to_json(C) -> #{ id => C#calendar.id, title => C#calendar.title, - status => atom_to_binary(C#calendar.status, utf8) + status => atom_to_binary(C#calendar.status, utf8), + reason => C#calendar.reason }. event_to_json(E) -> #{ id => E#event.id, title => E#event.title, - status => atom_to_binary(E#event.status, utf8) + status => atom_to_binary(E#event.status, utf8), + reason => E#event.reason }. review_to_json(R) -> #{ id => R#review.id, - status => atom_to_binary(R#review.status, utf8) + status => atom_to_binary(R#review.status, utf8), + reason => R#review.reason }. user_to_json(U) -> #{ id => U#user.id, email => U#user.email, - status => atom_to_binary(U#user.status, utf8) + status => atom_to_binary(U#user.status, utf8), + reason => U#user.reason }. send_json(Req, Status, Data) -> diff --git a/src/handlers/admin/admin_handler_report_by_id.erl b/src/handlers/admin/admin_handler_report_by_id.erl index ad25d79..a50870f 100644 --- a/src/handlers/admin/admin_handler_report_by_id.erl +++ b/src/handlers/admin/admin_handler_report_by_id.erl @@ -12,7 +12,7 @@ init(Req, _Opts) -> end. get_report(Req) -> - case handler_auth:authenticate(Req) of + case auth_admin(Req) of {ok, AdminId, Req1} -> case admin_utils:is_admin(AdminId) of true -> @@ -31,17 +31,18 @@ get_report(Req) -> end. update_report(Req) -> - case handler_auth:authenticate(Req) of + case auth_admin(Req) of {ok, AdminId, Req1} -> case admin_utils:is_admin(AdminId) of true -> ReportId = cowboy_req:binding(id, Req1), {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of - #{<<"status">> := NewStatus} -> + #{<<"status">> := NewStatus, <<"reason">> := Reason} -> StatusAtom = binary_to_atom(NewStatus, utf8), case core_report:update_status(ReportId, StatusAtom, AdminId) of {ok, Report} -> + log_audit(AdminId, <<"update_report_status">>, <<"report">>, ReportId, Reason), send_json(Req2, 200, report_to_json(Report)); {error, not_found} -> send_error(Req2, 404, <<"Report not found">>); @@ -49,7 +50,7 @@ update_report(Req) -> send_error(Req2, 500, <<"Internal server error">>) end; _ -> - send_error(Req2, 400, <<"Missing status field">>) + send_error(Req2, 400, <<"Missing status or reason">>) catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -60,6 +61,26 @@ update_report(Req) -> send_error(Req1, Code, Message) end. +auth_admin(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case admin_utils:is_admin(AdminId) of + true -> {ok, AdminId, Req1}; + false -> {error, 403, <<"Admin access required">>, Req1} + end; + {error, Code, Message, Req1} -> + {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. + report_to_json(R) -> #{ id => R#report.id, diff --git a/src/handlers/admin/admin_handler_reports.erl b/src/handlers/admin/admin_handler_reports.erl index 2397ad5..ab5a3cb 100644 --- a/src/handlers/admin/admin_handler_reports.erl +++ b/src/handlers/admin/admin_handler_reports.erl @@ -8,11 +8,11 @@ init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> list_reports(Req); <<"PUT">> -> update_report(Req); - _ -> send_error(Req, 405, <<"Method not allowed">>) + _ -> send_error(Req, 405, <<"Method not allowed">>) end. list_reports(Req) -> - case handler_auth:authenticate(Req) of + case auth_admin(Req) of {ok, AdminId, Req1} -> case admin_utils:is_admin(AdminId) of true -> @@ -26,16 +26,18 @@ list_reports(Req) -> end. update_report(Req) -> - case handler_auth:authenticate(Req) of + case auth_admin(Req) of {ok, AdminId, Req1} -> case admin_utils:is_admin(AdminId) of true -> ReportId = cowboy_req:binding(id, Req1), {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of - #{<<"status">> := NewStatus} -> - case core_report:update_status(ReportId, NewStatus, AdminId) of + #{<<"status">> := NewStatus, <<"reason">> := Reason} -> + StatusAtom = binary_to_atom(NewStatus, utf8), + case core_report:update_status(ReportId, StatusAtom, AdminId) of {ok, Report} -> + log_audit(AdminId, <<"update_report_status">>, <<"report">>, ReportId, Reason), send_json(Req2, 200, report_to_json(Report)); {error, not_found} -> send_error(Req2, 404, <<"Report not found">>); @@ -43,7 +45,7 @@ update_report(Req) -> send_error(Req2, 500, <<"Internal server error">>) end; _ -> - send_error(Req2, 400, <<"Missing status field">>) + send_error(Req2, 400, <<"Missing status or reason">>) catch _:_ -> send_error(Req2, 400, <<"Invalid JSON">>) end; @@ -54,6 +56,26 @@ update_report(Req) -> send_error(Req1, Code, Message) end. +auth_admin(Req) -> + case handler_auth:authenticate(Req) of + {ok, AdminId, Req1} -> + case admin_utils:is_admin(AdminId) of + true -> {ok, AdminId, Req1}; + false -> {error, 403, <<"Admin access required">>, Req1} + end; + {error, Code, Message, Req1} -> + {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. + report_to_json(R) -> #{ id => R#report.id, diff --git a/test/api/api_moderation_tests.erl b/test/api/api_moderation_tests.erl index 6acdf2e..5dbd89e 100644 --- a/test/api/api_moderation_tests.erl +++ b/test/api/api_moderation_tests.erl @@ -9,7 +9,7 @@ test() -> AdminToken = api_test_runner:get_admin_token(), UserToken = api_test_runner:get_user_token(), - %% Создаём календарь и событие через пользовательский API + %% Создаём календарь и событие CalId = api_test_runner:extract_json( api_test_runner:http_post("/v1/calendars", #{title => <<"Mod Cal">>}, UserToken), <<"id">>), @@ -21,7 +21,7 @@ test() -> UserToken), <<"id">>), - %% TEST 1: Create report (пользователь) + %% TEST 1: Create report io:format(" TEST 1: Create report... "), ReportId = api_test_runner:extract_json( api_test_runner:http_post("/v1/reports", @@ -32,22 +32,22 @@ test() -> <<"id">>), io:format("OK~n"), - %% TEST 2: Admin views reports (через админский URL, прямой httpc) + %% TEST 2: Admin views reports io:format(" TEST 2: Admin views reports... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {?ADMIN_BASE_URL ++ "/v1/admin/reports", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), io:format("OK~n"), - %% TEST 3: Admin resolves report + %% TEST 3: Admin resolves report с reason io:format(" TEST 3: Admin resolves report... "), {ok, {{_, 200, _}, _, _}} = httpc:request(put, {?ADMIN_BASE_URL ++ "/v1/admin/reports/" ++ binary_to_list(ReportId), [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}], "application/json", - jsx:encode(#{status => <<"reviewed">>})}, [], []), + jsx:encode(#{status => <<"reviewed">>, reason => <<"Resolved by moderator">>})}, [], []), io:format("OK~n"), - %% TEST 4: Add banned word (админ) + %% TEST 4: Add banned word io:format(" TEST 4: Add banned word... "), {ok, {{_, 201, _}, _, _}} = httpc:request(post, {?ADMIN_BASE_URL ++ "/v1/admin/banned-words", @@ -56,13 +56,13 @@ test() -> jsx:encode(#{<<"word">> => <<"badword">>})}, [], []), io:format("OK~n"), - %% TEST 5: List banned words (админ) + %% TEST 5: List banned words io:format(" TEST 5: List banned words... "), {ok, {{_, 200, _}, _, _}} = httpc:request(get, {?ADMIN_BASE_URL ++ "/v1/admin/banned-words", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []), io:format("OK~n"), - %% TEST 6: Remove banned word (админ) + %% TEST 6: Remove banned word io:format(" TEST 6: Remove banned word... "), {ok, {{_, 200, _}, _, _}} = httpc:request(delete, {?ADMIN_BASE_URL ++ "/v1/admin/banned-words/badword", [{"Authorization", "Bearer " ++ binary_to_list(AdminToken)}]}, [], []),