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', }