| | 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, |
| | }, |
| | }) |
| | } |
| |
|