Рефакторинг обработчиков. Часть 3 #21
This commit is contained in:
+11
-1
@@ -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.
|
||||
|
||||
%%%===================================================================
|
||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||
%%%===================================================================
|
||||
|
||||
@@ -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.
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
@@ -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, []}
|
||||
]}
|
||||
]),
|
||||
|
||||
|
||||
@@ -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)
|
||||
}.
|
||||
@@ -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),
|
||||
|
||||
@@ -17,7 +17,7 @@ init(Req, _State) ->
|
||||
trails() ->
|
||||
[
|
||||
#{
|
||||
path => <<"/v1/admin/health">>,
|
||||
path => <<"/admin/health">>,
|
||||
method => <<"GET">>,
|
||||
description => <<"Admin API health check">>,
|
||||
tags => [<<"Health">>],
|
||||
|
||||
@@ -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)
|
||||
}.
|
||||
@@ -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}) ->
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -19,7 +19,7 @@ init(Req, Opts) ->
|
||||
trails() ->
|
||||
[
|
||||
#{
|
||||
path => <<"/v1/health">>,
|
||||
path => <<"/health">>,
|
||||
method => <<"GET">>,
|
||||
description => <<"API health check">>,
|
||||
tags => [<<"Health">>],
|
||||
|
||||
@@ -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()}.
|
||||
|
||||
@@ -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">>
|
||||
},
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Внутренние функции
|
||||
%%%===================================================================
|
||||
|
||||
@@ -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
@@ -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, _ = '_'},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+970
-927
File diff suppressed because it is too large
Load Diff
+576
-628
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user