package repository import ( "context" "errors" "time" "gorm.io/gorm" "github.com/dhao2001/mygo/internal/model" ) // PasskeyParams contains the fields accepted when creating an app passkey. type PasskeyParams struct { ID string UserID string Label string SecretHash string } // CredentialRepository provides access to alternative credential records. type CredentialRepository interface { CreatePasskey(ctx context.Context, params PasskeyParams) error FindPasskeyByID(ctx context.Context, id string) (*model.Credential, error) ListPasskeys(ctx context.Context, userID string) ([]model.Credential, error) FindPasskeyByHash(ctx context.Context, hash string) (*model.Credential, error) RecordPasskeyUsed(ctx context.Context, id string) error RevokeOwnedPasskey(ctx context.Context, userID, id string) error } type credentialRepository struct { db *gorm.DB } // NewCredentialRepository creates a CredentialRepository backed by GORM. func NewCredentialRepository(db *gorm.DB) CredentialRepository { return &credentialRepository{db: db} } func (r *credentialRepository) CreatePasskey(ctx context.Context, params PasskeyParams) error { cred := &model.Credential{ ID: params.ID, UserID: params.UserID, Type: model.CredentialTypeAppPasskey, Label: params.Label, SecretHash: params.SecretHash, } result := r.db.WithContext(ctx).Create(cred) if result.Error != nil { if isDuplicateKeyError(result.Error) { return model.ErrDuplicate } return result.Error } return nil } func (r *credentialRepository) FindPasskeyByID(ctx context.Context, id string) (*model.Credential, error) { var cred model.Credential result := r.db.WithContext(ctx).First(&cred, "id = ? AND type = ?", id, model.CredentialTypeAppPasskey) if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, model.ErrNotFound } if result.Error != nil { return nil, result.Error } return &cred, nil } func (r *credentialRepository) ListPasskeys(ctx context.Context, userID string) ([]model.Credential, error) { var creds []model.Credential result := r.db.WithContext(ctx).Where("user_id = ? AND type = ?", userID, model.CredentialTypeAppPasskey).Find(&creds) if result.Error != nil { return nil, result.Error } return creds, nil } func (r *credentialRepository) FindPasskeyByHash(ctx context.Context, hash string) (*model.Credential, error) { var cred model.Credential result := r.db.WithContext(ctx).First(&cred, "secret_hash = ? AND type = ?", hash, model.CredentialTypeAppPasskey) if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, model.ErrNotFound } if result.Error != nil { return nil, result.Error } return &cred, nil } func (r *credentialRepository) RecordPasskeyUsed(ctx context.Context, id string) error { now := time.Now() result := r.db.WithContext(ctx).Model(&model.Credential{}). Where("id = ? AND type = ?", id, model.CredentialTypeAppPasskey). Update("last_used_at", now) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return model.ErrNotFound } return nil } func (r *credentialRepository) RevokeOwnedPasskey(ctx context.Context, userID, id string) error { result := r.db.WithContext(ctx).Delete(&model.Credential{}, "id = ? AND user_id = ? AND type = ?", id, userID, model.CredentialTypeAppPasskey) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return model.ErrNotFound } return nil }