Compare commits
2 Commits
99714ac476
...
4aae631c73
| Author | SHA1 | Date | |
|---|---|---|---|
| 4aae631c73 | |||
| e0f044b455 |
@@ -9,6 +9,7 @@ from src.application.abstractions.repositories import (
|
||||
IOrganizationWalletRepository,
|
||||
IPurchaseRequestRepository,
|
||||
IUserRepository,
|
||||
IWalletRepository,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,3 +38,6 @@ class IUnitOfWork(Protocol):
|
||||
|
||||
@property
|
||||
def purchase_request_repository(self) -> IPurchaseRequestRepository: ...
|
||||
|
||||
@property
|
||||
def wallet_repository(self) -> IWalletRepository: ...
|
||||
@@ -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_organization_wallet_repository import IOrganizationWalletRepository
|
||||
from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository
|
||||
from src.application.abstractions.repositories.i_wallets_repository import IWalletRepository
|
||||
|
||||
@@ -18,6 +18,14 @@ class IUserRepository(ABC):
|
||||
kyc_verified_at: datetime,
|
||||
) -> UserEntity:
|
||||
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
|
||||
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:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def count_all(self, *, search: str) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -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
|
||||
@@ -29,6 +29,15 @@ from src.application.commands.purchase_request_commands import (
|
||||
UpdatePurchaseRequestStatusCommand,
|
||||
SetPurchaseRequestQuoteCommand,
|
||||
)
|
||||
from src.application.commands.user_commands import (
|
||||
ListUsersCommand,
|
||||
GetUserCommand,
|
||||
)
|
||||
from src.application.commands.user_wallet_commands import (
|
||||
GetUserMnemonicCommand,
|
||||
GetUserSecretKeysCommand,
|
||||
ListUserWalletsCommand,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'AdminLoginCommand',
|
||||
@@ -54,4 +63,9 @@ __all__ = [
|
||||
'GetOrganizationMnemonicCommand',
|
||||
'GetOrganizationSecretKeysCommand',
|
||||
'SetPasswordCommand',
|
||||
'ListUsersCommand',
|
||||
'GetUserCommand',
|
||||
'ListUserWalletsCommand',
|
||||
'GetUserMnemonicCommand',
|
||||
'GetUserSecretKeysCommand',
|
||||
]
|
||||
|
||||
@@ -16,14 +16,14 @@ class SetPasswordCommand:
|
||||
self._logger = logger
|
||||
|
||||
@transactional
|
||||
async def __call__(self, email: str, password: str) -> bool:
|
||||
async def __call__(self, user_id: str, password: str) -> bool:
|
||||
try:
|
||||
password_hash = await self._hash_service.hash(password)
|
||||
await self._unit_of_work.user_repository.set_password(
|
||||
user_email=email,
|
||||
user_id=user_id,
|
||||
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
|
||||
except ApplicationException:
|
||||
raise
|
||||
|
||||
42
src/application/commands/user_commands.py
Normal file
42
src/application/commands/user_commands.py
Normal 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)
|
||||
63
src/application/commands/user_wallet_commands.py
Normal file
63
src/application/commands/user_wallet_commands.py
Normal 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
|
||||
@@ -32,3 +32,12 @@ class UserEntity:
|
||||
account_type: str | None = None
|
||||
provisioned_by: str | 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
|
||||
|
||||
@@ -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.organization_wallet import OrganizationWalletModel
|
||||
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
|
||||
from src.infrastructure.database.models.wallets import WalletModel
|
||||
|
||||
__all__ = [
|
||||
'Base',
|
||||
@@ -14,4 +15,5 @@ __all__ = [
|
||||
'LegalEntityModel',
|
||||
'OrganizationWalletModel',
|
||||
'PurchaseRequestModel',
|
||||
'WalletModel',
|
||||
]
|
||||
|
||||
28
src/infrastructure/database/models/wallets.py
Normal file
28
src/infrastructure/database/models/wallets.py
Normal 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(),
|
||||
)
|
||||
@@ -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.organization_wallet_repository import OrganizationWalletRepository
|
||||
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
|
||||
from src.infrastructure.database.repositories.wallets_repository import WalletRepository
|
||||
|
||||
__all__ = [
|
||||
'AdminUserRepository',
|
||||
@@ -12,4 +13,5 @@ __all__ = [
|
||||
'LegalEntityRepository',
|
||||
'OrganizationWalletRepository',
|
||||
'PurchaseRequestRepository',
|
||||
'WalletRepository',
|
||||
]
|
||||
|
||||
@@ -45,6 +45,16 @@ class UserRepository(IUserRepository):
|
||||
provisioned_by=user.provisioned_by,
|
||||
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(
|
||||
self,
|
||||
@@ -104,9 +114,8 @@ class UserRepository(IUserRepository):
|
||||
self._logger.exception(str(exception))
|
||||
raise InternalServerException(message=f'Database error: {str(exception)}')
|
||||
|
||||
async def set_password(self, user_email: 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)
|
||||
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
||||
return await self._update_field(user_id, password_hash=password_hash)
|
||||
|
||||
async def get_user_by_email(self, email: str) -> UserEntity:
|
||||
try:
|
||||
@@ -126,6 +135,41 @@ class UserRepository(IUserRepository):
|
||||
stmt = select(UserModel.id).where(UserModel.email == email, UserModel.is_deleted.is_(False)).limit(1)
|
||||
result = await self._session.execute(stmt)
|
||||
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):
|
||||
pattern = f'%{query.strip()}%'
|
||||
@@ -192,3 +236,14 @@ class UserRepository(IUserRepository):
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
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())
|
||||
|
||||
@@ -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
|
||||
@@ -8,6 +8,7 @@ from src.application.abstractions.repositories import (
|
||||
IOrganizationWalletRepository,
|
||||
IPurchaseRequestRepository,
|
||||
IUserRepository,
|
||||
IWalletRepository,
|
||||
)
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.exceptions import RefreshConcurrentException
|
||||
@@ -18,6 +19,7 @@ from src.infrastructure.database.repositories import (
|
||||
OrganizationWalletRepository,
|
||||
PurchaseRequestRepository,
|
||||
UserRepository,
|
||||
WalletRepository,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +33,7 @@ class UnitOfWork(IUnitOfWork):
|
||||
self._legal_entity_repository: ILegalEntityRepository | None = None
|
||||
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
|
||||
self._purchase_request_repository: IPurchaseRequestRepository | None = None
|
||||
self._wallet_repository: IWalletRepository | None = None
|
||||
self._logger: ILogger = logger
|
||||
|
||||
async def __aenter__(self):
|
||||
@@ -84,6 +87,14 @@ class UnitOfWork(IUnitOfWork):
|
||||
session=self._session, logger=self._logger
|
||||
)
|
||||
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
|
||||
def purchase_request_repository(self) -> IPurchaseRequestRepository:
|
||||
|
||||
@@ -27,6 +27,11 @@ from src.application.commands import (
|
||||
UpdatePurchaseRequestStatusCommand,
|
||||
PutOrganizationDocumentCommand,
|
||||
SetPasswordCommand,
|
||||
ListUsersCommand,
|
||||
GetUserCommand,
|
||||
ListUserWalletsCommand,
|
||||
GetUserMnemonicCommand,
|
||||
GetUserSecretKeysCommand,
|
||||
)
|
||||
from src.application.contracts import IHashService, IJwtService, ILogger
|
||||
from src.infrastructure.config import settings
|
||||
@@ -215,3 +220,33 @@ def get_set_password_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
) -> SetPasswordCommand:
|
||||
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)
|
||||
@@ -34,13 +34,19 @@ from src.presentation.schemas.mappers import (
|
||||
)
|
||||
from src.presentation.schemas.organization import (
|
||||
CreateOrganizationRequest,
|
||||
OrganizationListResponse,
|
||||
DocumentResponse,
|
||||
OrganizationResponse,
|
||||
UpdateOrganizationRequest,
|
||||
)
|
||||
from src.presentation.schemas.entity import (
|
||||
CreateWalletsResponse,
|
||||
MnemonicResponse,
|
||||
OrganizationListResponse,
|
||||
OrganizationResponse,
|
||||
PartySearchResponse,
|
||||
PartySearchListResponse,
|
||||
PurchaseRequestResponse,
|
||||
PurchaseRequestListResponse,
|
||||
SecretKeyResponse,
|
||||
UpdateOrganizationRequest,
|
||||
WalletResponse,
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from src.presentation.dependencies.commands import (
|
||||
get_update_purchase_request_status_command,
|
||||
)
|
||||
from src.presentation.schemas.mappers import purchase_request_to_response
|
||||
from src.presentation.schemas.organization import (
|
||||
from src.presentation.schemas.entity import (
|
||||
CreatePurchaseRequestBody,
|
||||
PurchaseRequestListResponse,
|
||||
PurchaseRequestResponse,
|
||||
|
||||
@@ -1,17 +1,116 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, Query, status
|
||||
from fastapi.responses import ORJSONResponse
|
||||
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.user_commands import (
|
||||
ListUsersCommand,
|
||||
GetUserCommand,
|
||||
)
|
||||
from src.application.commands.user_wallet_commands import (
|
||||
ListUserWalletsCommand,
|
||||
GetUserMnemonicCommand,
|
||||
GetUserSecretKeysCommand
|
||||
)
|
||||
from src.presentation.schemas.password import SetPasswordRequest
|
||||
|
||||
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(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
request: Request,
|
||||
body: SetPasswordRequest,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
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'})
|
||||
115
src/presentation/schemas/entity.py
Normal file
115
src/presentation/schemas/entity.py
Normal 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
|
||||
@@ -7,11 +7,19 @@ from src.application.domain.entities.organization import (
|
||||
PartySearchEntity,
|
||||
PurchaseRequestEntity,
|
||||
)
|
||||
from src.application.domain.entities.user import (
|
||||
UserEntity,
|
||||
)
|
||||
from src.presentation.schemas.user import (
|
||||
UserResponse,
|
||||
)
|
||||
from src.presentation.schemas.organization import (
|
||||
CreateWalletsResponse,
|
||||
DocumentResponse,
|
||||
MnemonicResponse,
|
||||
OrganizationResponse,
|
||||
)
|
||||
from src.presentation.schemas.entity import (
|
||||
CreateWalletsResponse,
|
||||
MnemonicResponse,
|
||||
PartySearchResponse,
|
||||
PurchaseRequestResponse,
|
||||
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,
|
||||
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,
|
||||
)
|
||||
@@ -62,48 +62,6 @@ class OrganizationListResponse(BaseModel):
|
||||
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):
|
||||
organization_id: str
|
||||
document_type: str
|
||||
@@ -112,70 +70,3 @@ class DocumentResponse(BaseModel):
|
||||
content_type: str | None = None
|
||||
file_size_bytes: int | 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
|
||||
|
||||
@@ -4,7 +4,6 @@ from src.application.domain.password_policy import validate_password_strength
|
||||
|
||||
|
||||
class SetPasswordRequest(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
@field_validator('password')
|
||||
|
||||
32
src/presentation/schemas/user.py
Normal file
32
src/presentation/schemas/user.py
Normal 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
|
||||
Reference in New Issue
Block a user