feat: change router
This commit is contained in:
@@ -23,7 +23,6 @@ from src.presentation.handlers import (
|
|||||||
)
|
)
|
||||||
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
||||||
from src.presentation.routing import me_router
|
from src.presentation.routing import me_router
|
||||||
from src.presentation.routing.password import password_router
|
|
||||||
|
|
||||||
security = HTTPBasic()
|
security = HTTPBasic()
|
||||||
|
|
||||||
@@ -91,7 +90,6 @@ app.add_exception_handler(ApplicationException, application_exception_handler)
|
|||||||
app.add_exception_handler(Exception, unhandled_exception_handler)
|
app.add_exception_handler(Exception, unhandled_exception_handler)
|
||||||
|
|
||||||
app.include_router(me_router)
|
app.include_router(me_router)
|
||||||
app.include_router(password_router)
|
|
||||||
# app.include_router(me_devices_router)
|
# app.include_router(me_devices_router)
|
||||||
# app.include_router(me_deals_router)
|
# app.include_router(me_deals_router)
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,27 @@ from src.application.commands import (
|
|||||||
DeleteAvatarCommand,
|
DeleteAvatarCommand,
|
||||||
ChangePasswordStartCommand,
|
ChangePasswordStartCommand,
|
||||||
ChangePasswordCompleteCommand,
|
ChangePasswordCompleteCommand,
|
||||||
|
ForgotPasswordStartCommand,
|
||||||
|
ForgotPasswordCompleteCommand,
|
||||||
)
|
)
|
||||||
from src.application.domain.dto import AuthContext
|
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 (
|
from src.presentation.dependencies import (
|
||||||
get_delete_avatar_command,
|
get_delete_avatar_command,
|
||||||
get_set_avatar_command,
|
get_set_avatar_command,
|
||||||
get_set_phone_command,
|
get_set_phone_command,
|
||||||
get_change_password_start_command,
|
get_change_password_start_command,
|
||||||
get_change_password_complete_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.api_errors import ApiErrorPayload, ApiValidationErrorsPayload
|
||||||
from src.presentation.schemas.me_public import MeUserPublicResponse, SetAvatarPublicResponse
|
from src.presentation.schemas.me_public import MeUserPublicResponse, SetAvatarPublicResponse
|
||||||
from src.presentation.serializers import me_user_public
|
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]] = {
|
_DELETE_AVATAR_ERROR_RESPONSES: dict[int, dict[str, object]] = {
|
||||||
status.HTTP_401_UNAUTHORIZED: {
|
status.HTTP_401_UNAUTHORIZED: {
|
||||||
'description': 'Не передан или неверен access token.',
|
'description': 'Не передан или неверен access token.',
|
||||||
@@ -211,6 +245,53 @@ async def change_password_complete(
|
|||||||
return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result})
|
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)
|
# @account_settings_router.post(path='/encrypted-mnemonic/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
|
||||||
# async def encrypted_mnemonic_start(
|
# async def encrypted_mnemonic_start(
|
||||||
|
|||||||
@@ -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})
|
|
||||||
Reference in New Issue
Block a user