| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- // @ts-check
- import minimist from 'minimist'
- 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 Errors from '../app/src/Features/Errors/Errors.js'
- import UserGetter from '../app/src/Features/User/UserGetter.mjs'
- import { READ_PREFERENCE_SECONDARY } from '@overleaf/mongo-utils/batchedUpdate.js'
- import UserUpdater from '../app/src/Features/User/UserUpdater.mjs'
- import EmailHelper from '../app/src/Features/Helpers/EmailHelper.js'
- import AsyncLocalStorage from '../app/src/infrastructure/AsyncLocalStorage.js'
- import AnalyticsManager from '../app/src/Features/Analytics/AnalyticsManager.mjs'
- import UserAuditLogHandler from '../app/src/Features/User/UserAuditLogHandler.mjs'
- import InstitutionsAPI from '../app/src/Features/Institutions/InstitutionsAPI.mjs'
- import OError from '@overleaf/o-error'
- import EmailChangeHelper from '../app/src/Features/Analytics/EmailChangeHelper.mjs'
- import logger from '@overleaf/logger'
- const CSV_FILENAME = '/tmp/re_add_deleted_emails.csv'
- /**
- * @type {(csvString: string) => Promise<string[][]>}
- */
- const parseAsync = promisify(csv.parse)
- function usage() {
- console.log('Usage: node re_add_deleted_emails.mjs [options]')
- console.log(
- 'fix wrongly removed emails by remove_unconfirmed_emails (see 2025-11-05 email removal)'
- )
- console.log(
- 'run this script with a CSV containing user IDs and emails to re-add save in ',
- CSV_FILENAME
- )
- console.log('Options:')
- console.log(' --commit apply the changes')
- process.exit(0)
- }
- const { commit, help } = minimist(process.argv.slice(2), {
- boolean: ['commit', 'help'],
- alias: { help: 'h' },
- default: { commit: false },
- })
- /**
- * @param {string} email
- */
- async function isEmailUsed(email) {
- try {
- await UserGetter.promises.ensureUniqueEmailAddress(email)
- return false
- } catch (err) {
- if (err instanceof Errors.EmailExistsError) {
- return true
- }
- throw err
- }
- }
- async function consumeCsvFile(trackProgress) {
- console.time('re_add_deleted_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 (!EmailHelper.parseEmail(email)) {
- throw new Error(`invalid email ${email}`)
- }
- if (!emailsByUserId[userId]) {
- emailsByUserId[userId] = []
- }
- emailsByUserId[userId].push(email)
- }
- const userIds = Object.keys(emailsByUserId)
- const counts = {
- /** @type {string[]} */
- processedUsers: [],
- /** @type {string[]} */
- userNotFound: [],
- /** @type {string[]} */
- emailsInUse: [],
- /** @type {string[]} */
- alreadyOk: [],
- /** @type {string[]} */
- primary: [],
- /** @type {string[]} */
- secondary: [],
- /** @type {string[]} */
- addedEmails: [],
- }
- console.log('Total emails in the CSV:', rows.length)
- console.log('Total users in the CSV:', userIds.length)
- for (const userId of userIds) {
- const candidateEmails = 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 candidateEmails) {
- if (user.emails.some(item => item.email === email)) {
- counts.alreadyOk.push(email)
- continue
- }
- const isUsed = await isEmailUsed(email)
- const isOwnPrimary = user.email === email
- if (isUsed && !isOwnPrimary) {
- counts.emailsInUse.push(email)
- continue
- }
- if (user.email === email) counts.primary.push(email)
- else counts.secondary.push(email)
- if (commit) {
- const auditLog = {
- initiatorId: null,
- ipAddress: null,
- info: {
- script: true,
- note: 'fix wrongly removed unconfirmed secondary email',
- },
- }
- if (isOwnPrimary) {
- // can't use addEmailAddress for primary email because ensureUniqueEmailAddress will throw
- // using an override instead
- await addEmailAddressOverride(user._id, email, {}, auditLog)
- } else {
- await UserUpdater.promises.addEmailAddress(
- user._id,
- email,
- {},
- auditLog
- )
- }
- await UserUpdater.promises.confirmEmail(user._id, email)
- }
- counts.addedEmails.push(email)
- }
- counts.processedUsers.push(userId)
- trackProgress(
- `Processed users: ${counts.processedUsers.length}/${userIds.length}`
- )
- }
- console.log()
- if (!commit) {
- console.log('Dry-run, use --commit to apply changes')
- console.log('This would be the result:')
- console.log()
- }
- console.log('Total emails in the CSV:', rows.length)
- console.log('Total users in the CSV:', userIds.length)
- console.log()
- console.log('Users not found:', counts.userNotFound.length)
- console.log('Users not found:', JSON.stringify(counts.userNotFound))
- console.log()
- console.log('Already OK:', counts.alreadyOk.length)
- console.log('Already OK:', JSON.stringify(counts.alreadyOk))
- console.log()
- console.log('Already in use:', counts.emailsInUse.length)
- console.log('Already in use:', JSON.stringify(counts.emailsInUse))
- console.log()
- console.log('Primary:', counts.primary.length)
- console.log('Primary:', JSON.stringify(counts.primary))
- console.log()
- console.log('Secondary:', counts.secondary.length)
- console.log('Secondary:', JSON.stringify(counts.secondary))
- console.log()
- console.log('Added emails:', counts.addedEmails.length)
- console.log('Added emails:', JSON.stringify(counts.addedEmails))
- console.log()
- console.log()
- console.timeEnd('re_add_deleted_emails')
- console.log()
- }
- async function main(trackProgress) {
- if (help) {
- return usage()
- }
- await consumeCsvFile(trackProgress)
- }
- try {
- await scriptRunner(main)
- process.exit(0)
- } catch (error) {
- console.error(error)
- process.exit(1)
- }
- async function addEmailAddressOverride(
- userId,
- newEmail,
- affiliationOptions,
- auditLog
- ) {
- AsyncLocalStorage.removeItem('userFullEmails')
- newEmail = EmailHelper.parseEmail(newEmail)
- if (!newEmail) {
- throw new Error('invalid email')
- }
- // Bypass ensureUniqueEmailAddress when re-adding primary emails
- // await UserGetter.promises.ensureUniqueEmailAddress(newEmail)
- AnalyticsManager.recordEventForUserInBackground(
- userId,
- 'secondary-email-added'
- )
- await UserAuditLogHandler.promises.addEntry(
- userId,
- 'add-email',
- auditLog.initiatorId,
- auditLog.ipAddress,
- {
- ...auditLog.info,
- newSecondaryEmail: newEmail,
- }
- )
- try {
- await InstitutionsAPI.promises.addAffiliation(
- userId,
- newEmail,
- affiliationOptions
- )
- } catch (error) {
- throw OError.tag(error, 'problem adding affiliation while adding email')
- }
- const createdAt = new Date()
- let res
- try {
- const reversedHostname = newEmail.split('@')[1].split('').reverse().join('')
- const update = {
- $push: {
- emails: { email: newEmail, createdAt, reversedHostname },
- },
- }
- res = await UserUpdater.promises.updateUser(
- { _id: userId, 'emails.email': { $ne: newEmail } },
- update
- )
- } catch (error) {
- throw OError.tag(error, 'problem updating users emails')
- }
- if (res.matchedCount !== 1) {
- return
- }
- try {
- await EmailChangeHelper.registerEmailCreation(userId, newEmail, {
- // @ts-expect-error - This is copied from UserUpdater.mjs
- createdAt: new Date(),
- emailCreatedAt: createdAt,
- })
- } catch (error) {
- logger.warn(
- { error, userId, newEmail },
- 'Error registering email creation with analytics'
- )
- }
- }
|