feat: add new column and delete old

This commit is contained in:
2026-05-13 11:23:59 +03:00
parent caa84525b1
commit 7019f14af2
14 changed files with 71 additions and 71 deletions

View File

@@ -19,7 +19,7 @@ class IUserRepository(ABC):
raise NotImplementedError
@abstractmethod
async def set_crypto_wallet(self, user_id: str, wallet_address: str) -> UserEntity:
async def set_encrypted_mnemonic(self, user_id: str, encrypted_mnemonic: str) -> UserEntity:
raise NotImplementedError
@abstractmethod

View File

@@ -1,7 +1,7 @@
from src.application.commands.get_me import GetMeCommand
from src.application.commands.set_phone import SetPhoneCommand
from src.application.commands.set_crypto_wallet_start import SetCryptoWalletStartCommand
from src.application.commands.set_crypto_wallet_complete import SetCryptoWalletCompleteCommand
from src.application.commands.set_encrypted_mnemonic_start import SetEncryptedMnemonicStartCommand
from src.application.commands.set_encrypted_mnemonic_complete import SetEncryptedMnemonicCompleteCommand
from src.application.commands.update_bank_details_start import UpdateBankDetailsStartCommand
from src.application.commands.update_bank_details_complete import UpdateBankDetailsCompleteCommand
from src.application.commands.change_password_start import ChangePasswordStartCommand

View File

