feat: add avatars

This commit is contained in:
2026-05-14 23:46:26 +03:00
parent 20ddb196ff
commit d426b02d25
26 changed files with 857 additions and 162 deletions

View File

@@ -1,27 +1,50 @@
from fastapi import APIRouter, Request, Depends
from fastapi import APIRouter, Depends, Request
from fastapi.responses import ORJSONResponse
from starlette import status
from src.application.commands import SetPhoneCommand, SetEncryptedMnemonicStartCommand, SetEncryptedMnemonicCompleteCommand, UpdateBankDetailsStartCommand, UpdateBankDetailsCompleteCommand, ChangePasswordStartCommand, ChangePasswordCompleteCommand, ChangeEmailStartCommand, ChangeEmailConfirmOldCommand, ChangeEmailCompleteCommand
from src.application.commands import SetPhoneCommand, SetAvatarCommand
from src.application.domain.dto import AuthContext
from src.presentation.decorators import require_access_token
from src.presentation.dependencies import (
get_set_avatar_command,
get_set_phone_command,
get_set_encrypted_mnemonic_start_command,
get_set_encrypted_mnemonic_complete_command,
get_update_bank_details_start_command,
get_update_bank_details_complete_command,
get_change_password_start_command,
get_change_password_complete_command,
get_change_email_start_command,
get_change_email_confirm_old_command,
get_change_email_complete_command,
)
from src.presentation.schemas import SetPhoneRequest, EncryptedMnemonicConfirmRequest, BankConfirmRequest, ChangePasswordConfirmRequest, ChangeEmailConfirmOldRequest, ChangeEmailCompleteRequest
from src.presentation.schemas import SetAvatarRequest, SetPhoneRequest
from src.presentation.schemas.api_errors import ApiErrorPayload, ApiValidationErrorsPayload
from src.presentation.schemas.me_public import SetAvatarPublicResponse
from src.presentation.serializers import me_user_public
account_settings_router = APIRouter(prefix='/settings')
_SET_AVATAR_ERROR_RESPONSES: dict[int, dict[str, object]] = {
status.HTTP_400_BAD_REQUEST: {
'description': 'Битый или неподдерживаемый формат изображения, либо Base64 ошибочный.',
'model': ApiErrorPayload,
},
status.HTTP_401_UNAUTHORIZED: {
'description': 'Не передан или неверен access token.',
'model': ApiErrorPayload,
},
status.HTTP_404_NOT_FOUND: {
'description': 'Учётная запись не найдена.',
'model': ApiErrorPayload,
},
status.HTTP_422_UNPROCESSABLE_ENTITY: {
'description': 'Тело запроса не соответствует схеме (например, неверный Base64 или превышен размер).',
'model': ApiValidationErrorsPayload,
},
status.HTTP_500_INTERNAL_SERVER_ERROR: {
'description': 'Внутренняя ошибка сервера; клиенту отдаётся обобщённое сообщение.',
'model': ApiErrorPayload,
},
status.HTTP_503_SERVICE_UNAVAILABLE: {
'description': 'S3 не сконфигурирован, ошибка записи в хранилище или временная недоступность сервиса.',
'model': ApiErrorPayload,
},
}
@account_settings_router.patch(path='/phone', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def set_phone(
request: Request,
@@ -33,118 +56,143 @@ async def set_phone(
return ORJSONResponse(status_code=status.HTTP_200_OK, content={'phone': user.phone})
@account_settings_router.post(path='/encrypted-mnemonic/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def encrypted_mnemonic_start(
@account_settings_router.patch(
path='/avatar',
response_class=ORJSONResponse,
status_code=status.HTTP_200_OK,
response_model=SetAvatarPublicResponse,
summary='Обновить аватар',
description=(
'Принимает фото в Base64, сохраняет как WebP в объектном хранилище и записывает публичный URL в профиль.'
),
response_description=(
'Профиль пользователя в том же формате, что и GET /me, плюс размер сохранённого WebP.'
),
responses=_SET_AVATAR_ERROR_RESPONSES,
)
async def set_avatar(
request: Request,
body: SetAvatarRequest,
auth: AuthContext = Depends(require_access_token),
command: SetEncryptedMnemonicStartCommand = Depends(get_set_encrypted_mnemonic_start_command),
):
result = await command(user_id=auth.user_id)
return {'success': result}
command: SetAvatarCommand = Depends(get_set_avatar_command),
) -> SetAvatarPublicResponse:
user, webp_size = await command(user_id=auth.user_id, image_bytes=body.decoded_bytes)
pub = me_user_public(user)
return SetAvatarPublicResponse(**pub.model_dump(), webp_size_bytes=webp_size)
@account_settings_router.post(path='/encrypted-mnemonic/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def encrypted_mnemonic_complete(
request: Request,
body: EncryptedMnemonicConfirmRequest,
auth: AuthContext = Depends(require_access_token),
command: SetEncryptedMnemonicCompleteCommand = Depends(get_set_encrypted_mnemonic_complete_command),
):
user = await command(
user_id=auth.user_id,
code=body.code,
encrypted_mnemonic=body.encrypted_mnemonic,
)
return {'encrypted_mnemonic': user.encrypted_mnemonic}
@account_settings_router.post(path='/email/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def change_email_start(
request: Request,
auth: AuthContext = Depends(require_access_token),
command: ChangeEmailStartCommand = Depends(get_change_email_start_command),
):
result = await command(user_id=auth.user_id)
return {'success': result}
@account_settings_router.post(path='/email/confirm-old', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def change_email_confirm_old(
request: Request,
body: ChangeEmailConfirmOldRequest,
auth: AuthContext = Depends(require_access_token),
command: ChangeEmailConfirmOldCommand = Depends(get_change_email_confirm_old_command),
):
result = await command(user_id=auth.user_id, code=body.code, new_email=body.new_email)
return {'success': result}
@account_settings_router.post(path='/email/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def change_email_complete(
request: Request,
body: ChangeEmailCompleteRequest,
auth: AuthContext = Depends(require_access_token),
command: ChangeEmailCompleteCommand = Depends(get_change_email_complete_command),
):
result = await command(user_id=auth.user_id, code=body.code)
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,
auth: AuthContext = Depends(require_access_token),
command: UpdateBankDetailsStartCommand = Depends(get_update_bank_details_start_command),
):
result = await command(user_id=auth.user_id)
return {'success': result}
@account_settings_router.post(path='/bank/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def bank_details_complete(
request: Request,
body: BankConfirmRequest,
auth: AuthContext = Depends(require_access_token),
command: UpdateBankDetailsCompleteCommand = Depends(get_update_bank_details_complete_command),
):
user = await command(
user_id=auth.user_id,
code=body.code,
passport_data=body.passport_data,
inn=body.inn,
erc20=body.erc20,
)
return ORJSONResponse(
status_code=status.HTTP_200_OK,
content={
'passport_data': user.passport_data,
'inn': user.inn,
'erc20': user.erc20,
},
)
#
# @account_settings_router.post(path='/encrypted-mnemonic/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def encrypted_mnemonic_start(
# request: Request,
# auth: AuthContext = Depends(require_access_token),
# command: SetEncryptedMnemonicStartCommand = Depends(get_set_encrypted_mnemonic_start_command),
# ):
# result = await command(user_id=auth.user_id)
# return {'success': result}
#
#
# @account_settings_router.post(path='/encrypted-mnemonic/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def encrypted_mnemonic_complete(
# request: Request,
# body: EncryptedMnemonicConfirmRequest,
# auth: AuthContext = Depends(require_access_token),
# command: SetEncryptedMnemonicCompleteCommand = Depends(get_set_encrypted_mnemonic_complete_command),
# ):
# user = await command(
# user_id=auth.user_id,
# code=body.code,
# encrypted_mnemonic=body.encrypted_mnemonic,
# )
# return {'encrypted_mnemonic': user.encrypted_mnemonic}
#
#
# @account_settings_router.post(path='/email/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def change_email_start(
# request: Request,
# auth: AuthContext = Depends(require_access_token),
# command: ChangeEmailStartCommand = Depends(get_change_email_start_command),
# ):
# result = await command(user_id=auth.user_id)
# return {'success': result}
#
#
# @account_settings_router.post(path='/email/confirm-old', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def change_email_confirm_old(
# request: Request,
# body: ChangeEmailConfirmOldRequest,
# auth: AuthContext = Depends(require_access_token),
# command: ChangeEmailConfirmOldCommand = Depends(get_change_email_confirm_old_command),
# ):
# result = await command(user_id=auth.user_id, code=body.code, new_email=body.new_email)
# return {'success': result}
#
#
# @account_settings_router.post(path='/email/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def change_email_complete(
# request: Request,
# body: ChangeEmailCompleteRequest,
# auth: AuthContext = Depends(require_access_token),
# command: ChangeEmailCompleteCommand = Depends(get_change_email_complete_command),
# ):
# result = await command(user_id=auth.user_id, code=body.code)
# 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,
# auth: AuthContext = Depends(require_access_token),
# command: UpdateBankDetailsStartCommand = Depends(get_update_bank_details_start_command),
# ):
# result = await command(user_id=auth.user_id)
# return {'success': result}
#
#
# @account_settings_router.post(path='/bank/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
# async def bank_details_complete(
# request: Request,
# body: BankConfirmRequest,
# auth: AuthContext = Depends(require_access_token),
# command: UpdateBankDetailsCompleteCommand = Depends(get_update_bank_details_complete_command),
# ):
# user = await command(
# user_id=auth.user_id,
# code=body.code,
# passport_data=body.passport_data,
# inn=body.inn,
# erc20=body.erc20,
# )
# return ORJSONResponse(
# status_code=status.HTTP_200_OK,
# content={
# 'passport_data': user.passport_data,
# 'inn': user.inn,
# 'erc20': user.erc20,
# },
# )