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:
@@ -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:
|
||||
"""
|
||||
用户登出。
|
||||
|
||||
Reference in New Issue
Block a user