feat: add endpoints for orgs
This commit is contained in:
47
src/presentation/decorators/admin_auth.py
Normal file
47
src/presentation/decorators/admin_auth.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
|
||||
from src.application.contracts import IJwtService
|
||||
from src.application.domain.dto import AdminAuthContext
|
||||
from src.application.domain.exceptions import ForbiddenException, UnauthorizedException
|
||||
from src.presentation.dependencies.security import get_jwt_service
|
||||
|
||||
|
||||
def _extract_bearer_token(request: Request) -> str | None:
|
||||
auth = request.headers.get('Authorization')
|
||||
if not auth:
|
||||
return None
|
||||
scheme, param = get_authorization_scheme_param(auth)
|
||||
if scheme.lower() == 'bearer' and param:
|
||||
return param
|
||||
return None
|
||||
|
||||
|
||||
async def require_admin_access(
|
||||
request: Request,
|
||||
jwt_service: IJwtService = Depends(get_jwt_service),
|
||||
) -> AdminAuthContext:
|
||||
token = _extract_bearer_token(request)
|
||||
if not token:
|
||||
raise UnauthorizedException(message='Authorization Bearer token required')
|
||||
|
||||
payload = await jwt_service.decode_access_token(token)
|
||||
if payload.type != 'access':
|
||||
raise UnauthorizedException(message='Invalid token type')
|
||||
|
||||
role = payload.role
|
||||
if not role:
|
||||
raise UnauthorizedException(message='Token missing role')
|
||||
|
||||
return AdminAuthContext(admin_user_id=payload.sub, role=role)
|
||||
|
||||
|
||||
def require_admin_role(*allowed_roles: str):
|
||||
allowed = set(allowed_roles)
|
||||
|
||||
async def dependency(auth: AdminAuthContext = Depends(require_admin_access)) -> AdminAuthContext:
|
||||
if auth.role not in allowed:
|
||||
raise ForbiddenException(message='Insufficient permissions')
|
||||
return auth
|
||||
|
||||
return dependency
|
||||
@@ -33,4 +33,7 @@ async def require_access_token(
|
||||
if payload.type != 'access':
|
||||
raise UnauthorizedException(message='Invalid token type')
|
||||
|
||||
if not payload.sid:
|
||||
raise UnauthorizedException(message='Invalid token type')
|
||||
|
||||
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)
|
||||
|
||||
@@ -2,7 +2,12 @@ from fastapi import Depends
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.commands import (
|
||||
CreatePurchaseRequestCommand,
|
||||
GetOrganizationCommand,
|
||||
GetOrganizationMnemonicCommand,
|
||||
GetOrganizationSecretKeysCommand,
|
||||
GetPurchaseRequestCommand,
|
||||
ListOrganizationWalletsCommand,
|
||||
ListOrganizationsCommand,
|
||||
ListPurchaseRequestsCommand,
|
||||
)
|
||||
from src.application.contracts import ILogger
|
||||
@@ -29,3 +34,38 @@ def get_get_purchase_request_command(
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
) -> GetPurchaseRequestCommand:
|
||||
return GetPurchaseRequestCommand(unit_of_work=unit_of_work, logger=logger)
|
||||
|
||||
|
||||
def get_list_organizations_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
) -> ListOrganizationsCommand:
|
||||
return ListOrganizationsCommand(unit_of_work=unit_of_work, logger=logger)
|
||||
|
||||
|
||||
def get_get_organization_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
) -> GetOrganizationCommand:
|
||||
return GetOrganizationCommand(unit_of_work=unit_of_work, logger=logger)
|
||||
|
||||
|
||||
def get_list_organization_wallets_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
) -> ListOrganizationWalletsCommand:
|
||||
return ListOrganizationWalletsCommand(unit_of_work=unit_of_work, logger=logger)
|
||||
|
||||
|
||||
def get_get_organization_mnemonic_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
) -> GetOrganizationMnemonicCommand:
|
||||
return GetOrganizationMnemonicCommand(unit_of_work=unit_of_work, logger=logger)
|
||||
|
||||
|
||||
def get_get_organization_secret_keys_command(
|
||||
logger: ILogger = Depends(get_logger),
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
) -> GetOrganizationSecretKeysCommand:
|
||||
return GetOrganizationSecretKeysCommand(unit_of_work=unit_of_work, logger=logger)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.presentation.routing.organizations import organizations_router
|
||||
from src.presentation.routing.purchase_requests import purchase_requests_router
|
||||
|
||||
__all__ = ['purchase_requests_router']
|
||||
v1_router = APIRouter(prefix='/v1')
|
||||
v1_router.include_router(organizations_router)
|
||||
|
||||
__all__ = ['purchase_requests_router', 'organizations_router', 'v1_router']
|
||||
|
||||
95
src/presentation/routing/organizations.py
Normal file
95
src/presentation/routing/organizations.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from src.application.commands import (
|
||||
GetOrganizationCommand,
|
||||
GetOrganizationMnemonicCommand,
|
||||
GetOrganizationSecretKeysCommand,
|
||||
ListOrganizationWalletsCommand,
|
||||
ListOrganizationsCommand,
|
||||
)
|
||||
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_get_organization_command,
|
||||
get_get_organization_mnemonic_command,
|
||||
get_get_organization_secret_keys_command,
|
||||
get_list_organization_wallets_command,
|
||||
get_list_organizations_command,
|
||||
)
|
||||
from src.presentation.schemas.organization import (
|
||||
MnemonicResponse,
|
||||
OrganizationListResponse,
|
||||
OrganizationResponse,
|
||||
SecretKeyResponse,
|
||||
WalletResponse,
|
||||
)
|
||||
from src.presentation.serializers.organization import (
|
||||
mnemonic_to_response,
|
||||
organization_to_response,
|
||||
secret_key_to_response,
|
||||
wallet_to_response,
|
||||
)
|
||||
|
||||
organizations_router = APIRouter(prefix='/organizations', tags=['organizations'])
|
||||
|
||||
|
||||
@organizations_router.get('', response_model=OrganizationListResponse)
|
||||
async def list_organizations(
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListOrganizationsCommand = Depends(get_list_organizations_command),
|
||||
):
|
||||
items, total = await command(limit=limit, offset=offset)
|
||||
return OrganizationListResponse(
|
||||
items=[organization_to_response(x) for x in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@organizations_router.get('/{organization_id}', response_model=OrganizationResponse)
|
||||
async def get_organization(
|
||||
organization_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: GetOrganizationCommand = Depends(get_get_organization_command),
|
||||
):
|
||||
org = await command(organization_id)
|
||||
return organization_to_response(org)
|
||||
|
||||
|
||||
@organizations_router.get('/{organization_id}/wallets', response_model=list[WalletResponse])
|
||||
async def list_organization_wallets(
|
||||
organization_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListOrganizationWalletsCommand = Depends(get_list_organization_wallets_command),
|
||||
):
|
||||
wallets = await command(organization_id)
|
||||
return [wallet_to_response(w) for w in wallets]
|
||||
|
||||
|
||||
@organizations_router.get('/{organization_id}/wallets/mnemonic', response_model=MnemonicResponse)
|
||||
async def get_organization_mnemonic(
|
||||
organization_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: GetOrganizationMnemonicCommand = Depends(get_get_organization_mnemonic_command),
|
||||
):
|
||||
mnemonic = await command(organization_id)
|
||||
return mnemonic_to_response(mnemonic)
|
||||
|
||||
|
||||
@organizations_router.get('/{organization_id}/wallets/secret-keys', response_model=list[SecretKeyResponse])
|
||||
async def get_organization_secret_keys(
|
||||
organization_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: GetOrganizationSecretKeysCommand = Depends(get_get_organization_secret_keys_command),
|
||||
):
|
||||
keys = await command(organization_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
|
||||
]
|
||||
51
src/presentation/schemas/organization.py
Normal file
51
src/presentation/schemas/organization.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OrganizationResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
short_name: str | None
|
||||
inn: str
|
||||
ogrn: str | None
|
||||
kpp: str | None
|
||||
legal_address: str | None
|
||||
actual_address: str | None
|
||||
bank_details: dict[str, Any] | None
|
||||
contact_person: str | None
|
||||
contact_phone: str | None
|
||||
status: str
|
||||
kyc_verified: bool
|
||||
kyc_verified_at: str | None
|
||||
has_wallets: bool
|
||||
created_by: str | None
|
||||
created_at: str | None
|
||||
updated_at: str | None
|
||||
|
||||
|
||||
class OrganizationListResponse(BaseModel):
|
||||
items: list[OrganizationResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class WalletResponse(BaseModel):
|
||||
id: str
|
||||
chain: str
|
||||
address: str
|
||||
derivation_path: str
|
||||
created_at: str | None
|
||||
|
||||
|
||||
class MnemonicResponse(BaseModel):
|
||||
mnemonic: str
|
||||
|
||||
|
||||
class SecretKeyResponse(BaseModel):
|
||||
chain: str
|
||||
address: str
|
||||
derivation_path: str
|
||||
private_key: str
|
||||
63
src/presentation/serializers/organization.py
Normal file
63
src/presentation/serializers/organization.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from src.application.domain.entities.legal_entity import LegalEntityEntity
|
||||
from src.application.domain.entities.organization_wallet import OrganizationWalletEntity
|
||||
from src.presentation.schemas.organization import (
|
||||
MnemonicResponse,
|
||||
OrganizationResponse,
|
||||
SecretKeyResponse,
|
||||
WalletResponse,
|
||||
)
|
||||
|
||||
|
||||
def organization_to_response(entity: LegalEntityEntity) -> OrganizationResponse:
|
||||
return OrganizationResponse(
|
||||
id=entity.id,
|
||||
user_id=entity.user_id,
|
||||
name=entity.name,
|
||||
short_name=entity.short_name,
|
||||
inn=entity.inn,
|
||||
ogrn=entity.ogrn,
|
||||
kpp=entity.kpp,
|
||||
legal_address=entity.legal_address,
|
||||
actual_address=entity.actual_address,
|
||||
bank_details=entity.bank_details,
|
||||
contact_person=entity.contact_person,
|
||||
contact_phone=entity.contact_phone,
|
||||
status=entity.status,
|
||||
kyc_verified=entity.kyc_verified,
|
||||
kyc_verified_at=entity.kyc_verified_at.isoformat() if entity.kyc_verified_at else None,
|
||||
has_wallets=bool(entity.encrypted_mnemonic),
|
||||
created_by=entity.created_by,
|
||||
created_at=entity.created_at.isoformat() if entity.created_at else None,
|
||||
updated_at=entity.updated_at.isoformat() if entity.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
def wallet_to_response(entity: OrganizationWalletEntity) -> WalletResponse:
|
||||
return WalletResponse(
|
||||
id=entity.id,
|
||||
chain=entity.chain,
|
||||
address=entity.address,
|
||||
derivation_path=entity.derivation_path,
|
||||
created_at=entity.created_at.isoformat() if entity.created_at else None,
|
||||
)
|
||||
|
||||
|
||||
def mnemonic_to_response(mnemonic: str) -> MnemonicResponse:
|
||||
return MnemonicResponse(mnemonic=mnemonic)
|
||||
|
||||
|
||||
def secret_key_to_response(
|
||||
*,
|
||||
chain: str,
|
||||
address: str,
|
||||
derivation_path: str,
|
||||
private_key: str,
|
||||
) -> SecretKeyResponse:
|
||||
return SecretKeyResponse(
|
||||
chain=chain,
|
||||
address=address,
|
||||
derivation_path=derivation_path,
|
||||
private_key=private_key,
|
||||
)
|
||||
Reference in New Issue
Block a user