From 95efb60fb50541189e931dd10069cbc8270c66fb Mon Sep 17 00:00:00 2001 From: Kate Crichton Date: Wed, 18 Feb 2026 13:56:43 +0000 Subject: [PATCH] Merge pull request #31536 from overleaf/kc-add-batch-download-audit-log Add logging for batch downloads GitOrigin-RevId: b3d03ebd20657b571be0d894bc1d2b335844d1fa --- .../Downloads/ProjectDownloadsController.mjs | 12 ++++- .../ProjectDownloadsController.test.mjs | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs index ca52ad1db4..b51aad3b3a 100644 --- a/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.mjs @@ -13,7 +13,7 @@ function getSafeProjectName(project) { export default { downloadProject(req, res, next) { - const userId = SessionManager.getSessionUser(req.session) + const userId = SessionManager.getLoggedInUserId(req.session) const projectId = req.params.Project_id Metrics.inc('zip-downloads') DocumentUpdaterHandler.flushProjectToMongo(projectId, function (error) { @@ -49,6 +49,7 @@ export default { }, downloadMultipleProjects(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) const projectIds = req.query.project_ids.split(',') Metrics.inc('zip-downloads-multiple') DocumentUpdaterHandler.flushMultipleProjectsToMongo( @@ -57,6 +58,15 @@ export default { if (error) { return next(error) } + // Log audit entry for each project in the batch + for (const projectId of projectIds) { + ProjectAuditLogHandler.addEntryInBackground( + projectId, + 'project-downloaded', + userId, + req.ip + ) + } ProjectZipStreamManager.createZipStreamForMultipleProjects( projectIds, function (error, stream) { diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs index 426af8b225..9c85e52e18 100644 --- a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs @@ -42,6 +42,15 @@ describe('ProjectDownloadsController', function () { }) ) + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs', + () => ({ + default: (ctx.ProjectAuditLogHandler = { + addEntryInBackground: sinon.stub(), + }), + }) + ) + ctx.ProjectDownloadsController = (await import(modulePath)).default }) @@ -52,6 +61,13 @@ describe('ProjectDownloadsController', function () { .stub() .callsArgWith(1, null, ctx.stream) ctx.req.params = { Project_id: ctx.project_id } + ctx.req.ip = '192.168.1.1' + ctx.req.session = { + user: { + _id: 'user-id-123', + email: 'user@example.com', + }, + } ctx.project_name = 'project name with accĂȘnts and % special characters' ctx.ProjectGetter.getProject = sinon .stub() @@ -104,6 +120,17 @@ describe('ProjectDownloadsController', function () { it('should record the action via Metrics', function (ctx) { return ctx.metrics.inc.calledWith('zip-downloads').should.equal(true) }) + + it('should add an audit log entry', function (ctx) { + return ctx.ProjectAuditLogHandler.addEntryInBackground + .calledWith( + ctx.project_id, + 'project-downloaded', + ctx.req.session.user._id, + ctx.req.ip + ) + .should.equal(true) + }) }) describe('downloadMultipleProjects', function () { @@ -114,6 +141,13 @@ describe('ProjectDownloadsController', function () { .callsArgWith(1, null, ctx.stream) ctx.project_ids = ['project-1', 'project-2'] ctx.req.query = { project_ids: ctx.project_ids.join(',') } + ctx.req.ip = '192.168.1.1' + ctx.req.session = { + user: { + _id: 'user-id-123', + email: 'user@example.com', + }, + } ctx.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon .stub() .callsArgWith(1) @@ -159,5 +193,21 @@ describe('ProjectDownloadsController', function () { .calledWith('zip-downloads-multiple') .should.equal(true) }) + + it('should add an audit log entry for each project', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.callCount.should.equal( + ctx.project_ids.length + ) + for (const projectId of ctx.project_ids) { + ctx.ProjectAuditLogHandler.addEntryInBackground + .calledWith( + projectId, + 'project-downloaded', + ctx.req.session.user._id, + ctx.req.ip + ) + .should.equal(true) + } + }) }) })