Compare commits

2 Commits

Author SHA1 Message Date
4aae631c73 feat: add user endpoints 2026-06-11 18:46:21 +03:00
e0f044b455 refactor: split org responses for unification 2026-06-11 13:25:45 +03:00
24 changed files with 632 additions and 127 deletions

View File

@@ -9,6 +9,7 @@ from src.application.abstractions.repositories import (
IOrganizationWalletRepository, IOrganizationWalletRepository,
IPurchaseRequestRepository, IPurchaseRequestRepository,
IUserRepository, IUserRepository,
IWalletRepository,
) )
@@ -37,3 +38,6 @@ class IUnitOfWork(Protocol):
@property @property
def purchase_request_repository(self) -> IPurchaseRequestRepository: ... def purchase_request_repository(self) -> IPurchaseRequestRepository: ...
@property
def wallet_repository(self) -> IWalletRepository: ...

View File

@@ -4,3 +4,4 @@ from src.application.abstractions.repositories.i_admin_session_repository import
from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository
from src.application.abstractions.repositories.i_organization_wallet_repository import IOrganizationWalletRepository from src.application.abstractions.repositories.i_organization_wallet_repository import IOrganizationWalletRepository
from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository
from src.application.abstractions.repositories.i_wallets_repository import IWalletRepository

View File

@@ -18,6 +18,14 @@ class IUserRepository(ABC):
kyc_verified_at: datetime, kyc_verified_at: datetime,
) -> UserEntity: ) -> UserEntity:
raise NotImplementedError raise NotImplementedError
@abstractmethod
async def list_all(self, *, limit: int, offset: int, search: str | None = None) -> list[UserEntity]:
raise NotImplementedError
@abstractmethod
async def get_by_id(self, user_id: str) -> UserEntity:
raise NotImplementedError
@abstractmethod @abstractmethod
async def get_user_by_email(self, email: str) -> UserEntity: async def get_user_by_email(self, email: str) -> UserEntity:
@@ -35,6 +43,10 @@ class IUserRepository(ABC):
async def count_individuals(self, *, query: str) -> int: async def count_individuals(self, *, query: str) -> int:
raise NotImplementedError raise NotImplementedError
@abstractmethod
async def count_all(self, *, search: str) -> int:
raise NotImplementedError
@abstractmethod @abstractmethod
async def set_password(self, user_id: str, password_hash: str) -> UserEntity: async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
raise NotImplementedError raise NotImplementedError

View File

@@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
from src.application.domain.entities.user import WalletEntity
class IWalletRepository(ABC):
@abstractmethod
async def list_by_user(self, user_id: str) -> list[WalletEntity]:
raise NotImplementedError
@abstractmethod
async def exists_for_user(self, user_id: str) -> bool:
raise NotImplementedError

View File

@@ -29,6 +29,15 @@ from src.application.commands.purchase_request_commands import (
UpdatePurchaseRequestStatusCommand, UpdatePurchaseRequestStatusCommand,
SetPurchaseRequestQuoteCommand, SetPurchaseRequestQuoteCommand,
) )
from src.application.commands.user_commands import (
ListUsersCommand,
GetUserCommand,
)
from src.application.commands.user_wallet_commands import (
GetUserMnemonicCommand,
GetUserSecretKeysCommand,
ListUserWalletsCommand,
)
__all__ = [ __all__ = [
'AdminLoginCommand', 'AdminLoginCommand',
@@ -54,4 +63,9 @@ __all__ = [
'GetOrganizationMnemonicCommand', 'GetOrganizationMnemonicCommand',
'GetOrganizationSecretKeysCommand', 'GetOrganizationSecretKeysCommand',
'SetPasswordCommand', 'SetPasswordCommand',
'ListUsersCommand',
'GetUserCommand',
'ListUserWalletsCommand',
'GetUserMnemonicCommand',
'GetUserSecretKeysCommand',
] ]

View File

@@ -16,14 +16,14 @@ class SetPasswordCommand:
self._logger = logger self._logger = logger
@transactional @transactional
async def __call__(self, email: str, password: str) -> bool: async def __call__(self, user_id: str, password: str) -> bool:
try: try:
password_hash = await self._hash_service.hash(password) password_hash = await self._hash_service.hash(password)
await self._unit_of_work.user_repository.set_password( await self._unit_of_work.user_repository.set_password(
user_email=email, user_id=user_id,
password_hash=password_hash, password_hash=password_hash,
) )
self._logger.info(f'Set password for user {email}') self._logger.info(f'Set password for user {user_id}')
return True return True
except ApplicationException: except ApplicationException:
raise raise

View File

@@ -0,0 +1,42 @@
from __future__ import annotations
from typing import Any
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ILogger
from src.application.domain.entities.organization import PartySearchEntity
from src.application.domain.entities.user import UserEntity
from src.application.domain.enums.account_type import AccountType
from src.infrastructure.database.decorators import transactional
class ListUsersCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(
self,
*,
limit: int = 50,
offset: int = 0,
search: str | None = None,
) -> tuple[list[UserEntity], int]:
items = await self._unit_of_work.user_repository.list_all(
limit=limit,
offset=offset,
search=search,
)
total = await self._unit_of_work.user_repository.count_all(search=search)
return items, total
class GetUserCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, user_id: str) -> UserEntity:
return await self._unit_of_work.user_repository.get_by_id(user_id)

