126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
"""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, browser_sessions, websites, auth_capture
|
|
from app.services.browser_session_service import browser_sessions as browser_session_service
|
|
|
|
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
|
|
await browser_session_service.shutdown()
|
|
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(browser_sessions.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))
|