Răsfoiți Sursa

Fix booking-2fa: click SMS link + Send verification code button; wait for link text not buttons

ZiB 4 luni în urmă
părinte
comite
d38351234d
1 a modificat fișierele cu 62 adăugiri și 14 ștergeri
  1. 62 14
      room_cal/service_api/typescript/booking/trigger-2fa.ts

+ 62 - 14
room_cal/service_api/typescript/booking/trigger-2fa.ts

@@ -12,23 +12,33 @@ const ENV_PATH = path.join(DATA_HOME, 'booking.env');
 const ADMIN_BASE = 'admin.booking.com';
 const STRIP = new Set(['OptanonConsent', 'pcm_consent', 'uz']);
 
-async function handleAuthAssurance(page: Page, masked: string): Promise<void> {
+async function handleAuthAssurance(page: Page, masked: string): Promise<boolean> {
   console.error(`[booking-2fa] Challenge required (${masked || 'unknown number'}) — attempting SMS automation`);
 
+  // Verification options are <a> links, not buttons. Click the SMS link.
   try {
-    await page.getByRole('button', { name: /sms|text message/i }).first().click({ timeout: 5_000 });
+    await page.getByText(/via Text message \(SMS\)/i).click({ timeout: 10_000 });
     console.error('[booking-2fa] Clicked SMS option');
-    await page.waitForTimeout(1_000);
+    await page.waitForLoadState('domcontentloaded');
   } catch {
-    // Already on PIN entry, or SMS is the only option — continue.
+    // Already on PIN entry, or SMS nav already happened — continue.
+  }
+
+  // Select-phone page: click "Send verification code" to trigger the SMS.
+  try {
+    await page.getByRole('button', { name: /send verification code/i }).click({ timeout: 10_000 });
+    console.error('[booking-2fa] Clicked Send verification code');
+  } catch {
+    // Already past this step.
   }
 
   const PIN_SEL = 'input[autocomplete="one-time-code"], input[inputmode="numeric"][maxlength], input[name*="pin"], input[name*="code"]';
   let pinInput;
   try {
-    pinInput = await page.waitForSelector(PIN_SEL, { timeout: 15_000 });
+    pinInput = await page.waitForSelector(PIN_SEL, { timeout: 30_000 });
   } catch {
-    throw new Error('[booking-2fa] No PIN input found — 2FA could not be completed automatically');
+    console.error('[booking-2fa] No PIN input found — no active 2FA challenge, or challenge not visible');
+    return false;
   }
 
   console.error('[booking-2fa] Polling /sms/code for up to 2 minutes...');
@@ -40,6 +50,7 @@ async function handleAuthAssurance(page: Page, masked: string): Promise<void> {
   console.error(`[booking-2fa] Filling code ${code}`);
   await pinInput.fill(code);
   await page.keyboard.press('Enter');
+  return true;
 }
 
 export async function triggerChatAuth(mode: string): Promise<CredentialsPayload> {
@@ -52,8 +63,9 @@ export async function triggerChatAuth(mode: string): Promise<CredentialsPayload>
   }
 
   const profile = profileDir('booking', mode);
+  const headless = !process.env.HEADED;
   let smsMasked = '';
-  const context = await openPersistentContext(profile, true);
+  const context = await openPersistentContext(profile, headless);
   context.on('response', async res => {
     if (res.url().includes('challenge-methods')) {
       try {
@@ -67,21 +79,57 @@ export async function triggerChatAuth(mode: string): Promise<CredentialsPayload>
   const page = context.pages()[0] ?? await context.newPage();
   const messagingUrl = `https://${ADMIN_BASE}/hotel/hoteladmin/extranet_ng/manage/messaging/inbox.html?hotel_id=${hotelId}&lang=en&ses=${ses}&source=nav&tlc=1`;
 
+  // Capture challenge-methods response before handleAuthAssurance needs it
+  const challengeMethodsReady = new Promise<void>(resolve => {
+    context.once('response', async res => {
+      if (res.url().includes('challenge-methods')) resolve();
+    });
+  });
+
   console.error('[booking-2fa] Navigating to messaging inbox (headless)...');
   await page.goto(messagingUrl);
   await page.waitForLoadState('domcontentloaded');
+  console.error(`[booking-2fa] Landed on: ${page.url()}`);
 
-  if (!page.url().includes(ADMIN_BASE)) {
+  const landedUrl = page.url();
+  const onAuthAssurance = landedUrl.includes('auth-assurance');
+  const onAdmin = landedUrl.includes(ADMIN_BASE);
+  if (!onAdmin && !onAuthAssurance) {
     await context.close();
-    throw new Error(`Session invalid — redirected to ${page.url()} — run refresh-creds first`);
+    throw new Error(`Session invalid — redirected to ${landedUrl} — run refresh-creds first`);
   }
 
-  if (page.url().includes('auth-assurance')) {
-    await handleAuthAssurance(page, smsMasked);
-    await page.waitForURL(`**${ADMIN_BASE}/**`, { timeout: 120_000 });
-    console.error('[booking-2fa] Auth-assurance completed.');
-  } else {
+  // Wait for the MFE JS to trigger auth-assurance and challenge-methods to fire,
+  // or bail after 10s if no challenge appears.
+  await Promise.race([
+    challengeMethodsReady,
+    page.waitForURL('**auth-assurance**', { timeout: 10_000 }).catch(() => {}),
+    new Promise<void>(r => setTimeout(r, 10_000)),
+  ]);
+  console.error(`[booking-2fa] After wait — url: ${page.url()}, masked: ${smsMasked || '(none)'}`);
+
+  if (!page.url().includes('auth-assurance')) {
     console.error('[booking-2fa] No auth-assurance challenge — messaging already primed for this session.');
+  } else {
+    // Wait for the auth-assurance page to render — options are links, not buttons.
+    await page.waitForSelector('text=Text message', { timeout: 30_000 }).catch(() => {});
+    const completed = await handleAuthAssurance(page, smsMasked);
+    if (completed) {
+      if (page.url().includes('auth-assurance')) {
+        await page.waitForURL(`**${ADMIN_BASE}/**`, { timeout: 120_000 });
+      }
+      console.error('[booking-2fa] Auth-assurance completed.');
+    }
+  }
+
+  // Diagnostic screenshot before closing
+  const screenshotPath = path.join(DATA_HOME, 'booking-2fa-debug.png');
+  await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => {});
+  console.error(`[booking-2fa] Screenshot: ${screenshotPath}`);
+
+  if (!headless) {
+    console.error('[booking-2fa] Headed mode — press Enter to close the browser...');
+    await new Promise<void>(resolve => process.stdin.once('data', () => resolve()));
   }
 
   const all = await context.cookies();