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 import APIRouter, Depends, Request
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from starlette import status 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.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
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_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.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
@@ -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]] = { _DELETE_AVATAR_ERROR_RESPONSES: dict[int, dict[str, object]] = {
status.HTTP_401_UNAUTHORIZED: { status.HTTP_401_UNAUTHORIZED: {
'description': 'Не передан или неверен access token.', 'description': 'Не передан или неверен access token.',
@@ -125,6 +165,52 @@ async def delete_avatar(
user = await command(user_id=auth.user_id) user = await command(user_id=auth.user_id)
return me_user_public(user) 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) # @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(
@@ -183,32 +269,6 @@ async def delete_avatar(
# return {'success': result} # 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) # @account_settings_router.post(path='/bank/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def bank_details_start( # async def bank_details_start(
# request: Request, # request: Request,