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