mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-23 08:21:04 +00:00
v3.1.0 initial
This commit is contained in:
129
src/browser/auth/methods/GetACodeLogin.ts
Normal file
129
src/browser/auth/methods/GetACodeLogin.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
import { getErrorMessage, getSubtitleMessage, promptInput } from './LoginUtils'
|
||||
|
||||
export class CodeLogin {
|
||||
private readonly textInputSelector = '[data-testid="codeInputWrapper"]'
|
||||
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
private async fillCode(page: Page, code: string): Promise<boolean> {
|
||||
try {
|
||||
const visibleInput = await page
|
||||
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||
.catch(() => null)
|
||||
|
||||
if (visibleInput) {
|
||||
await page.keyboard.type(code, { delay: 50 })
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
|
||||
return true
|
||||
}
|
||||
|
||||
const secondairyInput = await page.$(this.secondairyInputSelector)
|
||||
if (secondairyInput) {
|
||||
await page.keyboard.type(code, { delay: 50 })
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'No code input field found')
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Failed to fill code input: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async handle(page: Page): Promise<void> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code login authentication requested')
|
||||
|
||||
const emailMessage = await getSubtitleMessage(page)
|
||||
if (emailMessage) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', `Page message: "${emailMessage}"`)
|
||||
} else {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'Unable to retrieve email code destination')
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||
const code = await promptInput({
|
||||
question: `Enter the 6-digit code (waiting ${this.maxManualSeconds}s): `,
|
||||
timeoutSeconds: this.maxManualSeconds,
|
||||
validate: code => /^\d{6}$/.test(code)
|
||||
})
|
||||
|
||||
if (!code || !/^\d{6}$/.test(code)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual code input failed or timed out')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const filled = await this.fillCode(page, code)
|
||||
if (!filled) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Unable to fill code input (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Code input field not found')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(500)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
// Check if wrong code was entered
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error(`Maximum attempts reached: ${errorMessage}`)
|
||||
}
|
||||
|
||||
// Clear the input field before retrying
|
||||
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
|
||||
if (inputToClear) {
|
||||
await inputToClear.click()
|
||||
await page.keyboard.press('Control+A')
|
||||
await page.keyboard.press('Backspace')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Code input failed after ${this.maxManualAttempts} attempts`)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/browser/auth/methods/LoginUtils.ts
Normal file
66
src/browser/auth/methods/LoginUtils.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { Page } from 'patchright'
|
||||
import readline from 'readline'
|
||||
|
||||
export interface PromptOptions {
|
||||
question: string
|
||||
timeoutSeconds?: number
|
||||
validate?: (input: string) => boolean
|
||||
transform?: (input: string) => string
|
||||
}
|
||||
|
||||
export function promptInput(options: PromptOptions): Promise<string | null> {
|
||||
const { question, timeoutSeconds = 60, validate, transform } = options
|
||||
|
||||
return new Promise(resolve => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
let resolved = false
|
||||
|
||||
const cleanup = (result: string | null) => {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
clearTimeout(timer)
|
||||
rl.close()
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => cleanup(null), timeoutSeconds * 1000)
|
||||
|
||||
rl.question(question, answer => {
|
||||
let value = answer.trim()
|
||||
if (transform) value = transform(value)
|
||||
|
||||
if (validate && !validate(value)) {
|
||||
cleanup(null)
|
||||
return
|
||||
}
|
||||
|
||||
cleanup(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getSubtitleMessage(page: Page): Promise<string | null> {
|
||||
const message = await page
|
||||
.waitForSelector('[data-testid="subtitle"]', { state: 'visible', timeout: 1000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (!message) return null
|
||||
|
||||
const text = await message.innerText()
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
export async function getErrorMessage(page: Page): Promise<string | null> {
|
||||
const errorAlert = await page
|
||||
.waitForSelector('div[role="alert"]', { state: 'visible', timeout: 1000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (!errorAlert) return null
|
||||
|
||||
const text = await errorAlert.innerText()
|
||||
return text.trim()
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Page } from 'patchright'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { URLSearchParams } from 'url'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
|
||||
@@ -29,25 +28,70 @@ export class MobileAccessLogin {
|
||||
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
||||
authorizeUrl.searchParams.append('login_hint', email)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Auth URL constructed: ${authorizeUrl.origin}${authorizeUrl.pathname}`
|
||||
)
|
||||
|
||||
await this.bot.browser.utils.disableFido(this.page)
|
||||
|
||||
await this.page.goto(authorizeUrl.href).catch(() => {})
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Navigating to OAuth authorize URL')
|
||||
|
||||
await this.page.goto(authorizeUrl.href).catch(err => {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`page.goto() failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
})
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...')
|
||||
|
||||
const start = Date.now()
|
||||
let code = ''
|
||||
let lastUrl = ''
|
||||
|
||||
while (Date.now() - start < this.maxTimeout) {
|
||||
const url = new URL(this.page.url())
|
||||
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
|
||||
code = url.searchParams.get('code') || ''
|
||||
if (code) break
|
||||
const currentUrl = this.page.url()
|
||||
|
||||
// Log only when URL changes (high signal, no spam)
|
||||
if (currentUrl !== lastUrl) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `OAuth poll URL changed → ${currentUrl}`)
|
||||
lastUrl = currentUrl
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(currentUrl)
|
||||
|
||||
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
|
||||
code = url.searchParams.get('code') || ''
|
||||
|
||||
if (code) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'OAuth code detected in redirect URL')
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Invalid URL while polling: ${String(currentUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'Timed out waiting for OAuth code')
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Timed out waiting for OAuth code after ${Math.round((Date.now() - start) / 1000)}s`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `Final page URL: ${this.page.url()}`)
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -57,18 +101,24 @@ export class MobileAccessLogin {
|
||||
data.append('code', code)
|
||||
data.append('redirect_uri', this.redirectUrl)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Exchanging OAuth code for access token')
|
||||
|
||||
const response = await this.bot.axios.request({
|
||||
url: this.tokenUrl,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: data.toString()
|
||||
}
|
||||
})
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
const token = (response?.data?.access_token as string) ?? ''
|
||||
|
||||
if (!token) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Token response payload: ${JSON.stringify(response?.data)}`
|
||||
)
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -78,10 +128,11 @@ export class MobileAccessLogin {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`MobileAccess error: ${error instanceof Error ? error.message : String(error)}`
|
||||
`MobileAccess error: ${error instanceof Error ? error.stack || error.message : String(error)}`
|
||||
)
|
||||
return ''
|
||||
} finally {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Returning to base URL')
|
||||
await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
187
src/browser/auth/methods/RecoveryEmailLogin.ts
Normal file
187
src/browser/auth/methods/RecoveryEmailLogin.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
import { getErrorMessage, promptInput } from './LoginUtils'
|
||||
|
||||
export class RecoveryLogin {
|
||||
private readonly textInputSelector = '[data-testid="proof-confirmation"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
private async fillEmail(page: Page, email: string): Promise<boolean> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Attempting to fill email: ${email}`)
|
||||
|
||||
const visibleInput = await page
|
||||
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||
.catch(() => null)
|
||||
|
||||
if (visibleInput) {
|
||||
await page.keyboard.type(email, { delay: 50 })
|
||||
await page.keyboard.press('Enter')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Successfully filled email input field')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Email input field not found with selector: ${this.textInputSelector}`
|
||||
)
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Failed to fill email input: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async handle(page: Page, recoveryEmail: string): Promise<void> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email recovery authentication flow initiated')
|
||||
|
||||
if (recoveryEmail) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Using provided recovery email: ${recoveryEmail}`
|
||||
)
|
||||
|
||||
const filled = await this.fillEmail(page, recoveryEmail)
|
||||
if (!filled) {
|
||||
throw new Error('Email input field not found')
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
|
||||
await this.bot.utils.wait(500)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
|
||||
})
|
||||
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
throw new Error(`Email verification failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
'No recovery email provided, will prompt user for input'
|
||||
)
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Starting attempt ${attempt}/${this.maxManualAttempts}`
|
||||
)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Prompting user for email input (timeout: ${this.maxManualSeconds}s)`
|
||||
)
|
||||
|
||||
const email = await promptInput({
|
||||
question: `Recovery email (waiting ${this.maxManualSeconds}s): `,
|
||||
timeoutSeconds: this.maxManualSeconds,
|
||||
validate: email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
})
|
||||
|
||||
if (!email) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`No or invalid email input received (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual email input failed: no input received')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Invalid email format received (attempt ${attempt}/${this.maxManualAttempts}) | length=${email.length}`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual email input failed: invalid format')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Valid email received from user: ${email}`)
|
||||
|
||||
const filled = await this.fillEmail(page, email)
|
||||
if (!filled) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Failed to fill email input field (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Email input field not found after maximum attempts')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
|
||||
await this.bot.utils.wait(500)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
|
||||
})
|
||||
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Error from page: "${errorMessage}" (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error(`Maximum attempts reached. Last error: ${errorMessage}`)
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Clearing input field for retry')
|
||||
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
|
||||
if (inputToClear) {
|
||||
await inputToClear.click()
|
||||
await page.keyboard.press('Control+A')
|
||||
await page.keyboard.press('Backspace')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Input field cleared')
|
||||
} else {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-RECOVERY', 'Could not find input field to clear')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Email input failed after ${this.maxManualAttempts} attempts`)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-RECOVERY', `Fatal error: ${errorMsg}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Page } from 'patchright'
|
||||
import * as OTPAuth from 'otpauth'
|
||||
import readline from 'readline'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
import { getErrorMessage, promptInput } from './LoginUtils'
|
||||
|
||||
export class TotpLogin {
|
||||
private readonly textInputSelector =
|
||||
'form[name="OneTimeCodeViewForm"] input[type="text"], input#floatingLabelInput5'
|
||||
private readonly hiddenInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||
private readonly submitButtonSelector = 'button[type="submit"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
@@ -17,31 +17,6 @@ export class TotpLogin {
|
||||
return new OTPAuth.TOTP({ secret, digits: 6 }).generate()
|
||||
}
|
||||
|
||||
private async promptManualCode(): Promise<string | null> {
|
||||
return await new Promise(resolve => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
let resolved = false
|
||||
|
||||
const cleanup = (result: string | null) => {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
clearTimeout(timer)
|
||||
rl.close()
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => cleanup(null), this.maxManualSeconds * 1000)
|
||||
|
||||
rl.question(`Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `, answer => {
|
||||
cleanup(answer.trim())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async fillCode(page: Page, code: string): Promise<boolean> {
|
||||
try {
|
||||
const visibleInput = await page
|
||||
@@ -50,19 +25,18 @@ export class TotpLogin {
|
||||
|
||||
if (visibleInput) {
|
||||
await visibleInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled visible TOTP text input')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
|
||||
return true
|
||||
}
|
||||
|
||||
const hiddenInput = await page.$(this.hiddenInputSelector)
|
||||
|
||||
if (hiddenInput) {
|
||||
await hiddenInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled hidden TOTP input')
|
||||
const secondairyInput = await page.$(this.secondairyInputSelector)
|
||||
if (secondairyInput) {
|
||||
await secondairyInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found (visible or hidden)')
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found')
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
@@ -83,9 +57,8 @@ export class TotpLogin {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Generated TOTP code from secret')
|
||||
|
||||
const filled = await this.fillCode(page, code)
|
||||
|
||||
if (!filled) {
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to locate or fill TOTP input field')
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to fill TOTP input field')
|
||||
throw new Error('TOTP input field not found')
|
||||
}
|
||||
|
||||
@@ -93,6 +66,12 @@ export class TotpLogin {
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', `TOTP failed: ${errorMessage}`)
|
||||
throw new Error(`TOTP authentication failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||
return
|
||||
}
|
||||
@@ -100,45 +79,36 @@ export class TotpLogin {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input')
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||
const code = await this.promptManualCode()
|
||||
const code = await promptInput({
|
||||
question: `Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `,
|
||||
timeoutSeconds: this.maxManualSeconds,
|
||||
validate: code => /^\d{6}$/.test(code)
|
||||
})
|
||||
|
||||
if (!code || !/^\d{6}$/.test(code)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Invalid or missing TOTP code (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual TOTP input failed or timed out')
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
'Retrying manual TOTP input due to invalid code'
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const filled = await this.fillCode(page, code)
|
||||
|
||||
if (!filled) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Unable to locate or fill TOTP input field (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
`Unable to fill TOTP input (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('TOTP input field not found')
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
'Retrying manual TOTP input due to fill failure'
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -146,16 +116,31 @@ export class TotpLogin {
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
// Check if wrong code was entered
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error(`Maximum attempts reached: ${errorMessage}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Manual TOTP input failed after ${this.maxManualAttempts} attempts`)
|
||||
throw new Error(`TOTP input failed after ${this.maxManualAttempts} attempts`)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user