fix/status-review-sync-20260409 #1

Merged
long merged 65 commits from fix/status-review-sync-20260409 into main 2026-04-18 15:05:51 +00:00
Showing only changes of commit 4acd19f420 - Show all commits

View File

@@ -117,10 +117,16 @@ type UserInfo struct {
}
type LoginResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
User *UserInfo `json:"user"`
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
User *UserInfo `json:"user,omitempty"`
// RequiresTOTP 指示登录需要额外的TOTP验证当设备未信任时
RequiresTOTP bool `json:"requires_totp,omitempty"`
// TempToken 临时令牌用于TOTP验证阶段短生命周期不可用于常规API
TempToken string `json:"temp_token,omitempty"`
// UserID 当RequiresTOTP为true时返回用于后续TOTP验证
UserID int64 `json:"user_id,omitempty"`
}
type LogoutRequest struct {
@@ -751,6 +757,16 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) (
_ = s.cache.Delete(ctx, attemptKey)
}
// P0-07 安全修复检查是否需要TOTP验证用户启用了TOTP且设备未信任
if s.isTOTPRequiredForLogin(ctx, user, req.DeviceID) {
// 返回RequiresTOTP指示前端需要完成TOTP验证
// 前端应调用 /auth/login/totp-verify 接口完成验证
return &LoginResponse{
RequiresTOTP: true,
UserID: user.ID,
}, nil
}
s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "password")
s.cacheUserInfo(ctx, user)
s.writeLoginLog(ctx, &user.ID, domain.LoginTypePassword, ip, true, "")
@@ -766,6 +782,55 @@ func (s *AuthService) Login(ctx context.Context, req *LoginRequest, ip string) (
return s.generateLoginResponse(ctx, user, req.Remember)
}
// isTOTPRequiredForLogin 检查登录是否需要TOTP验证
// 条件用户启用了TOTP且尝试登录的设备未信任
func (s *AuthService) isTOTPRequiredForLogin(ctx context.Context, user *domain.User, deviceID string) bool {
if user == nil {
return false
}
// 检查用户是否启用了TOTP
if !user.TOTPEnabled || strings.TrimSpace(user.TOTPSecret) == "" {
return false
}
// 检查设备是否已信任
if deviceID != "" && s.deviceService != nil {
device, err := s.deviceService.GetDeviceByDeviceID(ctx, user.ID, deviceID)
if err == nil && device.IsTrusted {
// 设备已信任,检查信任是否过期
if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) {
return false // 设备已信任且未过期不需要TOTP
}
}
}
return true // 需要TOTP验证
}
// VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证
// 当用户启用了TOTP但设备未信任时密码登录会返回RequiresTOTP=true
// 前端需要调用此接口完成TOTP验证以获取令牌
func (s *AuthService) VerifyTOTPAfterPasswordLogin(ctx context.Context, userID int64, totpCode, deviceID string) (*LoginResponse, error) {
if s == nil {
return nil, errors.New("auth service is not initialized")
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, errors.New("用户不存在")
}
if err := s.ensureUserActive(user); err != nil {
return nil, err
}
// 验证TOTP
if err := s.VerifyTOTP(ctx, userID, totpCode, deviceID); err != nil {
return nil, err
}
// TOTP验证成功返回完整登录响应
return s.generateLoginResponseWithoutRemember(ctx, user)
}
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
if s == nil || s.jwtManager == nil || s.userRepo == nil {
return nil, errors.New("auth service is not fully configured")