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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user