View File

@@ -0,0 +1,63 @@
from __future__ import annotations
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ILogger
from src.application.domain.entities.user import UserEntity, WalletEntity
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.crypto.wallet_crypto import (
DerivedSecretKey,
decrypt_mnemonic,
derive_all_private_keys,
is_crypto_ready,
)
from src.infrastructure.database.decorators import transactional
class ListUserWalletsCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, user_id: str) -> list[WalletEntity]:
await self._unit_of_work.user_repository.get_by_id(user_id)
return await self._unit_of_work.wallet_repository.list_by_user(user_id)
class GetUserMnemonicCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, user_id: str) -> str:
user = await self._require_wallets_user(user_id)
if not is_crypto_ready():
raise ApplicationException(status_code=503, message='Crypto service not ready')
return decrypt_mnemonic(user.encrypted_mnemonic or '')
async def _require_wallets_user(self, user_id: str) -> UserEntity:
user = await self._unit_of_work.user_repository.get_by_id(user_id)
if not user.encrypted_mnemonic:
raise ApplicationException(status_code=404, message='Wallets not created for user')
return user
class GetUserSecretKeysCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work
self._logger = logger
@transactional
async def __call__(self, user_id: str) -> list[DerivedSecretKey]:
user = await self._require_wallets_user(user_id)
if not is_crypto_ready():
raise ApplicationException(status_code=503, message='Crypto service not ready')
mnemonic = decrypt_mnemonic(user.encrypted_mnemonic or '')
return derive_all_private_keys(mnemonic)
async def _require_wallets_user(self, user_id: str) -> UserEntity:
user = await self._unit_of_work.user_repository.get_by_id(user_id)
if not user.encrypted_mnemonic:
raise ApplicationException(status_code=404, message='Wallets not created for user')
return user

View File

@@ -32,3 +32,12 @@ class UserEntity:
account_type: str | None = None account_type: str | None = None
provisioned_by: str | None = None provisioned_by: str | None = None
provisioned_at: datetime | None = None provisioned_at: datetime | None = None
@dataclass
class WalletEntity:
id: str
user_id: str
chain: str
address: str
derivation_path: str
created_at: datetime | None = None

View File

@@ -5,6 +5,7 @@ from src.infrastructure.database.models.admin_session import AdminSessionModel
from src.infrastructure.database.models.legal_entity import LegalEntityModel from src.infrastructure.database.models.legal_entity import LegalEntityModel
from src.infrastructure.database.models.organization_wallet import OrganizationWalletModel from src.infrastructure.database.models.organization_wallet import OrganizationWalletModel
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
from src.infrastructure.database.models.wallets import WalletModel
__all__ = [ __all__ = [
'Base', 'Base',
@@ -14,4 +15,5 @@ __all__ = [
'LegalEntityModel', 'LegalEntityModel',
'OrganizationWalletModel', 'OrganizationWalletModel',
'PurchaseRequestModel', 'PurchaseRequestModel',
'WalletModel',
] ]

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from src.infrastructure.database.models.base import Base
from src.infrastructure.database.models.mixins import UlidPrimaryKeyMixin
class WalletModel(Base, UlidPrimaryKeyMixin):
__tablename__ = 'wallets'
user_id: Mapped[str] = mapped_column(
String(26),
ForeignKey('users.id', ondelete='RESTRICT'),
nullable=False,
index=True,
)
chain: Mapped[str] = mapped_column(String(16), nullable=False)
address: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
derivation_path: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)

