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:
congsh
2026-06-15 17:01:57 +08:00
commit ba6e7669e8
82 changed files with 6859 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
"""Settings router."""
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_admin, get_current_user, get_db
from app.models.user import User
from app.schemas.common import MessageResponse
from app.services.settings_service import (
apply_db_settings_to_config,
list_settings,
reset_settings,
set_setting,
)
router = APIRouter(prefix="/settings", tags=["settings"])
@router.get("")
async def get_settings(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""List all settings."""
return await list_settings(db, mask_sensitive=current_user.role != "admin")
@router.put("/{key}")
async def update_setting(
key: str,
value: dict[str, Any],
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
"""Update a single setting."""
if "value" not in value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Request body must contain 'value' field",
)
success = await set_setting(db, key, value["value"])
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid setting key: {key}",
)
await apply_db_settings_to_config(db)
return {"message": "Setting updated", "key": key}
@router.put("")
async def batch_update_settings(
data: dict[str, Any],
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
"""Update multiple settings."""
settings_data = data.get("settings", {})
if not settings_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Request body must contain 'settings' object",
)
errors = []
for key, value in settings_data.items():
success = await set_setting(db, key, value)
if not success:
errors.append(key)
if errors:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid setting keys: {', '.join(errors)}",
)
await apply_db_settings_to_config(db)
return {"message": "Settings updated", "count": len(settings_data)}
@router.post("/reset", response_model=MessageResponse)
async def reset_all_settings(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
"""Reset all settings to environment defaults."""
await reset_settings(db)
await apply_db_settings_to_config(db)
return {"message": "Settings reset to defaults"}