Рефакторинг обработчиков. Часть 3 #21

This commit is contained in:
2026-05-13 23:02:59 +03:00
parent 61bb44ab4a
commit 40806df62a
91 changed files with 6138 additions and 7150 deletions
+11 -1
View File
@@ -2,7 +2,7 @@
-include("records.hrl").
-export([create/3, get_by_email/1, get_by_id/1, list_all/0,
update_role/2, block/1, unblock/1, update_last_login/1]).
-export([update/2]).
-export([update/2, delete/1]).
create(Email, Password, Role) ->
case get_by_email(Email) of
@@ -90,6 +90,16 @@ update_status(Id, Status) ->
Error -> Error
end.
%% Физическое удаление администратора из базы
-spec delete(binary()) -> {ok, deleted} | {error, not_found}.
delete(AdminId) ->
case get_by_id(AdminId) of
{ok, _Admin} ->
mnesia:dirty_delete(admin, AdminId),
{ok, deleted};
Error -> Error
end.
%%%===================================================================
%%% ВНУТРЕННИЕ ФУНКЦИИ
%%%===================================================================
+9 -1
View File
@@ -177,8 +177,16 @@ apply_updates(User, Updates) ->
set_field(email, Value, U) -> U#user{email = Value};
set_field(password_hash, Value, U) -> U#user{password_hash = Value};
set_field(role, Value, U) when Value =:= user; Value =:= admin -> U#user{role = Value};
set_field(role, Value, U) when Value =:= user; Value =:= admin; Value =:= bot -> U#user{role = Value};
set_field(status, Value, U) when Value =:= active; Value =:= frozen; Value =:= deleted -> U#user{status = Value};
set_field(reason, Value, U) -> U#user{reason = Value};
set_field(nickname, Value, U) -> U#user{nickname = Value};
set_field(avatar_url, Value, U) -> U#user{avatar_url = Value};
set_field(timezone, Value, U) -> U#user{timezone = Value};
set_field(language, Value, U) -> U#user{language = Value};
set_field(social_links, Value, U) -> U#user{social_links = Value};
set_field(phone, Value, U) -> U#user{phone = Value};
set_field(preferences, Value, U) -> U#user{preferences = Value};
set_field(_, _, U) -> U.
%% ------------------------------------------------------------------
+7 -7
View File
@@ -35,9 +35,9 @@ start(_StartType, _StartArgs) ->
end,
case Nodes of
[] ->
io:format("Cluster: no nodes found or first node~n");
io:format("~nCluster: no nodes found or first node~n");
_ ->
io:format("Cluster: discovered nodes ~p, joining cluster~n", [Nodes]),
io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]),
application:set_env(eventhub, extra_db_nodes, Nodes)
end,
ok = infra_mnesia:init_tables(),
@@ -102,7 +102,7 @@ start_admin_http() ->
Dispatch = cowboy_router:compile([
{'_', [
% ================== БАЗОВЫЕ ==================
{"/v1/admin/health", admin_handler_health, []},
{"/admin/health", admin_handler_health, []},
{"/v1/admin/stats", admin_handler_stats, []},
{"/v1/admin/login", admin_handler_login, []},
% ================== ПОЛЬЗОВАТЕЛИ ==================
@@ -127,13 +127,13 @@ start_admin_http() ->
% ================== ПОДПИСКИ ==================
{"/v1/admin/subscriptions", admin_handler_subscriptions, []},
{"/v1/admin/subscriptions/:id", admin_handler_subscriptions_by_id, []},
% ================== МОДЕРАЦИЯ (общий маршрут) ==================
{"/v1/admin/:target_type/:id", admin_handler_moderation, []},
% ================== Управление ролями (только для superadmin) ==================
{"/v1/admin/me", admin_handler_me, []},
{"/v1/admin/admins", admin_handler_admins, []},
{"/v1/admin/admins/:id", admin_handler_admins, []},
{"/v1/admin/audit", admin_handler_audit, []}
{"/v1/admin/admins/:id", admin_handler_admins_by_id, []},
{"/v1/admin/audit", admin_handler_audit, []},
% ================== МОДЕРАЦИЯ (общий маршрут) ==================
{"/v1/admin/:target_type/:id", admin_handler_moderation, []}
]}
]),
+105 -29
View File
@@ -1,3 +1,9 @@
%%%-------------------------------------------------------------------
%%% @doc Административный обработчик списка администраторов.
%%% GET – список всех администраторов (только для superadmin).
%%% POST – создать нового администратора (только для superadmin).
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_admins).
-behaviour(cowboy_handler).
@@ -6,37 +12,66 @@
-include("records.hrl").
%%% cowboy_handler callback
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> list_admins(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
<<"GET">> -> list_admins(Req);
<<"POST">> -> create_admin(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
-spec trails() -> [map()].
trails() ->
[
#{
path => <<"/v1/admin/admins">>,
method => <<"GET">>,
description => <<"List all admins (superadmin only)">>,
tags => [<<"Admins">>],
parameters => [
#{name => <<"role">>, in => <<"query">>, schema => #{type => string}},
#{name => <<"status">>, in => <<"query">>, schema => #{type => string}},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}}
],
responses => #{
200 => #{
description => <<"Array of admins">>,
content => #{<<"application/json">> => #{schema => #{
type => array,
items => admin_schema()
}}}
}
ListGet = #{
path => <<"/v1/admin/admins">>,
method => <<"GET">>,
description => <<"List all admins (superadmin only)">>,
tags => [<<"Admins">>],
parameters => [
#{name => <<"role">>, in => <<"query">>, schema => #{type => string}},
#{name => <<"status">>, in => <<"query">>, schema => #{type => string}},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}}
],
responses => #{
200 => #{
description => <<"Array of admins">>,
content => #{<<"application/json">> => #{schema => #{
type => array,
items => admin_schema()
}}}
}
}
].
},
PostCreate = #{
path => <<"/v1/admin/admins">>,
method => <<"POST">>,
description => <<"Create a new admin (superadmin only)">>,
tags => [<<"Admins">>],
requestBody => #{
required => true,
content => #{<<"application/json">> => #{schema => #{
type => object,
required => [<<"email">>, <<"password">>, <<"role">>],
properties => #{
email => #{type => string, format => <<"email">>},
password => #{type => string},
role => #{type => string, enum => [<<"superadmin">>, <<"admin">>, <<"moderator">>, <<"support">>]}
}
}}}
},
responses => #{
201 => #{description => <<"Admin created">>},
400 => #{description => <<"Invalid fields">>},
403 => #{description => <<"Only superadmin can create admins">>},
409 => #{description => <<"Email already exists">>}
}
},
[ListGet, PostCreate].
-spec admin_schema() -> map().
admin_schema() ->
#{
type => object,
@@ -57,8 +92,12 @@ admin_schema() ->
}
}.
%%% Internal functions
%% @doc GET /v1/admin/admins – список администраторов.
-spec list_admins(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
list_admins(Req) ->
case handler_utils:auth_admin(Req) of
case handler_utils:is_superadmin(Req) of
{ok, _AdminId, Req1} ->
Filters = parse_admin_filters(Req1),
Pagination = handler_utils:parse_pagination_params(Req1),
@@ -70,6 +109,41 @@ list_admins(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
%% @doc POST /v1/admin/admins – создание администратора.
-spec create_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
create_admin(Req) ->
case handler_utils:is_superadmin(Req) of
{ok, _AdminId, Req1} ->
{ok, Body, Req2} = cowboy_req:read_body(Req1),
try jsx:decode(Body, [return_maps]) of
#{<<"email">> := Email, <<"password">> := Password, <<"role">> := RoleBin} ->
Role = try binary_to_existing_atom(RoleBin, utf8)
catch error:badarg -> undefined
end,
case Role of
undefined ->
handler_utils:send_error(Req2, 400, <<"Invalid role">>);
_ ->
case logic_admin:create_admin(Email, Password, Role) of
{ok, Admin} ->
handler_utils:send_json(Req2, 201, admin_to_json(Admin));
{error, email_exists} ->
handler_utils:send_error(Req2, 409, <<"Email already exists">>);
{error, invalid_role} ->
handler_utils:send_error(Req2, 400, <<"Invalid role">>);
{error, _} ->
handler_utils:send_error(Req2, 500, <<"Internal server error">>)
end
end;
_ -> handler_utils:send_error(Req2, 400, <<"Missing fields">>)
catch
_:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON">>)
end;
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
end.
-spec parse_admin_filters(cowboy_req:req()) -> map().
parse_admin_filters(Req) ->
Qs = cowboy_req:parse_qs(Req),
#{
@@ -77,23 +151,25 @@ parse_admin_filters(Req) ->
status => proplists:get_value(<<"status">>, Qs)
}.
-spec admin_to_json(#admin{}) -> map().
admin_to_json(Admin) ->
#{
id => Admin#admin.id,
email => Admin#admin.email,
role => Admin#admin.role,
status => Admin#admin.status,
role => atom_to_binary(Admin#admin.role, utf8),
status => atom_to_binary(Admin#admin.status, utf8),
nickname => Admin#admin.nickname,
avatar_url => Admin#admin.avatar_url,
timezone => Admin#admin.timezone,
language => Admin#admin.language,
phone => Admin#admin.phone,
preferences => Admin#admin.preferences,
last_login => handler_utils:parse_datetime(Admin#admin.last_login), % требует доработки – лучше общую функцию
created_at => handler_utils:parse_datetime(Admin#admin.created_at),
updated_at => handler_utils:parse_datetime(Admin#admin.updated_at)
last_login => handler_utils:datetime_to_iso8601(Admin#admin.last_login),
created_at => handler_utils:datetime_to_iso8601(Admin#admin.created_at),
updated_at => handler_utils:datetime_to_iso8601(Admin#admin.updated_at)
}.
-spec pagination_headers(map(), non_neg_integer()) -> map().
pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
RangeEnd = min(Offset + Limit - 1, Total - 1),
#{
@@ -0,0 +1,206 @@
%%%-------------------------------------------------------------------
%%% @doc Административный обработчик конкретного администратора.
%%% GET /v1/admin/admins/:id – получить администратора
%%% PUT /v1/admin/admins/:id – обновить администратора
%%% DELETE /v1/admin/admins/:id – удалить администратора
%%%
%%% Все операции доступны только суперадмину.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_admins_by_id).
-behaviour(cowboy_handler).
-export([init/2]).
-export([trails/0]).
-include("records.hrl").
%%% cowboy_handler callback
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> get_admin(Req);
<<"PUT">> -> update_admin(Req);
<<"DELETE">> -> delete_admin(Req);
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
-spec trails() -> [map()].
trails() ->
IdParam = [#{name => <<"id">>, in => <<"path">>, required => true, schema => #{type => string}}],
[
#{ % GET by id
path => <<"/v1/admin/admins/:id">>,
method => <<"GET">>,
description => <<"Get admin by ID (superadmin only)">>,
tags => [<<"Admins">>],
parameters => IdParam,
responses => #{
200 => #{
description => <<"Admin details">>,
content => #{<<"application/json">> => #{schema => admin_schema()}}
},
404 => #{description => <<"Admin not found">>}
}
},
#{ % PUT update
path => <<"/v1/admin/admins/:id">>,
method => <<"PUT">>,
description => <<"Update admin (superadmin only)">>,
tags => [<<"Admins">>],
parameters => IdParam,
requestBody => #{
required => true,
content => #{<<"application/json">> => #{schema => admin_update_schema()}}
},
responses => #{
200 => #{description => <<"Admin updated">>},
404 => #{description => <<"Admin not found">>}
}
},
#{ % DELETE
path => <<"/v1/admin/admins/:id">>,
method => <<"DELETE">>,
description => <<"Delete admin (superadmin only)">>,
tags => [<<"Admins">>],
parameters => IdParam,
responses => #{
200 => #{description => <<"Admin deleted">>},
404 => #{description => <<"Admin not found">>}
}
}
].
-spec admin_schema() -> map().
admin_schema() ->
#{
type => object,
properties => #{
id => #{type => string},
email => #{type => string},
role => #{type => string, enum => [<<"superadmin">>, <<"admin">>, <<"moderator">>, <<"support">>]},
status => #{type => string, enum => [<<"active">>, <<"blocked">>]},
nickname => #{type => string, nullable => true},
avatar_url => #{type => string, nullable => true},
timezone => #{type => string, nullable => true},
language => #{type => string, nullable => true},
phone => #{type => string, nullable => true},
preferences => #{type => object, nullable => true},
last_login => #{type => string, format => <<"date-time">>},
created_at => #{type => string, format => <<"date-time">>},
updated_at => #{type => string, format => <<"date-time">>}
}
}.
-spec admin_update_schema() -> map().
admin_update_schema() ->
#{
type => object,
properties => #{
nickname => #{type => string},
avatar_url => #{type => string},
timezone => #{type => string},
language => #{type => string},
phone => #{type => string},
preferences => #{type => object}
}
}.
%%% Internal functions
%% @doc GET /v1/admin/admins/:id получение администратора.
-spec get_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
get_admin(Req) ->
case handler_utils:is_superadmin(Req) of
{ok, _AdminId, Req1} ->
Id = cowboy_req:binding(id, Req1),
case logic_admin:get_admin(Id) of
{ok, Admin} ->
handler_utils:send_json(Req1, 200, admin_to_json(Admin));
{error, not_found} ->
handler_utils:send_error(Req1, 404, <<"Admin 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.
%% @doc PUT /v1/admin/admins/:id обновление администратора.
-spec update_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
update_admin(Req) ->
case handler_utils:is_superadmin(Req) of
{ok, _AdminId, Req1} ->
Id = cowboy_req:binding(id, Req1),
{ok, Body, Req2} = cowboy_req:read_body(Req1),
try jsx:decode(Body, [return_maps]) of
UpdatesMap when is_map(UpdatesMap) ->
Updates0 = maps:to_list(UpdatesMap),
Updates = convert_admin_fields(Updates0),
case logic_admin:update_admin(Id, Updates) of
{ok, Admin} ->
handler_utils:send_json(Req2, 200, admin_to_json(Admin));
{error, not_found} ->
handler_utils:send_error(Req2, 404, <<"Admin not found">>);
{error, _} ->
handler_utils:send_error(Req2, 500, <<"Internal server error">>)
end;
_ ->
handler_utils:send_error(Req2, 400, <<"Invalid JSON">>)
catch
_:_ -> handler_utils:send_error(Req1, 400, <<"Invalid JSON format">>)
end;
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
end.
%% @doc DELETE /v1/admin/admins/:id физическое удаление администратора.
-spec delete_admin(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
delete_admin(Req) ->
case handler_utils:is_superadmin(Req) of
{ok, _AdminId, Req1} ->
Id = cowboy_req:binding(id, Req1),
case logic_admin:delete_admin(Id) of
{ok, _} ->
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
{error, not_found} ->
handler_utils:send_error(Req1, 404, <<"Admin 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 Преобразует бинарные ключи в атомы для обновлений администратора.
-spec convert_admin_fields([{binary(), term()}]) -> [{atom(), term()}].
convert_admin_fields(Updates) ->
lists:map(fun
({<<"nickname">>, V}) -> {nickname, V};
({<<"avatar_url">>, V}) -> {avatar_url, V};
({<<"timezone">>, V}) -> {timezone, V};
({<<"language">>, V}) -> {language, V};
({<<"phone">>, V}) -> {phone, V};
({<<"preferences">>, V}) -> {preferences, V};
(Other) -> Other
end, Updates).
%% @private Преобразует запись администратора в JSON-совместимую карту.
-spec admin_to_json(#admin{}) -> map().
admin_to_json(Admin) ->
#{
id => Admin#admin.id,
email => Admin#admin.email,
role => atom_to_binary(Admin#admin.role, utf8),
status => atom_to_binary(Admin#admin.status, utf8),
nickname => Admin#admin.nickname,
avatar_url => Admin#admin.avatar_url,
timezone => Admin#admin.timezone,
language => Admin#admin.language,
phone => Admin#admin.phone,
preferences => Admin#admin.preferences,
last_login => handler_utils:datetime_to_iso8601(Admin#admin.last_login),
created_at => handler_utils:datetime_to_iso8601(Admin#admin.created_at),
updated_at => handler_utils:datetime_to_iso8601(Admin#admin.updated_at)
}.
+25 -70
View File
@@ -1,6 +1,7 @@
%%%-------------------------------------------------------------------
%%% @doc Административный обработчик журнала аудита.
%%% GET – список записей аудита с пагинацией и фильтрацией.
%%% Доступно только суперадмину.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_audit).
@@ -26,16 +27,15 @@ trails() ->
#{
path => <<"/v1/admin/audit">>,
method => <<"GET">>,
description => <<"List audit records (admin)">>,
description => <<"List audit records (superadmin only)">>,
tags => [<<"Audit">>],
parameters => [
#{name => <<"admin_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by admin ID">>},
#{name => <<"action">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by action">>},
#{name => <<"entity_type">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by entity type">>},
#{name => <<"from">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"Start timestamp (ISO8601)">>},
#{name => <<"to">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"End timestamp (ISO8601)">>},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>}
#{name => <<"admin_id">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by admin ID">>},
#{name => <<"action">>, in => <<"query">>, schema => #{type => string}, description => <<"Filter by action">>},
#{name => <<"date_from">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"Start timestamp (ISO8601)">>},
#{name => <<"date_to">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"End timestamp (ISO8601)">>},
#{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>},
#{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>}
],
responses => #{
200 => #{
@@ -71,16 +71,13 @@ audit_schema() ->
%% @doc Получить список записей аудита с пагинацией и фильтрацией.
-spec list_audit(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
list_audit(Req) ->
case handler_utils:auth_admin(Req) of
case handler_utils:is_superadmin(Req) of
{ok, _AdminId, Req1} ->
Filters = parse_audit_filters(Req1),
Pagination = handler_utils:parse_pagination_params(Req1),
%% Предполагается, что core_admin_audit (или аналогичный) предоставляет list_all/0
{ok, AllRecords} = core_admin_audit:list(),
Filtered = apply_filters(AllRecords, Filters),
Sorted = sort_audit(Filtered, Pagination),
Total = length(Sorted),
Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)),
AllRecords = core_admin_audit:list(Filters),
Total = length(AllRecords),
Page = lists:sublist(AllRecords, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)),
Json = [audit_to_json(R) || R <- Page],
ExtraHeaders = pagination_headers(Pagination, Total),
handler_utils:send_json(Req1, 200, Json, ExtraHeaders);
@@ -88,65 +85,24 @@ list_audit(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
%% @private Извлечь фильтры из query string.
-spec parse_audit_filters(cowboy_req:req()) -> map().
-spec parse_audit_filters(cowboy_req:req()) -> proplists:proplist().
parse_audit_filters(Req) ->
Qs = cowboy_req:parse_qs(Req),
#{
admin_id => proplists:get_value(<<"admin_id">>, Qs),
action => proplists:get_value(<<"action">>, Qs),
entity_type => proplists:get_value(<<"entity_type">>, Qs),
from => handler_utils:parse_datetime_qs(proplists:get_value(<<"from">>, Qs)),
to => handler_utils:parse_datetime_qs(proplists:get_value(<<"to">>, Qs))
}.
[
{admin_id, proplists:get_value(<<"admin_id">>, Qs)},
{action, proplists:get_value(<<"action">>, Qs)},
{date_from, parse_datetime_qs(proplists:get_value(<<"date_from">>, Qs))},
{date_to, parse_datetime_qs(proplists:get_value(<<"date_to">>, Qs))}
].
%% @private Применить фильтры к списку записей аудита.
-spec apply_filters([#admin_audit{}], map()) -> [#admin_audit{}].
apply_filters(Records, Filters) ->
AdminId = maps:get(admin_id, Filters, undefined),
Action = maps:get(action, Filters, undefined),
EntityType = maps:get(entity_type, Filters, undefined),
From = maps:get(from, Filters, undefined),
To = maps:get(to, Filters, undefined),
R1 = case AdminId of
undefined -> Records;
_ -> [R || R <- Records, R#admin_audit.admin_id =:= AdminId]
end,
R2 = case Action of
undefined -> R1;
_ -> [R || R <- R1, R#admin_audit.action =:= Action]
end,
R3 = case EntityType of
undefined -> R2;
_ -> [R || R <- R2, R#admin_audit.entity_type =:= EntityType]
end,
R4 = case From of
undefined -> R3;
_ -> [R || R <- R3, R#admin_audit.timestamp >= From]
end,
case To of
undefined -> R4;
_ -> [R || R <- R4, R#admin_audit.timestamp =< To]
-spec parse_datetime_qs(binary() | undefined) -> calendar:datetime() | undefined.
parse_datetime_qs(undefined) -> undefined;
parse_datetime_qs(Bin) ->
case handler_utils:parse_datetime(Bin) of
{ok, Dt} -> Dt;
_ -> undefined
end.
%% @private Отсортировать записи аудита.
-spec sort_audit([#admin_audit{}], map()) -> [#admin_audit{}].
sort_audit(Records, #{sort := Sort, order := Order}) ->
Field = binary_to_existing_atom(Sort, utf8),
lists:sort(
fun(A, B) ->
ValA = audit_field(A, Field),
ValB = audit_field(B, Field),
if Order == <<"asc">> -> ValA =< ValB;
true -> ValA >= ValB
end
end, Records).
audit_field(#admin_audit{timestamp = V}, timestamp) -> V;
audit_field(#admin_audit{action = V}, action) -> V;
audit_field(_, _) -> undefined.
%% @private Преобразовать запись аудита в JSON-карту.
-spec audit_to_json(#admin_audit{}) -> map().
audit_to_json(A) ->
#{
@@ -162,7 +118,6 @@ audit_to_json(A) ->
reason => A#admin_audit.reason
}.
%% @private Сформировать заголовки пагинации.
-spec pagination_headers(map(), non_neg_integer()) -> map().
pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
RangeEnd = min(Offset + Limit - 1, Total - 1),
+1 -1
View File
@@ -17,7 +17,7 @@ init(Req, _State) ->
trails() ->
[
#{
path => <<"/v1/admin/health">>,
path => <<"/admin/health">>,
method => <<"GET">>,
description => <<"Admin API health check">>,
tags => [<<"Health">>],
+37 -10
View File
@@ -1,3 +1,9 @@
%%%-------------------------------------------------------------------
%%% @doc Административный обработчик профиля текущего администратора.
%%% GET – получить свой профиль.
%%% PUT – обновить свой профиль.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_me).
-behaviour(cowboy_handler).
@@ -6,6 +12,8 @@
-include("records.hrl").
%%% cowboy_handler callback
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> get_me(Req);
@@ -13,6 +21,8 @@ init(Req, _Opts) ->
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
-spec trails() -> [map()].
trails() ->
[
#{ % GET
@@ -42,6 +52,7 @@ trails() ->
}
].
-spec admin_schema() -> map().
admin_schema() ->
#{
type => object,
@@ -62,6 +73,7 @@ admin_schema() ->
}
}.
-spec admin_update_schema() -> map().
admin_update_schema() ->
#{
type => object,
@@ -75,6 +87,9 @@ admin_update_schema() ->
}
}.
%%% Internal functions
-spec get_me(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
get_me(Req) ->
case handler_utils:auth_admin(Req) of
{ok, AdminId, Req1} ->
@@ -90,13 +105,15 @@ get_me(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
-spec update_me(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
update_me(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
UpdatesMap when is_map(UpdatesMap) ->
Updates = maps:to_list(UpdatesMap),
Updates0 = maps:to_list(UpdatesMap),
Updates = convert_admin_fields(Updates0),
case logic_admin:update_admin(AdminId, Updates) of
{ok, Admin} ->
handler_utils:send_json(Req2, 200, admin_to_json(Admin));
@@ -114,6 +131,21 @@ update_me(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
%% @private Преобразует бинарные ключи в атомы для обновлений администратора.
-spec convert_admin_fields([{binary(), term()}]) -> [{atom(), term()}].
convert_admin_fields(Updates) ->
lists:map(fun
({<<"nickname">>, V}) -> {nickname, V};
({<<"avatar_url">>, V}) -> {avatar_url, V};
({<<"timezone">>, V}) -> {timezone, V};
({<<"language">>, V}) -> {language, V};
({<<"phone">>, V}) -> {phone, V};
({<<"preferences">>, V}) -> {preferences, V};
(Other) -> Other
end, Updates).
%% @private Формирует JSON-представление администратора.
-spec admin_to_json(#admin{}) -> map().
admin_to_json(Admin) ->
#{
id => Admin#admin.id,
@@ -126,12 +158,7 @@ admin_to_json(Admin) ->
language => Admin#admin.language,
phone => Admin#admin.phone,
preferences => Admin#admin.preferences,
last_login => datetime_to_iso8601(Admin#admin.last_login),
created_at => datetime_to_iso8601(Admin#admin.created_at),
updated_at => datetime_to_iso8601(Admin#admin.updated_at)
}.
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])).
last_login => handler_utils:datetime_to_iso8601(Admin#admin.last_login),
created_at => handler_utils:datetime_to_iso8601(Admin#admin.created_at),
updated_at => handler_utils:datetime_to_iso8601(Admin#admin.updated_at)
}.
+14 -10
View File
@@ -88,7 +88,15 @@ review_schema() ->
list_reviews(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
Filters = parse_review_filters(Req1),
Qs = cowboy_req:parse_qs(Req1),
Filters0 = #{
target_type => normalize_target_type(proplists:get_value(<<"target_type">>, Qs)),
target_id => proplists:get_value(<<"target_id">>, Qs),
user_id => proplists:get_value(<<"user_id">>, Qs),
status => proplists:get_value(<<"status">>, Qs)
},
% Убираем ключи со значением undefined
Filters = maps:filter(fun(_, V) -> V =/= undefined end, Filters0),
Pagination = handler_utils:parse_pagination_params(Req1),
{ok, Total, Reviews} = logic_review:list_admin_reviews(Filters, Pagination),
Json = [handler_utils:review_to_json(R) || R <- Reviews],
@@ -98,6 +106,11 @@ list_reviews(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
%% @private Преобразует бинарный target_type в атом.
normalize_target_type(<<"event">>) -> event;
normalize_target_type(<<"calendar">>) -> calendar;
normalize_target_type(Other) -> Other.
bulk_update_reviews(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
@@ -118,15 +131,6 @@ bulk_update_reviews(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
parse_review_filters(Req) ->
Qs = cowboy_req:parse_qs(Req),
#{
target_type => proplists:get_value(<<"target_type">>, Qs),
target_id => proplists:get_value(<<"target_id">>, Qs),
user_id => proplists:get_value(<<"user_id">>, Qs),
status => proplists:get_value(<<"status">>, Qs)
}.
pagination_headers(#{limit := Limit, offset := Offset}, Total) ->
RangeEnd = min(Offset + Limit - 1, Total - 1),
#{
@@ -109,7 +109,8 @@ update_review(Req) ->
try jsx:decode(Body, [return_maps]) of
UpdatesMap when is_map(UpdatesMap) ->
Updates = maps:to_list(UpdatesMap),
case logic_review:update_review_admin(ReviewId, Updates) of
Converted = convert_review_fields(Updates),
case logic_review:update_review_admin(ReviewId, Converted) of
{ok, Review} ->
handler_utils:send_json(Req2, 200, handler_utils:review_to_json(Review));
{error, not_found} ->
@@ -124,4 +125,19 @@ update_review(Req) ->
end;
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
end.
end.
%% @private Преобразует бинарные ключи и значения в атомы, где необходимо.
convert_review_fields(Updates) ->
lists:map(fun convert_field/1, Updates).
convert_field({<<"status">>, Val}) ->
try binary_to_existing_atom(Val, utf8) of
Atom -> {status, Atom}
catch
error:badarg -> {status, Val}
end;
convert_field({<<"reason">>, Val}) -> {reason, Val};
convert_field({<<"comment">>, Val}) -> {comment, Val};
convert_field({<<"rating">>, Val}) -> {rating, Val};
convert_field(Other) -> Other.
@@ -87,15 +87,21 @@ parse_subscription_filters(Req) ->
}.
apply_filters(Subs, Filters) ->
Plan = maps:get(plan, Filters, undefined),
Status = maps:get(status, Filters, undefined),
F1 = case Plan of
PlanBin = maps:get(plan, Filters, undefined),
StatusBin = maps:get(status, Filters, undefined),
F1 = case PlanBin of
undefined -> Subs;
_ -> [S || S <- Subs, S#subscription.plan =:= Plan]
_ ->
Plan = try binary_to_existing_atom(PlanBin, utf8)
catch error:badarg -> PlanBin end,
[S || S <- Subs, S#subscription.plan =:= Plan]
end,
case Status of
case StatusBin of
undefined -> F1;
_ -> [S || S <- F1, S#subscription.status =:= Status]
_ ->
Status = try binary_to_existing_atom(StatusBin, utf8)
catch error:badarg -> StatusBin end,
[S || S <- F1, S#subscription.status =:= Status]
end.
sort_subscriptions(Subs, #{sort := Sort, order := Order}) ->
+55 -14
View File
@@ -1,3 +1,10 @@
%%%-------------------------------------------------------------------
%%% @doc Административный обработчик конкретного пользователя.
%%% GET – получить пользователя по ID.
%%% PUT – обновить пользователя (роль, статус, причина).
%%% DELETE – мягко удалить пользователя.
%%% @end
%%%-------------------------------------------------------------------
-module(admin_handler_user_by_id).
-behaviour(cowboy_handler).
@@ -6,6 +13,8 @@
-include("records.hrl").
%%% cowboy_handler callback
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> get_user(Req);
@@ -14,8 +23,16 @@ init(Req, _Opts) ->
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
end.
%%% Swagger metadata
-spec trails() -> [map()].
trails() ->
BaseParams = [#{name => <<"id">>, in => <<"path">>, required => true, schema => #{type => string}}],
BaseParams = [#{
name => <<"id">>,
in => <<"path">>,
description => <<"User ID">>,
required => true,
schema => #{type => string}
}],
[
#{ % GET
path => <<"/v1/admin/users/:id">>,
@@ -56,20 +73,21 @@ trails() ->
}
].
-spec user_schema() -> map().
user_schema() ->
#{
type => object,
properties => #{
id => #{type => string},
email => #{type => string},
role => #{type => string},
status => #{type => string},
role => #{type => string, enum => [<<"user">>, <<"bot">>]},
status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]},
reason => #{type => string, nullable => true},
nickname => #{type => string, nullable => true},
avatar_url => #{type => string, nullable => true},
timezone => #{type => string, nullable => true},
language => #{type => string, nullable => true},
social_links => #{type => array, items => #{type => string}},
social_links => #{type => array, items => #{type => string}, nullable => true},
phone => #{type => string, nullable => true},
preferences => #{type => object, nullable => true},
last_login => #{type => string, format => <<"date-time">>},
@@ -78,21 +96,20 @@ user_schema() ->
}
}.
-spec user_update_schema() -> map().
user_update_schema() ->
#{
type => object,
properties => #{
role => #{type => string, enum => [<<"user">>, <<"bot">>]},
status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]},
reason => #{type => string},
nickname => #{type => string},
timezone => #{type => string},
language => #{type => string},
phone => #{type => string},
preferences => #{type => object}
role => #{type => string, enum => [<<"user">>, <<"bot">>]},
status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]},
reason => #{type => string}
}
}.
%%% Internal functions
-spec get_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
get_user(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
@@ -109,6 +126,7 @@ get_user(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
-spec update_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
update_user(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
@@ -117,7 +135,8 @@ update_user(Req) ->
try jsx:decode(Body, [return_maps]) of
UpdatesMap when is_map(UpdatesMap) ->
Updates = maps:to_list(UpdatesMap),
case logic_user:update_user_admin(UserId, Updates) of
Converted = convert_user_fields(Updates),
case logic_user:update_user_admin(UserId, Converted) of
{ok, User} ->
handler_utils:send_json(Req2, 200, handler_utils:user_to_json(User));
{error, not_found} ->
@@ -134,6 +153,7 @@ update_user(Req) ->
handler_utils:send_error(Req1, Code, Msg)
end.
-spec delete_user(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
delete_user(Req) ->
case handler_utils:auth_admin(Req) of
{ok, _AdminId, Req1} ->
@@ -148,4 +168,25 @@ delete_user(Req) ->
end;
{error, Code, Msg, Req1} ->
handler_utils:send_error(Req1, Code, Msg)
end.
end.
%% @private Преобразует бинарные ключи и значения в атомы, где необходимо.
-spec convert_user_fields([{binary(), term()}]) -> [{atom(), term()}].
convert_user_fields(Updates) ->
lists:map(fun convert_field/1, Updates).
-spec convert_field({binary(), term()}) -> {atom(), term()}.
convert_field({<<"role">>, <<"user">>}) -> {role, user};
convert_field({<<"role">>, <<"bot">>}) -> {role, bot};
convert_field({<<"status">>, <<"active">>}) -> {status, active};
convert_field({<<"status">>, <<"frozen">>}) -> {status, frozen};
convert_field({<<"status">>, <<"deleted">>}) -> {status, deleted};
convert_field({<<"reason">>, Val}) -> {reason, Val};
convert_field({<<"nickname">>, Val}) -> {nickname, Val};
convert_field({<<"avatar_url">>, Val}) -> {avatar_url, Val};
convert_field({<<"timezone">>, Val}) -> {timezone, Val};
convert_field({<<"language">>, Val}) -> {language, Val};
convert_field({<<"social_links">>, Val}) -> {social_links, Val};
convert_field({<<"phone">>, Val}) -> {phone, Val};
convert_field({<<"preferences">>, Val}) -> {preferences, Val};
convert_field(Other) -> Other.
+35 -49
View File
@@ -1,6 +1,5 @@
%%%-------------------------------------------------------------------
%%% @doc Обработчик конкретного календаря (клиентский API).
%%%
%%% GET – получить информацию о календаре.
%%% PUT – обновить календарь (владельцем).
%%% DELETE – удалить календарь (владельцем).
@@ -16,11 +15,7 @@
%%% cowboy_handler callback
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, Opts) ->
handle(Req, Opts).
-spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
handle(Req, _Opts) ->
init(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"GET">> -> get_calendar(Req);
<<"PUT">> -> update_calendar(Req);
@@ -31,15 +26,13 @@ handle(Req, _Opts) ->
%%% Swagger metadata
-spec trails() -> [map()].
trails() ->
BaseParams = [
#{
name => <<"id">>,
in => <<"path">>,
description => <<"Calendar ID">>,
required => true,
schema => #{type => string}
}
],
BaseParams = [#{
name => <<"id">>,
in => <<"path">>,
description => <<"Calendar ID">>,
required => true,
schema => #{type => string}
}],
[
#{ % GET
path => <<"/v1/calendars/:id">>,
@@ -51,9 +44,7 @@ trails() ->
200 => #{
description => <<"Calendar details">>,
content => #{<<"application/json">> => #{schema => calendar_schema()}}
},
403 => #{description => <<"Access denied">>},
404 => #{description => <<"Calendar not found">>}
}
}
},
#{ % PUT
@@ -67,10 +58,7 @@ trails() ->
content => #{<<"application/json">> => #{schema => calendar_update_schema()}}
},
responses => #{
200 => #{description => <<"Calendar updated">>},
400 => #{description => <<"Invalid request">>},
403 => #{description => <<"Access denied">>},
404 => #{description => <<"Calendar not found">>}
200 => #{description => <<"Calendar updated">>}
}
},
#{ % DELETE
@@ -80,9 +68,7 @@ trails() ->
tags => [<<"Calendars">>],
parameters => BaseParams,
responses => #{
200 => #{description => <<"Calendar deleted">>},
403 => #{description => <<"Access denied">>},
404 => #{description => <<"Calendar not found">>}
200 => #{description => <<"Calendar deleted">>}
}
}
].
@@ -90,24 +76,18 @@ trails() ->
-spec calendar_schema() -> map().
calendar_schema() ->
#{
type => object,
type => object,
properties => #{
id => #{type => string},
owner_id => #{type => string},
title => #{type => string},
description => #{type => string},
short_name => #{type => string, nullable => true},
category => #{type => string, nullable => true},
color => #{type => string, nullable => true},
image_url => #{type => string, nullable => true},
settings => #{type => object, nullable => true},
tags => #{type => array, items => #{type => string}},
type => #{type => string, enum => [<<"personal">>, <<"commercial">>]},
confirmation => #{type => string, description => <<"auto, manual, or {timeout, N}">>},
confirmation => #{type => string, enum => [<<"auto">>, <<"manual">>]},
tags => #{type => array, items => #{type => string}},
rating_avg => #{type => number, format => float},
rating_count => #{type => integer},
status => #{type => string, enum => [<<"active">>, <<"frozen">>, <<"deleted">>]},
reason => #{type => string, nullable => true},
status => #{type => string},
created_at => #{type => string, format => <<"date-time">>},
updated_at => #{type => string, format => <<"date-time">>}
}
@@ -116,22 +96,18 @@ calendar_schema() ->
-spec calendar_update_schema() -> map().
calendar_update_schema() ->
#{
type => object,
type => object,
properties => #{
title => #{type => string},
description => #{type => string},
type => #{type => string, enum => [<<"personal">>, <<"commercial">>]},
confirmation => #{type => string, description => <<"auto, manual, or {timeout, N}">>},
type => #{type => string},
confirmation => #{type => string},
tags => #{type => array, items => #{type => string}}
}
}.
%%%===================================================================
%%% HTTP-методы
%%%===================================================================
%%% Internal functions
%% @doc GET /v1/calendars/:id — получение календаря.
-spec get_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
get_calendar(Req) ->
case handler_utils:auth_user(Req) of
{ok, UserId, Req1} ->
@@ -150,8 +126,6 @@ get_calendar(Req) ->
handler_utils:send_error(Req1, Code, Message)
end.
%% @doc PUT /v1/calendars/:id — обновление календаря.
-spec update_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
update_calendar(Req) ->
case handler_utils:auth_user(Req) of
{ok, UserId, Req1} ->
@@ -159,7 +133,8 @@ update_calendar(Req) ->
{ok, Body, Req2} = cowboy_req:read_body(Req1),
try jsx:decode(Body, [return_maps]) of
UpdatesMap when is_map(UpdatesMap) ->
Updates = maps:to_list(UpdatesMap),
Updates0 = maps:to_list(UpdatesMap),
Updates = convert_calendar_fields(Updates0),
case logic_calendar:update_calendar(UserId, CalendarId, Updates) of
{ok, Calendar} ->
handler_utils:send_json(Req2, 200, handler_utils:calendar_to_json(Calendar));
@@ -179,8 +154,6 @@ update_calendar(Req) ->
handler_utils:send_error(Req1, Code, Message)
end.
%% @doc DELETE /v1/calendars/:id — удаление календаря.
-spec delete_calendar(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
delete_calendar(Req) ->
case handler_utils:auth_user(Req) of
{ok, UserId, Req1} ->
@@ -197,4 +170,17 @@ delete_calendar(Req) ->
end;
{error, Code, Message, Req1} ->
handler_utils:send_error(Req1, Code, Message)
end.
end.
%% @private Преобразует бинарные ключи и значения в атомы для обновлений календаря.
-spec convert_calendar_fields([{binary(), term()}]) -> [{atom(), term()}].
convert_calendar_fields(Updates) ->
lists:map(fun convert_field/1, Updates).
-spec convert_field({binary(), term()}) -> {atom(), term()}.
convert_field({<<"title">>, Val}) -> {title, Val};
convert_field({<<"description">>, Val}) -> {description, Val};
convert_field({<<"type">>, Val}) -> {type, Val};
convert_field({<<"confirmation">>, Val}) -> {confirmation, Val};
convert_field({<<"tags">>, Val}) -> {tags, Val};
convert_field(Other) -> Other.
+1 -1
View File
@@ -19,7 +19,7 @@ init(Req, Opts) ->
trails() ->
[
#{
path => <<"/v1/health">>,
path => <<"/health">>,
method => <<"GET">>,
description => <<"API health check">>,
tags => [<<"Health">>],
+15 -2
View File
@@ -25,8 +25,8 @@
ticket_to_json/1,
calendar_to_json/1,
subscription_to_json/1,
trails_for_crud/4
]).
trails_for_crud/4,
is_superadmin/1]).
-include("records.hrl").
@@ -48,6 +48,19 @@ auth_admin(Req) ->
{error, Code, Msg, Req1}
end.
%% @doc Проверяет, что запрос выполняет суперадмин.
-spec is_superadmin(cowboy_req:req()) ->
{ok, binary(), cowboy_req:req()} | {error, integer(), binary(), cowboy_req:req()}.
is_superadmin(Req) ->
case handler_utils:auth_admin(Req) of
{ok, AdminId, Req1} ->
case admin_utils:is_superadmin(AdminId) of
true -> {ok, AdminId, Req1};
false -> {error, 403, <<"Only superadmin allowed">>, Req1}
end;
Error -> Error
end.
%% @doc Проверяет, что запрос содержит валидный токен пользователя.
-spec auth_user(cowboy_req:req()) ->
{ok, binary(), cowboy_req:req()} | {error, integer(), binary(), cowboy_req:req()}.
+14 -14
View File
@@ -7,8 +7,8 @@
%%% GET / – индексная страница с выбором API
%%% GET /admin/ Swagger UI для административного API
%%% GET /admin/swagger.json OpenAPI-спецификация (admin)
%%% GET /client/ Swagger UI для клиентского API
%%% GET /client/swagger.json OpenAPI-спецификация (client)
%%% GET /user/ Swagger UI для клиентского API
%%% GET /user/swagger.json OpenAPI-спецификация (user)
%%% @end
%%%-------------------------------------------------------------------
-module(swagger_docs_handler).
@@ -35,12 +35,12 @@ handle(<<"/admin/">>, Req) ->
serve_ui(admin, Req);
handle(<<"/admin/swagger.json">>, Req) ->
serve_json(admin, Req);
handle(<<"/client">>, Req) ->
redirect_to_slash(<<"/client/">>, Req);
handle(<<"/client/">>, Req) ->
serve_ui(client, Req);
handle(<<"/client/swagger.json">>, Req) ->
serve_json(client, Req);
handle(<<"/user">>, Req) ->
redirect_to_slash(<<"/user/">>, Req);
handle(<<"/user/">>, Req) ->
serve_ui(user, Req);
handle(<<"/user/swagger.json">>, Req) ->
serve_json(user, Req);
handle(_, Req) ->
cowboy_req:reply(404, #{}, <<"Not Found">>, Req),
{ok, [], []}.
@@ -58,7 +58,7 @@ serve_index(Req) ->
<h1>EventHub API Documentation</h1>
<ul>
<li><a href=\"/admin/\">Admin API</a></li>
<li><a href=\"/client/\">Client API</a></li>
<li><a href=\"/user/\">User API</a></li>
</ul>
</body>
</html>">>,
@@ -69,11 +69,11 @@ serve_index(Req) ->
%% Swagger UI
%%--------------------------------------------------------------------
-spec serve_ui(admin | client, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
-spec serve_ui(admin | user, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
serve_ui(Api, Req) ->
{Title, SpecUrl} = case Api of
admin -> {<<"EventHub Admin API">>, <<"/admin/swagger.json">>};
client -> {<<"EventHub Client API">>, <<"/client/swagger.json">>}
user -> {<<"EventHub User API">>, <<"/user/swagger.json">>}
end,
Html = iolist_to_binary([
"<!DOCTYPE html><html><head><title>", Title,
@@ -91,18 +91,18 @@ serve_ui(Api, Req) ->
%% OpenAPI JSON
%%--------------------------------------------------------------------
-spec serve_json(admin | client, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
-spec serve_json(admin | user, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
serve_json(Api, Req) ->
Trails = case Api of
admin -> trails:admin();
client -> trails:client()
user -> trails:user()
end,
OpenApi = #{
openapi => <<"3.0.3">>,
info => #{
title => case Api of
admin -> <<"EventHub Admin API">>;
client -> <<"EventHub Client API">>
user -> <<"EventHub User API">>
end,
version => <<"1.0.0">>
},
+7 -1
View File
@@ -1,7 +1,7 @@
-module(admin_utils).
-include("records.hrl").
-export([is_admin/1, check_role/2, get_permissions/1]).
-export([is_admin/1, check_role/2, get_permissions/1, is_superadmin/1]).
-export([client_ip/1]).
is_admin(UserId) ->
@@ -10,6 +10,12 @@ is_admin(UserId) ->
_ -> false
end.
is_superadmin(AdminId) ->
case core_admin:get_by_id(AdminId) of
{ok, #admin{role = superadmin}} -> true;
_ -> false
end.
%% Проверка конкретной роли (или одной из списка ролей)
-spec check_role(UserId :: binary(), RequiredRole :: atom() | [atom()]) -> boolean().
check_role(UserId, RequiredRoles) when is_list(RequiredRoles) ->
+22 -1
View File
@@ -1,9 +1,23 @@
-module(logic_admin).
-export([list_admins/2, get_admin/1, update_admin/2]).
-export([list_admins/2, get_admin/1, update_admin/2, delete_admin/1, create_admin/3]).
-include("records.hrl").
%% @doc Создаёт нового администратора (доступно только суперадмину).
%% Роль может быть superadmin, admin, moderator или support.
-spec create_admin(binary(), binary(), atom()) ->
{ok, #admin{}} | {error, email_exists | invalid_role}.
create_admin(Email, Password, Role) when Role =:= superadmin; Role =:= admin; Role =:= moderator; Role =:= support ->
case core_admin:get_by_email(Email) of
{ok, _} -> {error, email_exists};
{error, not_found} ->
{ok, PasswordHash} = logic_auth:hash_password(Password),
core_admin:create(Email, PasswordHash, Role)
end;
create_admin(_, _, _) ->
{error, invalid_role}.
%%%-------------------------------------------------------------------
%%% Административный список администраторов с пагинацией
%%%-------------------------------------------------------------------
@@ -39,6 +53,13 @@ update_admin(AdminId, Updates) ->
Error
end.
%%%-------------------------------------------------------------------
%%% Удалить администратора (физическое удаление из базы)
%%%-------------------------------------------------------------------
-spec delete_admin(binary()) -> {ok, deleted} | {error, not_found}.
delete_admin(AdminId) ->
core_admin:delete(AdminId).
%%%===================================================================
%%% Внутренние функции
%%%===================================================================
+1 -1
View File
@@ -268,7 +268,7 @@ target_exists(_, _) -> false.
update_target_rating(event, EventId) ->
{Avg, Count} = core_review:get_average_rating(event, EventId),
io:format("Updating event ~p rating: avg=~p, count=~p~n", [EventId, Avg, Count]),
%% io:format("Updating event ~p rating: avg=~p, count=~p~n", [EventId, Avg, Count]),
core_event:update(EventId, [{rating_avg, Avg}, {rating_count, Count}]);
update_target_rating(calendar, CalendarId) ->
{Avg, Count} = core_review:get_average_rating(calendar, CalendarId),
+14 -14
View File
@@ -13,10 +13,20 @@ search(Type, Query, UserId, Params) ->
Offset = maps:get(offset, Params, 0),
case Type of
<<"event">> -> search_events(Query, UserId, Params, Limit, Offset);
<<"calendar">> -> search_calendars(Query, UserId, Params, Limit, Offset);
_ -> search_all(Query, UserId, Params, Limit, Offset)
end.
<<"event">> ->
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, Total, #{<<"events">> => Events}};
<<"calendar">> ->
{ok, Total, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, Total, #{<<"calendars">> => Calendars}};
_ ->
{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, EventsTotal + CalendarsTotal, #{
<<"events">> => Events,
<<"calendars">> => Calendars
}}
end.
%% ============ Поиск событий ============
search_events(Query, UserId, Params, Limit, Offset) ->
@@ -37,16 +47,6 @@ search_calendars(Query, UserId, Params, Limit, Offset) ->
{ok, length(Filtered), format_calendars(Paginated)}.
%% ============ Поиск всего ============
search_all(Query, UserId, Params, Limit, Offset) ->
{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset),
{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
{ok, EventsTotal + CalendarsTotal, #{
events => Events,
calendars => Calendars
}}.
%% ============ Получение данных ============
get_all_events() ->
Match = #event{status = active, is_instance = false, _ = '_'},
+1 -1
View File
@@ -93,7 +93,7 @@ resolve_ticket(AdminId, TicketId, ResolutionNote) ->
case admin_utils:is_admin(AdminId) of
true ->
core_ticket:update_ticket(TicketId, #{
<<"status">> => <<"closed">>,
<<"status">> => <<"resolved">>,
<<"resolution_note">> => ResolutionNote
});
false -> {error, access_denied}
+8 -6
View File
@@ -45,9 +45,8 @@ update_user_admin(UserId, Updates) ->
-spec delete_user_admin(binary()) -> {ok, #user{}} | {error, not_found}.
delete_user_admin(UserId) ->
case core_user:get_by_id(UserId) of
{ok, User} ->
UpdatedUser = User#user{status = deleted},
core_user:update(UserId, UpdatedUser);
{ok, _User} ->
core_user:delete(UserId);
Error ->
Error
end.
@@ -58,15 +57,18 @@ delete_user_admin(UserId) ->
apply_filters(Users, Filters) ->
Role = maps:get(role, Filters, undefined),
Status = maps:get(status, Filters, undefined),
StatusBin = maps:get(status, Filters, undefined),
Q = maps:get(q, Filters, undefined),
F1 = case Role of
undefined -> Users;
_ -> [U || U <- Users, U#user.role =:= Role]
end,
F2 = case Status of
F2 = case StatusBin of
undefined -> F1;
_ -> [U || U <- F1, U#user.status =:= Status]
_ ->
StatusAtom = try binary_to_existing_atom(StatusBin, utf8)
catch error:badarg -> StatusBin end,
[U || U <- F1, U#user.status =:= StatusAtom]
end,
case Q of
undefined -> F2;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,5 +1,5 @@
-module(trails).
-export([admin/0, client/0, all/0]).
-export([admin/0, user/0, all/0]).
admin() ->
Modules = [
@@ -37,7 +37,7 @@ admin() ->
],
lists:flatmap(fun trails_from_module/1, Modules).
client() ->
user() ->
Modules = [
handler_health,
handler_register,
@@ -65,7 +65,7 @@ client() ->
lists:flatmap(fun trails_from_module/1, Modules).
all() ->
admin() ++ client().
admin() ++ user().
trails_from_module(Module) ->
try Module:trails() of