Files
SmartUp/backend/app/main.py
T
SmartUp Developer ad16618406 fix: address multiple code audit findings
- CORS: replace wildcard with explicit origin list from CORS_ORIGINS env
- Auth: enforce strong defaults, JWT blacklist (RevokedToken model), login rate limiting
- Auth: validate password length before bcrypt (72-byte limit)
- Scheduler: single-threaded worker to mitigate SQLite write contention
- Scheduler: graceful shutdown (wait=True)
- Snapshots: add prune_snapshots() with configurable retention count
- Storage: isolate localStorage keys via VITE_APP_KEY prefix
- Config: add cors_origins, login_rate_limit, snapshot_retention_count settings
2026-05-17 10:52:18 +08:00

117 lines
3.7 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.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, 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
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:
exists = db.query(AdminUser).filter(AdminUser.email == settings.admin_email).first()
if not exists:
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)
else:
logger.info("admin user already exists: %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=["*"],
)
# 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.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))