Files
mygo/internal/repository/credential.go
T
ld a18f96912d feat(repository): refactor query/mutation ports with capability-safe
interfaces

- refactor: split UserRepository into AuthUserRepository and
  AdminUserRepository capabilities
- refactor: split SessionRepository into AuthSessionRepository and
  repository-owned CLI/admin methods
- refactor: replace generic Repository Update/Delete with
  operation-specific params and ownership predicates
- refactor: replace CredentialRepository generic CRUD with
  passkey-specific methods
- refactor: replace FileRepository generic Create/Update/Delete with
  UploadedFileParams, DirectoryParams, and owned soft-delete
- refactor: remove repository fields from WebApp struct; repositories
  are now composition-time wiring only
- feat: add domain error kinds ErrParentNotFound, ErrParentNotDir,
  ErrDirectoryNotEmpty, ErrInvalidMove
- feat: add CredentialTypeAppPasskey constant
- feat: add testutil.SetUserAdmin for test fixture setup that bypasses
  production service ports
- test: add architecture test banning GORM Save in repository package
- test: add capability interface contract tests ensuring each service
  receives the minimal interface
- test: add blockingStorage helper for concurrent promotion tests
- test: add preserved DSN parameter test for sqliteImmediateDSN
- docs: update architecture decisions with repository write rules and
  capability separation
- docs: update roadmap to clarify atomic single-use refresh sessions
- docs: add -race test target to development docs
2026-07-16 12:24:36 +08:00

115 lines
3.4 KiB
Go

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
}