feat: add onboarding setup flow with nickname and password

Replace default auto-generated password with a first-run setup page that
lets users choose their own nickname and password. The /auth/setup endpoint
now accepts an optional nickname field (also sets site_name). Remove
set_default_password() since setup is now mandatory before login.
This commit is contained in:
祀梦
2026-05-17 19:45:36 +08:00
parent bfdf0c9987
commit f838840bda
8 changed files with 330 additions and 27 deletions

View File

@@ -4,10 +4,10 @@ from sqlalchemy.orm import Session
from app.database import get_db
from app.models.user_settings import UserSettings
from app.schemas.auth import LoginRequest, LoginResponse, ChangePasswordRequest, AuthStatusResponse
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, set_default_password
get_current_user,
)
from app.utils.datetime import utcnow
from app.utils.rate_limiter import login_limiter
@@ -22,6 +22,58 @@ def _get_client_ip(request: Request) -> str:
return request.client.host if request.client else "unknown"
def _get_or_create_settings(db: Session) -> UserSettings:
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
if not settings:
settings = UserSettings(id=1)
db.add(settings)
db.commit()
db.refresh(settings)
return settings
@router.get("/status", response_model=AuthSetupStatusResponse)
def auth_status(db: Session = Depends(get_db)):
"""检查系统密码是否已设置"""
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
has_password = bool(settings and settings.password_hash)
return AuthSetupStatusResponse(has_password=has_password)
@router.post("/setup")
def setup_password(data: SetupPasswordRequest, db: Session = Depends(get_db)):
"""首次设置密码(仅在无密码时可用),设置成功后自动登录"""
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
if settings and settings.password_hash:
raise HTTPException(status_code=400, detail="密码已设置,请使用登录接口")
if not settings:
settings = UserSettings(id=1)
db.add(settings)
settings.password_hash = hash_password(data.password)
settings.token_version = 1
if data.nickname and data.nickname.strip():
nick = data.nickname.strip()
settings.nickname = nick
settings.site_name = f"{nick}待办"
settings.updated_at = utcnow()
db.commit()
token = create_access_token({
"sub": str(settings.id),
"tv": settings.token_version,
})
response = JSONResponse(content={"message": "密码设置成功"})
response.set_cookie(
key="access_token",
value=token,
httponly=True,
samesite="strict",
max_age=86400,
path="/",
)
return response
@router.post("/login", response_model=LoginResponse)
def login(data: LoginRequest, request: Request, db: Session = Depends(get_db)):
ip = _get_client_ip(request)
@@ -37,14 +89,10 @@ def login(data: LoginRequest, request: Request, db: Session = Depends(get_db)):
headers={"Retry-After": str(retry_after)},
)
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
if not settings:
settings = UserSettings(id=1)
db.add(settings)
db.commit()
db.refresh(settings)
settings = _get_or_create_settings(db)
set_default_password(db, settings)
if not settings.password_hash:
raise HTTPException(status_code=400, detail="请先设置密码")
if not verify_password(data.password, settings.password_hash):
login_limiter.record_failure(ip)