[web] Add deletedReason parameter to project deletion methods (#32221)

* [web] Add deletedReason parameter to project deletion methods

* revert sinon.match.any in ProjectDuplicator negative assertion

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: d1595eefe0e36150231ee9646fe5eba0786fd1f5
This commit is contained in:
Domagoj Kriskovic
2026-03-23 09:06:04 +00:00
committed by Copybot
co-authored by Copilot Autofix powered by AI
parent c9ba2ac025
commit 6486ef3e1e
13 changed files with 53 additions and 12 deletions
@@ -5,6 +5,7 @@ import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.mj
import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.mjs'
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
import ProjectDeleter from '../Project/ProjectDeleter.mjs'
import { DeletedProjectReasons } from '../Project/DeletedProjectReasons.mjs'
import EditorRealTimeController from './EditorRealTimeController.mjs'
import async from 'async'
import PublicAccessLevels from '../Authorization/PublicAccessLevels.mjs'
@@ -445,7 +446,11 @@ const EditorController = {
deleteProject(projectId, callback) {
Metrics.inc('editor.delete-project')
ProjectDeleter.deleteProject(projectId, callback)
ProjectDeleter.deleteProject(
projectId,
{ deletedReason: DeletedProjectReasons.USER },
callback
)
},
renameEntity(
@@ -0,0 +1,8 @@
export const DeletedProjectReasons = /** @type {const} */ ({
USER: 'user',
ACCOUNT_DELETION: 'account-deletion',
ZIP_IMPORT_FAILURE: 'zip-import-failure',
CLONE_FAILURE: 'clone-failure',
GITHUB_IMPORT_FAILURE: 'github-import-failure',
SCRIPT: 'script',
})
@@ -7,6 +7,7 @@ import logger from '@overleaf/logger'
import { expressify } from '@overleaf/promise-utils'
import mongodb from 'mongodb-legacy'
import ProjectDeleter from './ProjectDeleter.mjs'
import { DeletedProjectReasons } from './DeletedProjectReasons.mjs'
import ProjectDuplicator from './ProjectDuplicator.mjs'
import ProjectCreationHandler from './ProjectCreationHandler.mjs'
import EditorController from '../Editor/EditorController.mjs'
@@ -171,6 +172,7 @@ const _ProjectController = {
await ProjectDeleter.promises.deleteProject(projectId, {
deleterUser: user,
ipAddress: req.ip,
deletedReason: DeletedProjectReasons.USER,
})
ProjectAuditLogHandler.addEntryIfManagedInBackground(
projectId,
@@ -22,6 +22,7 @@ import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
import HistoryManager from '../History/HistoryManager.mjs'
import ChatApiHandler from '../Chat/ChatApiHandler.mjs'
import { promiseMapWithLimit } from '@overleaf/promise-utils'
import { DeletedProjectReasons } from './DeletedProjectReasons.mjs'
const PROJECT_EXPIRATION_BATCH_SIZE = 10000
@@ -84,7 +85,11 @@ async function deleteUsersProjects(userId) {
{ userId, projectCount: projects.length },
'found user projects to delete'
)
await promiseMapWithLimit(5, projects, project => deleteProject(project._id))
await promiseMapWithLimit(5, projects, project =>
deleteProject(project._id, {
deletedReason: DeletedProjectReasons.ACCOUNT_DELETION,
})
)
logger.info({ userId }, 'deleted all user projects')
await CollaboratorsHandler.promises.removeUserFromAllProjects(userId)
}
@@ -214,6 +219,7 @@ async function deleteProject(projectId, options = {}) {
deleterId:
options.deleterUser != null ? options.deleterUser._id : undefined,
deleterIpAddress: options.ipAddress,
deletedReason: options.deletedReason,
deletedProjectId: project._id,
deletedProjectOwnerId: project.owner_ref,
deletedProjectCollaboratorIds: project.collaberator_refs,
@@ -10,6 +10,7 @@ import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.mj
import HistoryManager from '../History/HistoryManager.mjs'
import ProjectCreationHandler from './ProjectCreationHandler.mjs'
import ProjectDeleter from './ProjectDeleter.mjs'
import { DeletedProjectReasons } from './DeletedProjectReasons.mjs'
import ProjectEntityMongoUpdateHandler from './ProjectEntityMongoUpdateHandler.mjs'
import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.mjs'
import ProjectGetter from './ProjectGetter.mjs'
@@ -161,7 +162,9 @@ async function duplicate(
} catch (err) {
// Clean up broken clone on error.
// Make sure we delete the new failed project, not the original one!
await ProjectDeleter.promises.deleteProject(newProject._id)
await ProjectDeleter.promises.deleteProject(newProject._id, {
deletedReason: DeletedProjectReasons.CLONE_FAILURE,
})
throw OError.tag(err, 'error cloning project, broken clone deleted', {
originalProjectId,
newProjectName,
@@ -13,6 +13,7 @@ import ProjectEntityMongoUpdateHandler from '../Project/ProjectEntityMongoUpdate
import ProjectRootDocManager from '../Project/ProjectRootDocManager.mjs'
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
import ProjectDeleter from '../Project/ProjectDeleter.mjs'
import { DeletedProjectReasons } from '../Project/DeletedProjectReasons.mjs'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.mjs'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
@@ -55,7 +56,9 @@ async function createProjectFromZipArchive(ownerId, defaultName, zipPath) {
} catch (err) {
// no need to wait for the cleanup here
ProjectDeleter.promises
.deleteProject(project._id)
.deleteProject(project._id, {
deletedReason: DeletedProjectReasons.ZIP_IMPORT_FAILURE,
})
.catch(err =>
logger.error(
{ err, projectId: project._id },
@@ -97,7 +100,9 @@ async function createProjectFromZipArchiveWithName(
} catch (err) {
// no need to wait for the cleanup here
ProjectDeleter.promises
.deleteProject(project._id)
.deleteProject(project._id, {
deletedReason: DeletedProjectReasons.ZIP_IMPORT_FAILURE,
})
.catch(err =>
logger.error(
{ err, projectId: project._id },
@@ -20,6 +20,7 @@ export const DeleterDataSchema = new Schema({
deletedProjectLastUpdatedAt: { type: Date },
deletedProjectOverleafId: { type: Number },
deletedProjectOverleafHistoryId: { type: Schema.Types.Mixed },
deletedReason: { type: String },
})
const DeletedProjectSchema = new Schema(