Stage 2
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ logs
|
|||||||
rebar3.crashdump
|
rebar3.crashdump
|
||||||
*~
|
*~
|
||||||
/.tool-versions
|
/.tool-versions
|
||||||
|
/rebar.lock
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{erl_opts, [debug_info]}.
|
{erl_opts, [debug_info, {i, "include"}]}.
|
||||||
{deps, [
|
{deps, [
|
||||||
{cowboy, "2.10.0"},
|
{cowboy, "2.10.0"},
|
||||||
{jsx, "3.1.0"},
|
{jsx, "3.1.0"},
|
||||||
|
|||||||
93
src/core/core_user.erl
Normal file
93
src/core/core_user.erl
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
-module(core_user).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([create/2, get_by_id/1, get_by_email/1, update/2, delete/1]).
|
||||||
|
-export([email_exists/1]).
|
||||||
|
|
||||||
|
%% Создание пользователя
|
||||||
|
create(Email, Password) ->
|
||||||
|
% Сначала проверяем, существует ли email
|
||||||
|
case email_exists(Email) of
|
||||||
|
true ->
|
||||||
|
{error, email_exists};
|
||||||
|
false ->
|
||||||
|
Id = generate_id(),
|
||||||
|
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
||||||
|
User = #user{
|
||||||
|
id = Id,
|
||||||
|
email = Email,
|
||||||
|
password_hash = PasswordHash,
|
||||||
|
role = user,
|
||||||
|
status = active,
|
||||||
|
created_at = calendar:universal_time(),
|
||||||
|
updated_at = calendar:universal_time()
|
||||||
|
},
|
||||||
|
|
||||||
|
F = fun() ->
|
||||||
|
mnesia:write(User),
|
||||||
|
{ok, User}
|
||||||
|
end,
|
||||||
|
|
||||||
|
case mnesia:transaction(F) of
|
||||||
|
{atomic, Result} -> Result;
|
||||||
|
{aborted, Reason} -> {error, Reason}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Получение пользователя по ID
|
||||||
|
get_by_id(Id) ->
|
||||||
|
case mnesia:dirty_read(user, Id) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[User] -> {ok, User}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Получение пользователя по email (через индекс позже)
|
||||||
|
get_by_email(Email) ->
|
||||||
|
Match = #user{email = Email, _ = '_'},
|
||||||
|
case mnesia:dirty_match_object(Match) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[User] -> {ok, User}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Проверка существования email
|
||||||
|
email_exists(Email) ->
|
||||||
|
case get_by_email(Email) of
|
||||||
|
{ok, _} -> true;
|
||||||
|
{error, _} -> false
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Обновление пользователя
|
||||||
|
update(Id, Updates) ->
|
||||||
|
F = fun() ->
|
||||||
|
case mnesia:read(user, Id) of
|
||||||
|
[] ->
|
||||||
|
{error, not_found};
|
||||||
|
[User] ->
|
||||||
|
UpdatedUser = apply_updates(User, Updates),
|
||||||
|
mnesia:write(UpdatedUser),
|
||||||
|
{ok, UpdatedUser}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case mnesia:transaction(F) of
|
||||||
|
{atomic, Result} -> Result;
|
||||||
|
{aborted, Reason} -> {error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Удаление пользователя (soft delete)
|
||||||
|
delete(Id) ->
|
||||||
|
update(Id, [{status, deleted}]).
|
||||||
|
|
||||||
|
%% Внутренние функции
|
||||||
|
generate_id() ->
|
||||||
|
base64:encode(crypto:strong_rand_bytes(16)).
|
||||||
|
|
||||||
|
apply_updates(User, Updates) ->
|
||||||
|
lists:foldl(fun({Field, Value}, U) -> set_field(Field, Value, U) end, User, Updates).
|
||||||
|
|
||||||
|
set_field(email, Value, User) -> User#user{email = Value, updated_at = calendar:universal_time()};
|
||||||
|
set_field(password_hash, Value, User) -> User#user{password_hash = Value, updated_at = calendar:universal_time()};
|
||||||
|
set_field(role, Value, User) -> User#user{role = Value, updated_at = calendar:universal_time()};
|
||||||
|
set_field(status, Value, User) -> User#user{status = Value, updated_at = calendar:universal_time()};
|
||||||
|
set_field(_, _, User) -> User.
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
%% Основной модуль приложения
|
|
||||||
-module(eventhub_app).
|
-module(eventhub_app).
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
|
||||||
@@ -7,6 +6,7 @@
|
|||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
% Запускаем Mnesia
|
% Запускаем Mnesia
|
||||||
application:ensure_all_started(mnesia),
|
application:ensure_all_started(mnesia),
|
||||||
|
application:ensure_all_started(cowboy),
|
||||||
|
|
||||||
case infra_sup:start_link() of
|
case infra_sup:start_link() of
|
||||||
{ok, Pid} ->
|
{ok, Pid} ->
|
||||||
@@ -14,7 +14,9 @@ start(_StartType, _StartArgs) ->
|
|||||||
ok = infra_mnesia:init_tables(),
|
ok = infra_mnesia:init_tables(),
|
||||||
ok = infra_mnesia:wait_for_tables(),
|
ok = infra_mnesia:wait_for_tables(),
|
||||||
|
|
||||||
% Здесь позже запустим HTTP-сервер и другие сервисы
|
% Запускаем HTTP-сервер
|
||||||
|
start_http(),
|
||||||
|
|
||||||
{ok, Pid};
|
{ok, Pid};
|
||||||
Error ->
|
Error ->
|
||||||
Error
|
Error
|
||||||
@@ -22,3 +24,33 @@ start(_StartType, _StartArgs) ->
|
|||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
%% Internal functions
|
||||||
|
start_http() ->
|
||||||
|
Port = application:get_env(eventhub, http_port, 8080),
|
||||||
|
|
||||||
|
% Настройка маршрутов
|
||||||
|
Dispatch = cowboy_router:compile([
|
||||||
|
{'_', [
|
||||||
|
{"/health", handler_health, []},
|
||||||
|
{"/v1/register", handler_register, []},
|
||||||
|
{"/v1/login", handler_login, []},
|
||||||
|
{"/v1/refresh", handler_refresh, []},
|
||||||
|
{"/v1/user/me", handler_user_me, []}
|
||||||
|
]}
|
||||||
|
]),
|
||||||
|
|
||||||
|
% Настройка middleware
|
||||||
|
Middlewares = [
|
||||||
|
cowboy_router,
|
||||||
|
cowboy_handler
|
||||||
|
],
|
||||||
|
|
||||||
|
Env = #{dispatch => Dispatch},
|
||||||
|
|
||||||
|
cowboy:start_clear(http, [{port, Port}], #{
|
||||||
|
env => Env,
|
||||||
|
middlewares => Middlewares
|
||||||
|
}),
|
||||||
|
|
||||||
|
io:format("HTTP server started on port ~p~n", [Port]).
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
-module(handler_health).
|
-module(handler_health).
|
||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
|
|
||||||
init(Req, _Opts) ->
|
init(Req, _Opts) ->
|
||||||
{ok, Resp} = cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, <<"{\"status\":\"ok\"}">>, Req),
|
Body = jsx:encode(#{status => <<"ok">>}),
|
||||||
Resp.
|
cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
67
src/handlers/handler_login.erl
Normal file
67
src/handlers/handler_login.erl
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
-module(handler_login).
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
init(Req, Opts) ->
|
||||||
|
handle(Req, Opts).
|
||||||
|
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"POST">> ->
|
||||||
|
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||||
|
case jsx:decode(Body, [return_maps]) of
|
||||||
|
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||||
|
case core_user:get_by_email(Email) of
|
||||||
|
{ok, User} ->
|
||||||
|
case logic_auth:verify_password(Password, User#user.password_hash) of
|
||||||
|
{ok, true} ->
|
||||||
|
case User#user.status of
|
||||||
|
active ->
|
||||||
|
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||||
|
{RefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
||||||
|
save_refresh_token(User#user.id, RefreshToken, ExpiresAt),
|
||||||
|
Response = #{
|
||||||
|
user => #{
|
||||||
|
id => User#user.id,
|
||||||
|
email => User#user.email,
|
||||||
|
role => User#user.role
|
||||||
|
},
|
||||||
|
token => Token,
|
||||||
|
refresh_token => RefreshToken
|
||||||
|
},
|
||||||
|
send_json(Req1, 200, Response);
|
||||||
|
frozen ->
|
||||||
|
send_error(Req1, 403, <<"Account frozen">>);
|
||||||
|
deleted ->
|
||||||
|
send_error(Req1, 403, <<"Account deleted">>)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||||
|
end;
|
||||||
|
{error, not_found} ->
|
||||||
|
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req1, 400, <<"Invalid request body">>)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
save_refresh_token(UserId, Token, ExpiresAt) ->
|
||||||
|
Session = #session{
|
||||||
|
token = Token,
|
||||||
|
user_id = UserId,
|
||||||
|
expires_at = ExpiresAt,
|
||||||
|
type = refresh
|
||||||
|
},
|
||||||
|
mnesia:dirty_write(Session).
|
||||||
|
|
||||||
|
send_json(Req, Status, Data) ->
|
||||||
|
Body = jsx:encode(Data),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
|
|
||||||
|
send_error(Req, Status, Message) ->
|
||||||
|
Body = jsx:encode(#{error => Message}),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
91
src/handlers/handler_refresh.erl
Normal file
91
src/handlers/handler_refresh.erl
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
-module(handler_refresh).
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
init(Req, Opts) ->
|
||||||
|
handle(Req, Opts).
|
||||||
|
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"POST">> ->
|
||||||
|
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||||
|
case jsx:decode(Body, [return_maps]) of
|
||||||
|
#{<<"refresh_token">> := RefreshToken} ->
|
||||||
|
case validate_refresh_token(RefreshToken) of
|
||||||
|
{ok, UserId} ->
|
||||||
|
case core_user:get_by_id(UserId) of
|
||||||
|
{ok, User} ->
|
||||||
|
% Генерируем новые токены
|
||||||
|
NewToken = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||||
|
{NewRefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
||||||
|
|
||||||
|
% Сохраняем новый refresh token
|
||||||
|
save_refresh_token(User#user.id, NewRefreshToken, ExpiresAt),
|
||||||
|
|
||||||
|
% Удаляем старый refresh token
|
||||||
|
delete_refresh_token(RefreshToken),
|
||||||
|
|
||||||
|
Response = #{
|
||||||
|
token => NewToken,
|
||||||
|
refresh_token => NewRefreshToken
|
||||||
|
},
|
||||||
|
send_json(Req1, 200, Response);
|
||||||
|
{error, not_found} ->
|
||||||
|
send_error(Req1, 401, <<"User not found">>)
|
||||||
|
end;
|
||||||
|
{error, expired} ->
|
||||||
|
send_error(Req1, 401, <<"Refresh token expired">>);
|
||||||
|
{error, invalid} ->
|
||||||
|
send_error(Req1, 401, <<"Invalid refresh token">>)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req1, 400, <<"Missing refresh_token">>)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
validate_refresh_token(Token) ->
|
||||||
|
case get_session_by_token(Token) of
|
||||||
|
{ok, Session} ->
|
||||||
|
% Проверяем срок действия
|
||||||
|
Now = calendar:universal_time(),
|
||||||
|
case Session#session.expires_at > Now of
|
||||||
|
true -> {ok, Session#session.user_id};
|
||||||
|
false -> {error, expired}
|
||||||
|
end;
|
||||||
|
{error, not_found} ->
|
||||||
|
{error, invalid}
|
||||||
|
end.
|
||||||
|
|
||||||
|
get_session_by_token(Token) ->
|
||||||
|
Match = #session{token = Token, type = refresh, _ = '_'},
|
||||||
|
case mnesia:dirty_match_object(Match) of
|
||||||
|
[] -> {error, not_found};
|
||||||
|
[Session] -> {ok, Session}
|
||||||
|
end.
|
||||||
|
|
||||||
|
save_refresh_token(UserId, Token, ExpiresAt) ->
|
||||||
|
Session = #session{
|
||||||
|
token = Token,
|
||||||
|
user_id = UserId,
|
||||||
|
expires_at = ExpiresAt,
|
||||||
|
type = refresh
|
||||||
|
},
|
||||||
|
mnesia:dirty_write(Session).
|
||||||
|
|
||||||
|
delete_refresh_token(Token) ->
|
||||||
|
Match = #session{token = Token, type = refresh, _ = '_'},
|
||||||
|
case mnesia:dirty_match_object(Match) of
|
||||||
|
[] -> ok;
|
||||||
|
[Session] -> mnesia:dirty_delete_object(Session)
|
||||||
|
end.
|
||||||
|
|
||||||
|
send_json(Req, Status, Data) ->
|
||||||
|
Body = jsx:encode(Data),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
|
|
||||||
|
send_error(Req, Status, Message) ->
|
||||||
|
Body = jsx:encode(#{error => Message}),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
48
src/handlers/handler_register.erl
Normal file
48
src/handlers/handler_register.erl
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
-module(handler_register).
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
init(Req, Opts) ->
|
||||||
|
handle(Req, Opts).
|
||||||
|
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"POST">> ->
|
||||||
|
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||||
|
case jsx:decode(Body, [return_maps]) of
|
||||||
|
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||||
|
case core_user:email_exists(Email) of
|
||||||
|
true ->
|
||||||
|
send_error(Req1, 409, <<"Email already exists">>);
|
||||||
|
false ->
|
||||||
|
case core_user:create(Email, Password) of
|
||||||
|
{ok, User} ->
|
||||||
|
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||||
|
Response = #{
|
||||||
|
user => #{
|
||||||
|
id => User#user.id,
|
||||||
|
email => User#user.email,
|
||||||
|
role => User#user.role
|
||||||
|
},
|
||||||
|
token => Token
|
||||||
|
},
|
||||||
|
send_json(Req1, 201, Response);
|
||||||
|
{error, _} ->
|
||||||
|
send_error(Req1, 500, <<"Internal server error">>)
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req1, 400, <<"Invalid request body">>)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
send_json(Req, Status, Data) ->
|
||||||
|
Body = jsx:encode(Data),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
|
|
||||||
|
send_error(Req, Status, Message) ->
|
||||||
|
Body = jsx:encode(#{error => Message}),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
57
src/handlers/handler_user_me.erl
Normal file
57
src/handlers/handler_user_me.erl
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
-module(handler_user_me).
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
init(Req, Opts) ->
|
||||||
|
handle(Req, Opts).
|
||||||
|
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> ->
|
||||||
|
case authenticate(Req) of
|
||||||
|
{ok, UserId, Req1} ->
|
||||||
|
case core_user:get_by_id(UserId) of
|
||||||
|
{ok, User} ->
|
||||||
|
Response = #{
|
||||||
|
id => User#user.id,
|
||||||
|
email => User#user.email,
|
||||||
|
role => User#user.role,
|
||||||
|
status => User#user.status,
|
||||||
|
created_at => User#user.created_at,
|
||||||
|
updated_at => User#user.updated_at
|
||||||
|
},
|
||||||
|
send_json(Req1, 200, Response);
|
||||||
|
{error, not_found} ->
|
||||||
|
send_error(Req1, 404, <<"User not found">>)
|
||||||
|
end;
|
||||||
|
{error, Code, Message, Req1} ->
|
||||||
|
send_error(Req1, Code, Message)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
authenticate(Req) ->
|
||||||
|
case cowboy_req:parse_header(<<"authorization">>, Req) of
|
||||||
|
{bearer, Token} ->
|
||||||
|
case logic_auth:verify_jwt(Token) of
|
||||||
|
{ok, Claims} ->
|
||||||
|
UserId = maps:get(<<"user_id">>, Claims),
|
||||||
|
{ok, UserId, Req};
|
||||||
|
{error, expired} ->
|
||||||
|
{error, 401, <<"Token expired">>, Req};
|
||||||
|
{error, _} ->
|
||||||
|
{error, 401, <<"Invalid token">>, Req}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, 401, <<"Missing or invalid Authorization header">>, Req}
|
||||||
|
end.
|
||||||
|
|
||||||
|
send_json(Req, Status, Data) ->
|
||||||
|
Body = jsx:encode(Data),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
|
|
||||||
|
send_error(Req, Status, Message) ->
|
||||||
|
Body = jsx:encode(#{error => Message}),
|
||||||
|
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||||
88
src/logic/logic_auth.erl
Normal file
88
src/logic/logic_auth.erl
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
-module(logic_auth).
|
||||||
|
|
||||||
|
-export([hash_password/1, verify_password/2]).
|
||||||
|
-export([generate_jwt/2, verify_jwt/1, extract_claims/1]).
|
||||||
|
-export([generate_refresh_token/1]).
|
||||||
|
|
||||||
|
%% ============ Argon2 хеширование ============
|
||||||
|
hash_password(Password) when is_binary(Password) ->
|
||||||
|
argon2:hash(Password).
|
||||||
|
|
||||||
|
verify_password(Password, Hash) when is_binary(Password), is_binary(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(),
|
||||||
|
|
||||||
|
ExpTime = os:system_time(seconds) + 86400, % 24 часа
|
||||||
|
Claims = #{
|
||||||
|
<<"user_id">> => UserId,
|
||||||
|
<<"role">> => Role,
|
||||||
|
<<"exp">> => ExpTime,
|
||||||
|
<<"iat">> => os:system_time(seconds)
|
||||||
|
},
|
||||||
|
|
||||||
|
JWT = jose_jwt:sign(JWK, #{<<"alg">> => <<"HS256">>}, Claims),
|
||||||
|
{_, Token} = jose_jws:compact(JWT),
|
||||||
|
Token.
|
||||||
|
|
||||||
|
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}
|
||||||
|
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)),
|
||||||
|
ExpiresAt = calendar:universal_time_to_local_time(
|
||||||
|
calendar:gregorian_seconds_to_datetime(
|
||||||
|
calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 30 * 86400
|
||||||
|
)
|
||||||
|
),
|
||||||
|
{Token, ExpiresAt}.
|
||||||
36
src/middlewares/middleware_auth.erl
Normal file
36
src/middlewares/middleware_auth.erl
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-module(middleware_auth).
|
||||||
|
-behaviour(cowboy_middleware).
|
||||||
|
|
||||||
|
-export([execute/2]).
|
||||||
|
|
||||||
|
execute(Req, Env) ->
|
||||||
|
case authenticate(Req) of
|
||||||
|
{ok, UserId, Role} ->
|
||||||
|
% Добавляем user_id и role в env
|
||||||
|
NewReq = cowboy_req:set_resp_header(<<"X-User-Id">>, UserId, Req),
|
||||||
|
{ok, NewReq, [{user_id, UserId}, {role, Role} | Env]};
|
||||||
|
{error, {Code, Message}} ->
|
||||||
|
send_error(Req, Code, Message)
|
||||||
|
end.
|
||||||
|
|
||||||
|
authenticate(Req) ->
|
||||||
|
case cowboy_req:parse_header(<<"authorization">>, Req) of
|
||||||
|
{ok, <<"Bearer ", Token/binary>>, _} ->
|
||||||
|
case logic_auth:verify_jwt(Token) of
|
||||||
|
{ok, Claims} ->
|
||||||
|
UserId = maps:get(<<"user_id">>, Claims),
|
||||||
|
Role = maps:get(<<"role">>, Claims),
|
||||||
|
{ok, UserId, Role};
|
||||||
|
{error, expired} ->
|
||||||
|
{error, {401, <<"Token expired">>}};
|
||||||
|
{error, _} ->
|
||||||
|
{error, {401, <<"Invalid token">>}}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, {401, <<"Missing or invalid Authorization header">>}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
send_error(Req, Code, Message) ->
|
||||||
|
Body = jsx:encode(#{error => Message}),
|
||||||
|
NewReq = cowboy_req:reply(Code, #{<<"content-type">> => <<"application/json">>}, Body, Req),
|
||||||
|
{stop, NewReq}.
|
||||||
Reference in New Issue
Block a user