package db import ( "context" "database/sql" "errors" "fmt" ) var ErrNotFound = errors.New("secret not found") var ErrForbidden = errors.New("code owned by another user") type SecretsRepository struct { db *sql.DB } func NewSecretsRepository(db *sql.DB) *SecretsRepository { return &SecretsRepository{db: db} } func (r *SecretsRepository) Get(ctx context.Context, code string) (string, error) { var value string err := r.db.QueryRowContext(ctx, "SELECT value FROM secrets WHERE code = ?", code).Scan(&value) if errors.Is(err, sql.ErrNoRows) { return "", ErrNotFound } if err != nil { return "", fmt.Errorf("could not get secret: %w", err) } return value, nil } // Set stores value under code, owned by user. If code already exists and is // owned by a different user, it returns ErrForbidden and leaves the stored // value untouched. func (r *SecretsRepository) Set(ctx context.Context, code, value, user string) error { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("could not begin transaction: %w", err) } defer func() { _ = tx.Rollback() }() var owner string err = tx.QueryRowContext(ctx, "SELECT user FROM secrets WHERE code = ?", code).Scan(&owner) if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("could not check secret owner: %w", err) } if !errors.Is(err, sql.ErrNoRows) && owner != user { return ErrForbidden } if _, err := tx.ExecContext(ctx, ` INSERT INTO secrets (code, value, user, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT(code) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP `, code, value, user); err != nil { return fmt.Errorf("could not set secret: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("could not commit transaction: %w", err) } return nil }