diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index c84b840..97655ea 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -250,10 +250,10 @@ async def _handle_callback( trust_level=trust_level ) - # 3.1. 自动绑定密码(如果还没有设置) - if not await password_manager.has_password(user.user_id): - default_password = await password_manager.set_password(user.user_id, username) - logger.info(f"用户 {user.user_id} ({username}) 自动绑定默认密码: {default_password}") + # 3.1. 检查是否是首次登录(没有密码记录) + is_first_login = not await password_manager.has_password(user.user_id) + if is_first_login: + logger.info(f"用户 {user.user_id} ({username}) 首次登录,需要初始化密码") # Settings 将在首次访问设置页面时自动创建(延迟初始化) @@ -289,6 +289,17 @@ async def _handle_callback( samesite="lax" ) + # 如果是首次登录,设置标记 Cookie(5分钟有效,仅用于前端显示初始密码提示) + if is_first_login: + redirect_response.set_cookie( + key="first_login", + value="true", + max_age=300, # 5分钟有效 + httponly=False, # 前端需要读取 + samesite="lax" + ) + logger.info(f"✅ [OAuth登录] 用户 {user.user_id} 首次登录,已设置 first_login 标记") + return redirect_response @@ -446,6 +457,36 @@ async def set_user_password(request: Request, password_req: SetPasswordRequest): ) +@router.post("/password/initialize", response_model=SetPasswordResponse) +async def initialize_user_password(request: Request, password_req: SetPasswordRequest): + """ + 初始化首次登录用户的密码 + + 用于首次通过 Linux DO 授权登录的用户,可以选择设置自定义密码或使用默认密码 + """ + if not hasattr(request.state, "user") or not request.state.user: + raise HTTPException(status_code=401, detail="未登录") + + user = request.state.user + + # 检查是否已经有密码(防止重复初始化) + if await password_manager.has_password(user.user_id): + raise HTTPException(status_code=400, detail="密码已经初始化,请使用密码修改功能") + + # 验证密码强度(至少6个字符) + if len(password_req.password) < 6: + raise HTTPException(status_code=400, detail="密码长度至少为6个字符") + + # 设置密码 + await password_manager.set_password(user.user_id, user.username, password_req.password) + logger.info(f"用户 {user.user_id} ({user.username}) 初始化密码成功") + + return SetPasswordResponse( + success=True, + message="密码初始化成功" + ) + + @router.post("/bind/login", response_model=LocalLoginResponse) async def bind_account_login(request: LocalLoginRequest, response: Response): """使用绑定的账号密码登录(LinuxDO授权后绑定的账号)""" diff --git a/frontend/src/pages/AuthCallback.tsx b/frontend/src/pages/AuthCallback.tsx index 380d881..c92b565 100644 --- a/frontend/src/pages/AuthCallback.tsx +++ b/frontend/src/pages/AuthCallback.tsx @@ -20,23 +20,35 @@ export default function AuthCallback() { try { // 后端会通过 Cookie 自动设置认证信息 // 这里只需要验证登录状态 - await authApi.getCurrentUser(); - - // 检查密码状态 - const pwdStatus = await authApi.getPasswordStatus(); - setPasswordStatus(pwdStatus); + const currentUser = await authApi.getCurrentUser(); + // 检查是否是首次登录(通过 Cookie 标记) + const isFirstLogin = document.cookie.includes('first_login=true'); + setStatus('success'); - // 只有在用户完全没有密码时才显示密码设置提示 - // 如果已经有密码(无论是默认密码还是自定义密码),都不再提示 - if (!pwdStatus.has_password) { + if (isFirstLogin) { + // 首次登录:生成默认密码并显示提示 + const defaultPassword = `${currentUser.username}@666`; + const pwdStatus = { + has_password: false, + has_custom_password: false, + username: currentUser.username, + default_password: defaultPassword + }; + setPasswordStatus(pwdStatus); + + // 清除首次登录标记 Cookie + document.cookie = 'first_login=; path=/; max-age=0'; + + // 显示密码初始化弹窗 setTimeout(() => { setShowPasswordModal(true); }, 1000); return; } + // 非首次登录:正常流程 // 从 sessionStorage 获取重定向地址 const redirect = sessionStorage.getItem('login_redirect') || '/'; sessionStorage.removeItem('login_redirect'); @@ -129,23 +141,33 @@ export default function AuthCallback() { }; const handleSetPassword = async () => { - if (!newPassword) { + // 如果没有输入新密码,使用默认密码 + const passwordToSet = newPassword || passwordStatus?.default_password; + + if (!passwordToSet) { message.error('请输入新密码'); return; } - if (newPassword.length < 6) { + if (passwordToSet.length < 6) { message.error('密码长度至少为6个字符'); return; } - if (newPassword !== confirmPassword) { + if (newPassword && newPassword !== confirmPassword) { message.error('两次输入的密码不一致'); return; } setSettingPassword(true); try { - await authApi.setPassword(newPassword); - message.success('密码设置成功'); + // 首次登录使用初始化接口,后续使用修改接口 + const isFirstLogin = !passwordStatus?.has_password; + if (isFirstLogin) { + await authApi.initializePassword(passwordToSet); + message.success('密码初始化成功'); + } else { + await authApi.setPassword(passwordToSet); + message.success('密码设置成功'); + } setShowPasswordModal(false); // 继续后续流程 @@ -172,7 +194,17 @@ export default function AuthCallback() { } }; - const handleSkipPasswordSetting = () => { + const handleSkipPasswordSetting = async () => { + // 首次登录时,如果跳过设置,使用默认密码初始化 + const isFirstLogin = !passwordStatus?.has_password; + if (isFirstLogin && passwordStatus?.default_password) { + try { + await authApi.initializePassword(passwordStatus.default_password); + } catch (error) { + console.error('初始化默认密码失败:', error); + } + } + setShowPasswordModal(false); // 继续后续流程 diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1fb0f86..14d5948 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -148,6 +148,9 @@ export const authApi = { setPassword: (password: string) => api.post('/auth/password/set', { password }), + initializePassword: (password: string) => + api.post('/auth/password/initialize', { password }), + refreshSession: () => api.post('/auth/refresh'), logout: () => api.post('/auth/logout'),