View File

@@ -4,6 +4,7 @@ from src.infrastructure.database.repositories.user_repository import UserReposit
from src.infrastructure.database.repositories.legal_entity_repository import LegalEntityRepository from src.infrastructure.database.repositories.legal_entity_repository import LegalEntityRepository
from src.infrastructure.database.repositories.organization_wallet_repository import OrganizationWalletRepository from src.infrastructure.database.repositories.organization_wallet_repository import OrganizationWalletRepository
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
from src.infrastructure.database.repositories.wallets_repository import WalletRepository
__all__ = [ __all__ = [
'AdminUserRepository', 'AdminUserRepository',
@@ -12,4 +13,5 @@ __all__ = [
'LegalEntityRepository', 'LegalEntityRepository',
'OrganizationWalletRepository', 'OrganizationWalletRepository',
'PurchaseRequestRepository', 'PurchaseRequestRepository',
'WalletRepository',
] ]

View File

@@ -45,6 +45,16 @@ class UserRepository(IUserRepository):
provisioned_by=user.provisioned_by, provisioned_by=user.provisioned_by,
provisioned_at=user.provisioned_at, provisioned_at=user.provisioned_at,
) )
async def get_by_id(self, user_id: str) -> UserEntity:
try:
user = await self._get_user(user_id)
return self._to_entity(user)
except ApplicationException:
raise
except SQLAlchemyError as exception:
self._logger.exception(str(exception))
raise InternalServerException(message=f'Database error: {str(exception)}')
async def create_legal_entity_user( async def create_legal_entity_user(
self, self,
@@ -104,9 +114,8 @@ class UserRepository(IUserRepository):
self._logger.exception(str(exception)) self._logger.exception(str(exception))
raise InternalServerException(message=f'Database error: {str(exception)}') raise InternalServerException(message=f'Database error: {str(exception)}')
async def set_password(self, user_email: str, password_hash: str) -> UserEntity: async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
user = await self.get_user_by_email(user_email) return await self._update_field(user_id, password_hash=password_hash)
return await self._update_field(user.id, password_hash=password_hash)
async def get_user_by_email(self, email: str) -> UserEntity: async def get_user_by_email(self, email: str) -> UserEntity:
try: try:
@@ -126,6 +135,41 @@ class UserRepository(IUserRepository):
stmt = select(UserModel.id).where(UserModel.email == email, UserModel.is_deleted.is_(False)).limit(1) stmt = select(UserModel.id).where(UserModel.email == email, UserModel.is_deleted.is_(False)).limit(1)
result = await self._session.execute(stmt) result = await self._session.execute(stmt)
return result.scalar_one_or_none() is not None return result.scalar_one_or_none() is not None
def _search_filter(self, search: str | None):
if not search or not search.strip():
return None
pattern = f'%{search.strip()}%'
return or_(
UserModel.id.ilike(pattern),
UserModel.email.ilike(pattern),
UserModel.last_name.ilike(pattern),
UserModel.first_name.ilike(pattern),
UserModel.middle_name.ilike(pattern),
UserModel.phone.ilike(pattern),
UserModel.inn.ilike(pattern),
)
async def list_all(
self,
*,
limit: int,
offset: int,
search: str | None = None,
) -> list[UserEntity]:
stmt = (
select(UserModel)
.where(UserModel.is_deleted.is_(False))
.order_by(UserModel.created_at.desc())
.limit(limit)
.offset(offset)
)
search_filter = self._search_filter(search)
if search_filter is not None:
stmt = stmt.where(search_filter)
res = await self._session.execute(stmt)
return [self._to_entity(m) for m in res.scalars().all()]
def _individual_search_filter(self, query: str): def _individual_search_filter(self, query: str):
pattern = f'%{query.strip()}%' pattern = f'%{query.strip()}%'
@@ -192,3 +236,14 @@ class UserRepository(IUserRepository):
) )
result = await self._session.execute(stmt) result = await self._session.execute(stmt)
return int(result.scalar_one()) return int(result.scalar_one())
async def count_all(self, *, search: str | None = None) -> int:
stmt = (
select(func.count(UserModel.id))
.where(UserModel.is_deleted.is_(False))
)
search_filter = self._search_filter(search)
if search_filter is not None:
stmt = stmt.where(search_filter)
res = await self._session.execute(stmt)
return int(res.scalar_one())

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from src.application.abstractions.repositories import IWalletRepository
from src.application.contracts import ILogger
from src.application.domain.entities.user import WalletEntity
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.models import WalletModel
class WalletRepository(IWalletRepository):
def __init__(self, session: AsyncSession, logger: ILogger):
self._session = session
self._logger = logger
def _to_entity(self, m: WalletModel) -> WalletEntity:
return WalletEntity(
id=m.id,
user_id=m.user_id,
chain=m.chain,
address=m.address,
derivation_path=m.derivation_path,
created_at=m.created_at,
)
async def list_by_user(self, user_id: str) -> list[WalletEntity]:
res = await self._session.execute(
select(WalletModel)
.where(WalletModel.user_id == user_id)
.order_by(WalletModel.chain)
)
return [self._to_entity(m) for m in res.scalars().all()]
async def exists_for_user(self, user_id: str) -> bool:
res = await self._session.execute(
select(WalletModel.id)
.where(WalletModel.user_id == user_id)
.limit(1)
)
return res.scalar_one_or_none() is not None

