diff --git a/controller/passkey.go b/controller/passkey.go index 3bdec8f0f..7ffacf5d6 100644 --- a/controller/passkey.go +++ b/controller/passkey.go @@ -189,13 +189,8 @@ func PasskeyStatus(c *gin.Context) { } 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) + "enabled": true, + "last_used_at": credential.LastUsedAt, } c.JSON(http.StatusOK, gin.H{ @@ -278,14 +273,14 @@ func PasskeyLoginFinish(c *gin.Context) { 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("用户句柄与凭证不匹配") - } + 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("用户句柄与凭证不匹配") } - // 如果解析失败,不做严格验证,因为某些情况下userHandle可能为空或格式不同 } return passkeysvc.NewWebAuthnUser(user, credential), nil diff --git a/model/passkey.go b/model/passkey.go index 3f45e1764..5b2a15474 100644 --- a/model/passkey.go +++ b/model/passkey.go @@ -1,6 +1,7 @@ package model import ( + "encoding/base64" "encoding/json" "errors" "fmt" @@ -21,10 +22,10 @@ var ( type PasskeyCredential struct { ID int `json:"id" gorm:"primaryKey"` UserID int `json:"user_id" gorm:"uniqueIndex;not null"` - CredentialID []byte `json:"credential_id" gorm:"type:blob;uniqueIndex;not null"` - PublicKey []byte `json:"public_key" gorm:"type:blob;not null"` + CredentialID string `json:"credential_id" gorm:"type:varchar(512);uniqueIndex;not null"` // base64 encoded + PublicKey string `json:"public_key" gorm:"type:text;not null"` // base64 encoded AttestationType string `json:"attestation_type" gorm:"type:varchar(255)"` - AAGUID []byte `json:"aaguid" gorm:"type:blob"` + AAGUID string `json:"aaguid" gorm:"type:varchar(512)"` // base64 encoded SignCount uint32 `json:"sign_count" gorm:"default:0"` CloneWarning bool `json:"clone_warning"` UserPresent bool `json:"user_present"` @@ -78,14 +79,18 @@ func (p *PasskeyCredential) ToWebAuthnCredential() webauthn.Credential { BackupState: p.BackupState, } + credID, _ := base64.StdEncoding.DecodeString(p.CredentialID) + pubKey, _ := base64.StdEncoding.DecodeString(p.PublicKey) + aaguid, _ := base64.StdEncoding.DecodeString(p.AAGUID) + return webauthn.Credential{ - ID: p.CredentialID, - PublicKey: p.PublicKey, + ID: credID, + PublicKey: pubKey, AttestationType: p.AttestationType, Transport: p.TransportList(), Flags: flags, Authenticator: webauthn.Authenticator{ - AAGUID: p.AAGUID, + AAGUID: aaguid, SignCount: p.SignCount, CloneWarning: p.CloneWarning, Attachment: protocol.AuthenticatorAttachment(p.Attachment), @@ -99,10 +104,10 @@ func NewPasskeyCredentialFromWebAuthn(userID int, credential *webauthn.Credentia } passkey := &PasskeyCredential{ UserID: userID, - CredentialID: credential.ID, - PublicKey: credential.PublicKey, + CredentialID: base64.StdEncoding.EncodeToString(credential.ID), + PublicKey: base64.StdEncoding.EncodeToString(credential.PublicKey), AttestationType: credential.AttestationType, - AAGUID: credential.Authenticator.AAGUID, + AAGUID: base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID), SignCount: credential.Authenticator.SignCount, CloneWarning: credential.Authenticator.CloneWarning, UserPresent: credential.Flags.UserPresent, @@ -119,10 +124,10 @@ func (p *PasskeyCredential) ApplyValidatedCredential(credential *webauthn.Creden if credential == nil || p == nil { return } - p.CredentialID = credential.ID - p.PublicKey = credential.PublicKey + p.CredentialID = base64.StdEncoding.EncodeToString(credential.ID) + p.PublicKey = base64.StdEncoding.EncodeToString(credential.PublicKey) p.AttestationType = credential.AttestationType - p.AAGUID = credential.Authenticator.AAGUID + p.AAGUID = base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID) p.SignCount = credential.Authenticator.SignCount p.CloneWarning = credential.Authenticator.CloneWarning p.UserPresent = credential.Flags.UserPresent @@ -157,8 +162,9 @@ func GetPasskeyByCredentialID(credentialID []byte) (*PasskeyCredential, error) { return nil, ErrFriendlyPasskeyNotFound } + credIDStr := base64.StdEncoding.EncodeToString(credentialID) var credential PasskeyCredential - if err := DB.Where("credential_id = ?", credentialID).First(&credential).Error; err != nil { + if err := DB.Where("credential_id = ?", credIDStr).First(&credential).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { common.SysLog(fmt.Sprintf("GetPasskeyByCredentialID: passkey not found for credential ID length %d", len(credentialID))) return nil, ErrFriendlyPasskeyNotFound diff --git a/router/api-router.go b/router/api-router.go index 4afc0a0fa..d29615914 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -99,7 +99,7 @@ func SetApiRouter(router *gin.Engine) { adminRoute.POST("/manage", controller.ManageUser) adminRoute.PUT("/", controller.UpdateUser) adminRoute.DELETE("/:id", controller.DeleteUser) - adminRoute.DELETE("/:id/passkey", controller.AdminResetPasskey) + adminRoute.DELETE("/:id/reset_passkey", controller.AdminResetPasskey) // Admin 2FA routes adminRoute.GET("/2fa/stats", controller.Admin2FAStats) diff --git a/web/src/components/table/users/UsersColumnDefs.jsx b/web/src/components/table/users/UsersColumnDefs.jsx index 6cd9e9785..37b4c7f25 100644 --- a/web/src/components/table/users/UsersColumnDefs.jsx +++ b/web/src/components/table/users/UsersColumnDefs.jsx @@ -26,7 +26,9 @@ import { Progress, Popover, Typography, + Dropdown, } from '@douyinfe/semi-ui'; +import { IconMore } from '@douyinfe/semi-icons'; import { renderGroup, renderNumber, renderQuota } from '../../../helpers'; /** @@ -213,6 +215,28 @@ const renderOperations = ( return <>; } + const moreMenu = [ + { + node: 'item', + name: t('重置 Passkey'), + onClick: () => showResetPasskeyModal(record), + }, + { + node: 'item', + name: t('重置 2FA'), + onClick: () => showResetTwoFAModal(record), + }, + { + node: 'divider', + }, + { + node: 'item', + name: t('注销'), + type: 'danger', + onClick: () => showDeleteModal(record), + }, + ]; + return ( {record.status === 1 ? ( @@ -255,27 +279,17 @@ const renderOperations = ( > {t('降级')} - - - +