fix: auth join via 'Sign in' link on pre-join page (correct Teams flow: launcher -> pre-join -> sign in -> auth pre-join -> join)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1972f698b4
commit
b07910410e
1 changed files with 82 additions and 171 deletions
|
|
@ -133,193 +133,104 @@ export class BotOrchestrator {
|
||||||
// Launch browser
|
// Launch browser
|
||||||
await this._launchBrowser();
|
await this._launchBrowser();
|
||||||
|
|
||||||
// Authenticate with Microsoft if requested
|
// Update JoinProcedure with correct auth state
|
||||||
|
this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate);
|
||||||
|
|
||||||
|
this._setState('navigating');
|
||||||
|
|
||||||
|
// STEP 1: Navigate to meeting URL and click "Continue on this browser"
|
||||||
|
// This is the same for both authenticated and anonymous joins.
|
||||||
|
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
|
||||||
|
|
||||||
|
// STEP 2: For authenticated joins, click "Sign in" on the pre-join page
|
||||||
|
// instead of entering a name. The "Sign in" link is at the bottom of the
|
||||||
|
// anonymous pre-join page. Clicking it triggers the Microsoft login flow,
|
||||||
|
// which redirects back to an authenticated pre-join page within Teams v2.
|
||||||
if (authenticate) {
|
if (authenticate) {
|
||||||
|
this._logger.info('Authenticated join: looking for "Sign in" link on pre-join page...');
|
||||||
|
|
||||||
|
// Wait for the pre-join page to load
|
||||||
|
await this._page!.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Dismiss mic/camera permission overlay if present
|
||||||
|
// The "Continue without audio or video" modal may appear here
|
||||||
|
await this._joinProcedure.dismissBrowserPermissionModals();
|
||||||
|
|
||||||
|
// Find and click the "Sign in" link at the bottom of the pre-join page
|
||||||
|
const signInSelectors = [
|
||||||
|
'a:has-text("Sign in")',
|
||||||
|
'button:has-text("Sign in")',
|
||||||
|
'a:has-text("Anmelden")',
|
||||||
|
'button:has-text("Anmelden")',
|
||||||
|
'a[href*="login"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
let signInClicked = false;
|
||||||
|
for (const sel of signInSelectors) {
|
||||||
|
try {
|
||||||
|
const link = await this._page!.$(sel);
|
||||||
|
if (link) {
|
||||||
|
await link.click();
|
||||||
|
this._logger.info(`Clicked "Sign in" link: ${sel}`);
|
||||||
|
signInClicked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch { /* continue */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signInClicked) {
|
||||||
|
// Fallback: try to find "Sign in" by evaluating text content
|
||||||
|
signInClicked = await this._page!.evaluate(() => {
|
||||||
|
const links = document.querySelectorAll('a, button');
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
const el = links[i] as HTMLElement;
|
||||||
|
const text = el.innerText?.trim().toLowerCase() || '';
|
||||||
|
if (text === 'sign in' || text === 'anmelden') {
|
||||||
|
el.click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (signInClicked) {
|
||||||
|
this._logger.info('Clicked "Sign in" via DOM evaluation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signInClicked) {
|
||||||
|
// Wait for Microsoft login page to load
|
||||||
|
await this._page!.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Perform Microsoft login (email, password, stay signed in)
|
||||||
const { AuthProcedure } = await import('./authProcedure');
|
const { AuthProcedure } = await import('./authProcedure');
|
||||||
const authProcedure = new AuthProcedure(this._page!, this._logger);
|
const authProcedure = new AuthProcedure(this._page!, this._logger);
|
||||||
const authSuccess = await authProcedure.authenticateWithMicrosoft(
|
const authSuccess = await authProcedure.authenticateWithMicrosoft(
|
||||||
this._options.botAccountEmail!,
|
this._options.botAccountEmail!,
|
||||||
this._options.botAccountPassword!
|
this._options.botAccountPassword!
|
||||||
);
|
);
|
||||||
if (!authSuccess) {
|
|
||||||
throw new Error('Microsoft authentication failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRITICAL: After auth, navigate to Teams web app first to establish
|
if (authSuccess) {
|
||||||
// a Teams session. Without this, Teams redirects to anonymous mode
|
this._logger.info('Authentication via "Sign in" link succeeded');
|
||||||
// when navigating directly to the meeting URL.
|
// After auth, Teams redirects back to the authenticated pre-join page
|
||||||
this._logger.info('Establishing Teams session after auth...');
|
// within Teams v2 (/v2/) -- wait for it to load
|
||||||
try {
|
|
||||||
await this._page!.goto('https://teams.microsoft.com', {
|
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: 30000,
|
|
||||||
});
|
|
||||||
// Wait for Teams v2 to fully load and establish the auth session
|
|
||||||
// Teams v2 does multiple redirects (OAuth callback -> v2 app) which takes time
|
|
||||||
try {
|
|
||||||
await this._page!.waitForSelector('[data-tid="app-layout"], [data-tid="left-rail"], [class*="teams-"]', {
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
this._logger.info('Teams v2 app loaded (UI elements detected)');
|
|
||||||
} catch {
|
|
||||||
// Timeout waiting for UI elements, but auth session may still be established
|
|
||||||
await this._page!.waitForTimeout(5000);
|
await this._page!.waitForTimeout(5000);
|
||||||
}
|
|
||||||
const teamsUrl = this._page!.url();
|
|
||||||
this._logger.info(`Teams session established at: ${teamsUrl.substring(0, 80)}...`);
|
|
||||||
} catch (teamsNavError) {
|
|
||||||
this._logger.warn(`Teams session establishment failed (non-fatal): ${teamsNavError}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update JoinProcedure with correct auth state
|
const postAuthUrl = this._page!.url();
|
||||||
this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate);
|
this._logger.info(`Post-auth URL: ${postAuthUrl.substring(0, 80)}`);
|
||||||
|
|
||||||
this._setState('navigating');
|
// Verify we're on the authenticated pre-join page
|
||||||
|
|
||||||
if (authenticate) {
|
|
||||||
// AUTHENTICATED JOIN: Use the Teams v2 "Join a meeting" form.
|
|
||||||
// The Teams v2 app (already loaded after auth) has a "Join a meeting" page
|
|
||||||
// with Meeting ID and Passcode fields. This keeps the user authenticated
|
|
||||||
// and avoids the external launcher which always redirects to anonymous mode.
|
|
||||||
this._logger.info(`Authenticated: joining meeting via Teams v2 "Join a meeting" form...`);
|
|
||||||
|
|
||||||
// Parse the meeting URL to extract meeting code and passcode
|
|
||||||
const { parseMeetingUrl } = await import('./meetingUrlParser');
|
|
||||||
const parsed = parseMeetingUrl(this._meetingUrl);
|
|
||||||
const meetingId = parsed.meetingId || '';
|
|
||||||
const passcode = parsed.passcode || '';
|
|
||||||
|
|
||||||
this._logger.info(`Meeting ID: ${meetingId}, Passcode: ${passcode ? '***' : '(none)'}`);
|
|
||||||
|
|
||||||
let joinedViaForm = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Navigate to the Teams v2 "Join a meeting" page
|
|
||||||
// This page is available within the authenticated Teams v2 app
|
|
||||||
await this._page!.goto('https://teams.microsoft.com/v2/', {
|
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: 20000,
|
|
||||||
});
|
|
||||||
await this._page!.waitForTimeout(3000);
|
|
||||||
|
|
||||||
// Look for "Join a meeting" link/button in the Teams v2 sidebar or header
|
|
||||||
const joinMeetingSelectors = [
|
|
||||||
'button:has-text("Join a meeting")',
|
|
||||||
'a:has-text("Join a meeting")',
|
|
||||||
'button:has-text("Join with an ID")',
|
|
||||||
'a:has-text("Join with an ID")',
|
|
||||||
'[data-tid="join-meeting-button"]',
|
|
||||||
'button:has-text("An Besprechung teilnehmen")',
|
|
||||||
];
|
|
||||||
|
|
||||||
let foundJoinPage = false;
|
|
||||||
for (const sel of joinMeetingSelectors) {
|
|
||||||
try {
|
|
||||||
const btn = await this._page!.$(sel);
|
|
||||||
if (btn) {
|
|
||||||
await btn.click();
|
|
||||||
this._logger.info(`Clicked: ${sel}`);
|
|
||||||
await this._page!.waitForTimeout(2000);
|
|
||||||
foundJoinPage = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch { /* continue */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're on the "Join a meeting" form page
|
|
||||||
const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
|
const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
|
||||||
|
if (pageText.includes('Join now')) {
|
||||||
if (pageText.includes('Meeting ID') || pageText.includes('Join a meeting') || pageText.includes('Besprechungs-ID')) {
|
this._logger.info('On authenticated pre-join page with "Join now" button');
|
||||||
this._logger.info('On "Join a meeting" form page - filling in meeting details');
|
|
||||||
|
|
||||||
// Fill in the Meeting ID field
|
|
||||||
const meetingIdSelectors = [
|
|
||||||
'input[placeholder*="Meeting ID" i]',
|
|
||||||
'input[placeholder*="Besprechungs-ID" i]',
|
|
||||||
'input[aria-label*="Meeting ID" i]',
|
|
||||||
'input[aria-label*="Besprechungs-ID" i]',
|
|
||||||
'input[name*="meetingId" i]',
|
|
||||||
'input[type="text"]:first-of-type',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const sel of meetingIdSelectors) {
|
|
||||||
try {
|
|
||||||
const input = await this._page!.$(sel);
|
|
||||||
if (input) {
|
|
||||||
await input.click();
|
|
||||||
await input.fill(meetingId);
|
|
||||||
this._logger.info(`Filled Meeting ID: ${meetingId}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch { /* continue */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill in the Passcode field (if we have one)
|
|
||||||
if (passcode) {
|
|
||||||
const passcodeSelectors = [
|
|
||||||
'input[placeholder*="passcode" i]',
|
|
||||||
'input[placeholder*="Passcode" i]',
|
|
||||||
'input[placeholder*="Kennung" i]',
|
|
||||||
'input[aria-label*="passcode" i]',
|
|
||||||
'input[aria-label*="Kennung" i]',
|
|
||||||
'input[name*="passcode" i]',
|
|
||||||
'input[type="text"]:nth-of-type(2)',
|
|
||||||
'input[type="password"]',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const sel of passcodeSelectors) {
|
|
||||||
try {
|
|
||||||
const input = await this._page!.$(sel);
|
|
||||||
if (input) {
|
|
||||||
await input.click();
|
|
||||||
await input.fill(passcode);
|
|
||||||
this._logger.info('Filled Passcode');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch { /* continue */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click "Join meeting" button
|
|
||||||
await this._page!.waitForTimeout(500);
|
|
||||||
const joinBtnSelectors = [
|
|
||||||
'button:has-text("Join meeting")',
|
|
||||||
'button:has-text("An Besprechung teilnehmen")',
|
|
||||||
'button:has-text("Beitreten")',
|
|
||||||
'button[data-tid="join-meeting-submit"]',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const sel of joinBtnSelectors) {
|
|
||||||
try {
|
|
||||||
const btn = await this._page!.$(sel);
|
|
||||||
if (btn) {
|
|
||||||
await btn.click();
|
|
||||||
this._logger.info(`Clicked: ${sel}`);
|
|
||||||
joinedViaForm = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch { /* continue */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (joinedViaForm) {
|
|
||||||
// Wait for the pre-join/meeting page to load
|
|
||||||
await this._page!.waitForTimeout(5000);
|
|
||||||
this._logger.info('Submitted meeting join form - waiting for pre-join page');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (formError) {
|
|
||||||
this._logger.warn(`Teams v2 form join failed: ${formError}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!joinedViaForm) {
|
|
||||||
this._logger.warn('Teams v2 form join did not work - falling back to launcher flow');
|
|
||||||
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params)
|
this._logger.warn(`Post-auth page content: ${pageText.substring(0, 200)}`);
|
||||||
await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
|
}
|
||||||
|
} else {
|
||||||
|
this._logger.warn('Authentication via "Sign in" failed - continuing as anonymous');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._logger.warn('Could not find "Sign in" link - continuing as anonymous');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set virtual background if configured (must be done on pre-join screen, before "Join now")
|
// Set virtual background if configured (must be done on pre-join screen, before "Join now")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue