Перенести все админские эндпоинты на порт 8445 и добавить отдельную авторизацию для админов. Часть 2. Final #3

This commit is contained in:
2026-04-28 12:42:10 +03:00
parent 4ed6a961ab
commit 7ea4efd7d9
38 changed files with 1252 additions and 1124 deletions

View File

@@ -1,143 +1,86 @@
-module(core_ticket).
-include("records.hrl").
-export([list_all/0,
get_by_id/1,
update_ticket/2,
delete_ticket/1,
stats/0,
create_ticket/1,
list_by_user/1]).
-export([create_or_update/3, get_by_id/1, get_by_error_hash/1, list_all/0, list_by_status/1]).
-export([update_status/2, assign/2, add_resolution/2]).
-export([generate_id/0, generate_error_hash/2]).
%% Создать или обновить тикет (группировка по хэшу ошибки)
create_or_update(ErrorMessage, Stacktrace, Context) ->
ErrorHash = generate_error_hash(ErrorMessage, Stacktrace),
case get_by_error_hash(ErrorHash) of
{error, not_found} ->
% Создаём новый тикет
Id = generate_id(),
Now = calendar:universal_time(),
Ticket = #ticket{
id = Id,
error_hash = ErrorHash,
error_message = ErrorMessage,
stacktrace = Stacktrace,
context = term_to_binary(Context),
count = 1,
first_seen = Now,
last_seen = Now,
status = open,
assigned_to = undefined,
resolution_note = undefined
},
F = fun() ->
mnesia:write(Ticket),
{ok, Ticket}
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end;
{ok, Ticket} ->
% Обновляем существующий
F = fun() ->
Updated = Ticket#ticket{
count = Ticket#ticket.count + 1,
last_seen = calendar:universal_time()
},
mnesia:write(Updated),
{ok, Updated}
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end
end.
%% Получение тикета по ID
get_by_id(Id) ->
case mnesia:dirty_read(ticket, Id) of
[] -> {error, not_found};
[Ticket] -> {ok, Ticket}
end.
%% Получение тикета по хэшу ошибки
get_by_error_hash(ErrorHash) ->
Match = #ticket{error_hash = ErrorHash, _ = '_'},
case mnesia:dirty_match_object(Match) of
[] -> {error, not_found};
[Ticket] -> {ok, Ticket}
end.
%% Список всех тикетов
list_all() ->
Match = #ticket{_ = '_'},
Tickets = mnesia:dirty_match_object(Match),
{ok, lists:sort(fun(A, B) -> A#ticket.last_seen >= B#ticket.last_seen end, Tickets)}.
mnesia:dirty_match_object(#ticket{_ = '_'}).
%% Список тикетов по статусу
list_by_status(Status) ->
Match = #ticket{status = Status, _ = '_'},
Tickets = mnesia:dirty_match_object(Match),
{ok, lists:sort(fun(A, B) -> A#ticket.last_seen >= B#ticket.last_seen end, Tickets)}.
%% Обновление статуса тикета
update_status(Id, Status) when Status =:= open; Status =:= in_progress; Status =:= resolved; Status =:= closed ->
F = fun() ->
case mnesia:read(ticket, Id) of
[] ->
{error, not_found};
[Ticket] ->
Updated = Ticket#ticket{status = Status},
mnesia:write(Updated),
{ok, Updated}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
get_by_id(Id) ->
case mnesia:dirty_read({ticket, Id}) of
[Ticket] -> {ok, Ticket};
[] -> {error, not_found}
end.
%% Назначить тикет администратору
assign(Id, AdminId) ->
F = fun() ->
case mnesia:read(ticket, Id) of
[] ->
{error, not_found};
[Ticket] ->
Updated = Ticket#ticket{
assigned_to = AdminId,
status = case Ticket#ticket.status of
open -> in_progress;
S -> S
end
},
mnesia:write(Updated),
{ok, Updated}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
update_ticket(Id, Updates) ->
case get_by_id(Id) of
{ok, Ticket} ->
Updated = apply_updates(Ticket, Updates),
mnesia:dirty_write(Updated),
{ok, Updated};
Error -> Error
end.
%% Добавить примечание о решении
add_resolution(Id, Note) ->
F = fun() ->
case mnesia:read(ticket, Id) of
[] ->
{error, not_found};
[Ticket] ->
Updated = Ticket#ticket{resolution_note = Note},
mnesia:write(Updated),
{ok, Updated}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
delete_ticket(Id) ->
case get_by_id(Id) of
{ok, _Ticket} -> % переменная не используется
mnesia:dirty_delete({ticket, Id}),
{ok, deleted};
Error -> Error
end.
%% Внутренние функции
generate_id() ->
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
stats() ->
Tickets = list_all(),
#{
total => length(Tickets),
open => count_by_status(open, Tickets),
in_progress => count_by_status(in_progress, Tickets),
resolved => count_by_status(resolved, Tickets),
closed => count_by_status(closed, Tickets)
}.
generate_error_hash(ErrorMessage, Stacktrace) ->
Data = iolist_to_binary([ErrorMessage, "\n", Stacktrace]),
base64:encode(crypto:hash(sha256, Data), #{mode => urlsafe, padding => false}).
%% ── новые функции ──────────────────────────────────────
create_ticket(Data) ->
Id = base64:encode(crypto:strong_rand_bytes(9)),
Now = calendar:universal_time(),
Ticket = #ticket{
id = Id,
reporter_id = maps:get(<<"reporter_id">>, Data, undefined),
error_hash = maps:get(<<"error_hash">>, Data, <<"">>),
error_message = maps:get(<<"error_message">>, Data),
stacktrace = maps:get(<<"stacktrace">>, Data, <<"">>),
context = maps:get(<<"context">>, Data, <<"">>),
count = 1,
first_seen = Now,
last_seen = Now,
status = maps:get(<<"status">>, Data, open),
assigned_to = maps:get(<<"assigned_to">>, Data, undefined),
resolution_note = maps:get(<<"resolution_note">>, Data, undefined)
},
mnesia:dirty_write(Ticket),
{ok, Ticket}.
list_by_user(UserId) ->
mnesia:dirty_match_object(#ticket{reporter_id = UserId, _ = '_'}).
%% ── внутренние ─────────────────────────────────────────
apply_updates(Ticket, Updates) ->
lists:foldl(fun({Key, Value}, Acc) ->
case Key of
<<"status">> -> Acc#ticket{status = binary_to_atom(Value, utf8)};
<<"assigned_to">> -> Acc#ticket{assigned_to = Value};
<<"resolution_note">> -> Acc#ticket{resolution_note = Value};
<<"error_message">> -> Acc#ticket{error_message = Value};
<<"stacktrace">> -> Acc#ticket{stacktrace = Value};
<<"context">> -> Acc#ticket{context = Value};
_ -> Acc
end
end, Ticket, maps:to_list(Updates)).
count_by_status(Status, Tickets) ->
length([T || T <- Tickets, T#ticket.status =:= Status]).