|
|
@@ -0,0 +1,502 @@
|
|
|
+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,
|
|
|
+ "backup_eligible": credential.BackupEligible,
|
|
|
+ "backup_state": credential.BackupState,
|
|
|
+ }
|
|
|
+ if credential != nil {
|
|
|
+ data["credential_aaguid"] = fmt.Sprintf("%x", credential.AAGUID)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 {
|
|
|
+ if userID, parseErr := strconv.Atoi(string(userHandle)); parseErr == nil {
|
|
|
+ if userID != user.Id {
|
|
|
+ return nil, errors.New("用户句柄与凭证不匹配")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 如果解析失败,不做严格验证,因为某些情况下userHandle可能为空或格式不同
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
+}
|