Просмотр исходного кода

[web] Add script to check for wrongly removed confirmed emails (#29532)

* [web] Add script to check for wrongly removed confirmed emails

* List emails that were added again

* Use `trackProgress`

* Prettier logs

* Add `READ_PREFERENCE_SECONDARY`

* Check `change-primary-email`

* Add check with `confirm-email` should be always empty, but let's check

* Update services/web/scripts/check_removed_emails.mjs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 4f83d69cffd0aec2c5d1201f1f6130eacdbb2e39
Antoine Clausse 11 месяцев назад
Родитель
Сommit
25993cb858
1 измененных файлов с 214 добавлено и 0 удалено
  1. 214 0
      services/web/scripts/check_removed_emails.mjs

+ 214 - 0
services/web/scripts/check_removed_emails.mjs

@@ -0,0 +1,214 @@
+// @ts-check
+
+import { db, ObjectId } from '../app/src/infrastructure/mongodb.js'
+import fs from 'node:fs/promises'
+import * as csv from 'csv'
+import { promisify } from 'node:util'
+import { scriptRunner } from './lib/ScriptRunner.mjs'
+import { READ_PREFERENCE_SECONDARY } from '@overleaf/mongo-utils/batchedUpdate.js'
+
+const CSV_FILENAME = '/tmp/unconfirmed_emails.csv'
+
+/**
+ * @type {(csvString: string) => Promise<string[][]>}
+ */
+const parseAsync = promisify(csv.parse)
+
+/**
+ * Checks the fallout of services/web/scripts/remove_unconfirmed_emails.mjs
+ * which wrongly removed some emails that have been confirmed by users
+ */
+async function main(trackProgress) {
+  console.time('check_removed_emails')
+
+  const csvContent = await fs.readFile(CSV_FILENAME, 'utf8')
+  const rows = await parseAsync(csvContent)
+  rows.shift() // Remove header row
+  const emailsByUserId = {}
+
+  for (const [userId, email] of rows) {
+    if (!emailsByUserId[userId]) {
+      emailsByUserId[userId] = []
+    }
+    emailsByUserId[userId].push(email)
+  }
+
+  const userIds = Object.keys(emailsByUserId)
+  let processedUsersCount = 0
+
+  const counts = {
+    /** @type {string[]} */
+    userNotFound: [],
+    /** @type {string[]} */
+    notDeleted: [],
+    deleted: 0,
+    /** @type {string[]} */
+    wasConfirmed: [],
+    /** @type {string[]} */
+    wasConfirmedLegacy: [],
+    /** @type {string[]} */
+    madePrimary: [],
+    /** @type {string[]} */
+    madeSecondary: [],
+    /** @type {string[]} */
+    isPrimary: [],
+    /** @type {string[]} */
+    isAddedAgain: [],
+  }
+
+  console.log('Total emails in the CSV:', rows.length)
+  console.log('Total users in the CSV:', userIds.length)
+
+  for (const userId of userIds) {
+    const userEmails = emailsByUserId[userId]
+
+    const user = await db.users.findOne(
+      { _id: new ObjectId(userId) },
+      { readPreference: READ_PREFERENCE_SECONDARY }
+    )
+
+    if (!user) {
+      counts.userNotFound.push(userId)
+      continue
+    }
+
+    for (const email of userEmails) {
+      const deletionLog = await db.userAuditLogEntries.findOne(
+        {
+          userId: new ObjectId(userId),
+          operation: 'remove-email',
+          'info.removedEmail': email,
+          'info.note': 'remove unconfirmed secondary emails',
+        },
+        { readPreference: READ_PREFERENCE_SECONDARY }
+      )
+      if (!deletionLog) {
+        counts.notDeleted.push(email)
+        continue
+      }
+      counts.deleted++
+
+      if (user.email === email) {
+        counts.isPrimary.push(email)
+      }
+
+      const confirmationLog = await db.userAuditLogEntries.findOne(
+        {
+          userId: new ObjectId(userId),
+          operation: 'confirm-email-via-code',
+          'info.email': email,
+          timestamp: { $gt: new Date('2025-02-25') },
+        },
+        { readPreference: READ_PREFERENCE_SECONDARY }
+      )
+      if (confirmationLog) {
+        counts.wasConfirmed.push(email)
+      }
+
+      const confirmationLegacyLog = await db.userAuditLogEntries.findOne(
+        {
+          userId: new ObjectId(userId),
+          operation: 'confirm-email',
+          'info.email': email,
+          timestamp: { $gt: new Date('2025-02-25') },
+        },
+        { readPreference: READ_PREFERENCE_SECONDARY }
+      )
+      if (confirmationLegacyLog) {
+        counts.wasConfirmedLegacy.push(email)
+      }
+
+      const madePrimaryLog = await db.userAuditLogEntries.findOne(
+        {
+          userId: new ObjectId(userId),
+          operation: 'change-primary-email',
+          'info.newPrimaryEmail': email,
+          timestamp: { $gt: new Date('2025-02-25') },
+        },
+        { readPreference: READ_PREFERENCE_SECONDARY }
+      )
+      if (madePrimaryLog) {
+        counts.madePrimary.push(email)
+      }
+
+      const madeSecondaryLog = await db.userAuditLogEntries.findOne(
+        {
+          userId: new ObjectId(userId),
+          operation: 'change-primary-email',
+          'info.oldPrimaryEmail': email,
+          timestamp: { $gt: new Date('2025-02-25') },
+        },
+        { readPreference: READ_PREFERENCE_SECONDARY }
+      )
+      if (madeSecondaryLog) {
+        counts.madeSecondary.push(email)
+      }
+
+      if (user.emails.some(item => item.email === email)) {
+        counts.isAddedAgain.push(email)
+      }
+    }
+
+    processedUsersCount++
+    if (processedUsersCount % 100 === 0) {
+      trackProgress(`Processed ${processedUsersCount} users`)
+    }
+  }
+
+  console.log()
+  console.log('Total emails in the CSV:', rows.length)
+  console.log('Total users in the CSV:', userIds.length)
+  console.log('Total users processed:', processedUsersCount)
+  console.log()
+  console.log('Users not found:', JSON.stringify(counts.userNotFound))
+  console.log()
+  console.log('Emails not deleted:', counts.notDeleted.length)
+  console.log('Emails deleted:', counts.deleted)
+  console.log()
+  console.log('Emails that were confirmed:', counts.wasConfirmed.length)
+  console.log(
+    'Emails that were confirmed:',
+    JSON.stringify(counts.wasConfirmed)
+  )
+  console.log()
+  console.log(
+    'Emails that were confirmed (legacy):',
+    counts.wasConfirmedLegacy.length
+  )
+  console.log(
+    'Emails that were confirmed (legacy):',
+    JSON.stringify(counts.wasConfirmedLegacy)
+  )
+  console.log()
+  console.log('Emails that are primary:', counts.isPrimary.length)
+  console.log('Emails that are primary:', JSON.stringify(counts.isPrimary))
+  console.log()
+  console.log('Emails that were made primary:', counts.madePrimary.length)
+  console.log(
+    'Emails that were made primary:',
+    JSON.stringify(counts.madePrimary)
+  )
+  console.log()
+  console.log('Emails that were made secondary:', counts.madeSecondary.length)
+  console.log(
+    'Emails that were made secondary:',
+    JSON.stringify(counts.madeSecondary)
+  )
+  console.log()
+  console.log('Emails that were added again:', counts.isAddedAgain.length)
+  console.log(
+    'Emails that were added again:',
+    JSON.stringify(counts.isAddedAgain)
+  )
+  console.log()
+  console.timeEnd('check_removed_emails')
+  console.log()
+}
+
+try {
+  await scriptRunner(main)
+  process.exit(0)
+} catch (error) {
+  console.error(error)
+  process.exit(1)
+}