package store import ( "context" "fmt" "github.com/jackc/pgx/v5" ) // DeleteUser permanently removes a user and all of their data. It runs through // withUser so RLS confines every statement to the calling user's own rows. // // Deleting the users row cascades (ON DELETE CASCADE) to videos, transcripts, // summaries (→ sink_deliveries), video_connections, and the user_identities map // — referential-integrity cascades bypass RLS, so a user's child rows are removed // even though the deleting connection is scoped. summary_actions is the exception: // it carries a user_id but has NO foreign key to users (migration 002), so the // cascade does not reach it; it is deleted explicitly in the same scoped // transaction. Deleting an absent user is a no-op (idempotent). // // This is tapir-side only (decision 2026-06-03): it removes all tapir data; the // Dex login identity is left untouched — a later login simply re-enters // registration. The user's secrets (OAuth tokens) live in the SecretStore, not // the DB, and are removed by the caller (the account handler). func (s *Store) DeleteUser(ctx context.Context, userID string) error { return s.withUser(ctx, userID, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `DELETE FROM summary_actions WHERE user_id = $1`, userID); err != nil { return fmt.Errorf("store: delete summary_actions: %w", err) } if _, err := tx.Exec(ctx, `DELETE FROM users WHERE id = $1`, userID); err != nil { return fmt.Errorf("store: delete user: %w", err) } return nil }) } // DisplayName returns the user's registered display name (empty if unset). Scoped // by user_id via withUser, like every read in this package. func (s *Store) DisplayName(ctx context.Context, userID string) (string, error) { var name string if err := s.withUser(ctx, userID, func(tx pgx.Tx) error { return tx.QueryRow(ctx, `SELECT COALESCE(display_name, '') FROM users WHERE id = $1`, userID).Scan(&name) }); err != nil { return "", fmt.Errorf("store: display name: %w", err) } return name, nil }