From 41d0fe8aa77490b819759aae4a4b08eda7a3ce9b Mon Sep 17 00:00:00 2001 From: Noloquideus Date: Tue, 19 May 2026 15:26:50 +0300 Subject: [PATCH] feat: change router --- src/main.py | 2 - src/presentation/routing/account_settings.py | 85 +++++++++++++++++++- src/presentation/routing/password.py | 81 ------------------- 3 files changed, 83 insertions(+), 85 deletions(-) delete mode 100644 src/presentation/routing/password.py diff --git a/src/main.py b/src/main.py index 4f2ce6d..6cf14ed 100644 --- a/src/main.py +++ b/src/main.py @@ -23,7 +23,6 @@ from src.presentation.handlers import ( ) from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware from src.presentation.routing import me_router -from src.presentation.routing.password import password_router security = HTTPBasic() @@ -91,7 +90,6 @@ app.add_exception_handler(ApplicationException, application_exception_handler) app.add_exception_handler(Exception, unhandled_exception_handler) app.include_router(me_router) -app.include_router(password_router) # app.include_router(me_devices_router) # app.include_router(me_deals_router) diff --git a/src/presentation/routing/account_settings.py b/src/presentation/routing/account_settings.py index 36518d6..30abd28 100644 --- a/src/presentation/routing/account_settings.py +++ b/src/presentation/routing/account_settings.py @@ -7,17 +7,27 @@ from src.application.commands import ( DeleteAvatarCommand, ChangePasswordStartCommand, ChangePasswordCompleteCommand, + ForgotPasswordStartCommand, + ForgotPasswordCompleteCommand, ) from src.application.domain.dto import AuthContext -from src.presentation.decorators import require_access_token, csrf_protect +from src.presentation.decorators import require_access_token, csrf_protect, rate_limit, email_rl_key from src.presentation.dependencies import ( get_delete_avatar_command, get_set_avatar_command, get_set_phone_command, get_change_password_start_command, get_change_password_complete_command, + get_forgot_password_start_command, + get_forgot_password_complete_command, +) +from src.presentation.schemas import ( + SetAvatarRequest, + SetPhoneRequest, + ChangePasswordConfirmRequest, + ForgotPasswordStartRequest, + ForgotPasswordCompleteRequest, ) -from src.presentation.schemas import SetAvatarRequest, SetPhoneRequest, ChangePasswordConfirmRequest from src.presentation.schemas.api_errors import ApiErrorPayload, ApiValidationErrorsPayload from src.presentation.schemas.me_public import MeUserPublicResponse, SetAvatarPublicResponse from src.presentation.serializers import me_user_public @@ -86,6 +96,30 @@ _PASSWORD_ERROR_RESPONSES: dict[int, dict[str, object]] = { } +_FORGOT_PASSWORD_ERROR_RESPONSES: dict[int, dict[str, object]] = { + status.HTTP_400_BAD_REQUEST: { + 'description': 'Неверный или просроченный код, пароли не совпадают.', + 'model': ApiErrorPayload, + }, + status.HTTP_422_UNPROCESSABLE_ENTITY: { + 'description': 'Тело запроса не соответствует схеме.', + 'model': ApiValidationErrorsPayload, + }, + status.HTTP_429_TOO_MANY_REQUESTS: { + 'description': 'Код уже отправлен или слишком частые запросы.', + 'model': ApiErrorPayload, + }, + status.HTTP_500_INTERNAL_SERVER_ERROR: { + 'description': 'Внутренняя ошибка сервера.', + 'model': ApiErrorPayload, + }, + status.HTTP_503_SERVICE_UNAVAILABLE: { + 'description': 'Временная ошибка отправки кода или сохранения в кеш.', + 'model': ApiErrorPayload, + }, +} + + _DELETE_AVATAR_ERROR_RESPONSES: dict[int, dict[str, object]] = { status.HTTP_401_UNAUTHORIZED: { 'description': 'Не передан или неверен access token.', @@ -211,6 +245,53 @@ async def change_password_complete( return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result}) +@account_settings_router.post( + path='/password/forgot/start', + response_class=ORJSONResponse, + status_code=status.HTTP_200_OK, + summary='Запросить код для восстановления пароля', + description=( + 'Принимает email. Если учётная запись существует, отправляет шестизначный код. ' + 'Ответ всегда успешный при валидном email (без раскрытия наличия аккаунта).' + ), + responses=_FORGOT_PASSWORD_ERROR_RESPONSES, +) +@rate_limit(limit=5, window_seconds=300, scope='key', key_prefix='rl', key_builder=email_rl_key) +async def forgot_password_start( + request: Request, + body: ForgotPasswordStartRequest, + command: ForgotPasswordStartCommand = Depends(get_forgot_password_start_command), +): + result = await command(email=body.email) + return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result}) + + +@account_settings_router.post( + path='/password/forgot/complete', + response_class=ORJSONResponse, + status_code=status.HTTP_200_OK, + summary='Установить новый пароль по коду из письма', + description=( + 'Принимает email, код из письма, новый пароль и подтверждение. ' + 'Пароль: минимум 12 символов, строчная и заглавная буква, цифра, спецсимвол, без пробелов.' + ), + responses=_FORGOT_PASSWORD_ERROR_RESPONSES, +) +@rate_limit(limit=10, window_seconds=300, scope='key', key_prefix='rl', key_builder=email_rl_key) +async def forgot_password_complete( + request: Request, + body: ForgotPasswordCompleteRequest, + command: ForgotPasswordCompleteCommand = Depends(get_forgot_password_complete_command), +): + result = await command( + email=body.email, + code=body.code, + new_password=body.new_password, + confirm_password=body.confirm_password, + ) + return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result}) + + # # @account_settings_router.post(path='/encrypted-mnemonic/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK) # async def encrypted_mnemonic_start( diff --git a/src/presentation/routing/password.py b/src/presentation/routing/password.py deleted file mode 100644 index a40cb4d..0000000 --- a/src/presentation/routing/password.py +++ /dev/null @@ -1,81 +0,0 @@ -from fastapi import APIRouter, Depends, Request -from fastapi.responses import ORJSONResponse -from starlette import status -from src.application.commands import ForgotPasswordStartCommand, ForgotPasswordCompleteCommand -from src.presentation.decorators import rate_limit, email_rl_key -from src.presentation.dependencies import ( - get_forgot_password_start_command, - get_forgot_password_complete_command, -) -from src.presentation.schemas import ForgotPasswordStartRequest, ForgotPasswordCompleteRequest -from src.presentation.schemas.api_errors import ApiErrorPayload, ApiValidationErrorsPayload - - -password_router = APIRouter(prefix='/password', tags=['Password']) - - -_FORGOT_PASSWORD_ERROR_RESPONSES: dict[int, dict[str, object]] = { - status.HTTP_400_BAD_REQUEST: { - 'description': 'Неверный или просроченный код, пароли не совпадают.', - 'model': ApiErrorPayload, - }, - status.HTTP_422_UNPROCESSABLE_ENTITY: { - 'description': 'Тело запроса не соответствует схеме.', - 'model': ApiValidationErrorsPayload, - }, - status.HTTP_429_TOO_MANY_REQUESTS: { - 'description': 'Код уже отправлен или слишком частые запросы.', - 'model': ApiErrorPayload, - }, - status.HTTP_503_SERVICE_UNAVAILABLE: { - 'description': 'Временная ошибка отправки кода или сохранения в кеш.', - 'model': ApiErrorPayload, - }, -} - - -@password_router.post( - path='/forgot/start', - response_class=ORJSONResponse, - status_code=status.HTTP_200_OK, - summary='Запросить код для восстановления пароля', - description=( - 'Принимает email. Если учётная запись существует, отправляет шестизначный код. ' - 'Ответ всегда успешный при валидном email (без раскрытия наличия аккаунта).' - ), - responses=_FORGOT_PASSWORD_ERROR_RESPONSES, -) -@rate_limit(limit=5, window_seconds=300, scope='key', key_prefix='rl', key_builder=email_rl_key) -async def forgot_password_start( - request: Request, - body: ForgotPasswordStartRequest, - command: ForgotPasswordStartCommand = Depends(get_forgot_password_start_command), -): - result = await command(email=body.email) - return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result}) - - -@password_router.post( - path='/forgot/complete', - response_class=ORJSONResponse, - status_code=status.HTTP_200_OK, - summary='Установить новый пароль по коду из письма', - description=( - 'Принимает email, код из письма, новый пароль и подтверждение. ' - 'Пароль: минимум 12 символов, строчная и заглавная буква, цифра, спецсимвол, без пробелов.' - ), - responses=_FORGOT_PASSWORD_ERROR_RESPONSES, -) -@rate_limit(limit=10, window_seconds=300, scope='key', key_prefix='rl', key_builder=email_rl_key) -async def forgot_password_complete( - request: Request, - body: ForgotPasswordCompleteRequest, - command: ForgotPasswordCompleteCommand = Depends(get_forgot_password_complete_command), -): - result = await command( - email=body.email, - code=body.code, - new_password=body.new_password, - confirm_password=body.confirm_password, - ) - return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result})