import type { Page } from 'playwright'; import type { ILogger } from '../../../../application/ports/ILogger'; import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow'; import { IRACING_URLS, IRACING_SELECTORS, IRACING_TIMEOUTS } from '../dom/IRacingSelectors'; import { AuthenticationGuard } from './AuthenticationGuard'; export class IRacingPlaywrightAuthFlow implements IPlaywrightAuthFlow { constructor(private readonly logger?: ILogger) {} getLoginUrl(): string { return IRACING_URLS.login; } getPostLoginLandingUrl(): string { return IRACING_URLS.hostedSessions; } isLoginUrl(url: string): boolean { const lower = url.toLowerCase(); return ( lower.includes('oauth.iracing.com') || lower.includes('/membersite/login') || lower.includes('/login.jsp') || lower.includes('/login') ); } isAuthenticatedUrl(url: string): boolean { const lower = url.toLowerCase(); return ( lower.includes('/web/racing/hosted') || lower.includes('/membersite/member') || lower.includes('members-ng.iracing.com') || lower.startsWith(IRACING_URLS.hostedSessions.toLowerCase()) || lower.startsWith(IRACING_URLS.home.toLowerCase()) ); } isLoginSuccessUrl(url: string): boolean { return this.isAuthenticatedUrl(url) && !this.isLoginUrl(url); } async detectAuthenticatedUi(page: Page): Promise { const authSelectors = [ IRACING_SELECTORS.hostedRacing.createRaceButton, '[aria-label*="user menu" i]', '[aria-label*="account menu" i]', '.user-menu', '.account-menu', 'nav a[href*="/membersite"]', 'nav a[href*="/members"]', ]; for (const selector of authSelectors) { try { const element = page.locator(selector).first(); const isVisible = await element.isVisible().catch(() => false); if (isVisible) { this.logger?.info?.('Authenticated UI detected', { selector }); return true; } } catch { // Ignore selector errors, try next selector } } return false; } async detectLoginUi(page: Page): Promise { const guard = new AuthenticationGuard(page, this.logger); return guard.checkForLoginUI(); } async navigateToAuthenticatedArea(page: Page): Promise { await page.goto(this.getPostLoginLandingUrl(), { waitUntil: 'domcontentloaded', timeout: IRACING_TIMEOUTS.navigation, }); } async waitForPostLoginRedirect(page: Page, timeoutMs: number): Promise { const start = Date.now(); this.logger?.info?.('Waiting for post-login redirect', { timeoutMs }); while (Date.now() - start < timeoutMs) { try { if (page.isClosed()) { this.logger?.warn?.('Page closed while waiting for post-login redirect'); return false; } const url = page.url(); if (this.isLoginSuccessUrl(url)) { this.logger?.info?.('Login success detected by URL', { url }); return true; } // Fallback: detect authenticated UI even if URL is not the canonical one const hasAuthUi = await this.detectAuthenticatedUi(page); if (hasAuthUi) { this.logger?.info?.('Login success detected by authenticated UI', { url }); return true; } await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); this.logger?.debug?.('Error while waiting for post-login redirect', { error: message }); if (page.isClosed()) { return false; } await new Promise((resolve) => setTimeout(resolve, 500)); } } this.logger?.warn?.('Post-login redirect wait timed out', { timeoutMs }); return false; } }