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
+55
View File
@@ -0,0 +1,55 @@
"""Admin locks router."""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_admin, get_db
from app.models.lock import Lock
from app.models.user import User
from app.schemas.common import MessageResponse
router = APIRouter(prefix="/locks", tags=["admin"])
@router.get("")
async def list_locks(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
"""List active locks."""
result = await db.execute(select(Lock))
locks = result.scalars().all()
now = datetime.now(timezone.utc)
active_locks = [
{
"id": str(lock.id),
"lock_name": lock.lock_name,
"owner_id": lock.owner_id,
"acquired_at": lock.acquired_at.isoformat() if lock.acquired_at else None,
"expires_at": lock.expires_at.isoformat() if lock.expires_at else None,
"is_expired": lock.expires_at is not None and lock.expires_at < now,
}
for lock in locks
]
return {"total": len(active_locks), "items": active_locks}
@router.delete("/{lock_name}", response_model=MessageResponse)
async def force_release_lock(
lock_name: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
"""Force release a lock."""
result = await db.execute(select(Lock).where(Lock.lock_name == lock_name))
lock = result.scalar_one_or_none()
if not lock:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lock not found")
await db.delete(lock)
await db.commit()
return {"message": f"Lock {lock_name} released"}