132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
|
|
"""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")
|