Files
kyc/src/main.py
2026-07-01 12:21:51 +03:00

173 lines
6.4 KiB
Python

from __future__ import annotations
from contextlib import asynccontextmanager
import secrets
from typing import AsyncGenerator
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import Depends,FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from src.application.commands import PollKycSessionsCommand
from src.application.domain.enums import LogFormat,LogLevel
from src.application.domain.exceptions import ApplicationException,UnauthorizedException
from src.infrastructure.beorg import BeorgService
from src.infrastructure.config.settings import get_settings
from src.infrastructure.database.context import async_session_maker
from src.infrastructure.database.unit_of_work import UnitOfWork
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
from src.infrastructure.utils import generate_instance_id
from src.infrastructure.logger import logger
from src.infrastructure.config import settings
from src.presentation.handlers import application_exception_handler,unhandled_exception_handler,validation_exception_handler
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
from src.presentation.routing import kyc_router
security = HTTPBasic()
async def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)) -> HTTPBasicCredentials:
user_ok = secrets.compare_digest(credentials.username, settings.DOCS_USERNAME)
pass_ok = secrets.compare_digest(credentials.password, settings.DOCS_PASSWORD)
if not (user_ok and pass_ok):
exception = UnauthorizedException()
exception.headers = {'WWW-Authenticate': 'Basic'}
raise exception
return credentials
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
instance_id = generate_instance_id()
logger.set_format(LogFormat(settings.LOG_FORMAT.lower()))
logger.set_min_level(LogLevel[settings.LOG_LEVEL.upper()])
logger.set_instance_id(instance_id)
logger.info(f'Users service instance started with id {instance_id}')
jwt_store = JwtKeyStore(
vault_addr=settings.VAULT_ADDR,
vault_role_id=settings.VAULT_ROLE_ID,
vault_secret_id=settings.VAULT_SECRET_ID,
vault_namespace=settings.VAULT_NAMESPACE,
mount_point=settings.VAULT_MOUNT_POINT,
kid_path=settings.VAULT_JWT_KID_PATH,
kids_prefix=settings.VAULT_JWT_KIDS_PREFIX,
)
await jwt_store.refresh()
jwt_scheduler = start_jwt_keys_scheduler(jwt_store, refresh_seconds=settings.JWT_KEYS_REFRESH_SECONDS)
kyc_poll_command = PollKycSessionsCommand(
unit_of_work=UnitOfWork(session_factory=async_session_maker,logger=logger),
logger=logger,
beorg_service=BeorgService(
project_id=settings.BEORG_PROJECT_ID,
machine_uid=settings.BEORG_MACHINE_UID,
token=settings.BEORG_TOKEN,
process_info=settings.BEORG_PROCESS_INFO,
timeout=settings.BEORG_TIMEOUT,
),
batch_size=settings.KYC_POLL_BATCH_SIZE,
)
kyc_scheduler = AsyncIOScheduler()
kyc_scheduler.add_job(
kyc_poll_command.__call__,
'interval',
seconds=settings.KYC_POLL_SECONDS,
max_instances=1,
)
kyc_scheduler.start()
app.state.jwt_key_store = jwt_store
app.state.jwt_keys_scheduler = jwt_scheduler
app.state.kyc_scheduler = kyc_scheduler
try:
yield
finally:
app.state.kyc_scheduler.shutdown(wait=False)
app.state.jwt_keys_scheduler.shutdown(wait=False)
logger.info(f'KYC service instance ended with id {instance_id}')
app: FastAPI = FastAPI(
redoc_url=None,
docs_url=None,
lifespan=lifespan,
title='Elcsa Users Service'
)
app.add_exception_handler(ApplicationException, application_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(Exception, unhandled_exception_handler)
app.include_router(kyc_router)
app.add_middleware(TraceIDMiddleware, logger=logger)
app.add_middleware(
SecurityHeadersMiddleware,
hsts=True,
hsts_preload=False,
frame_options='DENY',
referrer_policy='strict-origin-when-cross-origin',
content_security_policy=(
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net; "
"img-src 'self' data: https:; "
"font-src 'self' https://unpkg.com https://cdn.jsdelivr.net data:; "
"connect-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"object-src 'none'"
),
)
app.add_middleware(
CORSMiddleware,
allow_origins=[],
allow_origin_regex=settings.CORS_ALLOW_ORIGIN_REGEX,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
@app.get('/docs', include_in_schema=False)
async def custom_swagger_ui_html(_credentials: HTTPBasicCredentials = Depends(verify_credentials)) -> HTMLResponse:
'''Custom Swagger documentation, optionally protected with basic authentication.'''
return get_swagger_ui_html(
openapi_url=getattr(app, 'openapi_url', '/openapi.json'),
title=getattr(app, 'title', 'FastAPI') + ' - Swagger UI',
oauth2_redirect_url=getattr(app, 'swagger_ui_oauth2_redirect_url', None),
swagger_js_url='https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js',
swagger_css_url='https://unpkg.com/swagger-ui-dist@5/swagger-ui.css',
)
@app.get('/redoc', include_in_schema=False)
async def custom_redoc_html(_credentials: HTTPBasicCredentials = Depends(verify_credentials)) -> HTMLResponse:
'''Custom ReDoc documentation, optionally protected with basic authentication.'''
return get_redoc_html(
openapi_url=getattr(app, 'openapi_url', '/openapi.json'),
title=getattr(app, 'title', 'FastAPI') + ' - ReDoc',
redoc_js_url='https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js',
)
@app.post('/ping')
async def ping() -> dict[str, str]:
return {
'message': 'pong',
'status': 'ok',
}
@app.get('/healthcheck')
@app.get('/health')
async def health() -> dict[str, str]:
return {
'status': 'ok',
}