Перенести все админские эндпоинты на порт 8445 и добавить отдельную авторизацию для админов. Часть 2. Final #3
This commit is contained in:
+30
-75
@@ -1,88 +1,43 @@
|
||||
-module(logic_auth).
|
||||
-export([hash_password/1, verify_password/2,
|
||||
generate_jwt/2, verify_jwt/1,
|
||||
generate_refresh_token/1,
|
||||
authenticate_user/2]).
|
||||
|
||||
-export([hash_password/1, verify_password/2]).
|
||||
-export([generate_jwt/2, verify_jwt/1, extract_claims/1]).
|
||||
-export([generate_refresh_token/1]).
|
||||
-include("records.hrl").
|
||||
|
||||
%% ============ Argon2 хеширование ============
|
||||
hash_password(Password) when is_binary(Password) ->
|
||||
hash_password(Password) ->
|
||||
argon2:hash(Password).
|
||||
|
||||
verify_password(Password, Hash) when is_binary(Password), is_binary(Hash) ->
|
||||
verify_password(Password, Hash) ->
|
||||
argon2:verify(Password, Hash).
|
||||
|
||||
%% ============ JWT с использованием jose ============
|
||||
get_jwt_secret() ->
|
||||
<<"my-super-secret-key-for-jwt-32-bytes!">>.
|
||||
|
||||
get_jwk() ->
|
||||
jose_jwk:from_oct(get_jwt_secret()).
|
||||
|
||||
generate_jwt(UserId, Role) ->
|
||||
JWK = get_jwk(),
|
||||
eventhub_auth:generate_user_token(UserId, Role).
|
||||
|
||||
ExpTime = os:system_time(seconds) + 86400, % 24 часа
|
||||
Claims = #{
|
||||
<<"user_id">> => UserId,
|
||||
<<"role">> => Role,
|
||||
<<"exp">> => ExpTime,
|
||||
<<"iat">> => os:system_time(seconds)
|
||||
},
|
||||
verify_jwt(Token) ->
|
||||
eventhub_auth:verify_user_token(Token).
|
||||
|
||||
JWT = jose_jwt:sign(JWK, #{<<"alg">> => <<"HS256">>}, Claims),
|
||||
{_, Token} = jose_jws:compact(JWT),
|
||||
Token.
|
||||
generate_refresh_token(UserId) ->
|
||||
eventhub_auth:generate_refresh_token(UserId).
|
||||
|
||||
verify_jwt(Token) when is_binary(Token) ->
|
||||
try
|
||||
JWK = get_jwk(),
|
||||
case jose_jwt:verify(JWK, Token) of
|
||||
{true, {jose_jwt, Claims}, _} ->
|
||||
case check_expiry(Claims) of
|
||||
true -> {ok, Claims};
|
||||
false -> {error, expired}
|
||||
end;
|
||||
{true, Claims, _} when is_map(Claims) ->
|
||||
case check_expiry(Claims) of
|
||||
true -> {ok, Claims};
|
||||
false -> {error, expired}
|
||||
end;
|
||||
{false, _, _} ->
|
||||
{error, invalid_signature}
|
||||
end
|
||||
catch
|
||||
_:_ -> {error, invalid_token}
|
||||
authenticate_user(Email, Password) ->
|
||||
case core_user:get_by_email(Email) of
|
||||
{ok, User} ->
|
||||
case verify_password(Password, User#user.password_hash) of
|
||||
{ok, true} ->
|
||||
{ok, user_to_map(User)};
|
||||
_ ->
|
||||
{error, invalid_credentials}
|
||||
end;
|
||||
{error, not_found} ->
|
||||
{error, invalid_credentials}
|
||||
end.
|
||||
|
||||
extract_claims(Token) when is_binary(Token) ->
|
||||
try
|
||||
JWK = get_jwk(),
|
||||
case jose_jwt:verify(JWK, Token) of
|
||||
{true, {jose_jwt, Claims}, _} ->
|
||||
{ok, Claims};
|
||||
{true, Claims, _} when is_map(Claims) ->
|
||||
{ok, Claims};
|
||||
_ ->
|
||||
{error, invalid_token}
|
||||
end
|
||||
catch
|
||||
_:_ -> {error, invalid_token}
|
||||
end.
|
||||
|
||||
check_expiry(Claims) ->
|
||||
case maps:find(<<"exp">>, Claims) of
|
||||
{ok, Exp} when is_integer(Exp) ->
|
||||
Exp > os:system_time(seconds);
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% ============ Refresh Token ============
|
||||
generate_refresh_token(_UserId) ->
|
||||
Token = base64:encode(crypto:strong_rand_bytes(32), #{mode => urlsafe, padding => false}),
|
||||
ExpiresAt = calendar:universal_time_to_local_time(
|
||||
calendar:gregorian_seconds_to_datetime(
|
||||
calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 30 * 86400
|
||||
)
|
||||
),
|
||||
{Token, ExpiresAt}.
|
||||
user_to_map(User) ->
|
||||
#{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => atom_to_binary(User#user.role, utf8),
|
||||
status => atom_to_binary(User#user.status, utf8)
|
||||
}.
|
||||
@@ -1,16 +1,27 @@
|
||||
-module(logic_moderation).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create_report/4, get_reports/1, get_reports_by_target/3, resolve_report/3]).
|
||||
-export([add_banned_word/2, remove_banned_word/2, list_banned_words/1]).
|
||||
-export([check_content/1, auto_moderate/1]).
|
||||
-export([freeze_calendar/2, unfreeze_calendar/2, freeze_event/2, unfreeze_event/2]).
|
||||
-export([create_report/4,
|
||||
get_reports/1,
|
||||
get_reports_by_target/3,
|
||||
resolve_report/3]).
|
||||
|
||||
-define(REPORT_THRESHOLD, 3). % Количество жалоб для авто-заморозки
|
||||
-export([add_banned_word/2,
|
||||
remove_banned_word/2,
|
||||
list_banned_words/1]).
|
||||
|
||||
%% ============ Жалобы ============
|
||||
-export([check_content/1,
|
||||
auto_moderate/1]).
|
||||
|
||||
-export([freeze_calendar/2,
|
||||
unfreeze_calendar/2,
|
||||
freeze_event/2,
|
||||
unfreeze_event/2]).
|
||||
|
||||
-define(REPORT_THRESHOLD, 3).
|
||||
|
||||
%% ============ Жалобы =====================================
|
||||
|
||||
%% Создание жалобы
|
||||
create_report(ReporterId, TargetType, TargetId, Reason) ->
|
||||
case target_exists(TargetType, TargetId) of
|
||||
true ->
|
||||
@@ -22,30 +33,29 @@ create_report(ReporterId, TargetType, TargetId, Reason) ->
|
||||
target_id => TargetId,
|
||||
reason => Reason
|
||||
}),
|
||||
% Проверяем порог для авто-модерации
|
||||
check_auto_freeze(TargetType, TargetId),
|
||||
{ok, Report};
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
false -> {error, target_not_found}
|
||||
false ->
|
||||
{error, target_not_found}
|
||||
end.
|
||||
|
||||
%% Получить все жалобы (для админа)
|
||||
get_reports(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_report:list_all();
|
||||
true -> core_report:list_all();
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Получить жалобы на конкретную цель
|
||||
get_reports_by_target(AdminId, TargetType, TargetId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_report:list_by_target(TargetType, TargetId);
|
||||
true -> core_report:list_by_target(TargetType, TargetId);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Рассмотреть жалобу (подтвердить или отклонить)
|
||||
resolve_report(AdminId, ReportId, Action) when Action =:= reviewed; Action =:= dismissed ->
|
||||
resolve_report(AdminId, ReportId, Action)
|
||||
when Action =:= reviewed; Action =:= dismissed ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
case core_report:get_by_id(ReportId) of
|
||||
@@ -53,19 +63,23 @@ resolve_report(AdminId, ReportId, Action) when Action =:= reviewed; Action =:= d
|
||||
case Report#report.status of
|
||||
pending ->
|
||||
core_report:update_status(ReportId, Action, AdminId);
|
||||
_ -> {error, already_resolved}
|
||||
_ ->
|
||||
{error, already_resolved}
|
||||
end;
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
false -> {error, access_denied}
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end.
|
||||
|
||||
%% Проверка порога для авто-заморозки
|
||||
check_auto_freeze(TargetType, TargetId) ->
|
||||
Count = core_report:get_count_by_target(TargetType, TargetId),
|
||||
if Count >= ?REPORT_THRESHOLD ->
|
||||
auto_freeze(TargetType, TargetId);
|
||||
true -> ok
|
||||
if
|
||||
Count >= ?REPORT_THRESHOLD ->
|
||||
auto_freeze(TargetType, TargetId);
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
|
||||
auto_freeze(event, EventId) ->
|
||||
@@ -82,42 +96,52 @@ auto_freeze(calendar, CalendarId) ->
|
||||
end;
|
||||
auto_freeze(_, _) -> ok.
|
||||
|
||||
%% ============ Бан-лист ============
|
||||
%% ============ Бан-лист ===================================
|
||||
|
||||
%% Добавить запрещённое слово
|
||||
add_banned_word(AdminId, Word) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_banned_word:add(Word);
|
||||
true -> core_banned_words:add_banned_word(Word, AdminId);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Удалить запрещённое слово
|
||||
remove_banned_word(AdminId, Word) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_banned_word:remove(Word);
|
||||
true -> core_banned_words:remove_banned_word(Word);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Список запрещённых слов
|
||||
list_banned_words(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_banned_word:list_all();
|
||||
true -> {ok, core_banned_words:list_banned_words()};
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% ============ Контент-фильтр ============
|
||||
%% ============ Контент-фильтр =============================
|
||||
|
||||
%% Проверить контент на запрещённые слова
|
||||
check_content(Text) ->
|
||||
core_banned_word:check_text(Text).
|
||||
Words = core_banned_words:list_banned_words(),
|
||||
LowerText = string:lowercase(binary_to_list(Text)),
|
||||
lists:any(fun(W) ->
|
||||
string:str(LowerText, binary_to_list(W#banned_word.word)) > 0
|
||||
end, Words).
|
||||
|
||||
%% Автоматическая модерация контента (замена запрещённых слов)
|
||||
auto_moderate(Text) ->
|
||||
core_banned_word:filter_text(Text).
|
||||
Words = core_banned_words:list_banned_words(),
|
||||
lists:foldl(fun(W, Acc) ->
|
||||
WordStr = binary_to_list(W#banned_word.word),
|
||||
LowerAccStr = string:lowercase(binary_to_list(Acc)),
|
||||
case string:str(LowerAccStr, WordStr) of
|
||||
0 -> Acc;
|
||||
Pos ->
|
||||
Len = length(WordStr),
|
||||
Start = binary:part(Acc, {0, Pos-1}),
|
||||
Rest = binary:part(Acc, {Pos-1+Len, byte_size(Acc)-Pos+1-Len}),
|
||||
<<Start/binary, "***", Rest/binary>>
|
||||
end
|
||||
end, Text, Words).
|
||||
|
||||
%% ============ Заморозка/разморозка ============
|
||||
%% ============ Заморозка/разморозка =======================
|
||||
|
||||
%% Заморозить календарь
|
||||
freeze_calendar(AdminId, CalendarId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -129,7 +153,6 @@ freeze_calendar(AdminId, CalendarId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Разморозить календарь
|
||||
unfreeze_calendar(AdminId, CalendarId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -141,7 +164,6 @@ unfreeze_calendar(AdminId, CalendarId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Заморозить событие
|
||||
freeze_event(AdminId, EventId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -153,7 +175,6 @@ freeze_event(AdminId, EventId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Разморозить событие
|
||||
unfreeze_event(AdminId, EventId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
@@ -165,7 +186,7 @@ unfreeze_event(AdminId, EventId) ->
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% ============ Вспомогательные функции ============
|
||||
%% ============ Вспомогательные функции ====================
|
||||
|
||||
target_exists(event, EventId) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
@@ -176,7 +197,7 @@ target_exists(calendar, CalendarId) ->
|
||||
case core_calendar:get_by_id(CalendarId) of
|
||||
{ok, _} -> true;
|
||||
_ -> false
|
||||
end; % ← точка с запятой здесь!
|
||||
end;
|
||||
target_exists(_, _) -> false.
|
||||
|
||||
is_admin(UserId) ->
|
||||
|
||||
+53
-34
@@ -1,89 +1,110 @@
|
||||
-module(logic_ticket).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([report_error/3, get_ticket/2, list_tickets/1, list_tickets_by_status/2]).
|
||||
-export([update_status/3, assign_ticket/3, resolve_ticket/3, close_ticket/2]).
|
||||
-export([get_statistics/1]).
|
||||
-export([report_error/3,
|
||||
get_ticket/2,
|
||||
list_tickets/1,
|
||||
list_tickets_by_status/2,
|
||||
update_status/3,
|
||||
assign_ticket/3,
|
||||
resolve_ticket/3,
|
||||
close_ticket/2,
|
||||
get_statistics/1]).
|
||||
|
||||
%% Зарегистрировать ошибку (создать или обновить тикет)
|
||||
report_error(ErrorMessage, Stacktrace, Context) ->
|
||||
case core_ticket:create_or_update(ErrorMessage, Stacktrace, Context) of
|
||||
{ok, Ticket} ->
|
||||
% Если это новый тикет, уведомляем администраторов (заглушка)
|
||||
case Ticket#ticket.count of
|
||||
1 -> notify_admins(Ticket);
|
||||
_ -> ok
|
||||
end,
|
||||
{ok, Ticket};
|
||||
Error -> Error
|
||||
Existing = [T || T <- core_ticket:list_all(), T#ticket.error_message =:= ErrorMessage],
|
||||
case Existing of
|
||||
[Ticket] ->
|
||||
% Увеличить счётчик и обновить last_seen
|
||||
Updated = Ticket#ticket{
|
||||
count = Ticket#ticket.count + 1,
|
||||
last_seen = calendar:universal_time()
|
||||
},
|
||||
mnesia:dirty_write(Updated),
|
||||
{ok, Updated};
|
||||
[] ->
|
||||
Data = #{
|
||||
<<"error_message">> => ErrorMessage,
|
||||
<<"stacktrace">> => Stacktrace,
|
||||
<<"context">> => list_to_binary(io_lib:format("~p", [Context]))
|
||||
},
|
||||
case core_ticket:create_ticket(Data) of
|
||||
{ok, Ticket} = Result ->
|
||||
% Уведомление администраторов (заглушка)
|
||||
notify_admins(Ticket),
|
||||
Result;
|
||||
Error -> Error
|
||||
end
|
||||
end.
|
||||
|
||||
%% Получить тикет (только для админов)
|
||||
get_ticket(AdminId, TicketId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:get_by_id(TicketId);
|
||||
true -> core_ticket:get_by_id(TicketId);
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Список всех тикетов (только для админов)
|
||||
list_tickets(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:list_all();
|
||||
true -> core_ticket:list_all();
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Список тикетов по статусу (только для админов)
|
||||
list_tickets_by_status(AdminId, Status) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:list_by_status(Status);
|
||||
true ->
|
||||
All = core_ticket:list_all(),
|
||||
[T || T <- All, T#ticket.status =:= Status];
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Обновить статус тикета
|
||||
update_status(AdminId, TicketId, Status) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:update_status(TicketId, Status);
|
||||
true -> core_ticket:update_ticket(TicketId, #{<<"status">> => Status});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Назначить тикет администратору
|
||||
assign_ticket(AdminId, TicketId, AssignToId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:assign(TicketId, AssignToId);
|
||||
true -> core_ticket:update_ticket(TicketId, #{<<"assigned_to">> => AssignToId});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Отметить тикет как решённый с примечанием
|
||||
resolve_ticket(AdminId, TicketId, ResolutionNote) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
case core_ticket:add_resolution(TicketId, ResolutionNote) of
|
||||
{ok, _Ticket} ->
|
||||
core_ticket:update_status(TicketId, resolved);
|
||||
Error -> Error
|
||||
end;
|
||||
true ->
|
||||
core_ticket:update_ticket(TicketId, #{
|
||||
<<"status">> => <<"closed">>,
|
||||
<<"resolution_note">> => ResolutionNote
|
||||
});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Закрыть тикет
|
||||
close_ticket(AdminId, TicketId) ->
|
||||
case is_admin(AdminId) of
|
||||
true -> core_ticket:update_status(TicketId, closed);
|
||||
true -> core_ticket:update_ticket(TicketId, #{<<"status">> => <<"closed">>});
|
||||
false -> {error, access_denied}
|
||||
end.
|
||||
|
||||
%% Получить статистику по тикетам
|
||||
get_statistics(AdminId) ->
|
||||
case is_admin(AdminId) of
|
||||
true ->
|
||||
{ok, AllTickets} = core_ticket:list_all(),
|
||||
Open = length([T || T <- AllTickets, T#ticket.status =:= open]),
|
||||
InProgress = length([T || T <- AllTickets, T#ticket.status =:= in_progress]),
|
||||
Resolved = length([T || T <- AllTickets, T#ticket.status =:= resolved]),
|
||||
Closed = length([T || T <- AllTickets, T#ticket.status =:= closed]),
|
||||
TotalErrors = lists:sum([T#ticket.count || T <- AllTickets]),
|
||||
true ->
|
||||
All = core_ticket:list_all(),
|
||||
Open = length([T || T <- All, T#ticket.status =:= open]),
|
||||
InProgress = length([T || T <- All, T#ticket.status =:= in_progress]),
|
||||
Resolved = length([T || T <- All, T#ticket.status =:= resolved]),
|
||||
Closed = length([T || T <- All, T#ticket.status =:= closed]),
|
||||
TotalErrors = lists:sum([T#ticket.count || T <- All]),
|
||||
#{
|
||||
total_tickets => length(AllTickets),
|
||||
total_tickets => length(All),
|
||||
open => Open,
|
||||
in_progress => InProgress,
|
||||
resolved => Resolved,
|
||||
@@ -102,6 +123,4 @@ is_admin(UserId) ->
|
||||
end.
|
||||
|
||||
notify_admins(_Ticket) ->
|
||||
% Заглушка для уведомлений администраторов
|
||||
% В будущем здесь будет отправка email/websocket
|
||||
ok.
|
||||
Reference in New Issue
Block a user