"""RSS Platform FastAPI application.""" from contextlib import asynccontextmanager from uuid import uuid4 from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from app.api.v1 import auth, articles, feeds, health, settings from app.api.v1.admin import locks from app.core.config import settings from app.core.database import close_db from app.core.exceptions import add_exception_handlers from app.core.logging import configure_logging, request_id_var from app.core.redis import close_redis from app.services.settings_service import apply_db_settings_to_config, init_default_settings configure_logging(settings.LOG_LEVEL) @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager.""" from app.core.database import AsyncSessionLocal app.state.startup_warnings = [] async with AsyncSessionLocal() as db: await init_default_settings(db) await apply_db_settings_to_config(db) warnings = await _create_default_admin(db) app.state.startup_warnings.extend(warnings) yield # Shutdown await close_db() await close_redis() async def _create_default_admin(db) -> list[str]: """Create default admin user if no users exist.""" from sqlalchemy import select from app.core.auth import get_password_hash from app.models.user import User warnings: list[str] = [] result = await db.execute(select(User)) if result.scalar_one_or_none(): return warnings if ( settings.DEFAULT_ADMIN_USERNAME == "admin" and settings.DEFAULT_ADMIN_PASSWORD == "admin" ): warnings.append( "Default admin credentials are admin/admin. Please change the password immediately." ) admin = User( username=settings.DEFAULT_ADMIN_USERNAME, password_hash=get_password_hash(settings.DEFAULT_ADMIN_PASSWORD), role="admin", is_active=True, ) db.add(admin) await db.commit() return warnings app = FastAPI( title="RSS Platform", description="模块化、工业化、AI 驱动的 RSS 信息处理平台", version="0.1.0", lifespan=lifespan, ) # CORS cors_origins = settings.cors_origins if not cors_origins: # In production, CORS_ALLOWED_ORIGINS must be configured explicitly. # Dev fallback uses the known frontend origin instead of wildcard. cors_origins = ["http://localhost:5173"] app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "Authorization", "X-API-Key", "X-Request-ID"], ) @app.middleware("http") async def request_id_middleware(request: Request, call_next): """Attach request_id from header or generate a new one for logging.""" request_id = request.headers.get("X-Request-ID") or str(uuid4()) token = request_id_var.set(request_id) try: response = await call_next(request) response.headers["X-Request-ID"] = request_id return response finally: request_id_var.reset(token) # Exception handlers add_exception_handlers(app) # API routers app.include_router(auth.router, prefix="/api/v1") app.include_router(feeds.router, prefix="/api/v1") app.include_router(articles.router, prefix="/api/v1") app.include_router(health.router, prefix="/api/v1") app.include_router(settings.router, prefix="/api/v1") app.include_router(locks.router, prefix="/api/v1/admin") @app.get("/") async def root(): """Root endpoint.""" return {"message": "RSS Platform API", "version": "0.1.0"} # Static files (frontend build) import os static_dir = os.path.join(os.path.dirname(__file__), "static") if os.path.isdir(static_dir): app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")