init
This commit is contained in:
12
src/presentation/routing/__init__.py
Normal file
12
src/presentation/routing/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.presentation.routing.auth import auth_router
|
||||
from src.presentation.routing.documents import documents_router
|
||||
from src.presentation.routing.organizations import organizations_router
|
||||
from src.presentation.routing.purchase_requests import purchase_requests_router
|
||||
|
||||
v1_router = APIRouter(prefix='/v1')
|
||||
v1_router.include_router(auth_router)
|
||||
v1_router.include_router(organizations_router)
|
||||
v1_router.include_router(documents_router)
|
||||
v1_router.include_router(purchase_requests_router)
|
||||
47
src/presentation/routing/auth.py
Normal file
47
src/presentation/routing/auth.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
from src.application.commands import AdminLoginCommand, GetAdminMeCommand
|
||||
from src.application.domain.dto import AdminAuthContext
|
||||
from src.presentation.decorators.admin_auth import require_admin_access
|
||||
from src.presentation.dependencies.commands import get_admin_login_command, get_admin_me_command
|
||||
from src.presentation.schemas.admin_auth import AdminLoginRequest, AdminLoginResponse, AdminMeResponse
|
||||
|
||||
auth_router = APIRouter(prefix='/auth', tags=['auth'])
|
||||
|
||||
|
||||
@auth_router.post('/login', response_model=AdminLoginResponse, status_code=status.HTTP_200_OK)
|
||||
async def admin_login(
|
||||
body: AdminLoginRequest,
|
||||
command: AdminLoginCommand = Depends(get_admin_login_command),
|
||||
):
|
||||
dto = await command(email=str(body.email), password=body.password)
|
||||
return AdminLoginResponse(
|
||||
access_token=dto.access_token,
|
||||
id=dto.id,
|
||||
email=dto.email,
|
||||
first_name=dto.first_name,
|
||||
last_name=dto.last_name,
|
||||
role=dto.role,
|
||||
)
|
||||
|
||||
|
||||
@auth_router.post('/logout', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
|
||||
async def admin_logout():
|
||||
"""Клиент удаляет access_token локально. Сервер stateless."""
|
||||
return {'ok': True}
|
||||
|
||||
|
||||
@auth_router.get('/me', response_model=AdminMeResponse)
|
||||
async def admin_me(
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: GetAdminMeCommand = Depends(get_admin_me_command),
|
||||
):
|
||||
admin = await command(auth.admin_user_id)
|
||||
return AdminMeResponse(
|
||||
id=admin.id,
|
||||
email=admin.email,
|
||||
first_name=admin.first_name,
|
||||
last_name=admin.last_name,
|
||||
role=admin.role,
|
||||
)
|
||||
70
src/presentation/routing/documents.py
Normal file
70
src/presentation/routing/documents.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from fastapi import APIRouter, Depends, File, Form, UploadFile, status
|
||||
|
||||
from src.application.commands import (
|
||||
GetOrganizationDocumentCommand,
|
||||
ListOrganizationDocumentsCommand,
|
||||
UploadOrganizationDocumentCommand,
|
||||
)
|
||||
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_document_command,
|
||||
get_list_organization_documents_command,
|
||||
get_s3_documents_service,
|
||||
get_upload_organization_document_command,
|
||||
)
|
||||
from src.infrastructure.storage.s3_documents_service import S3DocumentsService
|
||||
from src.presentation.schemas.mappers import document_to_response
|
||||
from src.presentation.schemas.organization import DocumentResponse
|
||||
|
||||
documents_router = APIRouter(prefix='/organizations/{organization_id}/documents', tags=['documents'])
|
||||
|
||||
|
||||
@documents_router.get('', response_model=list[DocumentResponse])
|
||||
async def list_documents(
|
||||
organization_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListOrganizationDocumentsCommand = Depends(get_list_organization_documents_command),
|
||||
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||
):
|
||||
docs = await command(organization_id)
|
||||
result: list[DocumentResponse] = []
|
||||
for doc in docs:
|
||||
url = await s3.generate_presigned_download_url(key=doc.s3_key)
|
||||
result.append(document_to_response(doc, download_url=url))
|
||||
return result
|
||||
|
||||
|
||||
@documents_router.post('', response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def upload_document(
|
||||
organization_id: str,
|
||||
document_type: str = Form(...),
|
||||
file: UploadFile = File(...),
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: UploadOrganizationDocumentCommand = Depends(get_upload_organization_document_command),
|
||||
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||
):
|
||||
body = await file.read()
|
||||
saved = await command(
|
||||
organization_id=organization_id,
|
||||
admin_user_id=auth.admin_user_id,
|
||||
document_type=document_type,
|
||||
file_name=file.filename or 'document',
|
||||
content_type=file.content_type or 'application/octet-stream',
|
||||
body=body,
|
||||
)
|
||||
url = await s3.generate_presigned_download_url(key=saved.s3_key)
|
||||
return document_to_response(saved, download_url=url)
|
||||
|
||||
|
||||
@documents_router.get('/{document_id}', response_model=DocumentResponse)
|
||||
async def get_document(
|
||||
organization_id: str,
|
||||
document_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||
):
|
||||
doc = await command(organization_id, document_id)
|
||||
url = await s3.generate_presigned_download_url(key=doc.s3_key)
|
||||
return document_to_response(doc, download_url=url)
|
||||
70
src/presentation/routing/jwt.py
Normal file
70
src/presentation/routing/jwt.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from starlette import status
|
||||
|
||||
from src.application.commands import AdminJwtRefreshCommand
|
||||
from src.application.domain.exceptions import ApplicationException, RefreshConcurrentException
|
||||
from src.infrastructure.config import settings
|
||||
from src.presentation.dependencies.commands import get_admin_jwt_refresh_command
|
||||
|
||||
jwt_router = APIRouter(prefix='/jwt', tags=['jwt'])
|
||||
|
||||
|
||||
def _clear_auth_cookies(response: ORJSONResponse) -> None:
|
||||
response.delete_cookie('access_token', path='/', domain=settings.ADMIN_COOKIE_DOMAIN)
|
||||
response.delete_cookie('refresh_token', path='/', domain=settings.ADMIN_COOKIE_DOMAIN)
|
||||
|
||||
|
||||
def _set_auth_cookies(response: ORJSONResponse, access: str, refresh: str) -> None:
|
||||
response.set_cookie(
|
||||
key='access_token',
|
||||
value=access,
|
||||
httponly=True,
|
||||
secure=settings.ADMIN_COOKIE_SECURE,
|
||||
samesite='lax',
|
||||
path='/',
|
||||
domain=settings.ADMIN_COOKIE_DOMAIN,
|
||||
max_age=int(settings.JWT_ACCESS_TTL_SECONDS),
|
||||
)
|
||||
response.set_cookie(
|
||||
key='refresh_token',
|
||||
value=refresh,
|
||||
httponly=True,
|
||||
secure=settings.ADMIN_COOKIE_SECURE,
|
||||
samesite='lax',
|
||||
path='/',
|
||||
domain=settings.ADMIN_COOKIE_DOMAIN,
|
||||
max_age=int(settings.JWT_REFRESH_TTL_SECONDS),
|
||||
)
|
||||
|
||||
|
||||
@jwt_router.post('/refresh', response_class=ORJSONResponse, status_code=status.HTTP_200_OK)
|
||||
async def refresh_tokens(
|
||||
request: Request,
|
||||
command: AdminJwtRefreshCommand = Depends(get_admin_jwt_refresh_command),
|
||||
):
|
||||
refresh_token = request.cookies.get('refresh_token')
|
||||
if not refresh_token:
|
||||
response = ORJSONResponse({'ok': False, 'error': 'No refresh token'}, status_code=401)
|
||||
_clear_auth_cookies(response)
|
||||
return response
|
||||
|
||||
xff = request.headers.get('x-forwarded-for')
|
||||
ip = xff.split(',')[0].strip() if xff else (request.client.host if request.client else None)
|
||||
user_agent = request.headers.get('user-agent')
|
||||
|
||||
try:
|
||||
tokens = await command(refresh_token=refresh_token, ip=ip, user_agent=user_agent)
|
||||
except RefreshConcurrentException:
|
||||
return ORJSONResponse({'result': True, 'concurrent': True}, status_code=status.HTTP_200_OK)
|
||||
except ApplicationException as exc:
|
||||
if exc.status_code == status.HTTP_401_UNAUTHORIZED:
|
||||
response = ORJSONResponse({'result': False}, status_code=401)
|
||||
_clear_auth_cookies(response)
|
||||
return response
|
||||
raise
|
||||
|
||||
access, refresh = tokens
|
||||
response = ORJSONResponse({'result': True})
|
||||
_set_auth_cookies(response, access, refresh)
|
||||
return response
|
||||
102
src/presentation/routing/organizations.py
Normal file
102
src/presentation/routing/organizations.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
|
||||
from src.application.commands import (
|
||||
CreateOrganizationCommand,
|
||||
CreateOrganizationWalletsCommand,
|
||||
GetOrganizationCommand,
|
||||
ListOrganizationsCommand,
|
||||
UpdateOrganizationCommand,
|
||||
)
|
||||
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_create_organization_command,
|
||||
get_create_organization_wallets_command,
|
||||
get_get_organization_command,
|
||||
get_list_organizations_command,
|
||||
get_update_organization_command,
|
||||
)
|
||||
from src.presentation.schemas.mappers import organization_to_response, wallet_to_response
|
||||
from src.presentation.schemas.organization import (
|
||||
CreateOrganizationRequest,
|
||||
OrganizationListResponse,
|
||||
OrganizationResponse,
|
||||
UpdateOrganizationRequest,
|
||||
WalletResponse,
|
||||
)
|
||||
|
||||
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.post('', response_model=OrganizationResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_organization(
|
||||
body: CreateOrganizationRequest,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: CreateOrganizationCommand = Depends(get_create_organization_command),
|
||||
):
|
||||
org = await command(
|
||||
admin_user_id=auth.admin_user_id,
|
||||
email=str(body.email),
|
||||
password=body.password,
|
||||
name=body.name,
|
||||
short_name=body.short_name,
|
||||
inn=body.inn,
|
||||
ogrn=body.ogrn,
|
||||
kpp=body.kpp,
|
||||
legal_address=body.legal_address,
|
||||
actual_address=body.actual_address,
|
||||
bank_details=body.bank_details,
|
||||
contact_person=body.contact_person,
|
||||
contact_phone=body.contact_phone,
|
||||
status=body.status,
|
||||
)
|
||||
return organization_to_response(org)
|
||||
|
||||
|
||||
@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.patch('/{organization_id}', response_model=OrganizationResponse)
|
||||
async def update_organization(
|
||||
organization_id: str,
|
||||
body: UpdateOrganizationRequest,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: UpdateOrganizationCommand = Depends(get_update_organization_command),
|
||||
):
|
||||
org = await command(organization_id, values=body.model_dump(exclude_unset=True))
|
||||
return organization_to_response(org)
|
||||
|
||||
|
||||
@organizations_router.post(
|
||||
'/{organization_id}/wallets/create',
|
||||
response_model=list[WalletResponse],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_organization_wallets(
|
||||
organization_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: CreateOrganizationWalletsCommand = Depends(get_create_organization_wallets_command),
|
||||
):
|
||||
wallets = await command(organization_id=organization_id)
|
||||
return [wallet_to_response(w) for w in wallets]
|
||||
90
src/presentation/routing/purchase_requests.py
Normal file
90
src/presentation/routing/purchase_requests.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
|
||||
from src.application.commands import (
|
||||
GetPurchaseRequestCommand,
|
||||
ListPurchaseRequestsCommand,
|
||||
SetPurchaseRequestQuoteCommand,
|
||||
UpdatePurchaseRequestStatusCommand,
|
||||
)
|
||||
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_purchase_request_command,
|
||||
get_list_purchase_requests_command,
|
||||
get_set_purchase_request_quote_command,
|
||||
get_update_purchase_request_status_command,
|
||||
)
|
||||
from src.presentation.schemas.mappers import purchase_request_to_response
|
||||
from src.presentation.schemas.organization import (
|
||||
PurchaseRequestListResponse,
|
||||
PurchaseRequestResponse,
|
||||
SetPurchaseRequestQuoteBody,
|
||||
UpdatePurchaseRequestStatusBody,
|
||||
)
|
||||
|
||||
purchase_requests_router = APIRouter(prefix='/purchase-requests', tags=['purchase-requests'])
|
||||
|
||||
|
||||
@purchase_requests_router.get('', response_model=PurchaseRequestListResponse)
|
||||
async def list_purchase_requests(
|
||||
status_filter: str | None = Query(default=None, alias='status'),
|
||||
organization_id: str | None = Query(default=None),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListPurchaseRequestsCommand = Depends(get_list_purchase_requests_command),
|
||||
):
|
||||
items, total = await command(
|
||||
status=status_filter,
|
||||
organization_id=organization_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return PurchaseRequestListResponse(
|
||||
items=[purchase_request_to_response(x) for x in items],
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
@purchase_requests_router.get('/{request_id}', response_model=PurchaseRequestResponse)
|
||||
async def get_purchase_request(
|
||||
request_id: str,
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: GetPurchaseRequestCommand = Depends(get_get_purchase_request_command),
|
||||
):
|
||||
item = await command(request_id)
|
||||
return purchase_request_to_response(item)
|
||||
|
||||
|
||||
@purchase_requests_router.patch('/{request_id}/status', response_model=PurchaseRequestResponse)
|
||||
async def update_purchase_request_status(
|
||||
request_id: str,
|
||||
body: UpdatePurchaseRequestStatusBody,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: UpdatePurchaseRequestStatusCommand = Depends(get_update_purchase_request_status_command),
|
||||
):
|
||||
item = await command(
|
||||
request_id,
|
||||
status=body.status,
|
||||
admin_comment=body.admin_comment,
|
||||
assigned_to=body.assigned_to,
|
||||
tx_hash=body.tx_hash,
|
||||
)
|
||||
return purchase_request_to_response(item)
|
||||
|
||||
|
||||
@purchase_requests_router.post('/{request_id}/quote', response_model=PurchaseRequestResponse)
|
||||
async def set_purchase_request_quote(
|
||||
request_id: str,
|
||||
body: SetPurchaseRequestQuoteBody,
|
||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||
command: SetPurchaseRequestQuoteCommand = Depends(get_set_purchase_request_quote_command),
|
||||
):
|
||||
item = await command(
|
||||
request_id,
|
||||
rub_amount=body.rub_amount,
|
||||
exchange_rate=body.exchange_rate,
|
||||
service_fee_percent=body.service_fee_percent,
|
||||
admin_comment=body.admin_comment,
|
||||
)
|
||||
return purchase_request_to_response(item)
|
||||
Reference in New Issue
Block a user