ba6e7669e8
Features: - FastAPI + SQLAlchemy 2.0 async + PostgreSQL/pgvector + Redis backend - Vue 3 + TypeScript + Element Plus frontend - JWT auth with access/refresh tokens and revocation - Admin/member RBAC - RSS feed CRUD and article listing - Settings management with Fernet encryption for sensitive values - Redis distributed lock service - Alembic initial migration - Docker Compose development environment Fixes from code review: - Fix DB session leak in dependency injection - Restrict registration to admin only - Add default admin password warning - Implement JWT refresh tokens and jti blacklist - Strengthen password policy - Use func.count for pagination totals - Replace NullPool with AsyncAdaptedQueuePool - Remove init_db from lifespan to enforce alembic migrations - Add request_id middleware and logging filter - Fix vite.config.ts env loading - Add frontend token refresh interceptor - Add Vue error handler Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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")
|