package repository import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/prabhupadasays/spsays-bff/internal/model" ) // ErrOAuthAccountNotFound mirrors the other repo sentinels. var ErrOAuthAccountNotFound = errors.New("oauth account not found") // OAuthAccountRepo manages provider linkage rows. type OAuthAccountRepo struct { pg *Postgres } func NewOAuthAccountRepo(pg *Postgres) *OAuthAccountRepo { return &OAuthAccountRepo{pg: pg} } // FindByProvider returns the linkage row for the given (provider, provider_user_id), // or ErrOAuthAccountNotFound. Use this first on every callback — a hit means // this provider identity has been seen before and we just log the user in. func (r *OAuthAccountRepo) FindByProvider(ctx context.Context, provider, providerUserID string) (*model.OAuthAccount, error) { const q = ` SELECT id, user_id, provider, provider_user_id, email, name, avatar_url, created_at, updated_at FROM oauth_accounts WHERE provider = $1 AND provider_user_id = $2 LIMIT 1` a := &model.OAuthAccount{} err := r.pg.Pool.QueryRow(ctx, q, provider, providerUserID).Scan( &a.ID, &a.UserID, &a.Provider, &a.ProviderUserID, &a.Email, &a.Name, &a.AvatarURL, &a.CreatedAt, &a.UpdatedAt, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrOAuthAccountNotFound } return nil, fmt.Errorf("find oauth account: %w", err) } return a, nil } // ListProvidersForUser returns the distinct provider names linked to this // user, ordered alphabetically. Used by the settings UI to mark each // social-connect row as "Connected" vs "Connect". func (r *OAuthAccountRepo) ListProvidersForUser(ctx context.Context, userID string) ([]string, error) { const q = ` SELECT DISTINCT provider FROM oauth_accounts WHERE user_id = $1 ORDER BY provider` rows, err := r.pg.Pool.Query(ctx, q, userID) if err != nil { return nil, fmt.Errorf("list oauth providers: %w", err) } defer rows.Close() out := []string{} for rows.Next() { var p string if err := rows.Scan(&p); err != nil { return nil, fmt.Errorf("scan oauth provider: %w", err) } out = append(out, p) } return out, rows.Err() } // Link inserts a new oauth_accounts row tying a provider identity to an // existing user. Idempotent: a second call for the same (provider, sub) // returns the existing row instead of erroring on the unique constraint. func (r *OAuthAccountRepo) Link(ctx context.Context, userID, provider, providerUserID string, email, name, avatarURL *string) (*model.OAuthAccount, error) { const q = ` INSERT INTO oauth_accounts (user_id, provider, provider_user_id, email, name, avatar_url) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (provider, provider_user_id) DO UPDATE SET email = EXCLUDED.email, name = EXCLUDED.name, avatar_url = EXCLUDED.avatar_url, updated_at = now() RETURNING id, user_id, provider, provider_user_id, email, name, avatar_url, created_at, updated_at` a := &model.OAuthAccount{} err := r.pg.Pool.QueryRow(ctx, q, userID, provider, providerUserID, email, name, avatarURL).Scan( &a.ID, &a.UserID, &a.Provider, &a.ProviderUserID, &a.Email, &a.Name, &a.AvatarURL, &a.CreatedAt, &a.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("link oauth account: %w", err) } return a, nil }