import { Page } from 'playwright'; import BrowserManager from './browser.js'; import { getVerificationCode } from './emailVerification.js'; interface Account { email: string; password: string; proofEmail: string; } export class AuthService { constructor(private env: Env) { } async authenticateEmail(email: string): Promise<{ success: boolean; error?: string }> { try { const accountsStr = await this.env.KV.get("accounts"); const accounts: Account[] = accountsStr ? JSON.parse(accountsStr) : []; const account = accounts.find(a => a.email === email); if (!account) { throw new Error("Account not found"); } await this.performAuthentication(account); await this.handleAuthorizationCallback(email); return { success: true }; } catch (error: any) { return { success: false, error: error.message }; } } private async performAuthentication(account: Account) { const clientId = this.env.ENTRA_CLIENT_ID; const redirectUri = this.env.AUTH_REDIRECT_URI; let browser; let context; try { browser = await BrowserManager.getInstance(); context = await browser.newContext(); const page = await context.newPage(); const authUrl = this.buildAuthUrl(clientId, redirectUri, account.email); await this.handleLoginProcess(page, account, authUrl); await this.handleMultiFactorAuth(page, account); await this.handleConsent(page, redirectUri); } finally { if (context) await context.close(); } } private buildAuthUrl(clientId: string, redirectUri: string, email: string): string { return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + `client_id=${clientId}` + `&response_type=code` + `&redirect_uri=${encodeURIComponent(redirectUri)}` + `&response_mode=query` + `&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` + `&prompt=consent` + `&state=${email}`; } private async handleLoginProcess(page: Page, account: Account, authUrl: string) { await page.goto(authUrl); await page.fill('input[type="email"]', account.email); await page.click('input[type="submit"]'); try { await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 }); await page.click('#idA_PWD_SwitchToPassword'); } catch (error) { console.log(`没有切换到密码登录,继续执行: ${error}`); } await page.waitForURL("https://login.live.com/**"); await page.fill('input[type="password"]', account.password); await page.fill('input[type="password"]', account.password); await page.click('button[type="submit"]'); try { await page.waitForURL("https://account.live.com/recover/**", { timeout: 5000 }); await page.click('#iLandingViewAction'); } catch (error) { console.log("验证确定 page not found or timeout, skipping..."); } } private async handleMultiFactorAuth(page: Page, account: Account) { for (let i = 0; i < 3; i++) { try { await page.waitForURL('https://account.live.com/identity/**', { timeout: 5000 }); const proofEmail = account.proofEmail; if (!proofEmail) { throw new Error("No proof email provided"); } const timestamp = Math.floor(Date.now() / 1000); try { await page.waitForSelector('#iProof0', { timeout: 3000 }); await page.click('#iProof0'); } catch (error) { console.log(`没有#iProof0,继续执行: ${error}`); } await page.fill("#iProofEmail", proofEmail); await page.click('input[type="submit"]'); const proof = [ { "suffix": "godgodgame.com", "apiUrl": "https://seedmail.igiven.com/api/latest-email", "token": this.env.PROOF_GODGODGAME_TOKEN }, { "suffix": "igiven.com", "apiUrl": "https://mail.igiven.com/api/latest-email", "token": this.env.PROOF_IGIVEN_TOKEN } ]; const suffix = proofEmail.substring(proofEmail.indexOf('@') + 1); const proofConfig = proof.find(p => p.suffix === suffix)!; const verificationCode = await getVerificationCode( proofConfig.apiUrl, proofConfig.token!, proofEmail, timestamp ); await page.fill('input[type="tel"]', verificationCode); await page.click('input[type="submit"]'); await page.waitForTimeout(5 * 1000) } catch (error) { console.log(account.email, `没有多重验证,继续执行: ${error}`); } } } private async handleConsent(page: Page, redirectUri: string) { await page.waitForURL((url: any) => url.href.startsWith("https://login.live.com")); await page.click('button[type="submit"]#acceptButton'); try { await page.waitForURL("https://account.live.com/Consent/**", { timeout: 10000 }); await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]'); } catch (error) { console.log("Consent page not found or timeout, skipping..."); } await page.waitForURL((url: any) => url.href.startsWith(redirectUri)); } private async handleAuthorizationCallback(email: string) { let code = null; const maxRetries = 30; let retries = 0; while (!code && retries < maxRetries) { const codeKey = `code_${email}`; code = await this.env.KV.get(codeKey); if (!code) { await new Promise(resolve => setTimeout(resolve, 1000)); retries++; } } if (!code) { throw new Error("Authorization timeout"); } const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: this.env.ENTRA_CLIENT_ID, client_secret: this.env.ENTRA_CLIENT_SECRET, code: code, redirect_uri: this.env.AUTH_REDIRECT_URI, grant_type: 'authorization_code' }) }); const tokenData: any = await tokenResponse.json(); if (!tokenData.refresh_token) { throw new Error("Failed to get refresh token"); } if (!tokenData.expires_in) { throw new Error("Missing expires_in in token response"); } const tokenInfo = { ...tokenData, timestamp: Date.now() }; await this.env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo)); await this.env.KV.delete(`code_${email}`); } }