@@ -5,7 +5,7 @@ from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.decorators import transactional
class SetCryptoWalletCompleteCommand:
class SetEncryptedMnemonicCompleteCommand:
def __init__(
self,
unit_of_work: IUnitOfWork,
@@ -19,42 +19,42 @@ class SetCryptoWalletCompleteCommand:
self._logger = logger
@transactional
async def __call__(self, *, user_id: str, code: str, wallet_address: str) -> UserEntity:
async def __call__(self, *, user_id: str, code: str, encrypted_mnemonic: str) -> UserEntity:
code = (code or '').strip()
USER_PREFIX = 'crypto_wallet:user:'
CODE_PREFIX = 'crypto_wallet:code:'
USER_PREFIX = 'encrypted_mnemonic:user:'
CODE_PREFIX = 'encrypted_mnemonic:code:'
user_key = f'{USER_PREFIX}{user_id}'
code_key = f'{CODE_PREFIX}{code}'
user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id)
if user.crypto_wallet is not None:
self._logger.info(f'Crypto wallet already set for user_id={user_id}')
raise ApplicationException(409, 'Crypto wallet already set and cannot be changed')
if user.encrypted_mnemonic is not None:
self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}')
raise ApplicationException(409, 'Encrypted mnemonic already set and cannot be changed')
cached_user_id = await self._cache.get(code_key)
if not cached_user_id:
self._logger.info(f'Crypto wallet set failed: code not found (user_id={user_id})')
self._logger.info(f'Encrypted mnemonic set failed: code not found (user_id={user_id})')
raise ApplicationException(400, 'Invalid or expired code')
if cached_user_id != user_id:
self._logger.info(f'Crypto wallet set failed: code-user mismatch (user_id={user_id})')
self._logger.info(f'Encrypted mnemonic set failed: code-user mismatch (user_id={user_id})')
raise ApplicationException(400, 'Invalid or expired code')
code_hash = await self._cache.get(user_key)
if not code_hash:
self._logger.info(f'Crypto wallet set failed: user key missing (user_id={user_id})')
self._logger.info(f'Encrypted mnemonic set failed: user key missing (user_id={user_id})')
raise ApplicationException(400, 'Invalid or expired code')
ok = await self._hash_service.verify(hashed_value=code_hash, plain_value=code)
if not ok:
self._logger.info(f'Crypto wallet set failed: code hash mismatch (user_id={user_id})')
self._logger.info(f'Encrypted mnemonic set failed: code hash mismatch (user_id={user_id})')
raise ApplicationException(400, 'Invalid or expired code')
user = await self._unit_of_work.user_repository.set_crypto_wallet(
user = await self._unit_of_work.user_repository.set_encrypted_mnemonic(
user_id=user_id,
wallet_address=wallet_address,
encrypted_mnemonic=encrypted_mnemonic,
)
await self._cache.set_user(user_id, user)
@@ -62,7 +62,7 @@ class SetCryptoWalletCompleteCommand:
await self._cache.delete(code_key)
await self._cache.delete(user_key)
except Exception as e:
self._logger.warning(f'Crypto wallet set cleanup failed (user_id={user_id}): {e}')
self._logger.warning(f'Encrypted mnemonic set cleanup failed (user_id={user_id}): {e}')
self._logger.info(f'Crypto wallet set for user_id={user_id}')
self._logger.info(f'Encrypted mnemonic set for user_id={user_id}')
return user

View File

@@ -9,7 +9,7 @@ from src.infrastructure.context_vars import trace_id_var
from src.infrastructure.database.decorators import transactional
class SetCryptoWalletStartCommand:
class SetEncryptedMnemonicStartCommand:
def __init__(
self,
hash_service: IHashService,
@@ -30,15 +30,15 @@ class SetCryptoWalletStartCommand:
LOCK_TTL = 30
MAX_ATTEMPTS = 20
USER_PREFIX = 'crypto_wallet:user:'
CODE_PREFIX = 'crypto_wallet:code:'
LOCK_PREFIX = 'crypto_wallet:lock:'
USER_PREFIX = 'encrypted_mnemonic:user:'
CODE_PREFIX = 'encrypted_mnemonic:code:'
LOCK_PREFIX = 'encrypted_mnemonic:lock:'
user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id)
if user.crypto_wallet is not None:
self._logger.info(f'Crypto wallet already set for user_id={user_id}')
raise ApplicationException(409, 'Crypto wallet already set and cannot be changed')
if user.encrypted_mnemonic is not None:
self._logger.info(f'Encrypted mnemonic already set for user_id={user_id}')
raise ApplicationException(409, 'Encrypted mnemonic already set and cannot be changed')
if not user.email:
self._logger.warning(f'User {user_id} does not have an email address')
@@ -51,7 +51,7 @@ class SetCryptoWalletStartCommand:
lock_key = f'{LOCK_PREFIX}{user_id}'
locked = await self._cache.set_nx(lock_key, '1', ttl=LOCK_TTL)
if not locked:
self._logger.info(f'Crypto wallet set throttled by lock (user_id={user_id})')
self._logger.info(f'Encrypted mnemonic set throttled by lock (user_id={user_id})')
raise ApplicationException(429, 'Too many requests. Please wait.')
try:
@@ -59,7 +59,7 @@ class SetCryptoWalletStartCommand:
existing = await self._cache.get(user_key)
if existing:
self._logger.info(f'Crypto wallet set denied: code already exists for user_id={user_id}')
self._logger.info(f'Encrypted mnemonic set denied: code already exists for user_id={user_id}')
raise ApplicationException(429, 'Code already sent. Please wait before retrying.')
for _ in range(MAX_ATTEMPTS):
@@ -75,7 +75,7 @@ class SetCryptoWalletStartCommand:
saved = await self._cache.set(user_key, code_hash, ttl=TTL)
if not saved:
await self._cache.delete(code_key)
self._logger.error(f'Crypto wallet set failed: cannot save code hash for user_id={user_id}')
self._logger.error(f'Encrypted mnemonic set failed: cannot save code hash for user_id={user_id}')
raise ApplicationException(503, 'Temporary error. Please try again.')
message_id = str(ULID())
@@ -95,12 +95,12 @@ class SetCryptoWalletStartCommand:
}
message = {
'event': 'crypto_wallet_set',
'event': 'encrypted_mnemonic_set',
'payload': payload,
'metadata': metadata,
}
self._logger.info(f'Crypto wallet set code created for user_id={user_id}')
self._logger.info(f'Encrypted mnemonic set code created for user_id={user_id}')
try:
await self._messanger.publish_to_queue(
@@ -118,12 +118,12 @@ class SetCryptoWalletStartCommand:
except Exception as rollback_err:
self._logger.error(f'Publish failed and rollback cache failed for user_id={user_id}: {str(rollback_err)}')
self._logger.error(f'Failed to publish crypto wallet set email for user_id={user_id}: {str(exception)}')
self._logger.error(f'Failed to publish encrypted mnemonic set email for user_id={user_id}: {str(exception)}')
raise ApplicationException(503, 'Temporary error. Please try again.')
return True
self._logger.error(f'Crypto wallet set failed: code space exhausted for user_id={user_id}')
self._logger.error(f'Encrypted mnemonic set failed: code space exhausted for user_id={user_id}')
raise ApplicationException(503, 'Temporary error. Please try again.')
finally:

View File

@@ -14,7 +14,7 @@ class UserEntity:
last_name: str | None = None
birth_date: date | None = None
crypto_wallet: str | None = None
encrypted_mnemonic: str | None = None
phone: str | None = None
passport_data: str | None = None

View File

@@ -37,7 +37,7 @@ class KeydbCache(ICache):
'middle_name': user.middle_name,
'last_name': user.last_name,
'birth_date': str(user.birth_date) if user.birth_date else None,
'crypto_wallet': user.crypto_wallet,
'encrypted_mnemonic': user.encrypted_mnemonic,
'phone': user.phone,
'passport_data': user.passport_data,
'inn': user.inn,

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from sqlalchemy import Boolean,Date,String,DateTime
from sqlalchemy.orm import Mapped,mapped_column
from sqlalchemy import Boolean, Date, DateTime, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from src.infrastructure.database.models.base import Base
from src.infrastructure.database.models.mixins import UlidPrimaryKeyMixin,AuditTimestampsMixin,SoftDeleteMixin
from src.infrastructure.database.models.mixins import UlidPrimaryKeyMixin, AuditTimestampsMixin, SoftDeleteMixin
class UserModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin, SoftDeleteMixin):
@@ -17,7 +17,7 @@ class UserModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin, SoftDeleteMixin
middle_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
birth_date: Mapped[Date | None] = mapped_column(Date, nullable=True)
crypto_wallet: Mapped[str | None] = mapped_column(String(255), nullable=True)
encrypted_mnemonic: Mapped[str | None] = mapped_column(Text, nullable=True)
phone: Mapped[str | None] = mapped_column(String(16), nullable=True)
passport_data: Mapped[str | None] = mapped_column(String(255), nullable=True)

