Initial commit: RSS platform phase 1 skeleton with code review fixes
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>
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user