feat: add data backup/import, goal step ordering, and PostgreSQL migration

- Add GET /api/backup/export and POST /api/backup/import endpoints for full data backup
- Add drag-and-drop reorder for goal steps with PUT /api/goals/{id}/steps/reorder
- Auto-assign sort_order on step creation (preserves creation order)
- Fix duplicate milestone rendering in goal detail page
- Add category management button in goal dialog
- Migrate database default from SQLite to PostgreSQL
- Fix router guard redirect loop for logged-in users on setup/login pages
- Fix ALTER TABLE ADD COLUMN crash on callable defaults (uuid.uuid4)
- Add auth status rate limiter and token version caching
- Update CLAUDE.md to reflect current architecture

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
祀梦
2026-05-18 00:02:18 +08:00
parent 0ab719500b
commit 5048de4fa1
21 changed files with 543 additions and 225 deletions

View File

@@ -1,16 +1,18 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
import time
from app.database import get_db
from app.models.user_settings import UserSettings
from app.schemas.auth import LoginRequest, LoginResponse, ChangePasswordRequest, AuthStatusResponse, SetupPasswordRequest, AuthSetupStatusResponse
from app.utils.auth import (
hash_password, verify_password, create_access_token,
get_current_user,
get_current_user, set_cached_token_version,
)
from app.utils.datetime import utcnow
from app.utils.rate_limiter import login_limiter
from app.config import ACCESS_TOKEN_EXPIRE_SECONDS
router = APIRouter(prefix="/api/auth", tags=["认证"])
@@ -32,9 +34,31 @@ def _get_or_create_settings(db: Session) -> UserSettings:
return settings
class _StatusLimiter:
MAX_REQUESTS = 30
WINDOW_SECONDS = 60
def __init__(self):
self._requests: dict[str, list[float]] = {}
def check(self, ip: str) -> bool:
now = time.time()
times = [t for t in self._requests.get(ip, []) if now - t < self.WINDOW_SECONDS]
self._requests[ip] = times
if len(times) >= self.MAX_REQUESTS:
return False
times.append(now)
return True
_status_limiter = _StatusLimiter()
@router.get("/status", response_model=AuthSetupStatusResponse)
def auth_status(db: Session = Depends(get_db)):
def auth_status(request: Request, db: Session = Depends(get_db)):
"""检查系统密码是否已设置"""
ip = _get_client_ip(request)
if not _status_limiter.check(ip):
raise HTTPException(status_code=429, detail="请求过于频繁")
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
has_password = bool(settings and settings.password_hash)
return AuthSetupStatusResponse(has_password=has_password)
@@ -68,7 +92,7 @@ def setup_password(data: SetupPasswordRequest, db: Session = Depends(get_db)):
value=token,
httponly=True,
samesite="strict",
max_age=86400,
max_age=ACCESS_TOKEN_EXPIRE_SECONDS,
path="/",
)
return response
@@ -111,7 +135,7 @@ def login(data: LoginRequest, request: Request, db: Session = Depends(get_db)):
value=token,
httponly=True,
samesite="strict",
max_age=86400,
max_age=ACCESS_TOKEN_EXPIRE_SECONDS,
path="/",
)
return response
@@ -147,6 +171,7 @@ def change_password(
settings.password_hash = hash_password(data.new_password)
settings.token_version = (settings.token_version or 0) + 1
set_cached_token_version(str(settings.id), settings.token_version)
settings.updated_at = utcnow()
db.commit()