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