From 520af23cc692ddbf3b4356ad81727b67d014f95a Mon Sep 17 00:00:00 2001 From: Aleksey Sabilin Date: Fri, 14 Aug 2026 11:02:58 +0300 Subject: [PATCH] feat(api): optional auth for public search and studio week. Refs EventHub/EventHubFront#58 --- src/handlers/handler_calendar_by_id.erl | 4 ++-- src/handlers/handler_calendar_specialists.erl | 2 +- src/handlers/handler_events.erl | 2 +- src/handlers/handler_search.erl | 2 +- src/handlers/handler_utils.erl | 12 ++++++++++++ test/api/users/user_calendar_by_id_tests.erl | 15 +++++++++------ test/api/users/user_search_tests.erl | 10 ++++++---- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/handlers/handler_calendar_by_id.erl b/src/handlers/handler_calendar_by_id.erl index 925facf..c9b2c82 100755 --- a/src/handlers/handler_calendar_by_id.erl +++ b/src/handlers/handler_calendar_by_id.erl @@ -115,13 +115,13 @@ calendar_update_schema() -> %%% Internal functions get_calendar(Req) -> - case handler_utils:auth_user(Req) of + case handler_utils:auth_user_optional(Req) of {ok, UserId, Req1} -> CalendarId = cowboy_req:binding(id, Req1), case logic_calendar:get_calendar(UserId, CalendarId) of {ok, Calendar} -> Json0 = handler_utils:calendar_to_json(Calendar), - Following = logic_calendar_follow:is_following(UserId, CalendarId), + Following = UserId =/= <<>> andalso logic_calendar_follow:is_following(UserId, CalendarId), handler_utils:send_json(Req1, 200, Json0#{following => Following}); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); diff --git a/src/handlers/handler_calendar_specialists.erl b/src/handlers/handler_calendar_specialists.erl index 4fd1fb8..536f25c 100755 --- a/src/handlers/handler_calendar_specialists.erl +++ b/src/handlers/handler_calendar_specialists.erl @@ -94,7 +94,7 @@ has_user_binding(Req) -> cowboy_req:binding(user_id, Req) =/= undefined. list_specialists(Req) -> - case handler_utils:auth_user(Req) of + case handler_utils:auth_user_optional(Req) of {ok, UserId, Req1} -> CalendarId = cowboy_req:binding(id, Req1), case logic_calendar_specialist:list(UserId, CalendarId) of diff --git a/src/handlers/handler_events.erl b/src/handlers/handler_events.erl index 4cb583e..4d84dda 100644 --- a/src/handlers/handler_events.erl +++ b/src/handlers/handler_events.erl @@ -225,7 +225,7 @@ create_event(Req) -> %% @doc GET /v1/calendars/:calendar_id/events — список событий. -spec list_events(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. list_events(Req) -> - case handler_utils:auth_user(Req) of + case handler_utils:auth_user_optional(Req) of {ok, UserId, Req1} -> CalendarId = cowboy_req:binding(calendar_id, Req1), Qs = cowboy_req:parse_qs(Req1), diff --git a/src/handlers/handler_search.erl b/src/handlers/handler_search.erl index 4e86691..de612b0 100644 --- a/src/handlers/handler_search.erl +++ b/src/handlers/handler_search.erl @@ -74,7 +74,7 @@ handle(Req, _Opts) -> %% @doc GET /v1/search — полнотекстовый поиск с фильтрами. -spec search(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. search(Req) -> - case handler_utils:auth_user(Req) of + case handler_utils:auth_user_optional(Req) of {ok, UserId, Req1} -> Qs = cowboy_req:parse_qs(Req1), Type = proplists:get_value(<<"type">>, Qs, undefined), diff --git a/src/handlers/handler_utils.erl b/src/handlers/handler_utils.erl index 881bd6f..c782b4a 100755 --- a/src/handlers/handler_utils.erl +++ b/src/handlers/handler_utils.erl @@ -10,6 +10,7 @@ -export([ auth_admin/1, auth_user/1, + auth_user_optional/1, send_json/3, send_json/4, send_error/3, @@ -75,6 +76,17 @@ is_superadmin(Req) -> auth_user(Req) -> handler_auth:authenticate(Req). +%% @doc Как auth_user/1, но без заголовка Authorization — гость (`<<>>`). +%% Невалидный Bearer по-прежнему 401. +-spec auth_user_optional(cowboy_req:req()) -> + {ok, binary(), cowboy_req:req()} | {error, integer(), binary(), cowboy_req:req()}. +auth_user_optional(Req) -> + case cowboy_req:header(<<"authorization">>, Req) of + undefined -> {ok, <<>>, Req}; + <<>> -> {ok, <<>>, Req}; + _ -> auth_user(Req) + end. + %%%=================================================================== %%% HTTP‑ответы %%%=================================================================== diff --git a/test/api/users/user_calendar_by_id_tests.erl b/test/api/users/user_calendar_by_id_tests.erl index 86b820d..d4d6732 100644 --- a/test/api/users/user_calendar_by_id_tests.erl +++ b/test/api/users/user_calendar_by_id_tests.erl @@ -19,7 +19,7 @@ test() -> CalId = api_test_runner:create_calendar(Token, #{title => <<"TestCal">>}), test_get_calendar(Token, CalId), - test_get_calendar_unauthorized(CalId), + test_get_calendar_guest(Token, CalId), test_get_calendar_not_found(Token), test_update_calendar(Token, CalId), test_update_calendar_settings(Token, CalId), @@ -39,12 +39,15 @@ test_get_calendar(Token, CalId) -> ?assert(maps:is_key(<<"title">>, Cal)), ct:pal(" OK: ~s", [maps:get(<<"title">>, Cal)]). -test_get_calendar_unauthorized(CalId) -> - ct:pal(" TEST: Get calendar without token (401)"), +test_get_calendar_guest(Token, CalId) -> + ct:pal(" TEST: Guest GET commercial calendar (200), personal (403)"), Path = <<"/v1/calendars/", CalId/binary>>, - Resp = api_test_runner:client_request(get, Path, <<>>), - ?assertMatch({ok, 401, _, _}, Resp), - ct:pal(" OK: got 401"). + {ok, 200, _, Body} = api_test_runner:client_request(get, Path, <<>>), + #{<<"id">> := CalId} = jsx:decode(list_to_binary(Body), [return_maps]), + PersonalId = api_test_runner:existing_personal_calendar_id(Token), + PPath = <<"/v1/calendars/", PersonalId/binary>>, + {ok, 403, _, _} = api_test_runner:client_request(get, PPath, <<>>), + ct:pal(" OK: guest commercial 200, personal 403"). test_get_calendar_not_found(Token) -> ct:pal(" TEST: Get non-existent calendar (404)"), diff --git a/test/api/users/user_search_tests.erl b/test/api/users/user_search_tests.erl index 6cfb981..3cd646c 100644 --- a/test/api/users/user_search_tests.erl +++ b/test/api/users/user_search_tests.erl @@ -11,7 +11,7 @@ %%% - поиск с фильтрацией по датам (from/to) %%% - геопоиск (lat, lon, radius) %%% - пагинацию результатов -%%% - ошибку 401 без токена +%%% - публичный поиск без токена (200) %%% @end %%%------------------------------------------------------------------- -module(user_search_tests). @@ -120,7 +120,9 @@ test_search_pagination(Token) -> ct:pal(" OK"). test_search_unauthorized() -> - ct:pal(" TEST: Search without token"), + ct:pal(" TEST: Search without token (public)"), Resp = api_test_runner:client_request(get, <<"/v1/search?q=test">>, <<>>), - ?assertMatch({ok, 401, _, _}, Resp), - ct:pal(" OK: got 401"). \ No newline at end of file + {ok, 200, _, Body} = Resp, + Decoded = jsx:decode(list_to_binary(Body), [return_maps]), + ?assert(is_map(Decoded)), + ct:pal(" OK: guest search 200"). \ No newline at end of file