| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import { callbackify } from 'node:util'
- import logger from '@overleaf/logger'
- import Settings from '@overleaf/settings'
- import { User } from '../../models/User.js'
- import { DeletedUser } from '../../models/DeletedUser.js'
- import { UserAuditLogEntry } from '../../models/UserAuditLogEntry.js'
- import { Feedback } from '../../models/Feedback.js'
- import NewsletterManager from '../Newsletter/NewsletterManager.js'
- import ProjectDeleter from '../Project/ProjectDeleter.mjs'
- import SubscriptionHandler from '../Subscription/SubscriptionHandler.mjs'
- import SubscriptionUpdater from '../Subscription/SubscriptionUpdater.js'
- import SubscriptionLocator from '../Subscription/SubscriptionLocator.js'
- import UserMembershipsHandler from '../UserMembership/UserMembershipsHandler.js'
- import UserSessionsManager from './UserSessionsManager.js'
- import UserAuditLogHandler from './UserAuditLogHandler.js'
- import InstitutionsAPI from '../Institutions/InstitutionsAPI.js'
- import Modules from '../../infrastructure/Modules.js'
- import Errors from '../Errors/Errors.js'
- import OnboardingDataCollectionManager from '../OnboardingDataCollection/OnboardingDataCollectionManager.js'
- import EmailHandler from '../Email/EmailHandler.js'
- export default {
- deleteUser: callbackify(deleteUser),
- deleteMongoUser: callbackify(deleteMongoUser),
- expireDeletedUser: callbackify(expireDeletedUser),
- ensureCanDeleteUser: callbackify(ensureCanDeleteUser),
- expireDeletedUsersAfterDuration: callbackify(expireDeletedUsersAfterDuration),
- promises: {
- deleteUser,
- deleteMongoUser,
- expireDeletedUser,
- ensureCanDeleteUser,
- expireDeletedUsersAfterDuration,
- },
- }
- async function deleteUser(userId, options) {
- if (!userId) {
- logger.warn('user_id is null when trying to delete user')
- throw new Error('no user_id')
- }
- try {
- const user = await User.findById(userId).exec()
- logger.info({ userId }, 'deleting user')
- await ensureCanDeleteUser(user)
- logger.info({ userId }, 'cleaning up user')
- await _cleanupUser(user)
- logger.info({ userId }, 'firing deleteUser hook')
- await Modules.promises.hooks.fire('deleteUser', userId)
- logger.info({ userId }, 'adding delete-account audit log entry')
- await UserAuditLogHandler.promises.addEntry(
- userId,
- 'delete-account',
- options.deleterUser ? options.deleterUser._id : userId,
- options.ipAddress
- )
- logger.info({ userId }, 'creating deleted user record')
- await _createDeletedUser(user, options)
- logger.info({ userId }, 'deleting user projects')
- await ProjectDeleter.promises.deleteUsersProjects(user._id)
- if (options.skipEmail) {
- logger.info({ userId }, 'skipping sending deletion email to user')
- } else {
- logger.info({ userId }, 'sending deletion email to user')
- await _sendDeleteEmail(user, options.force)
- }
- logger.info({ userId }, 'deleting user record')
- await deleteMongoUser(user._id)
- logger.info({ userId }, 'user deletion complete')
- } catch (error) {
- logger.warn({ error, userId }, 'something went wrong deleting the user')
- throw error
- }
- }
- /**
- * delete a user document only
- */
- async function deleteMongoUser(userId) {
- if (!userId) {
- throw new Error('no user_id')
- }
- await User.deleteOne({ _id: userId }).exec()
- }
- async function expireDeletedUser(userId) {
- logger.info({ userId }, 'expiring deleted user')
- try {
- logger.info({ userId }, 'firing expireDeletedUser hook')
- await Modules.promises.hooks.fire('expireDeletedUser', userId)
- logger.info({ userId }, 'removing deleted user feedback records')
- await Feedback.deleteMany({ userId }).exec()
- logger.info({ userId }, 'removing deleted user onboarding data')
- await OnboardingDataCollectionManager.deleteOnboardingDataCollection(userId)
- logger.info({ userId }, 'redacting PII from the deleted user record')
- const deletedUser = await DeletedUser.findOne({
- 'deleterData.deletedUserId': userId,
- }).exec()
- deletedUser.user = undefined
- deletedUser.deleterData.deleterIpAddress = undefined
- await deletedUser.save()
- logger.info({ userId }, 'deleted user expiry complete')
- } catch (error) {
- logger.warn(
- { error, userId },
- 'something went wrong expiring the deleted user'
- )
- throw error
- }
- }
- async function expireDeletedUsersAfterDuration() {
- const deletedUsers = await DeletedUser.find({
- 'deleterData.deletedAt': {
- $lt: new Date(Date.now() - Settings.userHardDeletionDelay),
- },
- user: { $type: 'object' },
- }).exec()
- if (deletedUsers.length === 0) {
- return
- }
- logger.info(
- {
- deletedUsers: deletedUsers.length,
- retentionPeriodInDays:
- Settings.userHardDeletionDelay / (1000 * 60 * 60 * 24),
- },
- 'expiring batch of deleted users older than retention period'
- )
- try {
- for (let i = 0; i < deletedUsers.length; i++) {
- const deletedUserId = deletedUsers[i].deleterData.deletedUserId
- await expireDeletedUser(deletedUserId)
- logger.info({ deletedUserId }, 'removing deleted user audit log entries')
- await UserAuditLogEntry.deleteMany({ userId: deletedUserId }).exec()
- }
- logger.info(
- { deletedUsers: deletedUsers.length },
- 'batch of deleted users expired successfully'
- )
- } catch (error) {
- logger.warn(
- { error },
- 'something went wrong expiring batch of deleted users'
- )
- throw error
- }
- }
- async function ensureCanDeleteUser(user) {
- const subscription =
- await SubscriptionLocator.promises.getUsersSubscription(user)
- if (subscription) {
- throw new Errors.SubscriptionAdminDeletionError({})
- }
- }
- async function _sendDeleteEmail(user, force) {
- const emailOptions = {
- to: user.email,
- action: 'account deleted',
- actionDescribed: 'your Overleaf account was deleted',
- }
- try {
- await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
- } catch (error) {
- if (force) {
- logger.error(
- { error },
- 'error sending account deletion email notification'
- )
- } else {
- throw error
- }
- }
- }
- async function _createDeletedUser(user, options) {
- await DeletedUser.updateOne(
- { 'deleterData.deletedUserId': user._id },
- {
- user,
- deleterData: {
- deletedAt: new Date(),
- deleterId: options.deleterUser ? options.deleterUser._id : undefined,
- deleterIpAddress: options.ipAddress,
- deletedUserId: user._id,
- deletedUserLastLoggedIn: user.lastLoggedIn,
- deletedUserSignUpDate: user.signUpDate,
- deletedUserLoginCount: user.loginCount,
- deletedUserReferralId: user.referal_id,
- deletedUserReferredUsers: user.refered_users,
- deletedUserReferredUserCount: user.refered_user_count,
- deletedUserOverleafId: user.overleaf ? user.overleaf.id : undefined,
- },
- },
- { upsert: true }
- )
- }
- async function _cleanupUser(user) {
- const userId = user._id
- logger.info({ userId }, '[cleanupUser] removing user sessions from Redis')
- await UserSessionsManager.promises.removeSessionsFromRedis(user)
- logger.info({ userId }, '[cleanupUser] unsubscribing from newsletters')
- await NewsletterManager.promises.unsubscribe(user, { delete: true })
- logger.info({ userId }, '[cleanupUser] cancelling subscription')
- await SubscriptionHandler.promises.cancelSubscription(user)
- logger.info({ userId }, '[cleanupUser] deleting affiliations')
- await InstitutionsAPI.promises.deleteAffiliations(userId)
- logger.info({ userId }, '[cleanupUser] removing user from groups')
- await SubscriptionUpdater.promises.removeUserFromAllGroups(userId)
- logger.info({ userId }, '[cleanupUser] removing user from memberships')
- await UserMembershipsHandler.promises.removeUserFromAllEntities(userId)
- logger.info({ userId }, '[cleanupUser] removing personal access tokens')
- await Modules.promises.hooks.fire('cleanupPersonalAccessTokens', userId, [
- 'collabratec',
- 'git_bridge',
- ])
- }
|