package store import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // PlatformConnection ist die per OAuth hergestellte Verbindung eines // Accounts zu seinem eigenen Instagram- oder TikTok-Account (siehe // internal/socialconnect). Anders als asset/finding/extraction NICHT // append-only — ein abgelaufenes oder erneuertes Token ersetzt das alte, // eine getrennte Verbindung wird wirklich gelöscht. type PlatformConnection struct { ID string AccountID string Platform string PlatformUserID string AccessToken string RefreshToken string ExpiresAt *time.Time ConnectedAt time.Time } // UpsertPlatformConnection legt eine Verbindung an oder ersetzt die // bestehende für dasselbe Account+Plattform-Paar (z. B. bei erneutem // Verbinden nach Trennen, oder wenn refresh_token erneuert wurde). func (s *Store) UpsertPlatformConnection(ctx context.Context, accountID, platform, platformUserID, accessToken, refreshToken string, expiresAt *time.Time) (PlatformConnection, error) { var c PlatformConnection err := s.Pool.QueryRow(ctx, ` INSERT INTO platform_connection (account_id, platform, platform_user_id, access_token, refresh_token, expires_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (account_id, platform) DO UPDATE SET platform_user_id = EXCLUDED.platform_user_id, access_token = EXCLUDED.access_token, refresh_token = EXCLUDED.refresh_token, expires_at = EXCLUDED.expires_at, connected_at = now() RETURNING id, account_id, platform, platform_user_id, access_token, refresh_token, expires_at, connected_at `, accountID, platform, platformUserID, accessToken, refreshToken, expiresAt).Scan( &c.ID, &c.AccountID, &c.Platform, &c.PlatformUserID, &c.AccessToken, &c.RefreshToken, &c.ExpiresAt, &c.ConnectedAt, ) if err != nil { return PlatformConnection{}, fmt.Errorf("store: upsert platform connection: %w", err) } return c, nil } // ListPlatformConnectionsForAccount liefert alle Plattform-Verbindungen // eines Accounts. func (s *Store) ListPlatformConnectionsForAccount(ctx context.Context, accountID string) ([]PlatformConnection, error) { rows, err := s.Pool.Query(ctx, ` SELECT id, account_id, platform, platform_user_id, access_token, refresh_token, expires_at, connected_at FROM platform_connection WHERE account_id = $1 ORDER BY platform `, accountID) if err != nil { return nil, fmt.Errorf("store: list platform connections: %w", err) } defer rows.Close() var out []PlatformConnection for rows.Next() { var c PlatformConnection if err := rows.Scan(&c.ID, &c.AccountID, &c.Platform, &c.PlatformUserID, &c.AccessToken, &c.RefreshToken, &c.ExpiresAt, &c.ConnectedAt); err != nil { return nil, fmt.Errorf("store: scan platform connection: %w", err) } out = append(out, c) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list platform connections: %w", err) } return out, nil } // GetPlatformConnection liest die Verbindung eines Accounts zu einer // bestimmten Plattform. Liefert ErrNotFound, wenn keine Verbindung // besteht — der Normalfall, solange der Kunde nichts verbunden hat. func (s *Store) GetPlatformConnection(ctx context.Context, accountID, platform string) (PlatformConnection, error) { var c PlatformConnection err := s.Pool.QueryRow(ctx, ` SELECT id, account_id, platform, platform_user_id, access_token, refresh_token, expires_at, connected_at FROM platform_connection WHERE account_id = $1 AND platform = $2 `, accountID, platform).Scan( &c.ID, &c.AccountID, &c.Platform, &c.PlatformUserID, &c.AccessToken, &c.RefreshToken, &c.ExpiresAt, &c.ConnectedAt, ) if errors.Is(err, pgx.ErrNoRows) { return PlatformConnection{}, ErrNotFound } if err != nil { return PlatformConnection{}, fmt.Errorf("store: get platform connection: %w", err) } return c, nil } // DeletePlatformConnection trennt eine Plattform-Verbindung. func (s *Store) DeletePlatformConnection(ctx context.Context, accountID, platform string) error { tag, err := s.Pool.Exec(ctx, `DELETE FROM platform_connection WHERE account_id = $1 AND platform = $2`, accountID, platform) if err != nil { return fmt.Errorf("store: delete platform connection: %w", err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil }