Update
This commit is contained in:
1 parent
9602403f8a
commit
cad1f7b842
1 file changed
+87
-4
+87
-4
@@ -6,6 +6,7 @@ import fs from 'fs';
|
|||||||
|
|
||||||
export class BrowserClient {
|
export class BrowserClient {
|
||||||
private context: BrowserContext | null = null;
|
private context: BrowserContext | null = null;
|
||||||
|
private sessionDir: string | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private config: Config,
|
private config: Config,
|
||||||
@@ -14,6 +15,7 @@ export class BrowserClient {
|
|||||||
|
|
||||||
async initialize(): Promise<BrowserContext> {
|
async initialize(): Promise<BrowserContext> {
|
||||||
const sessionDir = path.resolve(process.cwd(), this.config.browser.sessionDir);
|
const sessionDir = path.resolve(process.cwd(), this.config.browser.sessionDir);
|
||||||
|
this.sessionDir = sessionDir;
|
||||||
|
|
||||||
// Ensure session directory exists
|
// Ensure session directory exists
|
||||||
if (!fs.existsSync(sessionDir)) {
|
if (!fs.existsSync(sessionDir)) {
|
||||||
@@ -171,6 +173,26 @@ export class BrowserClient {
|
|||||||
page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 20000 }).catch(() => undefined),
|
page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 20000 }).catch(() => undefined),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Give cookies/time to settle
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const obstacleInfo = await this.detectLoginObstacles(page);
|
||||||
|
if (obstacleInfo.twoFactor) {
|
||||||
|
this.logger.error('Login requires 2FA. Please run "npm run login" locally to complete it once.');
|
||||||
|
await this.captureLoginDebug(page, 'login-2fa');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (obstacleInfo.captcha) {
|
||||||
|
this.logger.error('Login blocked by CAPTCHA. Please run "npm run login" locally to complete it once.');
|
||||||
|
await this.captureLoginDebug(page, 'login-captcha');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (obstacleInfo.errorText) {
|
||||||
|
this.logger.error(`Login failed with error: ${obstacleInfo.errorText}`);
|
||||||
|
await this.captureLoginDebug(page, 'login-error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Confirm auth via UI or API check
|
// Confirm auth via UI or API check
|
||||||
const isValid = await this.isSessionValid();
|
const isValid = await this.isSessionValid();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
@@ -179,10 +201,7 @@ export class BrowserClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for error messages
|
// Check for error messages
|
||||||
const errorContent = await page.evaluate(() => {
|
const errorContent = obstacleInfo.errorText;
|
||||||
const err = document.querySelector('[role="alert"], .AnimatedForm__errorMessage');
|
|
||||||
return err ? err.textContent : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errorContent) {
|
if (errorContent) {
|
||||||
this.logger.error(`Login failed with error: ${errorContent}`);
|
this.logger.error(`Login failed with error: ${errorContent}`);
|
||||||
@@ -190,6 +209,7 @@ export class BrowserClient {
|
|||||||
this.logger.error('Login flow finished but session is invalid.');
|
this.logger.error('Login flow finished but session is invalid.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.captureLoginDebug(page, 'login-invalid-session');
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -197,4 +217,67 @@ export class BrowserClient {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async detectLoginObstacles(page: Page): Promise<{
|
||||||
|
twoFactor: boolean;
|
||||||
|
captcha: boolean;
|
||||||
|
errorText: string | null;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const info = await page.evaluate(() => {
|
||||||
|
const errorCandidates = Array.from(
|
||||||
|
document.querySelectorAll('[role="alert"], .AnimatedForm__errorMessage, [data-testid*="error"], [id*="error"]')
|
||||||
|
)
|
||||||
|
.map((el) => el.textContent?.trim())
|
||||||
|
.filter((text) => text && text.length > 0) as string[];
|
||||||
|
|
||||||
|
const otpInput = document.querySelector('input[name="otp"], input[autocomplete="one-time-code"], input[type="tel"][maxlength="6"]');
|
||||||
|
const captchaElement = document.querySelector('iframe[src*="captcha"], iframe[src*="recaptcha"], [data-sitekey], #captcha, .captcha');
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorText: errorCandidates[0] ?? null,
|
||||||
|
hasOtp: Boolean(otpInput),
|
||||||
|
hasCaptcha: Boolean(captchaElement),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const frameCaptcha = page.frames().some((frame) => /captcha|recaptcha/i.test(frame.url()));
|
||||||
|
|
||||||
|
return {
|
||||||
|
twoFactor: info.hasOtp,
|
||||||
|
captcha: info.hasCaptcha || frameCaptcha,
|
||||||
|
errorText: info.errorText,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { twoFactor: false, captcha: false, errorText: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async captureLoginDebug(page: Page, label: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const baseDir = this.sessionDir ?? path.resolve(process.cwd(), 'data');
|
||||||
|
if (!fs.existsSync(baseDir)) {
|
||||||
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeTimestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const screenshotPath = path.join(baseDir, `${label}-${safeTimestamp}.png`);
|
||||||
|
const htmlPath = path.join(baseDir, `${label}-${safeTimestamp}.html`);
|
||||||
|
|
||||||
|
await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => undefined);
|
||||||
|
const html = await page.content().catch(() => '');
|
||||||
|
if (html) {
|
||||||
|
fs.writeFileSync(htmlPath, html, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Saved login debug artifacts to ${screenshotPath} and ${htmlPath}`);
|
||||||
|
this.logger.info(`Login debug URL: ${page.url()}`);
|
||||||
|
const title = await page.title().catch(() => '');
|
||||||
|
if (title) {
|
||||||
|
this.logger.info(`Login debug title: ${title}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Failed to capture login debug artifacts:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user