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