Files
congsh ba6e7669e8 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>
2026-06-15 17:01:57 +08:00

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")