123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
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<boolean> {
|
|
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<boolean> {
|
|
const guard = new AuthenticationGuard(page, this.logger);
|
|
return guard.checkForLoginUI();
|
|
}
|
|
|
|
async navigateToAuthenticatedArea(page: Page): Promise<void> {
|
|
await page.goto(this.getPostLoginLandingUrl(), {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: IRACING_TIMEOUTS.navigation,
|
|
});
|
|
}
|
|
|
|
async waitForPostLoginRedirect(page: Page, timeoutMs: number): Promise<boolean> {
|
|
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;
|
|
}
|
|
} |