Init
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user