mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 09:16:16 +00:00
feat: Implement account suspension handling and disable accounts in configuration
This commit is contained in:
@@ -186,7 +186,59 @@ export class Browser {
|
|||||||
await page.addInitScript(antiDetectScript)
|
await page.addInitScript(antiDetectScript)
|
||||||
await page.addInitScript(timezoneScript)
|
await page.addInitScript(timezoneScript)
|
||||||
|
|
||||||
// Virtual Authenticator support removed — no CDP WebAuthn setup performed here
|
// CRITICAL: Block WebAuthn API calls to prevent passkey dialogs
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
// Override navigator.credentials to block passkey requests
|
||||||
|
if (window.navigator.credentials) {
|
||||||
|
// Block credential creation (passkey enrollment)
|
||||||
|
window.navigator.credentials.create = async function (...args: any[]) {
|
||||||
|
console.log('[MRS] Blocked WebAuthn credential.create() call')
|
||||||
|
// Reject with NotAllowedError (user cancelled)
|
||||||
|
throw new DOMException('The operation either timed out or was not allowed.', 'NotAllowedError')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block credential retrieval (passkey authentication)
|
||||||
|
window.navigator.credentials.get = async function (...args: any[]) {
|
||||||
|
console.log('[MRS] Blocked WebAuthn credential.get() call')
|
||||||
|
// Reject with NotAllowedError (user cancelled)
|
||||||
|
throw new DOMException('The operation either timed out or was not allowed.', 'NotAllowedError')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also remove PublicKeyCredential if it exists
|
||||||
|
if (window.PublicKeyCredential) {
|
||||||
|
// @ts-ignore - Override isUserVerifyingPlatformAuthenticatorAvailable
|
||||||
|
window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = async () => false
|
||||||
|
// @ts-ignore - Override isConditionalMediationAvailable
|
||||||
|
window.PublicKeyCredential.isConditionalMediationAvailable = async () => false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// CRITICAL: Disable WebAuthn popups using Virtual Authenticator
|
||||||
|
// This prevents native "Choose where to save your passkey" dialogs
|
||||||
|
try {
|
||||||
|
const client = await page.context().newCDPSession(page)
|
||||||
|
|
||||||
|
// Enable WebAuthn and add a virtual authenticator that auto-rejects
|
||||||
|
await client.send('WebAuthn.enable')
|
||||||
|
|
||||||
|
// Add virtual authenticator with settings that prevent UI prompts
|
||||||
|
await client.send('WebAuthn.addVirtualAuthenticator', {
|
||||||
|
options: {
|
||||||
|
protocol: 'ctap2',
|
||||||
|
transport: 'internal',
|
||||||
|
hasResidentKey: false, // No resident keys = no passkey storage
|
||||||
|
hasUserVerification: false, // No biometric/PIN verification
|
||||||
|
isUserVerified: false, // Always fail verification
|
||||||
|
automaticPresenceSimulation: false // No automatic approval
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bot.log(this.bot.isMobile, 'BROWSER', 'WebAuthn Virtual Authenticator enabled (passkey dialogs disabled)')
|
||||||
|
} catch (cdpError) {
|
||||||
|
// Non-critical: CDP might not be available on all browsers
|
||||||
|
this.bot.log(this.bot.isMobile, 'BROWSER', `WebAuthn setup skipped: ${cdpError instanceof Error ? cdpError.message : String(cdpError)}`, 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
// IMPROVED: Use crypto-secure random for viewport sizes
|
// IMPROVED: Use crypto-secure random for viewport sizes
|
||||||
const { secureRandomInt } = await import('../util/security/SecureRandom')
|
const { secureRandomInt } = await import('../util/security/SecureRandom')
|
||||||
|
|||||||
@@ -243,6 +243,21 @@ export class BrowserFunc {
|
|||||||
const currentURL = new URL(target.url())
|
const currentURL = new URL(target.url())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if account is suspended BEFORE attempting to fetch dashboard data
|
||||||
|
const suspendedError = await target.locator('#rewards-user-suspended-error, #fraudErrorBody').first().isVisible({ timeout: 1000 }).catch(() => false)
|
||||||
|
if (suspendedError) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', '⛔ Account suspension detected, checking details...', 'error')
|
||||||
|
|
||||||
|
// Use SecurityDetector to handle suspension
|
||||||
|
const { SecurityDetector } = await import('../functions/login/SecurityDetector')
|
||||||
|
const { SecurityUtils } = await import('../functions/login/SecurityUtils')
|
||||||
|
const securityUtils = new SecurityUtils(this.bot)
|
||||||
|
const securityDetector = new SecurityDetector(this.bot, securityUtils)
|
||||||
|
|
||||||
|
await securityDetector.checkAccountSuspended(target)
|
||||||
|
throw new Error('Account suspended by Microsoft Rewards - account disabled in accounts.jsonc')
|
||||||
|
}
|
||||||
|
|
||||||
// Should never happen since tasks are opened in a new tab!
|
// Should never happen since tasks are opened in a new tab!
|
||||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||||
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ export class Login {
|
|||||||
this.recoveryHandler = new RecoveryHandler(bot, this.securityUtils)
|
this.recoveryHandler = new RecoveryHandler(bot, this.securityUtils)
|
||||||
this.securityDetector = new SecurityDetector(bot, this.securityUtils)
|
this.securityDetector = new SecurityDetector(bot, this.securityUtils)
|
||||||
|
|
||||||
|
// Connect PasskeyHandler to TotpHandler for post-TOTP passkey handling
|
||||||
|
this.totpHandler.setPasskeyHandler(this.passkeyHandler)
|
||||||
|
|
||||||
this.securityUtils.cleanupCompromisedInterval()
|
this.securityUtils.cleanupCompromisedInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +864,12 @@ export class Login {
|
|||||||
const portalSelector = await this.waitForRewardsRoot(page, DEFAULT_TIMEOUTS.portalWaitMs)
|
const portalSelector = await this.waitForRewardsRoot(page, DEFAULT_TIMEOUTS.portalWaitMs)
|
||||||
|
|
||||||
if (!portalSelector) {
|
if (!portalSelector) {
|
||||||
|
// Before trying fallback, check if account is suspended/banned
|
||||||
|
const isSuspended = await this.securityDetector.checkAccountSuspended(page)
|
||||||
|
if (isSuspended) {
|
||||||
|
throw new Error('Account suspended by Microsoft Rewards - disabled in accounts.jsonc')
|
||||||
|
}
|
||||||
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Portal not found, trying goHome() fallback...', 'warn')
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Portal not found, trying goHome() fallback...', 'warn')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ export class PasskeyHandler {
|
|||||||
passkeyPrimary: 'button[data-testid="primaryButton"]',
|
passkeyPrimary: 'button[data-testid="primaryButton"]',
|
||||||
passkeyTitle: '[data-testid="title"]',
|
passkeyTitle: '[data-testid="title"]',
|
||||||
kmsiVideo: '[data-testid="kmsiVideo"]',
|
kmsiVideo: '[data-testid="kmsiVideo"]',
|
||||||
biometricVideo: '[data-testid="biometricVideo"]'
|
biometricVideo: '[data-testid="biometricVideo"]',
|
||||||
|
// QR Code Passkey dialog specific selectors
|
||||||
|
qrCodeDialog: 'div[role="dialog"]',
|
||||||
|
qrCodeImage: 'img[alt*="QR"], canvas[aria-label*="QR"], div[class*="qr"]',
|
||||||
|
backButton: 'button:has-text("Back")',
|
||||||
|
cancelButton: 'button:has-text("Cancel")'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
constructor(bot: MicrosoftRewardsBot) {
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
@@ -37,6 +42,13 @@ export class PasskeyHandler {
|
|||||||
public async handlePasskeyPrompts(page: Page, context: 'main' | 'oauth') {
|
public async handlePasskeyPrompts(page: Page, context: 'main' | 'oauth') {
|
||||||
let did = false
|
let did = false
|
||||||
|
|
||||||
|
// Priority 0: Handle QR Code Passkey dialog (appears after TOTP)
|
||||||
|
const qrCodeHandled = await this.handleQrCodePasskeyDialog(page)
|
||||||
|
if (qrCodeHandled) {
|
||||||
|
did = true
|
||||||
|
this.logPasskeyOnce('QR code passkey dialog')
|
||||||
|
}
|
||||||
|
|
||||||
// Early exit for passkey creation flows (common on mobile): hit cancel/skip if present
|
// Early exit for passkey creation flows (common on mobile): hit cancel/skip if present
|
||||||
const currentUrl = page.url()
|
const currentUrl = page.url()
|
||||||
if (/fido\/create|passkey/i.test(currentUrl)) {
|
if (/fido\/create|passkey/i.test(currentUrl)) {
|
||||||
@@ -234,4 +246,89 @@ export class PasskeyHandler {
|
|||||||
this.passkeyHandled = true
|
this.passkeyHandled = true
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Dismissed passkey prompt (${reason})`)
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Dismissed passkey prompt (${reason})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle QR Code Passkey dialog that appears after TOTP authentication
|
||||||
|
* This dialog is a modal that blocks interaction with the page
|
||||||
|
*/
|
||||||
|
private async handleQrCodePasskeyDialog(page: Page): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Method 1: Check for specific text content indicating QR code dialog
|
||||||
|
const qrCodeTextVisible = await page.locator('text=/use your phone or tablet|scan this QR code|passkeys/i')
|
||||||
|
.first()
|
||||||
|
.isVisible({ timeout: 800 })
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (!qrCodeTextVisible) return false
|
||||||
|
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'Detected QR code passkey dialog, attempting dismissal')
|
||||||
|
|
||||||
|
// Method 2: Try keyboard ESC first (works for many dialogs)
|
||||||
|
await page.keyboard.press('Escape').catch(() => { })
|
||||||
|
await this.bot.utils.wait(300)
|
||||||
|
|
||||||
|
// Method 3: Check if dialog still visible after ESC
|
||||||
|
const stillVisible = await page.locator('text=/use your phone or tablet|scan this QR code/i')
|
||||||
|
.first()
|
||||||
|
.isVisible({ timeout: 500 })
|
||||||
|
.catch(() => false)
|
||||||
|
|
||||||
|
if (!stillVisible) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'QR code dialog dismissed via ESC')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Try clicking Back or Cancel buttons
|
||||||
|
const dismissed = await this.clickFirstVisible(page, [
|
||||||
|
PasskeyHandler.SELECTORS.backButton,
|
||||||
|
PasskeyHandler.SELECTORS.cancelButton,
|
||||||
|
'button:has-text("Retour")', // French
|
||||||
|
'button:has-text("Annuler")', // French
|
||||||
|
'button:has-text("No thanks")',
|
||||||
|
'button:has-text("Maybe later")',
|
||||||
|
'[data-testid="secondaryButton"]',
|
||||||
|
'button[class*="secondary"]'
|
||||||
|
], 500)
|
||||||
|
|
||||||
|
if (dismissed) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'QR code dialog dismissed via button click')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 5: JavaScript injection to close dialog
|
||||||
|
const jsResult = await page.evaluate(() => {
|
||||||
|
// Find dialog by role
|
||||||
|
const dialogs = document.querySelectorAll('[role="dialog"]')
|
||||||
|
for (const dialog of dialogs) {
|
||||||
|
const text = dialog.textContent || ''
|
||||||
|
if (/passkey|qr code|phone or tablet/i.test(text)) {
|
||||||
|
// Try to find and click cancel/back button
|
||||||
|
const buttons = dialog.querySelectorAll('button')
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const btnText = (btn.textContent || '').toLowerCase()
|
||||||
|
if (/back|cancel|retour|annuler|no thanks|maybe later|skip/i.test(btnText)) {
|
||||||
|
btn.click()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no button found, try to remove dialog from DOM
|
||||||
|
dialog.remove()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}).catch(() => false)
|
||||||
|
|
||||||
|
if (jsResult) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'QR code dialog dismissed via JavaScript injection')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'Failed to dismiss QR code dialog with all methods', 'warn')
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `QR code dialog handler error: ${error}`, 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,4 +63,79 @@ export class SecurityDetector {
|
|||||||
throw new Error('Account locked by Microsoft - please review account status')
|
throw new Error('Account locked by Microsoft - please review account status')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if account is suspended/banned and disable it in accounts.jsonc
|
||||||
|
* @param page Playwright page
|
||||||
|
* @returns True if account is suspended
|
||||||
|
*/
|
||||||
|
public async checkAccountSuspended(page: Page): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Check for suspension page elements
|
||||||
|
const suspendedSelectors = [
|
||||||
|
'#rewards-user-suspended-error',
|
||||||
|
'#fraudErrorBody',
|
||||||
|
'#suspendedAccountHeader'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const selector of suspendedSelectors) {
|
||||||
|
const element = await page.waitForSelector(selector, { timeout: 800 }).catch(() => null)
|
||||||
|
if (element) {
|
||||||
|
const email = this.bot.currentAccountEmail || 'unknown'
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACCOUNT-SUSPENDED', `⛔ Account ${email} has been SUSPENDED by Microsoft`, 'error')
|
||||||
|
|
||||||
|
// Get suspension details from page
|
||||||
|
const headerText = await page.locator('#suspendedAccountHeader').textContent().catch(() => '')
|
||||||
|
const summaryText = await page.locator('#fraudSummary').textContent().catch(() => '')
|
||||||
|
|
||||||
|
// Log detailed information
|
||||||
|
if (headerText) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACCOUNT-SUSPENDED', `Header: ${headerText.trim()}`, 'error')
|
||||||
|
}
|
||||||
|
if (summaryText) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACCOUNT-SUSPENDED', `Summary: ${summaryText.trim().substring(0, 200)}...`, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable account in accounts.jsonc
|
||||||
|
try {
|
||||||
|
const { disableBannedAccount } = await import('../../util/state/AccountDisabler')
|
||||||
|
await disableBannedAccount(email, 'Account suspended by Microsoft Rewards')
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACCOUNT-SUSPENDED', `✓ Account ${email} disabled in accounts.jsonc`, 'warn')
|
||||||
|
} catch (disableError) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACCOUNT-SUSPENDED', `Failed to disable account in config: ${disableError}`, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send incident alert
|
||||||
|
const incident: SecurityIncident = {
|
||||||
|
kind: 'Account Suspended',
|
||||||
|
account: email,
|
||||||
|
details: [
|
||||||
|
headerText?.trim() || 'Account suspended',
|
||||||
|
summaryText?.trim().substring(0, 300) || 'Microsoft Rewards violations detected'
|
||||||
|
],
|
||||||
|
next: [
|
||||||
|
'Account has been automatically disabled in accounts.jsonc',
|
||||||
|
'Review suspension details at https://rewards.bing.com',
|
||||||
|
'Contact Microsoft Support if you believe this is an error'
|
||||||
|
],
|
||||||
|
docsUrl: 'https://support.microsoft.com/topic/c5ab735d-c6d9-4bb9-30ad-d828e954b6a9'
|
||||||
|
}
|
||||||
|
await this.securityUtils.sendIncidentAlert(incident, 'critical')
|
||||||
|
|
||||||
|
// Engage global standby
|
||||||
|
this.bot.compromisedModeActive = true
|
||||||
|
this.bot.compromisedReason = 'account-suspended'
|
||||||
|
this.securityUtils.startCompromisedInterval()
|
||||||
|
await this.bot.engageGlobalStandby('account-suspended', email).catch(logError('LOGIN-SECURITY', 'Global standby engagement failed', this.bot.isMobile))
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'ACCOUNT-SUSPENDED', `Check failed: ${error}`, 'warn')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class TotpHandler {
|
|||||||
private lastTotpSubmit = 0
|
private lastTotpSubmit = 0
|
||||||
private totpAttempts = 0
|
private totpAttempts = 0
|
||||||
private currentTotpSecret?: string
|
private currentTotpSecret?: string
|
||||||
|
private passkeyHandler?: any // Will be set by Login class to avoid circular dependency
|
||||||
|
|
||||||
// Unified selector system - DRY principle
|
// Unified selector system - DRY principle
|
||||||
private static readonly TOTP_SELECTORS = {
|
private static readonly TOTP_SELECTORS = {
|
||||||
@@ -62,6 +63,10 @@ export class TotpHandler {
|
|||||||
this.bot = bot
|
this.bot = bot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setPasskeyHandler(handler: any) {
|
||||||
|
this.passkeyHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
public setTotpSecret(secret?: string) {
|
public setTotpSecret(secret?: string) {
|
||||||
this.currentTotpSecret = (secret && secret.trim()) || undefined
|
this.currentTotpSecret = (secret && secret.trim()) || undefined
|
||||||
this.lastTotpSubmit = 0
|
this.lastTotpSubmit = 0
|
||||||
@@ -109,6 +114,18 @@ export class TotpHandler {
|
|||||||
await this.submitTotpCode(page, selector, secret)
|
await this.submitTotpCode(page, selector, secret)
|
||||||
this.totpAttempts += 1
|
this.totpAttempts += 1
|
||||||
this.lastTotpSubmit = Date.now()
|
this.lastTotpSubmit = Date.now()
|
||||||
|
|
||||||
|
// Handle potential Passkey QR code dialog that appears after TOTP submission
|
||||||
|
if (this.passkeyHandler) {
|
||||||
|
await this.bot.utils.wait(800) // Brief wait for dialog to appear
|
||||||
|
try {
|
||||||
|
await this.passkeyHandler.handlePasskeyPrompts(page, 'main')
|
||||||
|
} catch (error) {
|
||||||
|
// Non-critical: continue even if passkey handling fails
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', `Passkey handling after TOTP: ${error}`, 'warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.bot.utils.wait(1200)
|
await this.bot.utils.wait(1200)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/util/state/AccountDisabler.ts
Normal file
120
src/util/state/AccountDisabler.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
// Strip JSON comments helper
|
||||||
|
function stripJsonComments(text: string): string {
|
||||||
|
return text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable a banned account in accounts.jsonc by setting enabled=false and adding a comment
|
||||||
|
* @param email Account email to disable
|
||||||
|
* @param reason Ban reason (e.g., 'Account suspended by Microsoft')
|
||||||
|
*/
|
||||||
|
export async function disableBannedAccount(email: string, reason: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Find accounts.jsonc file
|
||||||
|
const candidates = [
|
||||||
|
path.join(process.cwd(), 'src', 'accounts.jsonc'),
|
||||||
|
path.join(process.cwd(), 'accounts.jsonc'),
|
||||||
|
path.join(__dirname, '../../src', 'accounts.jsonc'),
|
||||||
|
path.join(__dirname, '../../', 'accounts.jsonc')
|
||||||
|
]
|
||||||
|
|
||||||
|
let accountsFilePath: string | null = null
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
accountsFilePath = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountsFilePath) {
|
||||||
|
throw new Error('accounts.jsonc file not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current content
|
||||||
|
const rawContent = fs.readFileSync(accountsFilePath, 'utf-8')
|
||||||
|
|
||||||
|
// Parse accounts (support both array and object with accounts property)
|
||||||
|
const cleaned = stripJsonComments(rawContent)
|
||||||
|
const parsed = JSON.parse(cleaned)
|
||||||
|
const accountsArray = Array.isArray(parsed) ? parsed : parsed.accounts
|
||||||
|
|
||||||
|
if (!Array.isArray(accountsArray)) {
|
||||||
|
throw new Error('Invalid accounts.jsonc structure')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the account
|
||||||
|
const accountIndex = accountsArray.findIndex((acc: unknown) => acc.email === email)
|
||||||
|
if (accountIndex === -1) {
|
||||||
|
throw new Error(`Account ${email} not found in accounts.jsonc`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already disabled
|
||||||
|
if (accountsArray[accountIndex].enabled === false) {
|
||||||
|
console.log(`[ACCOUNT-BAN] Account ${email} is already disabled`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the account
|
||||||
|
accountsArray[accountIndex].enabled = false
|
||||||
|
|
||||||
|
// Rebuild the file with comments
|
||||||
|
const timestamp = new Date().toISOString().split('T')[0]
|
||||||
|
const banComment = `// BANNED ${timestamp}: ${reason}`
|
||||||
|
|
||||||
|
// Convert back to JSON with formatting
|
||||||
|
let newContent: string
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
// Array format
|
||||||
|
newContent = '[\n'
|
||||||
|
accountsArray.forEach((acc: unknown, idx: number) => {
|
||||||
|
if (idx === accountIndex) {
|
||||||
|
newContent += ` ${banComment}\n`
|
||||||
|
}
|
||||||
|
newContent += ' ' + JSON.stringify(acc, null, 2).split('\n').join('\n ')
|
||||||
|
if (idx < accountsArray.length - 1) {
|
||||||
|
newContent += ','
|
||||||
|
}
|
||||||
|
newContent += '\n'
|
||||||
|
})
|
||||||
|
newContent += ']\n'
|
||||||
|
} else {
|
||||||
|
// Object format with accounts property
|
||||||
|
const updatedParsed = { ...parsed, accounts: accountsArray }
|
||||||
|
const jsonStr = JSON.stringify(updatedParsed, null, 2)
|
||||||
|
|
||||||
|
// Insert comment before the banned account
|
||||||
|
const lines = jsonStr.split('\n')
|
||||||
|
const emailPattern = `"email": "${email}"`
|
||||||
|
const emailLineIndex = lines.findIndex(line => line.includes(emailPattern))
|
||||||
|
|
||||||
|
if (emailLineIndex > 0) {
|
||||||
|
// Find the start of this account object (opening brace)
|
||||||
|
let accountStartIndex = emailLineIndex
|
||||||
|
for (let i = emailLineIndex; i >= 0; i--) {
|
||||||
|
const line = lines[i]
|
||||||
|
if (line && line.trim().startsWith('{')) {
|
||||||
|
accountStartIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert comment before the account object
|
||||||
|
const targetLine = lines[accountStartIndex]
|
||||||
|
const indent = (targetLine && targetLine.match(/^\s*/)?.[0]) || ' '
|
||||||
|
lines.splice(accountStartIndex, 0, `${indent}${banComment}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent = lines.join('\n') + '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
fs.writeFileSync(accountsFilePath, newContent, 'utf-8')
|
||||||
|
|
||||||
|
console.log(`[ACCOUNT-BAN] ✓ Disabled account ${email} in ${accountsFilePath}`)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to disable banned account ${email}: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user