View File

@@ -40,7 +40,7 @@ class UserRepository(IUserRepository):
middle_name=user.middle_name,
last_name=user.last_name,
birth_date=user.birth_date,
crypto_wallet=user.crypto_wallet,
encrypted_mnemonic=user.encrypted_mnemonic,
phone=user.phone,
passport_data=user.passport_data,
inn=user.inn,
@@ -89,8 +89,8 @@ class UserRepository(IUserRepository):
)
return await self._update_field(user_id, **payload)
async def set_crypto_wallet(self, user_id: str, wallet_address: str) -> UserEntity:
return await self._update_field(user_id, crypto_wallet=wallet_address)
async def set_encrypted_mnemonic(self, user_id: str, encrypted_mnemonic: str) -> UserEntity:
return await self._update_field(user_id, encrypted_mnemonic=encrypted_mnemonic)
async def get_password_hash(self, user_id: str) -> str:
try:

View File

@@ -1,8 +1,8 @@
from src.presentation.dependencies.commands import (
get_get_me_command,
get_set_phone_command,
get_set_crypto_wallet_start_command,
get_set_crypto_wallet_complete_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,

View File

@@ -1,6 +1,6 @@
from fastapi import Depends
from src.application.abstractions import IUnitOfWork
from src.application.commands import GetMeCommand, SetPhoneCommand, SetCryptoWalletStartCommand, SetCryptoWalletCompleteCommand, UpdateBankDetailsStartCommand, UpdateBankDetailsCompleteCommand, ChangePasswordStartCommand, ChangePasswordCompleteCommand, ChangeEmailStartCommand, ChangeEmailConfirmOldCommand, ChangeEmailCompleteCommand
from src.application.commands import GetMeCommand, SetPhoneCommand, SetEncryptedMnemonicStartCommand, SetEncryptedMnemonicCompleteCommand, UpdateBankDetailsStartCommand, UpdateBankDetailsCompleteCommand, ChangePasswordStartCommand, ChangePasswordCompleteCommand, ChangeEmailStartCommand, ChangeEmailConfirmOldCommand, ChangeEmailCompleteCommand
from src.application.contracts import ILogger, ICache, IQueueMessanger, IHashService
from src.presentation.dependencies.cache import get_cache
from src.presentation.dependencies.logger import get_logger
@@ -25,14 +25,14 @@ def get_set_phone_command(
return SetPhoneCommand(logger=logger, unit_of_work=unit_of_work, cache=cache)
def get_set_crypto_wallet_start_command(
def get_set_encrypted_mnemonic_start_command(
logger: ILogger = Depends(get_logger),
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
cache: ICache = Depends(get_cache),
messanger: IQueueMessanger = Depends(get_rabbit),
hash_service: IHashService = Depends(get_hash_service),
) -> SetCryptoWalletStartCommand:
return SetCryptoWalletStartCommand(
) -> SetEncryptedMnemonicStartCommand:
return SetEncryptedMnemonicStartCommand(
logger=logger,
unit_of_work=unit_of_work,
cache=cache,
@@ -41,13 +41,13 @@ def get_set_crypto_wallet_start_command(
)
def get_set_crypto_wallet_complete_command(
def get_set_encrypted_mnemonic_complete_command(
logger: ILogger = Depends(get_logger),
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
cache: ICache = Depends(get_cache),
hash_service: IHashService = Depends(get_hash_service),
) -> SetCryptoWalletCompleteCommand:
return SetCryptoWalletCompleteCommand(
) -> SetEncryptedMnemonicCompleteCommand:
return SetEncryptedMnemonicCompleteCommand(
logger=logger,
unit_of_work=unit_of_work,
cache=cache,

View File

@@ -30,7 +30,7 @@ async def me(
'middle_name': user.middle_name,
'last_name': user.last_name,
'birth_date': str(user.birth_date) if user.birth_date else None,
'crypto_wallet': user.crypto_wallet,
'encrypted_mnemonic': user.encrypted_mnemonic,
'phone': user.phone,
'passport_data': user.passport_data,
'inn': user.inn,

View File

@@ -1,13 +1,13 @@
from fastapi import APIRouter, Request, Depends
from fastapi.responses import ORJSONResponse
from starlette import status
from src.application.commands import SetPhoneCommand, SetCryptoWalletStartCommand, SetCryptoWalletCompleteCommand, UpdateBankDetailsStartCommand, UpdateBankDetailsCompleteCommand, ChangePasswordStartCommand, ChangePasswordCompleteCommand, ChangeEmailStartCommand, ChangeEmailConfirmOldCommand, ChangeEmailCompleteCommand
from src.application.commands import SetPhoneCommand, SetEncryptedMnemonicStartCommand, SetEncryptedMnemonicCompleteCommand, UpdateBankDetailsStartCommand, UpdateBankDetailsCompleteCommand, ChangePasswordStartCommand, ChangePasswordCompleteCommand, ChangeEmailStartCommand, ChangeEmailConfirmOldCommand, ChangeEmailCompleteCommand
from src.application.domain.dto import AuthContext
from src.presentation.decorators import require_access_token
from src.presentation.dependencies import (
get_set_phone_command,
get_set_crypto_wallet_start_command,
get_set_crypto_wallet_complete_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,
@@ -16,7 +16,7 @@ from src.presentation.dependencies import (
get_change_email_confirm_old_command,
get_change_email_complete_command,
)
from src.presentation.schemas import SetPhoneRequest, CryptoWalletConfirmRequest, BankConfirmRequest, ChangePasswordConfirmRequest, ChangeEmailConfirmOldRequest, ChangeEmailCompleteRequest
from src.presentation.schemas import SetPhoneRequest, EncryptedMnemonicConfirmRequest, BankConfirmRequest, ChangePasswordConfirmRequest, ChangeEmailConfirmOldRequest, ChangeEmailCompleteRequest
account_settings_router = APIRouter(prefix='/settings')
@@ -33,29 +33,29 @@ async def set_phone(
return ORJSONResponse(status_code=status.HTTP_200_OK, content={'phone': user.phone})
@account_settings_router.post(path='/crypto-wallet/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def crypto_wallet_start(
@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: SetCryptoWalletStartCommand = Depends(get_set_crypto_wallet_start_command),
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='/crypto-wallet/complete', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def crypto_wallet_complete(
@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: CryptoWalletConfirmRequest,
body: EncryptedMnemonicConfirmRequest,
auth: AuthContext = Depends(require_access_token),
command: SetCryptoWalletCompleteCommand = Depends(get_set_crypto_wallet_complete_command),
command: SetEncryptedMnemonicCompleteCommand = Depends(get_set_encrypted_mnemonic_complete_command),
):
user = await command(
user_id=auth.user_id,
code=body.code,
wallet_address=body.wallet_address,
encrypted_mnemonic=body.encrypted_mnemonic,
)
return {'crypto_wallet': user.crypto_wallet}
return {'encrypted_mnemonic': user.encrypted_mnemonic}
@account_settings_router.post(path='/email/start', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)

View File

@@ -1,5 +1,5 @@
from src.presentation.schemas.phone import SetPhoneRequest
from src.presentation.schemas.bank import BankUpdateRequest, BankConfirmRequest
from src.presentation.schemas.crypto_wallet import CryptoWalletConfirmRequest
from src.presentation.schemas.encrypted_mnemonic import EncryptedMnemonicConfirmRequest
from src.presentation.schemas.password import ChangePasswordConfirmRequest
from src.presentation.schemas.email import ChangeEmailConfirmOldRequest, ChangeEmailCompleteRequest

View File

@@ -2,9 +2,9 @@ import re
from pydantic import BaseModel, field_validator
class CryptoWalletConfirmRequest(BaseModel):
class EncryptedMnemonicConfirmRequest(BaseModel):
code: str
wallet_address: str
encrypted_mnemonic: str
@field_validator('code')
@classmethod
@@ -14,10 +14,10 @@ class CryptoWalletConfirmRequest(BaseModel):
raise ValueError('Code must be exactly 6 digits')
return v
@field_validator('wallet_address')
@field_validator('encrypted_mnemonic')
@classmethod
def validate_tron_address(cls, v: str) -> str:
def validate_encrypted_mnemonic(cls, v: str) -> str:
v = v.strip()
if not re.match(r'^T[1-9A-HJ-NP-Za-km-z]{33}$', v):
raise ValueError('Invalid TRON wallet address')
if not v:
raise ValueError('encrypted_mnemonic must not be empty')
return v