View File

@@ -8,6 +8,7 @@ from src.application.abstractions.repositories import (
IOrganizationWalletRepository, IOrganizationWalletRepository,
IPurchaseRequestRepository, IPurchaseRequestRepository,
IUserRepository, IUserRepository,
IWalletRepository,
) )
from src.application.contracts import ILogger from src.application.contracts import ILogger
from src.application.domain.exceptions import RefreshConcurrentException from src.application.domain.exceptions import RefreshConcurrentException
@@ -18,6 +19,7 @@ from src.infrastructure.database.repositories import (
OrganizationWalletRepository, OrganizationWalletRepository,
PurchaseRequestRepository, PurchaseRequestRepository,
UserRepository, UserRepository,
WalletRepository,
) )
@@ -31,6 +33,7 @@ class UnitOfWork(IUnitOfWork):
self._legal_entity_repository: ILegalEntityRepository | None = None self._legal_entity_repository: ILegalEntityRepository | None = None
self._organization_wallet_repository: IOrganizationWalletRepository | None = None self._organization_wallet_repository: IOrganizationWalletRepository | None = None
self._purchase_request_repository: IPurchaseRequestRepository | None = None self._purchase_request_repository: IPurchaseRequestRepository | None = None
self._wallet_repository: IWalletRepository | None = None
self._logger: ILogger = logger self._logger: ILogger = logger
async def __aenter__(self): async def __aenter__(self):
@@ -84,6 +87,14 @@ class UnitOfWork(IUnitOfWork):
session=self._session, logger=self._logger session=self._session, logger=self._logger
) )
return self._organization_wallet_repository return self._organization_wallet_repository
@property
def wallet_repository(self) -> IWalletRepository:
if self._wallet_repository is None:
self._wallet_repository = WalletRepository(
session=self._session, logger=self._logger
)
return self._wallet_repository
@property @property
def purchase_request_repository(self) -> IPurchaseRequestRepository: def purchase_request_repository(self) -> IPurchaseRequestRepository:

View File

