| package controller |
|
|
| import ( |
| "fmt" |
| "net/http" |
| "time" |
|
|
| "github.com/QuantumNous/new-api/common" |
| "github.com/QuantumNous/new-api/model" |
| passkeysvc "github.com/QuantumNous/new-api/service/passkey" |
| "github.com/QuantumNous/new-api/setting/system_setting" |
|
|
| "github.com/gin-contrib/sessions" |
| "github.com/gin-gonic/gin" |
| ) |
|
|
| const ( |
| |
| SecureVerificationSessionKey = "secure_verified_at" |
| |
| SecureVerificationTimeout = 300 |
| ) |
|
|
| type UniversalVerifyRequest struct { |
| Method string `json:"method"` |
| Code string `json:"code,omitempty"` |
| } |
|
|
| type VerificationStatusResponse struct { |
| Verified bool `json:"verified"` |
| ExpiresAt int64 `json:"expires_at,omitempty"` |
| } |
|
|
| |
| |
| func UniversalVerify(c *gin.Context) { |
| userId := c.GetInt("id") |
| if userId == 0 { |
| c.JSON(http.StatusUnauthorized, gin.H{ |
| "success": false, |
| "message": "未登录", |
| }) |
| return |
| } |
|
|
| var req UniversalVerifyRequest |
| if err := c.ShouldBindJSON(&req); err != nil { |
| common.ApiError(c, fmt.Errorf("参数错误: %v", err)) |
| return |
| } |
|
|
| |
| user := &model.User{Id: userId} |
| if err := user.FillUserById(); err != nil { |
| common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err)) |
| return |
| } |
|
|
| if user.Status != common.UserStatusEnabled { |
| common.ApiError(c, fmt.Errorf("该用户已被禁用")) |
| return |
| } |
|
|
| |
| twoFA, _ := model.GetTwoFAByUserId(userId) |
| has2FA := twoFA != nil && twoFA.IsEnabled |
|
|
| passkey, passkeyErr := model.GetPasskeyByUserID(userId) |
| hasPasskey := passkeyErr == nil && passkey != nil |
|
|
| if !has2FA && !hasPasskey { |
| common.ApiError(c, fmt.Errorf("用户未启用2FA或Passkey")) |
| return |
| } |
|
|
| |
| var verified bool |
| var verifyMethod string |
|
|
| switch req.Method { |
| case "2fa": |
| if !has2FA { |
| common.ApiError(c, fmt.Errorf("用户未启用2FA")) |
| return |
| } |
| if req.Code == "" { |
| common.ApiError(c, fmt.Errorf("验证码不能为空")) |
| return |
| } |
| verified = validateTwoFactorAuth(twoFA, req.Code) |
| verifyMethod = "2FA" |
|
|
| case "passkey": |
| if !hasPasskey { |
| common.ApiError(c, fmt.Errorf("用户未启用Passkey")) |
| return |
| } |
| |
| |
| |
| verified = true |
| verifyMethod = "Passkey" |
|
|
| default: |
| common.ApiError(c, fmt.Errorf("不支持的验证方式: %s", req.Method)) |
| return |
| } |
|
|
| if !verified { |
| common.ApiError(c, fmt.Errorf("验证失败,请检查验证码")) |
| return |
| } |
|
|
| |
| session := sessions.Default(c) |
| now := time.Now().Unix() |
| session.Set(SecureVerificationSessionKey, now) |
| if err := session.Save(); err != nil { |
| common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err)) |
| return |
| } |
|
|
| |
| model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("通用安全验证成功 (验证方式: %s)", verifyMethod)) |
|
|
| c.JSON(http.StatusOK, gin.H{ |
| "success": true, |
| "message": "验证成功", |
| "data": gin.H{ |
| "verified": true, |
| "expires_at": now + SecureVerificationTimeout, |
| }, |
| }) |
| } |
|
|
| |
| func GetVerificationStatus(c *gin.Context) { |
| userId := c.GetInt("id") |
| if userId == 0 { |
| c.JSON(http.StatusUnauthorized, gin.H{ |
| "success": false, |
| "message": "未登录", |
| }) |
| return |
| } |
|
|
| session := sessions.Default(c) |
| verifiedAtRaw := session.Get(SecureVerificationSessionKey) |
|
|
| if verifiedAtRaw == nil { |
| c.JSON(http.StatusOK, gin.H{ |
| "success": true, |
| "message": "", |
| "data": VerificationStatusResponse{ |
| Verified: false, |
| }, |
| }) |
| return |
| } |
|
|
| verifiedAt, ok := verifiedAtRaw.(int64) |
| if !ok { |
| c.JSON(http.StatusOK, gin.H{ |
| "success": true, |
| "message": "", |
| "data": VerificationStatusResponse{ |
| Verified: false, |
| }, |
| }) |
| return |
| } |
|
|
| elapsed := time.Now().Unix() - verifiedAt |
| if elapsed >= SecureVerificationTimeout { |
| |
| session.Delete(SecureVerificationSessionKey) |
| _ = session.Save() |
| c.JSON(http.StatusOK, gin.H{ |
| "success": true, |
| "message": "", |
| "data": VerificationStatusResponse{ |
| Verified: false, |
| }, |
| }) |
| return |
| } |
|
|
| c.JSON(http.StatusOK, gin.H{ |
| "success": true, |
| "message": "", |
| "data": VerificationStatusResponse{ |
| Verified: true, |
| ExpiresAt: verifiedAt + SecureVerificationTimeout, |
| }, |
| }) |
| } |
|
|
| |
| |
| func CheckSecureVerification(c *gin.Context) bool { |
| session := sessions.Default(c) |
| verifiedAtRaw := session.Get(SecureVerificationSessionKey) |
|
|
| if verifiedAtRaw == nil { |
| return false |
| } |
|
|
| verifiedAt, ok := verifiedAtRaw.(int64) |
| if !ok { |
| return false |
| } |
|
|
| elapsed := time.Now().Unix() - verifiedAt |
| if elapsed >= SecureVerificationTimeout { |
| |
| session.Delete(SecureVerificationSessionKey) |
| _ = session.Save() |
| return false |
| } |
|
|
| return true |
| } |
|
|
| |
| |
| func PasskeyVerifyAndSetSession(c *gin.Context) { |
| session := sessions.Default(c) |
| now := time.Now().Unix() |
| session.Set(SecureVerificationSessionKey, now) |
| _ = session.Save() |
| } |
|
|
| |
| |
| func PasskeyVerifyForSecure(c *gin.Context) { |
| if !system_setting.GetPasskeySettings().Enabled { |
| c.JSON(http.StatusOK, gin.H{ |
| "success": false, |
| "message": "管理员未启用 Passkey 登录", |
| }) |
| return |
| } |
|
|
| userId := c.GetInt("id") |
| if userId == 0 { |
| c.JSON(http.StatusUnauthorized, gin.H{ |
| "success": false, |
| "message": "未登录", |
| }) |
| return |
| } |
|
|
| user := &model.User{Id: userId} |
| if err := user.FillUserById(); err != nil { |
| common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err)) |
| return |
| } |
|
|
| if user.Status != common.UserStatusEnabled { |
| common.ApiError(c, fmt.Errorf("该用户已被禁用")) |
| return |
| } |
|
|
| credential, err := model.GetPasskeyByUserID(userId) |
| if err != nil { |
| c.JSON(http.StatusOK, gin.H{ |
| "success": false, |
| "message": "该用户尚未绑定 Passkey", |
| }) |
| return |
| } |
|
|
| wa, err := passkeysvc.BuildWebAuthn(c.Request) |
| if err != nil { |
| common.ApiError(c, err) |
| return |
| } |
|
|
| waUser := passkeysvc.NewWebAuthnUser(user, credential) |
| sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey) |
| if err != nil { |
| common.ApiError(c, err) |
| return |
| } |
|
|
| _, err = wa.FinishLogin(waUser, *sessionData, c.Request) |
| if err != nil { |
| common.ApiError(c, err) |
| return |
| } |
|
|
| |
| now := time.Now() |
| credential.LastUsedAt = &now |
| if err := model.UpsertPasskeyCredential(credential); err != nil { |
| common.ApiError(c, err) |
| return |
| } |
|
|
| |
| PasskeyVerifyAndSetSession(c) |
|
|
| |
| model.RecordLog(userId, model.LogTypeSystem, "Passkey 安全验证成功") |
|
|
| c.JSON(http.StatusOK, gin.H{ |
| "success": true, |
| "message": "Passkey 验证成功", |
| "data": gin.H{ |
| "verified": true, |
| "expires_at": time.Now().Unix() + SecureVerificationTimeout, |
| }, |
| }) |
| } |
|
|