feat: add change password event

This commit is contained in:
2026-05-19 08:58:12 +03:00
parent f426495d47
commit bd1faffbb0

View File

@@ -1,15 +1,23 @@
from fastapi import APIRouter, Depends, Request
from fastapi.responses import ORJSONResponse
from starlette import status
from src.application.commands import SetPhoneCommand, SetAvatarCommand, DeleteAvatarCommand
from src.application.commands import (
SetPhoneCommand,
SetAvatarCommand,
DeleteAvatarCommand,
ChangePasswordStartCommand,
ChangePasswordCompleteCommand,
)
from src.application.domain.dto import AuthContext
from src.presentation.decorators import require_access_token, csrf_protect
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,
)
from src.presentation.schemas import SetAvatarRequest, SetPhoneRequest
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
@@ -46,6 +54,38 @@ _SET_AVATAR_ERROR_RESPONSES: dict[int, dict[str, object]] = {
}
_PASSWORD_ERROR_RESPONSES: dict[int, dict[str, object]] = {
status.HTTP_400_BAD_REQUEST: {
'description': 'Неверный или просроченный код, пароли не совпадают или совпадают с текущим.',
'model': ApiErrorPayload,
},
status.HTTP_401_UNAUTHORIZED: {
'description': 'Не передан или неверен access token.',
'model': ApiErrorPayload,
},
status.HTTP_404_NOT_FOUND: {
'description': 'Учётная запись не найдена или у пользователя нет email.',
'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.',
@@ -125,6 +165,52 @@ async def delete_avatar(
user = await command(user_id=auth.user_id)
return me_user_public(user)
@account_settings_router.post(
path='/password/start',
response_class=ORJSONResponse,
status_code=status.HTTP_200_OK,
summary='Запросить код для смены пароля',
description='Отправляет шестизначный код на email текущего пользователя. Повторный запрос возможен после истечения TTL.',
responses=_PASSWORD_ERROR_RESPONSES,
)
@csrf_protect()
async def change_password_start(
request: Request,
auth: AuthContext = Depends(require_access_token),
command: ChangePasswordStartCommand = Depends(get_change_password_start_command),
):
result = await command(user_id=auth.user_id)
return ORJSONResponse(status_code=status.HTTP_200_OK, content={'success': result})
@account_settings_router.post(
path='/password/complete',
response_class=ORJSONResponse,
status_code=status.HTTP_200_OK,
summary='Подтвердить смену пароля',
description=(
'Принимает код из письма, новый пароль и его подтверждение. '
'Новый пароль должен отличаться от текущего (минимум 8 символов).'
),
responses=_PASSWORD_ERROR_RESPONSES,
)
@csrf_protect()
async def change_password_complete(
request: Request,
body: ChangePasswordConfirmRequest,
auth: AuthContext = Depends(require_access_token),
command: ChangePasswordCompleteCommand = Depends(get_change_password_complete_command),
):
result = await command(
user_id=auth.user_id,
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(
@@ -183,32 +269,6 @@ async def delete_avatar(
# return {'success': result}
#
#
# @account_settings_router.post(path='/password/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def change_password_start(
# request: Request,
# auth: AuthContext = Depends(require_access_token),
# command: ChangePasswordStartCommand = Depends(get_change_password_start_command),
# ):
# result = await command(user_id=auth.user_id)
# return {'success': result}
#
#
# @account_settings_router.post(path='/password/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def change_password_complete(
# request: Request,
# body: ChangePasswordConfirmRequest,
# auth: AuthContext = Depends(require_access_token),
# command: ChangePasswordCompleteCommand = Depends(get_change_password_complete_command),
# ):
# result = await command(
# user_id=auth.user_id,
# code=body.code,
# new_password=body.new_password,
# confirm_password=body.confirm_password,
# )
# return {'success': result}
#
#
# @account_settings_router.post(path='/bank/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def bank_details_start(
# request: Request,