@@ -27,6 +27,11 @@ from src.application.commands import (
UpdatePurchaseRequestStatusCommand, UpdatePurchaseRequestStatusCommand,
PutOrganizationDocumentCommand, PutOrganizationDocumentCommand,
SetPasswordCommand, SetPasswordCommand,
ListUsersCommand,
GetUserCommand,
ListUserWalletsCommand,
GetUserMnemonicCommand,
GetUserSecretKeysCommand,
) )
from src.application.contracts import IHashService, IJwtService, ILogger from src.application.contracts import IHashService, IJwtService, ILogger
from src.infrastructure.config import settings from src.infrastructure.config import settings
@@ -215,3 +220,33 @@ def get_set_password_command(
logger: ILogger = Depends(get_logger), logger: ILogger = Depends(get_logger),
) -> SetPasswordCommand: ) -> SetPasswordCommand:
return SetPasswordCommand(uow, hash_service, logger) return SetPasswordCommand(uow, hash_service, logger)
def get_list_users_command(
uow: IUnitOfWork = Depends(get_unit_of_work),
logger: ILogger = Depends(get_logger),
) -> ListUsersCommand:
return ListUsersCommand(uow, logger)
def get_get_user_command(
uow: IUnitOfWork = Depends(get_unit_of_work),
logger: ILogger = Depends(get_logger),
) -> GetUserCommand:
return GetUserCommand(uow, logger)
def get_list_user_wallets_command(
uow: IUnitOfWork = Depends(get_unit_of_work),
logger: ILogger = Depends(get_logger),
) -> ListUserWalletsCommand:
return ListUserWalletsCommand(uow, logger)
def get_get_user_mnemonic_command(
uow: IUnitOfWork = Depends(get_unit_of_work),
logger: ILogger = Depends(get_logger),
) -> GetUserMnemonicCommand:
return GetUserMnemonicCommand(uow, logger)
def get_get_user_secret_keys_command(
uow: IUnitOfWork = Depends(get_unit_of_work),
logger: ILogger = Depends(get_logger),
) -> GetUserSecretKeysCommand:
return GetUserSecretKeysCommand(uow, logger)

View File

@@ -34,13 +34,19 @@ from src.presentation.schemas.mappers import (
) )
from src.presentation.schemas.organization import ( from src.presentation.schemas.organization import (
CreateOrganizationRequest, CreateOrganizationRequest,
OrganizationListResponse,
DocumentResponse,
OrganizationResponse,
UpdateOrganizationRequest,
)
from src.presentation.schemas.entity import (
CreateWalletsResponse, CreateWalletsResponse,
MnemonicResponse, MnemonicResponse,
OrganizationListResponse, PartySearchResponse,
OrganizationResponse,
PartySearchListResponse, PartySearchListResponse,
PurchaseRequestResponse,
PurchaseRequestListResponse,
SecretKeyResponse, SecretKeyResponse,
UpdateOrganizationRequest,
WalletResponse, WalletResponse,
) )

View File

