feat: 密码登录功能(验证码/密码双模式 + 忘记密码 + 设置密码)

后端:
- security.py: 新增 bcrypt 密码哈希/校验工具
- auth_service.py: 新增 login_with_password、reset_password_with_sms
- auth.py: 新增 /login-password、/has-password、/set-password、/reset-password 接口
- schemas/auth.py: 新增 PasswordLoginRequest、SetPasswordRequest、ResetPasswordRequest、CheckPasswordResponse
- crud/user.py: 新增 update_password

前端:
- Login.tsx: 支持验证码/密码切换登录,密码模式下显示忘记密码入口
- Login.css: 新增登录方式切换标签、密码输入框样式
- authStore.ts: 新增 loginWithPassword
- Settings.tsx: 新增账号安全区块,显示密码状态,打开设置/修改密码弹窗
- SetPasswordModal.tsx: 设置/修改密码弹窗(旧密码校验、密码显示切换、表单验证)
- ResetPasswordModal.tsx: 忘记密码弹窗(手机号+验证码+新密码重置)

兼容:
- 零数据库迁移,password_hash 字段已存在(nullable)
- 现有接口不变,完全向后兼容旧版本
This commit is contained in:
小鱼开发
2026-06-09 23:26:50 +08:00
parent c2209dec85
commit 923ff63a3d
11 changed files with 1222 additions and 91 deletions
+109
View File
@@ -288,6 +288,115 @@ async def refresh_access_token(
}
async def login_with_password(
db: AsyncSession,
*,
mobile: str,
password: str,
device_id: str,
device_name: str | None = None,
os_info: str | None = None,
app_version: str | None = None,
ip: str | None = None,
source: str = "mobile_password",
) -> dict[str, Any]:
"""
手机号密码登录。
流程:
1. 查询用户
2. 校验密码
3. 更新登录信息
4. 踢掉旧设备(SSE 推送)
5. 创建/覆盖设备记录
6. 签发双 Token
"""
from app.core.security import verify_password
# 1. 查询用户
user = await user_crud.get_by_mobile(db, mobile=mobile)
if user is None:
raise ValueError("用户不存在")
# 2. 校验密码
if not user.password_hash:
raise ValueError("该账号未设置密码,请使用验证码登录")
if not verify_password(password, user.password_hash):
raise ValueError("密码错误")
# 检查用户状态
if not user.is_active:
raise ValueError("账号已被封禁,请联系客服")
# 3. 更新登录信息
await user_crud.update_login_info(db, user_id=user.id, ip=ip)
# 4. 踢掉旧设备(SSE 推送)
await _kick_old_device(str(user.id))
# 5. 签发双 Token
access_token = create_access_token(data={"sub": str(user.id)})
refresh_token = create_refresh_token(data={"sub": str(user.id)})
refresh_token_hash = _hash_refresh_token(refresh_token)
# 6. 创建/覆盖设备记录
await device_crud.create_or_update(
db,
user_id=user.id,
device_id=device_id,
device_name=device_name,
os_info=os_info,
app_version=app_version,
refresh_token_hash=refresh_token_hash,
)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"user": {
"id": user.id,
"mobile": user.mobile,
"nickname": user.nickname or "",
"avatar": user.avatar_url or "",
},
}
async def reset_password_with_sms(
db: AsyncSession,
*,
mobile: str,
code: str,
new_password: str,
) -> bool:
"""
短信验证码重置密码。
流程:
1. 校验验证码
2. 查询用户
3. 更新密码哈希
"""
from app.core.security import hash_password
settings = get_settings()
# 1. 校验验证码(白名单内的手机号跳过校验)
if mobile not in settings.sms_code_whitelist_set and not await verify_sms_code(mobile, code):
raise ValueError("验证码错误或已过期")
# 2. 查询用户
user = await user_crud.get_by_mobile(db, mobile=mobile)
if user is None:
raise ValueError("用户不存在")
# 3. 更新密码
new_hash = hash_password(new_password)
await user_crud.update_password(db, user_id=user.id, password_hash=new_hash)
return True
async def logout(db: AsyncSession, *, user_id: str) -> bool:
"""
用户登出。