"""SmartUp FastAPI application entry point.""" import logging from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from app.config import get_settings from app.database import init_db from app.models.admin_user import AdminUser from app.database import SessionLocal from app.utils.auth import hash_password, verify_password, validate_password_supported from app.services.scheduler import start_scheduler, stop_scheduler from app.routers import auth, upstreams, webhooks, logs, custom_pages, websites, auth_capture logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") logger = logging.getLogger(__name__) def _validate_runtime_settings() -> None: settings = get_settings() if not settings.admin_password: raise RuntimeError("ADMIN_PASSWORD must be set") if settings.admin_password in {"changeme", "changeme123"}: raise RuntimeError("ADMIN_PASSWORD must not use the default placeholder") if not settings.jwt_secret or settings.jwt_secret == "change-me-in-production": raise RuntimeError("JWT_SECRET must be set to a non-default value") if not settings.cors_origin_list: raise RuntimeError("CORS_ORIGINS must include at least one explicit origin") validate_password_supported(settings.admin_password) def _init_admin() -> None: settings = get_settings() db = SessionLocal() try: existing = db.query(AdminUser).filter(AdminUser.email == settings.admin_email).first() if existing: # Sync password hash if .env has changed since first creation if not verify_password(settings.admin_password, existing.password_hash): existing.password_hash = hash_password(settings.admin_password) db.commit() logger.info("admin password updated: %s", settings.admin_email) else: logger.info("admin user already exists: %s", settings.admin_email) else: user = AdminUser( email=settings.admin_email, password_hash=hash_password(settings.admin_password), ) db.add(user) db.commit() logger.info("admin user created: %s", settings.admin_email) finally: db.close() @asynccontextmanager async def lifespan(app: FastAPI): _validate_runtime_settings() init_db() _init_admin() start_scheduler() yield stop_scheduler() app = FastAPI( title="SmartUp", description="API 上游管理与 Webhook 通知系统", version="1.0.0", lifespan=lifespan, docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", ) settings = get_settings() app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(GZipMiddleware, minimum_size=1024) # API routers app.include_router(auth.router) app.include_router(upstreams.router) app.include_router(webhooks.router) app.include_router(logs.router) app.include_router(custom_pages.router) app.include_router(websites.router) app.include_router(auth_capture.router) @app.get("/healthz") def health(): return {"status": "ok"} # Serve frontend static files STATIC_DIR = Path(__file__).parent.parent / "static" if STATIC_DIR.exists(): app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets") @app.api_route("/favicon.svg", methods=["GET", "HEAD"]) def serve_favicon(): favicon = STATIC_DIR / "favicon.svg" if not favicon.exists(): raise HTTPException(status_code=404, detail="favicon not found") return FileResponse(str(favicon), media_type="image/svg+xml") @app.get("/{full_path:path}") def serve_spa(full_path: str): index = STATIC_DIR / "index.html" return FileResponse(str(index))