@@ -19,7 +19,7 @@ from src.presentation.dependencies.commands import (
get_update_purchase_request_status_command, get_update_purchase_request_status_command,
) )
from src.presentation.schemas.mappers import purchase_request_to_response from src.presentation.schemas.mappers import purchase_request_to_response
from src.presentation.schemas.organization import ( from src.presentation.schemas.entity import (
CreatePurchaseRequestBody, CreatePurchaseRequestBody,
PurchaseRequestListResponse, PurchaseRequestListResponse,
PurchaseRequestResponse, PurchaseRequestResponse,

View File

@@ -1,17 +1,116 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request, Query, status
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from starlette import status from starlette import status
from src.presentation.dependencies.commands import get_set_password_command
from src.application.domain.dto import AdminAuthContext
from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role
from src.presentation.dependencies.commands import (
get_set_password_command,
get_list_users_command,
get_get_user_command,
get_list_user_wallets_command,
get_get_user_mnemonic_command,
get_get_user_secret_keys_command,
)
from src.presentation.schemas.mappers import (
mnemonic_to_response,
user_to_response,
party_search_to_response,
secret_key_to_response,
wallet_to_response,
)
from src.presentation.schemas.user import (
UserResponse,
UserListResponse,
)
from src.presentation.schemas.entity import (
PartySearchResponse,
MnemonicResponse,
PartySearchListResponse,
SecretKeyResponse,
WalletResponse,
)
from src.application.commands.set_password import SetPasswordCommand from src.application.commands.set_password import SetPasswordCommand
from src.application.commands.user_commands import (
ListUsersCommand,
GetUserCommand,
)
from src.application.commands.user_wallet_commands import (
ListUserWalletsCommand,
GetUserMnemonicCommand,
GetUserSecretKeysCommand
)
from src.presentation.schemas.password import SetPasswordRequest from src.presentation.schemas.password import SetPasswordRequest
users_router = APIRouter(prefix='/users', tags=['users']) users_router = APIRouter(prefix='/users', tags=['users'])
@users_router.patch(path='/password', response_class=ORJSONResponse, status_code=status.HTTP_200_OK) @users_router.get('', response_model=UserListResponse)
async def list_users(
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
q: str | None = Query(default=None, min_length=1, max_length=255),
auth: AdminAuthContext = Depends(require_admin_access),
command: ListUsersCommand = Depends(get_list_users_command),
):
items, total = await command(limit=limit, offset=offset, search=q)
return UserListResponse(
items=[user_to_response(x) for x in items],
total=total,
)
@users_router.get('/{user_id}', response_model=UserResponse)
async def get_user(
user_id: str,
auth: AdminAuthContext = Depends(require_admin_access),
command: GetUserCommand = Depends(get_get_user_command),
):
user = await command(user_id)
return user_to_response(user)
@users_router.get('/{user_id}/wallets', response_model=list[WalletResponse])
async def list_user_wallets(
user_id: str,
auth: AdminAuthContext = Depends(require_admin_access),
command: ListUserWalletsCommand = Depends(get_list_user_wallets_command),
):
wallets = await command(user_id)
return [wallet_to_response(w) for w in wallets]
@users_router.get('/{user_id}/wallets/mnemonic', response_model=MnemonicResponse)
async def get_user_mnemonic(
user_id: str,
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
command: GetUserMnemonicCommand = Depends(get_get_user_mnemonic_command),
):
mnemonic = await command(user_id)
return mnemonic_to_response(mnemonic)
@users_router.get('/{user_id}/wallets/secret-keys', response_model=list[SecretKeyResponse])
async def get_user_secret_keys(
user_id: str,
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
command: GetUserSecretKeysCommand = Depends(get_get_user_secret_keys_command),
):
keys = await command(user_id)
return [
secret_key_to_response(
chain=k.chain,
address=k.address,
derivation_path=k.derivation_path,
private_key=k.private_key,
)
for k in keys
]
@users_router.patch(path='/{user_id}/set_password', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
async def set_password( async def set_password(
request: Request, user_id: str,
request: Request,
body: SetPasswordRequest, body: SetPasswordRequest,
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
command: SetPasswordCommand = Depends(get_set_password_command), command: SetPasswordCommand = Depends(get_set_password_command),
): ):
await command(email=body.email, password=body.password) await command(user_id=user_id, password=body.password)
return ORJSONResponse(content={'message': 'Password updated successfully'}) return ORJSONResponse(content={'message': 'Password updated successfully'})

View File

@@ -0,0 +1,115 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from pydantic import BaseModel, Field
class PartySearchResponse(BaseModel):
id: str
account_type: str
user_id: str
email: str | None
name: str | None
inn: str | None
phone: str | None
status: str | None
kyc_verified: bool | None
created_at: str | None
class PartySearchListResponse(BaseModel):
items: list[PartySearchResponse]
total: int
class WalletResponse(BaseModel):
id: str
chain: str
address: str
derivation_path: str
created_at: str | None
class CreateWalletsResponse(BaseModel):
wallets: list[WalletResponse]
mnemonic: str
class MnemonicResponse(BaseModel):
mnemonic: str
class SecretKeyResponse(BaseModel):
chain: str
address: str
derivation_path: str
private_key: str
class CreatePurchaseRequestBody(BaseModel):
organization_id: str
usdt_amount: Decimal = Field(gt=0)
status: str = 'submitted'
rub_amount: Decimal | None = Field(default=None, gt=0)
exchange_rate: Decimal | None = Field(default=None, gt=0)
service_fee_percent: Decimal | None = Field(default=None, ge=0)
comment: str | None = None
admin_comment: str | None = None
target_wallet_chain: str | None = Field(default='ETH', max_length=16)
target_wallet_address: str | None = Field(default=None, max_length=128)
tx_hash: str | None = Field(default=None, max_length=128)
assigned_to: str | None = None
class UpdatePurchaseRequestStatusBody(BaseModel):
status: str
admin_comment: str | None = None
assigned_to: str | None = None
tx_hash: str | None = None
class UpdatePurchaseRequestBody(BaseModel):
status: str | None = None
usdt_amount: Decimal | None = Field(default=None, gt=0)
rub_amount: Decimal | None = Field(default=None, gt=0)
exchange_rate: Decimal | None = Field(default=None, gt=0)
service_fee_percent: Decimal | None = Field(default=None, ge=0)
comment: str | None = None
admin_comment: str | None = None
target_wallet_chain: str | None = Field(default=None, max_length=16)
target_wallet_address: str | None = Field(default=None, max_length=128)
tx_hash: str | None = Field(default=None, max_length=128)
assigned_to: str | None = None
class SetPurchaseRequestQuoteBody(BaseModel):
rub_amount: Decimal = Field(gt=0)
exchange_rate: Decimal = Field(gt=0)
service_fee_percent: Decimal | None = None
admin_comment: str | None = None
class PurchaseRequestResponse(BaseModel):
id: str
organization_id: str
status: str
usdt_amount: str
rub_amount: str | None
exchange_rate: str | None
service_fee_percent: str | None
comment: str | None
admin_comment: str | None
target_wallet_chain: str | None
target_wallet_address: str | None
tx_hash: str | None
assigned_to: str | None
created_at: str | None
updated_at: str | None
completed_at: str | None
class PurchaseRequestListResponse(BaseModel):
items: list[PurchaseRequestResponse]
total: int

View File

@@ -7,11 +7,19 @@ from src.application.domain.entities.organization import (
PartySearchEntity, PartySearchEntity,
PurchaseRequestEntity, PurchaseRequestEntity,
) )
from src.application.domain.entities.user import (
UserEntity,
)
from src.presentation.schemas.user import (
UserResponse,
)
from src.presentation.schemas.organization import ( from src.presentation.schemas.organization import (
CreateWalletsResponse,
DocumentResponse, DocumentResponse,
MnemonicResponse,
OrganizationResponse, OrganizationResponse,
)
from src.presentation.schemas.entity import (
CreateWalletsResponse,
MnemonicResponse,
PartySearchResponse, PartySearchResponse,
PurchaseRequestResponse, PurchaseRequestResponse,
SecretKeyResponse, SecretKeyResponse,
@@ -129,3 +137,23 @@ def purchase_request_to_response(entity: PurchaseRequestEntity) -> PurchaseReque
updated_at=entity.updated_at.isoformat() if entity.updated_at else None, updated_at=entity.updated_at.isoformat() if entity.updated_at else None,
completed_at=entity.completed_at.isoformat() if entity.completed_at else None, completed_at=entity.completed_at.isoformat() if entity.completed_at else None,
) )
def user_to_response(entity: UserEntity) -> UserResponse:
return UserResponse(
id=entity.id,
email=entity.email,
first_name=entity.first_name,
middle_name=entity.middle_name,
last_name=entity.last_name,
birth_date=entity.birth_date,
phone=entity.phone,
passport_data=entity.passport_data,
inn=entity.inn,
avatar_link=entity.avatar_link,
account_type=entity.account_type,
kyc_verified=entity.kyc_verified,
is_deleted=entity.is_deleted,
created_at=entity.created_at.isoformat() if entity.created_at else None,
updated_at=entity.updated_at.isoformat() if entity.updated_at else None,
kyc_verified_at=entity.kyc_verified_at.isoformat() if entity.kyc_verified_at else None,
)

View File

@@ -62,48 +62,6 @@ class OrganizationListResponse(BaseModel):
total: int total: int
class PartySearchResponse(BaseModel):
id: str
account_type: str
user_id: str
email: str | None
name: str | None
inn: str | None
phone: str | None
status: str | None
kyc_verified: bool | None
created_at: str | None
class PartySearchListResponse(BaseModel):
items: list[PartySearchResponse]
total: int
class WalletResponse(BaseModel):
id: str
chain: str
address: str
derivation_path: str
created_at: str | None
class CreateWalletsResponse(BaseModel):
wallets: list[WalletResponse]
mnemonic: str
class MnemonicResponse(BaseModel):
mnemonic: str
class SecretKeyResponse(BaseModel):
chain: str
address: str
derivation_path: str
private_key: str
class DocumentResponse(BaseModel): class DocumentResponse(BaseModel):
organization_id: str organization_id: str
document_type: str document_type: str
@@ -112,70 +70,3 @@ class DocumentResponse(BaseModel):
content_type: str | None = None content_type: str | None = None
file_size_bytes: int | None = None file_size_bytes: int | None = None
download_url: str | None = None download_url: str | None = None
class CreatePurchaseRequestBody(BaseModel):
organization_id: str
usdt_amount: Decimal = Field(gt=0)
status: str = 'submitted'
rub_amount: Decimal | None = Field(default=None, gt=0)
exchange_rate: Decimal | None = Field(default=None, gt=0)
service_fee_percent: Decimal | None = Field(default=None, ge=0)
comment: str | None = None
admin_comment: str | None = None
target_wallet_chain: str | None = Field(default='ETH', max_length=16)
target_wallet_address: str | None = Field(default=None, max_length=128)
tx_hash: str | None = Field(default=None, max_length=128)
assigned_to: str | None = None
class UpdatePurchaseRequestStatusBody(BaseModel):
status: str
admin_comment: str | None = None
assigned_to: str | None = None
tx_hash: str | None = None
class UpdatePurchaseRequestBody(BaseModel):
status: str | None = None
usdt_amount: Decimal | None = Field(default=None, gt=0)
rub_amount: Decimal | None = Field(default=None, gt=0)
exchange_rate: Decimal | None = Field(default=None, gt=0)
service_fee_percent: Decimal | None = Field(default=None, ge=0)
comment: str | None = None
admin_comment: str | None = None
target_wallet_chain: str | None = Field(default=None, max_length=16)
target_wallet_address: str | None = Field(default=None, max_length=128)
tx_hash: str | None = Field(default=None, max_length=128)
assigned_to: str | None = None
class SetPurchaseRequestQuoteBody(BaseModel):
rub_amount: Decimal = Field(gt=0)
exchange_rate: Decimal = Field(gt=0)
service_fee_percent: Decimal | None = None
admin_comment: str | None = None
class PurchaseRequestResponse(BaseModel):
id: str
organization_id: str
status: str
usdt_amount: str
rub_amount: str | None
exchange_rate: str | None
service_fee_percent: str | None
comment: str | None
admin_comment: str | None
target_wallet_chain: str | None
target_wallet_address: str | None
tx_hash: str | None
assigned_to: str | None
created_at: str | None
updated_at: str | None
completed_at: str | None
class PurchaseRequestListResponse(BaseModel):
items: list[PurchaseRequestResponse]
total: int

View File

@@ -4,7 +4,6 @@ from src.application.domain.password_policy import validate_password_strength
class SetPasswordRequest(BaseModel): class SetPasswordRequest(BaseModel):
email: str
password: str password: str
@field_validator('password') @field_validator('password')

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from decimal import Decimal
from typing import Any
from datetime import date, datetime
from pydantic import BaseModel, Field
class UserResponse(BaseModel):
id: str | None = Field(None, description='Идентификатор пользователя')
email: str | None = Field(None, description='Email')
first_name: str | None = Field(None, description='Имя')
middle_name: str | None = Field(None, description='Отчество')
last_name: str | None = Field(None, description='Фамилия')
birth_date: date | None = Field(None, description='Дата рождения')
phone: str | None = Field(None, description='Телефон')
passport_data: str | None = Field(None, description='Паспортные данные')
inn: str | None = Field(None, description='ИНН')
avatar_link: str | None = Field(None, description='HTTPS-ссылка на текущий аватар в хранилище')
account_type: str | None = Field(None, description='Тип аккаунта')
kyc_verified: bool | None = Field(None, description='Признак пройденного KYC')
is_deleted: bool | None = Field(None, description='Удалён ли аккаунт')
created_at: datetime | None = Field(None, description='Время создания записи')
updated_at: datetime | None = Field(None, description='Время последнего обновления')
kyc_verified_at: datetime | None = Field(None, description='Время подтверждения KYC')
class UserListResponse(BaseModel):
items: list[UserResponse]
total: int