feat: add secure passkey login for issue 536

This commit is contained in:
sagitchu
2026-09-30 19:03:03 +08:00
parent c952d2fb3a
commit 1995759dbe
19 changed files with 1144 additions and 5 deletions
@@ -0,0 +1,13 @@
package model
// Passkey stores a WebAuthn credential for exactly one panel user.
type Passkey struct {
ID string `gorm:"primaryKey;type:varchar(512)"`
UserID int64 `gorm:"column:user_id;not null;index"`
Name string `gorm:"type:varchar(100);not null"`
CredentialJSON string `gorm:"column:credential_json;type:text;not null"`
CreatedAt int64 `gorm:"column:created_at;not null"`
LastUsedAt int64 `gorm:"column:last_used_at;not null;default:0"`
}
func (Passkey) TableName() string { return "passkey" }
@@ -288,6 +288,7 @@ func autoMigrateAll(db *gorm.DB) error {
models := []interface{}{
&model.User{},
&model.Passkey{},
&model.UserQuota{},
&model.Forward{},
&model.ForwardPort{},
@@ -156,6 +156,9 @@ func (r *Repository) DeleteUserCascade(userID int64) error {
return errors.New("repository not initialized")
}
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.Passkey{}).Error; err != nil {
return err
}
forwardIDs := tx.Model(&model.Forward{}).Select("id").Where("user_id = ?", userID)
if err := tx.Where("forward_id IN (?)", forwardIDs).Delete(&model.ForwardPort{}).Error; err != nil {
return err
@@ -0,0 +1,40 @@
package repo
import (
"errors"
"go-backend/internal/store/model"
"gorm.io/gorm"
)
func (r *Repository) ListPasskeys(userID int64) ([]model.Passkey, error) {
var keys []model.Passkey
err := r.db.Where("user_id = ?", userID).Order("created_at desc").Find(&keys).Error
return keys, err
}
func (r *Repository) GetPasskey(userID int64, id string) (*model.Passkey, error) {
var key model.Passkey
err := r.db.Where("user_id = ? AND id = ?", userID, id).Take(&key).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return &key, err
}
func (r *Repository) CreatePasskey(key *model.Passkey) error {
return r.db.Create(key).Error
}
func (r *Repository) DeletePasskey(userID int64, id string) (bool, error) {
result := r.db.Where("user_id = ? AND id = ?", userID, id).Delete(&model.Passkey{})
return result.RowsAffected == 1, result.Error
}
// Compare-and-swap prevents a concurrent assertion from reusing an old sign counter.
func (r *Repository) UpdatePasskeyCredential(userID int64, id, oldJSON, newJSON string, now int64) (bool, error) {
result := r.db.Model(&model.Passkey{}).
Where("user_id = ? AND id = ? AND credential_json = ?", userID, id, oldJSON).
Updates(map[string]interface{}{"credential_json": newJSON, "last_used_at": now})
return result.RowsAffected == 1, result.Error
}