Spaces:
Build error
Build error
| package controller | |
| import ( | |
| "errors" | |
| "fmt" | |
| "net/http" | |
| "strconv" | |
| "time" | |
| "one-api/common" | |
| "one-api/model" | |
| passkeysvc "one-api/service/passkey" | |
| "one-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 | |
| } | |