Files
Sergey Antropoff e4240a7be5 Init
2026-07-17 15:57:36 +03:00

151 lines
4.9 KiB
Python

from __future__ import annotations
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.openapi.utils import get_openapi
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from app.api import admin, wraps
from app.config import get_settings
from app.db import SessionLocal
from app.services.settings_service import get_or_create_settings
from app.services.storage import storage
OPENAPI_TAGS = [
{
"name": "System",
"description": "Service health and runtime checks.",
},
{
"name": "Public",
"description": "Anonymous client configuration (limits, captcha, password mode).",
},
{
"name": "Wraps",
"description": "Zero-knowledge create / one-time unwrap of encrypted packages.",
},
{
"name": "Admin",
"description": "Protected admin JSON API. Authorize with HTTP Basic (admin username/password from env).",
},
]
@asynccontextmanager
async def lifespan(_app: FastAPI):
await storage.ensure_bucket()
async with SessionLocal() as db:
await get_or_create_settings(db)
yield
def custom_openapi(app: FastAPI):
if app.openapi_schema:
return app.openapi_schema
schema = get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
tags=OPENAPI_TAGS,
)
schema.setdefault("components", {}).setdefault("securitySchemes", {})
schema["components"]["securitySchemes"]["HTTPBasic"] = {
"type": "http",
"scheme": "basic",
"description": "Admin username and password from environment (ADMIN_USERNAME / ADMIN_PASSWORD).",
}
# Mark Admin-tagged operations as secured
for path_item in schema.get("paths", {}).values():
for operation in path_item.values():
if not isinstance(operation, dict):
continue
if "Admin" in (operation.get("tags") or []):
operation["security"] = [{"HTTPBasic": []}]
app.openapi_schema = schema
return app.openapi_schema
def create_app() -> FastAPI:
settings = get_settings()
docs_on = settings.is_docs_enabled
app = FastAPI(
title=settings.app_name,
version="0.1.0",
description=(
"Wrapped — zero-knowledge one-time encrypted drop.\n\n"
"Public wrap APIs are anonymous. Admin APIs require HTTP Basic."
),
lifespan=lifespan,
openapi_tags=OPENAPI_TAGS if docs_on else None,
docs_url="/docs" if docs_on else None,
redoc_url="/redoc" if docs_on else None,
openapi_url="/openapi.json" if docs_on else None,
)
# Honor X-Forwarded-For / X-Real-IP from ingress / reverse proxies.
trusted = (settings.trusted_proxies or "").strip() or "*"
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=trusted)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(wraps.router)
app.include_router(admin.router)
app.include_router(admin.api_router)
templates = Jinja2Templates(directory="app/templates")
@app.get("/health", tags=["System"], summary="Health check", include_in_schema=docs_on)
async def health():
return {"status": "ok", "service": settings.app_name}
@app.get("/", include_in_schema=False)
async def index(request: Request):
return templates.TemplateResponse(
request,
"create.html",
{"title": "Wrapped", "page": "create"},
)
@app.get("/w/{wrap_id}", include_in_schema=False)
async def unwrap_page(request: Request, wrap_id: str):
return templates.TemplateResponse(
request,
"unwrap.html",
{"title": "Unwrap", "page": "unwrap", "wrap_id": wrap_id},
)
@app.get("/unwrap", include_in_schema=False)
async def unwrap_token_page(request: Request):
return templates.TemplateResponse(
request,
"unwrap.html",
{"title": "Unwrap", "page": "unwrap", "wrap_id": ""},
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
if (
exc.status_code == 401
and request.url.path.startswith("/admin")
and not request.url.path.startswith("/admin/api")
):
return RedirectResponse("/admin/login", status_code=302)
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
headers=exc.headers,
)
if docs_on:
app.openapi = lambda: custom_openapi(app)
return app
app = create_app()