UserDeleter.mjs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { callbackify } from 'node:util'
  2. import logger from '@overleaf/logger'
  3. import Settings from '@overleaf/settings'
  4. import { User } from '../../models/User.js'
  5. import { DeletedUser } from '../../models/DeletedUser.js'
  6. import { UserAuditLogEntry } from '../../models/UserAuditLogEntry.js'
  7. import { Feedback } from '../../models/Feedback.js'
  8. import NewsletterManager from '../Newsletter/NewsletterManager.js'
  9. import ProjectDeleter from '../Project/ProjectDeleter.mjs'
  10. import SubscriptionHandler from '../Subscription/SubscriptionHandler.mjs'
  11. import SubscriptionUpdater from '../Subscription/SubscriptionUpdater.js'
  12. import SubscriptionLocator from '../Subscription/SubscriptionLocator.js'
  13. import UserMembershipsHandler from '../UserMembership/UserMembershipsHandler.js'
  14. import UserSessionsManager from './UserSessionsManager.js'
  15. import UserAuditLogHandler from './UserAuditLogHandler.js'
  16. import InstitutionsAPI from '../Institutions/InstitutionsAPI.js'
  17. import Modules from '../../infrastructure/Modules.js'
  18. import Errors from '../Errors/Errors.js'
  19. import OnboardingDataCollectionManager from '../OnboardingDataCollection/OnboardingDataCollectionManager.js'
  20. import EmailHandler from '../Email/EmailHandler.js'
  21. export default {
  22. deleteUser: callbackify(deleteUser),
  23. deleteMongoUser: callbackify(deleteMongoUser),
  24. expireDeletedUser: callbackify(expireDeletedUser),
  25. ensureCanDeleteUser: callbackify(ensureCanDeleteUser),
  26. expireDeletedUsersAfterDuration: callbackify(expireDeletedUsersAfterDuration),
  27. promises: {
  28. deleteUser,
  29. deleteMongoUser,
  30. expireDeletedUser,
  31. ensureCanDeleteUser,
  32. expireDeletedUsersAfterDuration,
  33. },
  34. }
  35. async function deleteUser(userId, options) {
  36. if (!userId) {
  37. logger.warn('user_id is null when trying to delete user')
  38. throw new Error('no user_id')
  39. }
  40. try {
  41. const user = await User.findById(userId).exec()
  42. logger.info({ userId }, 'deleting user')
  43. await ensureCanDeleteUser(user)
  44. logger.info({ userId }, 'cleaning up user')
  45. await _cleanupUser(user)
  46. logger.info({ userId }, 'firing deleteUser hook')
  47. await Modules.promises.hooks.fire('deleteUser', userId)
  48. logger.info({ userId }, 'adding delete-account audit log entry')
  49. await UserAuditLogHandler.promises.addEntry(
  50. userId,
  51. 'delete-account',
  52. options.deleterUser ? options.deleterUser._id : userId,
  53. options.ipAddress
  54. )
  55. logger.info({ userId }, 'creating deleted user record')
  56. await _createDeletedUser(user, options)
  57. logger.info({ userId }, 'deleting user projects')
  58. await ProjectDeleter.promises.deleteUsersProjects(user._id)
  59. if (options.skipEmail) {
  60. logger.info({ userId }, 'skipping sending deletion email to user')
  61. } else {
  62. logger.info({ userId }, 'sending deletion email to user')
  63. await _sendDeleteEmail(user, options.force)
  64. }
  65. logger.info({ userId }, 'deleting user record')
  66. await deleteMongoUser(user._id)
  67. logger.info({ userId }, 'user deletion complete')
  68. } catch (error) {
  69. logger.warn({ error, userId }, 'something went wrong deleting the user')
  70. throw error
  71. }
  72. }
  73. /**
  74. * delete a user document only
  75. */
  76. async function deleteMongoUser(userId) {
  77. if (!userId) {
  78. throw new Error('no user_id')
  79. }
  80. await User.deleteOne({ _id: userId }).exec()
  81. }
  82. async function expireDeletedUser(userId) {
  83. logger.info({ userId }, 'expiring deleted user')
  84. try {
  85. logger.info({ userId }, 'firing expireDeletedUser hook')
  86. await Modules.promises.hooks.fire('expireDeletedUser', userId)
  87. logger.info({ userId }, 'removing deleted user feedback records')
  88. await Feedback.deleteMany({ userId }).exec()
  89. logger.info({ userId }, 'removing deleted user onboarding data')
  90. await OnboardingDataCollectionManager.deleteOnboardingDataCollection(userId)
  91. logger.info({ userId }, 'redacting PII from the deleted user record')
  92. const deletedUser = await DeletedUser.findOne({
  93. 'deleterData.deletedUserId': userId,
  94. }).exec()
  95. deletedUser.user = undefined
  96. deletedUser.deleterData.deleterIpAddress = undefined
  97. await deletedUser.save()
  98. logger.info({ userId }, 'deleted user expiry complete')
  99. } catch (error) {
  100. logger.warn(
  101. { error, userId },
  102. 'something went wrong expiring the deleted user'
  103. )
  104. throw error
  105. }
  106. }
  107. async function expireDeletedUsersAfterDuration() {
  108. const deletedUsers = await DeletedUser.find({
  109. 'deleterData.deletedAt': {
  110. $lt: new Date(Date.now() - Settings.userHardDeletionDelay),
  111. },
  112. user: { $type: 'object' },
  113. }).exec()
  114. if (deletedUsers.length === 0) {
  115. return
  116. }
  117. logger.info(
  118. {
  119. deletedUsers: deletedUsers.length,
  120. retentionPeriodInDays:
  121. Settings.userHardDeletionDelay / (1000 * 60 * 60 * 24),
  122. },
  123. 'expiring batch of deleted users older than retention period'
  124. )
  125. try {
  126. for (let i = 0; i < deletedUsers.length; i++) {
  127. const deletedUserId = deletedUsers[i].deleterData.deletedUserId
  128. await expireDeletedUser(deletedUserId)
  129. logger.info({ deletedUserId }, 'removing deleted user audit log entries')
  130. await UserAuditLogEntry.deleteMany({ userId: deletedUserId }).exec()
  131. }
  132. logger.info(
  133. { deletedUsers: deletedUsers.length },
  134. 'batch of deleted users expired successfully'
  135. )
  136. } catch (error) {
  137. logger.warn(
  138. { error },
  139. 'something went wrong expiring batch of deleted users'
  140. )
  141. throw error
  142. }
  143. }
  144. async function ensureCanDeleteUser(user) {
  145. const subscription =
  146. await SubscriptionLocator.promises.getUsersSubscription(user)
  147. if (subscription) {
  148. throw new Errors.SubscriptionAdminDeletionError({})
  149. }
  150. }
  151. async function _sendDeleteEmail(user, force) {
  152. const emailOptions = {
  153. to: user.email,
  154. action: 'account deleted',
  155. actionDescribed: 'your Overleaf account was deleted',
  156. }
  157. try {
  158. await EmailHandler.promises.sendEmail('securityAlert', emailOptions)
  159. } catch (error) {
  160. if (force) {
  161. logger.error(
  162. { error },
  163. 'error sending account deletion email notification'
  164. )
  165. } else {
  166. throw error
  167. }
  168. }
  169. }
  170. async function _createDeletedUser(user, options) {
  171. await DeletedUser.updateOne(
  172. { 'deleterData.deletedUserId': user._id },
  173. {
  174. user,
  175. deleterData: {
  176. deletedAt: new Date(),
  177. deleterId: options.deleterUser ? options.deleterUser._id : undefined,
  178. deleterIpAddress: options.ipAddress,
  179. deletedUserId: user._id,
  180. deletedUserLastLoggedIn: user.lastLoggedIn,
  181. deletedUserSignUpDate: user.signUpDate,
  182. deletedUserLoginCount: user.loginCount,
  183. deletedUserReferralId: user.referal_id,
  184. deletedUserReferredUsers: user.refered_users,
  185. deletedUserReferredUserCount: user.refered_user_count,
  186. deletedUserOverleafId: user.overleaf ? user.overleaf.id : undefined,
  187. },
  188. },
  189. { upsert: true }
  190. )
  191. }
  192. async function _cleanupUser(user) {
  193. const userId = user._id
  194. logger.info({ userId }, '[cleanupUser] removing user sessions from Redis')
  195. await UserSessionsManager.promises.removeSessionsFromRedis(user)
  196. logger.info({ userId }, '[cleanupUser] unsubscribing from newsletters')
  197. await NewsletterManager.promises.unsubscribe(user, { delete: true })
  198. logger.info({ userId }, '[cleanupUser] cancelling subscription')
  199. await SubscriptionHandler.promises.cancelSubscription(user)
  200. logger.info({ userId }, '[cleanupUser] deleting affiliations')
  201. await InstitutionsAPI.promises.deleteAffiliations(userId)
  202. logger.info({ userId }, '[cleanupUser] removing user from groups')
  203. await SubscriptionUpdater.promises.removeUserFromAllGroups(userId)
  204. logger.info({ userId }, '[cleanupUser] removing user from memberships')
  205. await UserMembershipsHandler.promises.removeUserFromAllEntities(userId)
  206. logger.info({ userId }, '[cleanupUser] removing personal access tokens')
  207. await Modules.promises.hooks.fire('cleanupPersonalAccessTokens', userId, [
  208. 'collabratec',
  209. 'git_bridge',
  210. ])
  211. }