init
This commit is contained in:
184
src/main.py
Normal file
184
src/main.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
import secrets
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import Depends, FastAPI, status
|
||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.config.settings import get_settings
|
||||
from src.infrastructure.crypto.wallet_crypto import load_master_key_from_vault
|
||||
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.handler import application_exception_handler
|
||||
from src.presentation.handler import unhandled_exception_handler
|
||||
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
||||
from src.presentation.routing import v1_router
|
||||
from src.presentation.schemas import ErrorResponse
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
ERROR_RESPONSES: dict[int, str] = {
|
||||
status.HTTP_400_BAD_REQUEST: 'Bad Request',
|
||||
status.HTTP_401_UNAUTHORIZED: 'Unauthorized',
|
||||
status.HTTP_403_FORBIDDEN: 'Forbidden',
|
||||
status.HTTP_404_NOT_FOUND: 'Not Found',
|
||||
status.HTTP_409_CONFLICT: 'Conflict',
|
||||
status.HTTP_429_TOO_MANY_REQUESTS: 'Too Many Requests',
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR: 'Internal Server Error',
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE: 'Service Unavailable',
|
||||
}
|
||||
|
||||
|
||||
def custom_openapi() -> dict:
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
|
||||
openapi_schema = get_openapi(
|
||||
title=app.title,
|
||||
version=app.version,
|
||||
description=app.description,
|
||||
routes=app.routes,
|
||||
license_info=app.license_info,
|
||||
)
|
||||
components = openapi_schema.setdefault('components', {})
|
||||
schemas = components.setdefault('schemas', {})
|
||||
schemas['ErrorResponse'] = ErrorResponse.model_json_schema()
|
||||
|
||||
for path_item in openapi_schema.get('paths', {}).values():
|
||||
for operation in path_item.values():
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
responses = operation.setdefault('responses', {})
|
||||
for status_code, description in ERROR_RESPONSES.items():
|
||||
responses.setdefault(
|
||||
str(status_code),
|
||||
{
|
||||
'description': description,
|
||||
'content': {
|
||||
'application/json': {
|
||||
'schema': {'$ref': '#/components/schemas/ErrorResponse'},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
app.openapi_schema = openapi_schema
|
||||
return app.openapi_schema
|
||||
|
||||
|
||||
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):
|
||||
raise ApplicationException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
message='Unauthorized',
|
||||
headers={'WWW-Authenticate': 'Basic'},
|
||||
)
|
||||
return credentials
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
instance_id = generate_instance_id()
|
||||
logger.set_instance_id(instance_id)
|
||||
logger.info(f'Admin 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)
|
||||
app.state.jwt_key_store = jwt_store
|
||||
app.state.jwt_keys_scheduler = jwt_scheduler
|
||||
|
||||
load_master_key_from_vault(
|
||||
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,
|
||||
path=settings.VAULT_CRYPTO_MASTER_KEY_PATH,
|
||||
)
|
||||
logger.info('Crypto master key loaded from Vault')
|
||||
yield
|
||||
logger.info('Shutting down...')
|
||||
sched = getattr(app.state, 'jwt_keys_scheduler', None)
|
||||
if sched:
|
||||
sched.shutdown(wait=False)
|
||||
logger.info('Admin service stopped')
|
||||
|
||||
|
||||
app: FastAPI = FastAPI(
|
||||
redoc_url=None,
|
||||
docs_url=None,
|
||||
lifespan=lifespan,
|
||||
title='Bitforce. Admin Service',
|
||||
version='1.0.0',
|
||||
description='Admin API for legal entities, wallets, documents, and B2B purchase requests',
|
||||
license_info={'name': 'MIT', 'url': 'https://opensource.org/licenses/MIT'},
|
||||
)
|
||||
|
||||
app.add_exception_handler(ApplicationException, application_exception_handler)
|
||||
app.add_exception_handler(Exception, unhandled_exception_handler)
|
||||
app.include_router(v1_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'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'",
|
||||
)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
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:
|
||||
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:
|
||||
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.openapi = custom_openapi
|
||||
Reference in New Issue
Block a user