diff --git a/epic-claimer-new.js b/epic-claimer-new.js index 3aba57f..74ac415 100644 --- a/epic-claimer-new.js +++ b/epic-claimer-new.js @@ -103,26 +103,27 @@ const claimGame = async (page, game) => { return notify_game; }; -// Ensure user is logged in using OAuth Device Flow (bypasses Cloudflare) +// Ensure user is logged in - tries OAuth Device Flow first, falls back to browser login const ensureLoggedIn = async (page, context) => { const isLoggedIn = async () => await page.locator('egs-navigation').getAttribute('isloggedin') === 'true'; const user = cfg.eg_email || 'default'; - L.info('Attempting OAuth Device Flow login (Cloudflare bypass)'); + // Try OAuth Device Flow first (bypasses Cloudflare) + let useDeviceFlow = true; + let accessToken = null; - // Step 1: Try to get valid access token from stored device auth - let accessToken = await getValidAccessToken(user); + try { + L.info('Attempting OAuth Device Flow login (Cloudflare bypass)'); - if (accessToken) { - L.info('Using existing valid access token'); - } else { - // Step 2: No valid token - start new device auth flow - L.info('No valid token found, starting device auth flow'); - await notify( - 'epic-games: Login required! Visit the link to authorize: DEVICE_AUTH_PENDING', - ); + // Step 1: Try to get valid access token from stored device auth + accessToken = await getValidAccessToken(user); + + if (accessToken) { + L.info('Using existing valid access token'); + } else { + // Step 2: No valid token - start new device auth flow + L.info('No valid token found, starting device auth flow'); - try { const { verificationUrl, userCode, expiresAt } = await startDeviceAuthLogin(user); // Notify user with verification URL @@ -140,8 +141,6 @@ const ensureLoggedIn = async (page, context) => { // Wait for user to complete authorization const interval = 5; // poll every 5 seconds const authToken = await completeDeviceAuthLogin( - // We need to get device_code from the startDeviceAuthLogin response - // For now, we'll re-fetch it verificationUrl.split('userCode=')[1]?.split('&')[0] || '', expiresAt, interval, @@ -149,64 +148,161 @@ const ensureLoggedIn = async (page, context) => { accessToken = authToken.access_token; L.info('Device auth completed successfully'); - } catch (err) { - L.error({ err }, 'Device auth flow failed'); - await notify('epic-games: Device auth failed. Please login manually in browser.'); - throw err; } + + // Step 3: Apply bearer token to browser + L.info('Applying access token to browser'); + + /** @type {import('playwright-firefox').Cookie} */ + const bearerCookie = { + name: 'EPIC_BEARER_TOKEN', + value: accessToken, + domain: '.epicgames.com', + path: '/', + secure: true, + httpOnly: true, + sameSite: 'Lax', + }; + + await context.addCookies([bearerCookie]); + + // Visit store to get session cookies + await page.goto(STORE_HOMEPAGE_EN, { waitUntil: 'networkidle', timeout: cfg.timeout }); + + // Verify login worked + const loggedIn = await isLoggedIn(); + if (loggedIn) { + const displayName = await page.locator('egs-navigation').getAttribute('displayname'); + L.info({ user: displayName }, 'Successfully logged in via Device Flow'); + console.log(`✅ Signed in as ${displayName} (OAuth Device Flow)`); + + if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); + return displayName; + } + + L.warn('Bearer token did not result in logged-in state'); + useDeviceFlow = false; + } catch (err) { + L.warn({ err: err.message }, 'OAuth Device Flow failed, falling back to browser login'); + console.log('⚠️ Device Auth failed:', err.message); + console.log('📝 Falling back to browser-based login with email/password...'); + useDeviceFlow = false; } - // Step 3: Apply bearer token to browser - L.info('Applying access token to browser'); + // Fallback: Browser-based login with email/password + if (!useDeviceFlow) { + L.info('Using browser-based login (email/password)'); - /** @type {import('playwright-firefox').Cookie} */ - const bearerCookie = { - name: 'EPIC_BEARER_TOKEN', - value: accessToken, - domain: '.epicgames.com', - path: '/', - secure: true, - httpOnly: true, - sameSite: 'Lax', - }; - - await context.addCookies([bearerCookie]); - - // Visit store to get session cookies - await page.goto(STORE_HOMEPAGE_EN, { waitUntil: 'networkidle', timeout: cfg.timeout }); - - // Verify login worked - const loggedIn = await isLoggedIn(); - if (!loggedIn) { - L.warn('Bearer token did not result in logged-in state, may need manual login'); - // Fall back to manual browser login - console.log('Token-based login did not work. Please login manually in the browser.'); - await notify('epic-games: Please login manually in browser.'); - - if (cfg.headless) { - console.log('Run `SHOW=1 node epic-games` to login in the opened browser.'); - await context.close(); - process.exit(1); + // Check if already logged in (from cookies) + if (await isLoggedIn()) { + const displayName = await page.locator('egs-navigation').getAttribute('displayname'); + L.info({ user: displayName }, 'Already logged in (from cookies)'); + console.log(`✅ Already signed in as ${displayName}`); + return displayName; } - await page.waitForTimeout(cfg.login_timeout); + // Try browser login + console.log('📝 Attempting browser login with email/password...'); + const logged = await attemptBrowserLogin(page, context, isLoggedIn); - if (!await isLoggedIn()) { - throw new Error('Manual login did not complete within timeout'); + if (!logged) { + L.error('Browser login failed'); + console.log('❌ Browser login failed. Please login manually.'); + await notify('epic-games: Login failed. Please login manually in browser.'); + + if (cfg.headless) { + console.log('Run `SHOW=1 node epic-games` to login in the opened browser.'); + await context.close(); + process.exit(1); + } + + console.log('Waiting for manual login in browser...'); + await page.waitForTimeout(cfg.login_timeout); + + if (!await isLoggedIn()) { + throw new Error('Login did not complete within timeout'); + } } + + const displayName = await page.locator('egs-navigation').getAttribute('displayname'); + L.info({ user: displayName }, 'Successfully logged in via browser'); + console.log(`✅ Signed in as ${displayName} (Browser Login)`); + + if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); + return displayName; } - const displayName = await page.locator('egs-navigation').getAttribute('displayname'); - L.info({ user: displayName }, 'Successfully logged in'); - console.log(`✅ Signed in as ${displayName}`); + throw new Error('Login failed'); +}; - if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); - return displayName; +// Browser-based login helper function +const attemptBrowserLogin = async (page, context, isLoggedIn) => { + if (!cfg.eg_email || !cfg.eg_password) { + L.warn('No email/password configured'); + return false; + } + + try { + await page.goto(URL_LOGIN, { + waitUntil: 'domcontentloaded', + timeout: cfg.login_timeout, + }); + + const emailField = page.locator('input[name="email"], input#email, input[aria-label="Sign in with email"]').first(); + const passwordField = page.locator('input[name="password"], input#password').first(); + const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first(); + + // Step 1: Email + continue + if (await emailField.count() > 0) { + await emailField.fill(cfg.eg_email); + await continueBtn.click(); + } + + // Step 2: Password + submit + await passwordField.waitFor({ timeout: cfg.login_visible_timeout }); + await passwordField.fill(cfg.eg_password); + + const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first(); + if (await rememberMe.count() > 0) await rememberMe.check(); + await continueBtn.click(); + + // MFA step + try { + await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout }); + const otp = cfg.eg_otpkey + ? authenticator.generate(cfg.eg_otpkey) + : await prompt({ + type: 'text', + message: 'Enter two-factor sign in code', + validate: n => n.toString().length === 6 || 'The code must be 6 digits!', + }); + + const codeInputs = page.locator('input[name^="code-input"]'); + if (await codeInputs.count() > 0) { + const digits = otp.toString().split(''); + for (let i = 0; i < digits.length; i++) { + const input = codeInputs.nth(i); + await input.fill(digits[i]); + } + } else { + await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); + } + await continueBtn.click(); + } catch { + // No MFA + } + + await page.waitForURL('**/free-games', { timeout: cfg.login_timeout }); + return await isLoggedIn(); + } catch (err) { + L.error({ err }, 'Browser login failed'); + return false; + } }; // Main function to claim Epic Games export const claimEpicGamesNew = async () => { - console.log('Starting Epic Games claimer (new mode, browser-based)'); + console.log('Starting Epic Games claimer (new mode, cookies + API)'); const db = await jsonDb('epic-games.json', {}); const notify_games = [];