package controller import ( "errors" "fmt" "net/http" "strconv" "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" "github.com/go-webauthn/webauthn/protocol" webauthnlib "github.com/go-webauthn/webauthn/webauthn" ) func PasskeyRegisterBegin(c *gin.Context) { if !system_setting.GetPasskeySettings().Enabled { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "管理员未启用 Passkey 登录", }) return } user, err := getSessionUser(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": err.Error(), }) return } credential, err := model.GetPasskeyByUserID(user.Id) if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) { common.ApiError(c, err) return } if errors.Is(err, model.ErrPasskeyNotFound) { credential = nil } wa, err := passkeysvc.BuildWebAuthn(c.Request) if err != nil { common.ApiError(c, err) return } waUser := passkeysvc.NewWebAuthnUser(user, credential) var options []webauthnlib.RegistrationOption if credential != nil { descriptor := credential.ToWebAuthnCredential().Descriptor() options = append(options, webauthnlib.WithExclusions([]protocol.CredentialDescriptor{descriptor})) } creation, sessionData, err := wa.BeginRegistration(waUser, options...) if err != nil { common.ApiError(c, err) return } if err := passkeysvc.SaveSessionData(c, passkeysvc.RegistrationSessionKey, sessionData); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": gin.H{ "options": creation, }, }) } func PasskeyRegisterFinish(c *gin.Context) { if !system_setting.GetPasskeySettings().Enabled { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "管理员未启用 Passkey 登录", }) return } user, err := getSessionUser(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": err.Error(), }) return } wa, err := passkeysvc.BuildWebAuthn(c.Request) if err != nil { common.ApiError(c, err) return } credentialRecord, err := model.GetPasskeyByUserID(user.Id) if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) { common.ApiError(c, err) return } if errors.Is(err, model.ErrPasskeyNotFound) { credentialRecord = nil } sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.RegistrationSessionKey) if err != nil { common.ApiError(c, err) return } waUser := passkeysvc.NewWebAuthnUser(user, credentialRecord) credential, err := wa.FinishRegistration(waUser, *sessionData, c.Request) if err != nil { common.ApiError(c, err) return } passkeyCredential := model.NewPasskeyCredentialFromWebAuthn(user.Id, credential) if passkeyCredential == nil { common.ApiErrorMsg(c, "无法创建 Passkey 凭证") return } if err := model.UpsertPasskeyCredential(passkeyCredential); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Passkey 注册成功", }) } func PasskeyDelete(c *gin.Context) { user, err := getSessionUser(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": err.Error(), }) return } if err := model.DeletePasskeyByUserID(user.Id); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Passkey 已解绑", }) } func PasskeyStatus(c *gin.Context) { user, err := getSessionUser(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": err.Error(), }) return } credential, err := model.GetPasskeyByUserID(user.Id) if errors.Is(err, model.ErrPasskeyNotFound) { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": gin.H{ "enabled": false, }, }) return } if err != nil { common.ApiError(c, err) return } data := gin.H{ "enabled": true, "last_used_at": credential.LastUsedAt, } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": data, }) } func PasskeyLoginBegin(c *gin.Context) { if !system_setting.GetPasskeySettings().Enabled { 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 } assertion, sessionData, err := wa.BeginDiscoverableLogin() if err != nil { common.ApiError(c, err) return } if err := passkeysvc.SaveSessionData(c, passkeysvc.LoginSessionKey, sessionData); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": gin.H{ "options": assertion, }, }) } func PasskeyLoginFinish(c *gin.Context) { if !system_setting.GetPasskeySettings().Enabled { 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 } sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.LoginSessionKey) if err != nil { common.ApiError(c, err) return } handler := func(rawID, userHandle []byte) (webauthnlib.User, error) { // 首先通过凭证ID查找用户 credential, err := model.GetPasskeyByCredentialID(rawID) if err != nil { return nil, fmt.Errorf("未找到 Passkey 凭证: %w", err) } // 通过凭证获取用户 user := &model.User{Id: credential.UserID} if err := user.FillUserById(); err != nil { return nil, fmt.Errorf("用户信息获取失败: %w", err) } if user.Status != common.UserStatusEnabled { return nil, errors.New("该用户已被禁用") } if len(userHandle) > 0 { userID, parseErr := strconv.Atoi(string(userHandle)) if parseErr != nil { // 记录异常但继续验证,因为某些客户端可能使用非数字格式 common.SysLog(fmt.Sprintf("PasskeyLogin: userHandle parse error for credential, length: %d", len(userHandle))) } else if userID != user.Id { return nil, errors.New("用户句柄与凭证不匹配") } } return passkeysvc.NewWebAuthnUser(user, credential), nil } waUser, credential, err := wa.FinishPasskeyLogin(handler, *sessionData, c.Request) if err != nil { common.ApiError(c, err) return } userWrapper, ok := waUser.(*passkeysvc.WebAuthnUser) if !ok { common.ApiErrorMsg(c, "Passkey 登录状态异常") return } modelUser := userWrapper.ModelUser() if modelUser == nil { common.ApiErrorMsg(c, "Passkey 登录状态异常") return } if modelUser.Status != common.UserStatusEnabled { common.ApiErrorMsg(c, "该用户已被禁用") return } // 更新凭证信息 updatedCredential := model.NewPasskeyCredentialFromWebAuthn(modelUser.Id, credential) if updatedCredential == nil { common.ApiErrorMsg(c, "Passkey 凭证更新失败") return } now := time.Now() updatedCredential.LastUsedAt = &now if err := model.UpsertPasskeyCredential(updatedCredential); err != nil { common.ApiError(c, err) return } setupLogin(modelUser, c) return } func AdminResetPasskey(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { common.ApiErrorMsg(c, "无效的用户 ID") return } user := &model.User{Id: id} if err := user.FillUserById(); err != nil { common.ApiError(c, err) return } if _, err := model.GetPasskeyByUserID(user.Id); err != nil { if errors.Is(err, model.ErrPasskeyNotFound) { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "该用户尚未绑定 Passkey", }) return } common.ApiError(c, err) return } if err := model.DeletePasskeyByUserID(user.Id); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Passkey 已重置", }) } func PasskeyVerifyBegin(c *gin.Context) { if !system_setting.GetPasskeySettings().Enabled { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "管理员未启用 Passkey 登录", }) return } user, err := getSessionUser(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": err.Error(), }) return } credential, err := model.GetPasskeyByUserID(user.Id) 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) assertion, sessionData, err := wa.BeginLogin(waUser) if err != nil { common.ApiError(c, err) return } if err := passkeysvc.SaveSessionData(c, passkeysvc.VerifySessionKey, sessionData); err != nil { common.ApiError(c, err) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": gin.H{ "options": assertion, }, }) } func PasskeyVerifyFinish(c *gin.Context) { if !system_setting.GetPasskeySettings().Enabled { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "管理员未启用 Passkey 登录", }) return } user, err := getSessionUser(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": err.Error(), }) return } wa, err := passkeysvc.BuildWebAuthn(c.Request) if err != nil { common.ApiError(c, err) return } credential, err := model.GetPasskeyByUserID(user.Id) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "该用户尚未绑定 Passkey", }) return } sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey) if err != nil { common.ApiError(c, err) return } waUser := passkeysvc.NewWebAuthnUser(user, credential) _, 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 } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Passkey 验证成功", }) } func getSessionUser(c *gin.Context) (*model.User, error) { session := sessions.Default(c) idRaw := session.Get("id") if idRaw == nil { return nil, errors.New("未登录") } id, ok := idRaw.(int) if !ok { return nil, errors.New("无效的会话信息") } user := &model.User{Id: id} if err := user.FillUserById(); err != nil { return nil, err } if user.Status != common.UserStatusEnabled { return nil, errors.New("该用户已被禁用") } return user, nil }