| import os |
| import time |
| import uuid |
| import logging |
| from contextlib import asynccontextmanager |
|
|
| from fastapi import FastAPI, Request, status |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.exceptions import RequestValidationError |
| from fastapi.responses import JSONResponse, FileResponse |
| from fastapi.staticfiles import StaticFiles |
|
|
| from app.database import init_db |
| from app.routers import auth, redact, feedback, history, health |
| from app.services.ner_engine import get_spacy_model, preload_fallback_model |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" |
| ) |
| logger = logging.getLogger("redactx") |
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| logger.info("Starting RedactX API server...") |
| |
| await init_db() |
| |
| |
| try: |
| logger.info("Pre-warming NLP models...") |
| get_spacy_model(level=1) |
| preload_fallback_model() |
| except Exception as e: |
| logger.warning(f"Model pre-warm warning: {e}") |
| |
| yield |
| logger.info("Shutting down RedactX API server...") |
|
|
| app = FastAPI(title="RedactX API", version="2.0.0", lifespan=lifespan) |
|
|
| |
| allowed_origins_env = os.environ.get("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:4433,http://127.0.0.1:5173,http://127.0.0.1:4433") |
| allowed_origins = [o.strip() for o in allowed_origins_env.split(",") if o.strip()] |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=allowed_origins, |
| allow_origin_regex=".*", |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| @app.middleware("http") |
| async def log_request_timing_middleware(request: Request, call_next): |
| request_id = str(uuid.uuid4())[:8] |
| start_time = time.time() |
| |
| health.REQUEST_COUNTS["total"] = health.REQUEST_COUNTS.get("total", 0) + 1 |
| |
| try: |
| response = await call_next(request) |
| duration = time.time() - start_time |
| logger.info(f"[{request_id}] {request.method} {request.url.path} -> {response.status_code} ({duration*1000:.2f}ms)") |
| |
| |
| path_key = request.url.path |
| if path_key not in health.PROCESSING_TIMES: |
| health.PROCESSING_TIMES[path_key] = [] |
| health.PROCESSING_TIMES[path_key].append(duration) |
| if len(health.PROCESSING_TIMES[path_key]) > 1000: |
| health.PROCESSING_TIMES[path_key].pop(0) |
| |
| return response |
| except Exception as e: |
| health.REQUEST_COUNTS["errors"] = health.REQUEST_COUNTS.get("errors", 0) + 1 |
| duration = time.time() - start_time |
| logger.error(f"[{request_id}] {request.method} {request.url.path} -> ERROR: {e} ({duration*1000:.2f}ms)") |
| raise |
|
|
| @app.exception_handler(RequestValidationError) |
| async def validation_exception_handler(request: Request, exc: RequestValidationError): |
| logger.error(f"Validation error on {request.url.path}: {exc}") |
| return JSONResponse( |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
| content={"detail": exc.errors()}, |
| ) |
|
|
| |
| app.include_router(auth.router) |
| app.include_router(redact.router) |
| app.include_router(feedback.router) |
| app.include_router(history.router) |
| app.include_router(health.router) |
|
|
| |
| frontend_dist_1 = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dist-react") |
| frontend_dist_2 = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "dist-react") |
| frontend_dist = frontend_dist_1 if os.path.exists(frontend_dist_1) else frontend_dist_2 |
|
|
| if os.path.exists(frontend_dist): |
| assets_dir = os.path.join(frontend_dist, "assets") |
| if os.path.exists(assets_dir): |
| app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") |
| |
| @app.get("/{full_path:path}") |
| async def serve_frontend(full_path: str): |
| if full_path.startswith("api/") or full_path in ["health", "metrics", "docs", "openapi.json"]: |
| return JSONResponse(status_code=404, content={"detail": "Not found"}) |
| file_path = os.path.join(frontend_dist, full_path) |
| if os.path.exists(file_path) and os.path.isfile(file_path): |
| return FileResponse(file_path) |
| return FileResponse(os.path.join(frontend_dist, "index.html")) |
| else: |
| @app.get("/") |
| def read_root(): |
| return {"message": "Welcome to the Redact API v2.0"} |
|
|