%%%------------------------------------------------------------------- %%% @doc Обработчик профиля текущего пользователя (клиентский API). %%% %%% GET – получить информацию о своём профиле. %%% PATCH – частичное обновление профиля / смена пароля. %%% @end %%%------------------------------------------------------------------- -module(handler_user_me). -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) -> handle(Req, Opts). %%% Swagger metadata -spec trails() -> [map()]. trails() -> [ #{ path => <<"/v1/user/me">>, method => <<"GET">>, description => <<"Get current user profile">>, tags => [<<"Users">>], responses => #{ 200 => #{ description => <<"User profile">>, content => #{<<"application/json">> => #{schema => user_schema()}} }, 401 => #{description => <<"Unauthorized">>}, 404 => #{description => <<"User not found">>} } }, #{ path => <<"/v1/user/me">>, method => <<"PATCH">>, description => <<"Update current user profile (partial) or change password">>, tags => [<<"Users">>], requestBody => #{ required => true, content => #{<<"application/json">> => #{schema => user_update_schema()}} }, responses => #{ 200 => #{ description => <<"Updated profile">>, content => #{<<"application/json">> => #{schema => user_schema()}} }, 400 => #{description => <<"Invalid body / unknown field">>}, 401 => #{description => <<"Unauthorized">>}, 403 => #{description => <<"Wrong current password or account not active">>}, 404 => #{description => <<"User not found">>} } } ]. user_schema() -> #{ type => object, properties => #{ id => #{type => string}, email => #{type => string, format => <<"email">>}, 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, enum => [<<"ru">>, <<"en">>], nullable => true}, 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">>}, created_at => #{type => string, format => <<"date-time">>}, updated_at => #{type => string, format => <<"date-time">>} } }. user_update_schema() -> #{ type => object, properties => #{ language => #{type => string, enum => [<<"ru">>, <<"en">>]}, nickname => #{type => string, nullable => true}, timezone => #{type => string, nullable => true}, phone => #{type => string, nullable => true}, avatar_url => #{type => string, nullable => true}, preferences => #{type => object, nullable => true}, password => #{type => string, format => <<"password">>, description => <<"New password; requires current_password">>}, current_password => #{type => string, format => <<"password">>, description => <<"Current password; required with password">>} } }. %%%=================================================================== %%% HTTP-методы %%%=================================================================== %% @private -spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. handle(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_me(Req); <<"PATCH">> -> patch_me(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %% @doc GET /v1/user/me — получение профиля текущего пользователя. -spec get_me(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_me(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> case core_user:get_by_id(UserId) of {ok, User} -> handler_utils:send_json(Req1, 200, handler_utils:user_to_json(User)); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"User not found">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @doc PATCH /v1/user/me — частичное обновление профиля / смена пароля. -spec patch_me(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. patch_me(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of Decoded when is_map(Decoded) -> case logic_user:update_me(UserId, Decoded) of {ok, User} -> handler_utils:send_json(Req2, 200, handler_utils:user_to_json(User)); {error, not_found} -> handler_utils:send_error(Req2, 404, <<"User not found">>); {error, wrong_password} -> handler_utils:send_error(Req2, 403, <<"Wrong current password">>); {error, forbidden} -> handler_utils:send_error(Req2, 403, <<"Account is not active">>); {error, unknown_field} -> handler_utils:send_error(Req2, 400, <<"Unknown field">>); {error, empty_body} -> handler_utils:send_error(Req2, 400, <<"Empty update body">>); {error, password_pair_required} -> handler_utils:send_error(Req2, 400, <<"password and current_password must be sent together">>); {error, invalid_password} -> handler_utils:send_error(Req2, 400, <<"Invalid password fields">>); {error, invalid_language} -> handler_utils:send_error(Req2, 400, <<"language must be ru or en">>); {error, Reason} when is_atom(Reason) -> handler_utils:send_error(Req2, 400, atom_to_binary(Reason, utf8)); {error, _} -> handler_utils:send_error(Req2, 400, <<"Invalid request">>) end; _ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON">>) catch _:_ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON format">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end.