feat: add WebDAV sync support and startup/shutdown scripts
Backend: - Add uuid, sync_version, is_deleted fields to all syncable models - Add SyncSettings model for WebDAV configuration (AES-256-GCM encrypted passwords) - Add crypto.py: AES-256-GCM encryption derived from JWT_SECRET via PBKDF2 - Add sync_lock.py: thread-level sync lock with 503 middleware for write blocking - Add webdav.py: WebDAV client using requests (PUT/GET/MKCOL/DELETE) - Add sync_service.py: push/pull/bidirectional merge with LWW conflict resolution - Add sync router with 8 endpoints: config, test, push, pull, sync, status, remote delete - Add UUID backfill for existing records in init_db() - Add SQLAlchemy before_update event to auto-increment sync_version - Register sync middleware to block writes during sync (503) Frontend: - Add sync API client (WebUI/src/api/sync.ts) - Add useSyncStore with config, test, push/pull/sync operations - Add WebDAV config + sync UI in SettingsView - Add 503 status code handling in axios interceptor - Add uuid field to all TypeScript type definitions Scripts: - Add scripts/start.bat and scripts/stop.bat for project management Design doc: docs/plan/webdav-sync-design.md
This commit is contained in:
@@ -11,7 +11,8 @@ from app.database import init_db, SessionLocal
|
||||
from app.models.user_settings import UserSettings
|
||||
from app.routers import api_router
|
||||
from app.utils.logger import logger
|
||||
from app.utils.auth import decode_access_token
|
||||
from app.utils.auth import decode_access_token, get_cached_token_version, set_cached_token_version
|
||||
from app.utils.sync_lock import is_syncing
|
||||
from jose import JWTError
|
||||
|
||||
|
||||
@@ -111,19 +112,39 @@ async def auth_middleware(request: Request, call_next):
|
||||
except JWTError:
|
||||
return JSONResponse(status_code=401, content={"detail": "登录已过期,请重新登录"})
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
|
||||
if settings and payload.get("tv") != settings.token_version:
|
||||
user_id = payload.get("sub", "")
|
||||
token_tv = payload.get("tv")
|
||||
|
||||
if token_tv is not None and user_id:
|
||||
cached_tv = get_cached_token_version(user_id)
|
||||
if cached_tv is None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
|
||||
cached_tv = settings.token_version if settings else 0
|
||||
set_cached_token_version(user_id, cached_tv)
|
||||
finally:
|
||||
db.close()
|
||||
if token_tv != cached_tv:
|
||||
return JSONResponse(status_code=401, content={"detail": "密码已修改,请重新登录"})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
request.state.user = payload
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# 同步锁中间件(同步期间禁止写操作)
|
||||
@app.middleware("http")
|
||||
async def sync_lock_middleware(request: Request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
if is_syncing() and request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
||||
if not path.startswith("/api/sync"):
|
||||
return JSONResponse(status_code=503, content={"detail": "正在同步数据,请稍后再试"})
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# 全局异常处理器
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
|
||||
Reference in New Issue
Block a user