package store import ( "context" "fmt" "time" "github.com/jackc/pgx/v5" ) // Connection is one connected video account (data-model.md VIDEO_CONNECTION). // TokenRef is the opaque SecretStore reference that resolves to the OAuth refresh // token — never the token itself. ConnectedAt is set by the DB and is read-only // on writes (UpsertConnection ignores it). type Connection struct { Provider string ProviderAccount string TokenRef string Status string ConnectedAt time.Time } // UpsertConnection records (or refreshes) the user's connection to a provider, // keyed on (user_id, provider): re-connecting the same provider overwrites the // token_ref/status/account and bumps connected_at, never duplicating. Like every // access in this package it routes through withUser, so RLS scopes the write to // the calling user — a connection can only be written for the current user. func (s *Store) UpsertConnection(ctx context.Context, userID string, c Connection) error { return s.withUser(ctx, userID, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `INSERT INTO video_connections (user_id, provider, provider_account, token_ref, status) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id, provider) DO UPDATE SET provider_account = EXCLUDED.provider_account, token_ref = EXCLUDED.token_ref, status = EXCLUDED.status, connected_at = now()`, userID, c.Provider, nullIfEmpty(c.ProviderAccount), c.TokenRef, c.Status, ); err != nil { return fmt.Errorf("store: upsert connection: %w", err) } return nil }) } // ConnectionsForUser returns the user's connections, most-recently-connected // first. Scoped by user_id via withUser: one user never sees another's. func (s *Store) ConnectionsForUser(ctx context.Context, userID string) ([]Connection, error) { var out []Connection if err := s.withUser(ctx, userID, func(tx pgx.Tx) error { rows, err := tx.Query(ctx, `SELECT provider, COALESCE(provider_account, ''), token_ref, status, connected_at FROM video_connections WHERE user_id = $1 ORDER BY connected_at DESC, provider`, userID) if err != nil { return fmt.Errorf("store: connections for user: %w", err) } defer rows.Close() for rows.Next() { var c Connection if err := rows.Scan(&c.Provider, &c.ProviderAccount, &c.TokenRef, &c.Status, &c.ConnectedAt); err != nil { return fmt.Errorf("store: scan connection: %w", err) } out = append(out, c) } if err := rows.Err(); err != nil { return fmt.Errorf("store: iterate connections: %w", err) } return nil }); err != nil { return nil, err } return out, nil } // DeleteConnection removes the user's connection to a provider. Deleting an // absent connection is a no-op (no error). func (s *Store) DeleteConnection(ctx context.Context, userID, provider string) error { return s.withUser(ctx, userID, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `DELETE FROM video_connections WHERE user_id = $1 AND provider = $2`, userID, provider); err != nil { return fmt.Errorf("store: delete connection: %w", err) } return nil }) } // nullIfEmpty maps "" to a SQL NULL so an unknown provider_account is stored as // NULL (the column is nullable) rather than an empty string. func nullIfEmpty(s string) *string { if s == "" { return nil } return &s }