From 7ecee2e0aa6005190116c5e9633ddf2be15383b3 Mon Sep 17 00:00:00 2001 From: Christopher Hoskin <4855578+mans0954@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:01:53 +0100 Subject: [PATCH 01/19] Merge pull request #27255 from overleaf/revert-27252-revert-26843-csh-issue-26608-mongo8-dev-ci Revert "Revert "Upgrade the dev environment and CI to mongo 8"" GitOrigin-RevId: 5074b012504e65240017f1fde9b0d8d04c7b8b61 --- server-ce/test/docker-compose.yml | 2 +- services/chat/docker-compose.ci.yml | 2 +- services/chat/docker-compose.yml | 2 +- services/contacts/docker-compose.ci.yml | 2 +- services/contacts/docker-compose.yml | 2 +- services/docstore/docker-compose.ci.yml | 2 +- services/docstore/docker-compose.yml | 2 +- services/document-updater/docker-compose.ci.yml | 2 +- services/document-updater/docker-compose.yml | 2 +- services/history-v1/docker-compose.ci.yml | 2 +- services/history-v1/docker-compose.yml | 2 +- services/notifications/docker-compose.ci.yml | 2 +- services/notifications/docker-compose.yml | 2 +- services/project-history/docker-compose.ci.yml | 2 +- services/project-history/docker-compose.yml | 2 +- services/web/docker-compose.ci.yml | 2 +- services/web/docker-compose.yml | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/server-ce/test/docker-compose.yml b/server-ce/test/docker-compose.yml index 029b73fc62..1652baeae9 100644 --- a/server-ce/test/docker-compose.yml +++ b/server-ce/test/docker-compose.yml @@ -35,7 +35,7 @@ services: MAILTRAP_PASSWORD: 'password-for-mailtrap' mongo: - image: mongo:6.0 + image: mongo:8.0.11 command: '--replSet overleaf' volumes: - ../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/chat/docker-compose.ci.yml b/services/chat/docker-compose.ci.yml index 24b57ab084..ca3303a079 100644 --- a/services/chat/docker-compose.ci.yml +++ b/services/chat/docker-compose.ci.yml @@ -42,7 +42,7 @@ services: command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . user: root mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/chat/docker-compose.yml b/services/chat/docker-compose.yml index ddc5f9e698..e7b8ce7385 100644 --- a/services/chat/docker-compose.yml +++ b/services/chat/docker-compose.yml @@ -44,7 +44,7 @@ services: command: npm run --silent test:acceptance mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/contacts/docker-compose.ci.yml b/services/contacts/docker-compose.ci.yml index 24b57ab084..ca3303a079 100644 --- a/services/contacts/docker-compose.ci.yml +++ b/services/contacts/docker-compose.ci.yml @@ -42,7 +42,7 @@ services: command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . user: root mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/contacts/docker-compose.yml b/services/contacts/docker-compose.yml index 6c77ef5e31..474ea224f8 100644 --- a/services/contacts/docker-compose.yml +++ b/services/contacts/docker-compose.yml @@ -44,7 +44,7 @@ services: command: npm run --silent test:acceptance mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/docstore/docker-compose.ci.yml b/services/docstore/docker-compose.ci.yml index 40decc4aea..cdb4783c5a 100644 --- a/services/docstore/docker-compose.ci.yml +++ b/services/docstore/docker-compose.ci.yml @@ -47,7 +47,7 @@ services: command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . user: root mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/docstore/docker-compose.yml b/services/docstore/docker-compose.yml index 8c11eb5a91..a9099c7e7b 100644 --- a/services/docstore/docker-compose.yml +++ b/services/docstore/docker-compose.yml @@ -49,7 +49,7 @@ services: command: npm run --silent test:acceptance mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/document-updater/docker-compose.ci.yml b/services/document-updater/docker-compose.ci.yml index ca15f35fef..c6ec24a84b 100644 --- a/services/document-updater/docker-compose.ci.yml +++ b/services/document-updater/docker-compose.ci.yml @@ -55,7 +55,7 @@ services: retries: 20 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/document-updater/docker-compose.yml b/services/document-updater/docker-compose.yml index cf7c9a2eb6..c1b23c11c5 100644 --- a/services/document-updater/docker-compose.yml +++ b/services/document-updater/docker-compose.yml @@ -57,7 +57,7 @@ services: retries: 20 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/history-v1/docker-compose.ci.yml b/services/history-v1/docker-compose.ci.yml index da664d6b30..cf6ec3357d 100644 --- a/services/history-v1/docker-compose.ci.yml +++ b/services/history-v1/docker-compose.ci.yml @@ -75,7 +75,7 @@ services: retries: 20 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/history-v1/docker-compose.yml b/services/history-v1/docker-compose.yml index 22b739abf9..3a33882d28 100644 --- a/services/history-v1/docker-compose.yml +++ b/services/history-v1/docker-compose.yml @@ -83,7 +83,7 @@ services: retries: 20 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/notifications/docker-compose.ci.yml b/services/notifications/docker-compose.ci.yml index 24b57ab084..ca3303a079 100644 --- a/services/notifications/docker-compose.ci.yml +++ b/services/notifications/docker-compose.ci.yml @@ -42,7 +42,7 @@ services: command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . user: root mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/notifications/docker-compose.yml b/services/notifications/docker-compose.yml index 081bbfa002..e43e9aeef5 100644 --- a/services/notifications/docker-compose.yml +++ b/services/notifications/docker-compose.yml @@ -44,7 +44,7 @@ services: command: npm run --silent test:acceptance mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/project-history/docker-compose.ci.yml b/services/project-history/docker-compose.ci.yml index ca15f35fef..c6ec24a84b 100644 --- a/services/project-history/docker-compose.ci.yml +++ b/services/project-history/docker-compose.ci.yml @@ -55,7 +55,7 @@ services: retries: 20 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/project-history/docker-compose.yml b/services/project-history/docker-compose.yml index eeca03de6e..dd3c6468fe 100644 --- a/services/project-history/docker-compose.yml +++ b/services/project-history/docker-compose.yml @@ -57,7 +57,7 @@ services: retries: 20 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js diff --git a/services/web/docker-compose.ci.yml b/services/web/docker-compose.ci.yml index 33b5a3ca2e..8376103315 100644 --- a/services/web/docker-compose.ci.yml +++ b/services/web/docker-compose.ci.yml @@ -95,7 +95,7 @@ services: image: redis:7.4.3 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 logging: driver: none command: --replSet overleaf diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index 069c1e77de..e0a4a064c5 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -91,7 +91,7 @@ services: image: redis:7.4.3 mongo: - image: mongo:7.0.20 + image: mongo:8.0.11 command: --replSet overleaf volumes: - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js From 5d79cf18c0b880e39a2679b58c66721c93bf25c5 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Thu, 17 Jul 2025 14:25:48 +0100 Subject: [PATCH 02/19] Define all initial roles GitOrigin-RevId: ad613bad4d8a47e327281e90b5475e989a3ccec4 --- services/web/types/admin-capabilities.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/web/types/admin-capabilities.ts b/services/web/types/admin-capabilities.ts index 7d87c77a15..0c98d7df04 100644 --- a/services/web/types/admin-capabilities.ts +++ b/services/web/types/admin-capabilities.ts @@ -1,3 +1,10 @@ export type AdminCapability = 'modify-user-email' | 'view-project' -export type AdminRole = 'engineering' +export type AdminRole = + | 'engagement' + | 'engineering' + | 'finance' + | 'product' + | 'sales' + | 'support' + | 'support_tier_1' From 868d562d96ba768b96bd2d0ec10591646a992fce Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Mon, 21 Jul 2025 11:53:05 +0200 Subject: [PATCH 03/19] Support password-fallbackPassword array in requireBasicAuth (#27237) GitOrigin-RevId: 33b15a05996bfa0190041f347772867a9667e2ca --- .../AuthenticationController.js | 17 +- .../AuthenticationControllerTests.js | 327 ++++++++++++++++++ 2 files changed, 343 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js index 7a97d2ac9c..99c418df1b 100644 --- a/services/web/app/src/Features/Authentication/AuthenticationController.js +++ b/services/web/app/src/Features/Authentication/AuthenticationController.js @@ -36,7 +36,22 @@ function send401WithChallenge(res) { function checkCredentials(userDetailsMap, user, password) { const expectedPassword = userDetailsMap.get(user) const userExists = userDetailsMap.has(user) && expectedPassword // user exists with a non-null password - const isValid = userExists && tsscmp(expectedPassword, password) + + let isValid = false + if (userExists) { + if (Array.isArray(expectedPassword)) { + const isValidPrimary = Boolean( + expectedPassword[0] && tsscmp(expectedPassword[0], password) + ) + const isValidFallback = Boolean( + expectedPassword[1] && tsscmp(expectedPassword[1], password) + ) + isValid = isValidPrimary || isValidFallback + } else { + isValid = tsscmp(expectedPassword, password) + } + } + if (!isValid) { logger.err({ user }, 'invalid login details') } diff --git a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js index 0e4f675b1b..1fa3aba6a6 100644 --- a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js +++ b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js @@ -1500,4 +1500,331 @@ describe('AuthenticationController', function () { }) }) }) + + describe('checkCredentials', function () { + beforeEach(function () { + this.userDetailsMap = new Map() + this.logger.err = sinon.stub() + this.Metrics.inc = sinon.stub() + }) + + describe('with valid credentials', function () { + describe('single password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', 'correctpassword') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'correctpassword' + ) + }) + + it('should return true', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + + it('should record success metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'pass', + } + ) + }) + }) + + describe('array with primary password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'primary' + ) + }) + + it('should return true', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + + it('should record success metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'pass', + } + ) + }) + }) + + describe('array with fallback password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'fallback' + ) + }) + + it('should return true', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + + it('should record success metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'pass', + } + ) + }) + }) + }) + + describe('with invalid credentials', function () { + describe('unknown user', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', 'correctpassword') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'unknownuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'unknownuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'unknown-user', + status: 'fail', + } + ) + }) + }) + + describe('wrong password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', 'correctpassword') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'wrongpassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'fail', + } + ) + }) + }) + + describe('wrong password with array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'wrongpassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'fail', + } + ) + }) + }) + + describe('null user entry', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', null) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics for unknown user', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'unknown-user', + status: 'fail', + } + ) + }) + }) + + describe('empty primary password in array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'fallback' + ) + }) + + it('should return true with fallback password', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + }) + + describe('empty fallback password in array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', '']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'primary' + ) + }) + + it('should return true with primary password', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + }) + + describe('both passwords empty in array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['', '']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + }) + + describe('empty single password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', '') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics for unknown user', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'unknown-user', + status: 'fail', + } + ) + }) + }) + }) + }) }) From d5b5710d018dea1f3ba5e84fd4869af18c99c756 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Mon, 21 Jul 2025 11:53:48 +0200 Subject: [PATCH 04/19] Add docModified hook in ds-mobile-app module (#27196) * Add docModified hook in ds-mobile-app module * use Object.entries when iterating over promises * avoid project lookup * update tests GitOrigin-RevId: 88676746f56558a97ce31010b57f5eeb254fefef --- .../Features/Documents/DocumentController.mjs | 4 ++++ .../web/app/src/infrastructure/Modules.js | 3 +-- .../src/Documents/DocumentController.test.mjs | 21 +++++++++++++++++++ services/web/types/web-module.ts | 5 ++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/services/web/app/src/Features/Documents/DocumentController.mjs b/services/web/app/src/Features/Documents/DocumentController.mjs index 6998c0b36a..9a16811894 100644 --- a/services/web/app/src/Features/Documents/DocumentController.mjs +++ b/services/web/app/src/Features/Documents/DocumentController.mjs @@ -7,6 +7,7 @@ import logger from '@overleaf/logger' import _ from 'lodash' import { plainTextResponse } from '../../infrastructure/Response.js' import { expressify } from '@overleaf/promise-utils' +import Modules from '../../infrastructure/Modules.js' async function getDocument(req, res) { const { Project_id: projectId, doc_id: docId } = req.params @@ -92,6 +93,9 @@ async function setDocument(req, res) { { docId, projectId }, 'finished receiving set document request from api (docupdater)' ) + + await Modules.promises.hooks.fire('docModified', projectId, docId) + res.json(result) } diff --git a/services/web/app/src/infrastructure/Modules.js b/services/web/app/src/infrastructure/Modules.js index 20975a3642..aea3aeb087 100644 --- a/services/web/app/src/infrastructure/Modules.js +++ b/services/web/app/src/infrastructure/Modules.js @@ -150,8 +150,7 @@ async function linkedFileAgentsIncludes() { async function attachHooks() { for (const module of await modules()) { const { promises, ...hooks } = module.hooks || {} - for (const hook in promises || {}) { - const method = promises[hook] + for (const [hook, method] of Object.entries(promises || {})) { attachHook(hook, method) } for (const hook in hooks || {}) { diff --git a/services/web/test/unit/src/Documents/DocumentController.test.mjs b/services/web/test/unit/src/Documents/DocumentController.test.mjs index e3fe3bdec2..b683cc5d14 100644 --- a/services/web/test/unit/src/Documents/DocumentController.test.mjs +++ b/services/web/test/unit/src/Documents/DocumentController.test.mjs @@ -87,6 +87,14 @@ describe('DocumentController', function () { }, } + ctx.Modules = { + promises: { + hooks: { + fire: sinon.stub().resolves(), + }, + }, + } + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ default: ctx.ProjectGetter, })) @@ -113,6 +121,10 @@ describe('DocumentController', function () { default: ctx.ChatApiHandler, })) + vi.doMock('../../../../app/src/infrastructure/Modules.js', () => ({ + default: ctx.Modules, + })) + ctx.DocumentController = (await import(MODULE_PATH)).default }) @@ -208,6 +220,15 @@ describe('DocumentController', function () { it('should return a successful response', function (ctx) { ctx.res.success.should.equal(true) }) + + it('should call the docModified hook', function (ctx) { + sinon.assert.calledWith( + ctx.Modules.promises.hooks.fire, + 'docModified', + ctx.project._id, + ctx.doc._id + ) + }) }) describe("when the document doesn't exist", function () { diff --git a/services/web/types/web-module.ts b/services/web/types/web-module.ts index 298f430df2..f6b59cdf6f 100644 --- a/services/web/types/web-module.ts +++ b/services/web/types/web-module.ts @@ -53,7 +53,10 @@ export type WebModule = { apply: (webRouter: any, privateApiRouter: any, publicApiRouter: any) => void } hooks?: { - [name: string]: (args: any[]) => void + promises?: { + [name: string]: (...args: any[]) => Promise + } + [name: string]: ((...args: any[]) => void) | any } middleware?: { [name: string]: RequestHandler From 0778bab9103c1441ba4101319d08e65490d7b2d5 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:50:29 +0100 Subject: [PATCH 05/19] Merge pull request #27254 from overleaf/td-project-dashboard-cookie-banner Implement React cookie banner on project dashboard GitOrigin-RevId: 95d2778d7ce7cb3054a06b06486b815a3453a623 --- services/web/app/views/_cookie_banner.pug | 8 +-- .../web/app/views/general/post-gateway.pug | 2 +- services/web/app/views/layout-marketing.pug | 2 +- services/web/app/views/layout-react.pug | 2 +- .../web/app/views/layout-website-redesign.pug | 2 +- .../project/editor/new_from_template.pug | 2 +- .../app/views/project/ide-react-detached.pug | 2 +- services/web/app/views/project/list-react.pug | 1 + .../app/views/project/token/access-react.pug | 2 +- .../views/project/token/sharing-updates.pug | 2 +- .../web/frontend/extracted-translations.json | 4 ++ .../js/features/cookie-banner/index.js | 53 ----------------- .../js/features/cookie-banner/index.ts | 32 ++++++++++ .../js/features/cookie-banner/utils.ts | 43 ++++++++++++++ .../components/project-list-ds-nav.tsx | 2 + .../components/project-list-root.tsx | 10 +++- .../js/shared/components/cookie-banner.tsx | 58 +++++++++++++++++++ .../pages/project-list-ds-nav.scss | 18 +++++- services/web/locales/en.json | 4 ++ services/web/types/window.ts | 1 + 20 files changed, 181 insertions(+), 69 deletions(-) delete mode 100644 services/web/frontend/js/features/cookie-banner/index.js create mode 100644 services/web/frontend/js/features/cookie-banner/index.ts create mode 100644 services/web/frontend/js/features/cookie-banner/utils.ts create mode 100644 services/web/frontend/js/shared/components/cookie-banner.tsx diff --git a/services/web/app/views/_cookie_banner.pug b/services/web/app/views/_cookie_banner.pug index 56974326cd..7cbc569bc1 100644 --- a/services/web/app/views/_cookie_banner.pug +++ b/services/web/app/views/_cookie_banner.pug @@ -1,13 +1,13 @@ -section.cookie-banner.hidden-print.hidden(aria-label='Cookie banner') - .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our cookie policy. +section.cookie-banner.hidden-print.hidden(aria-label=translate('cookie_banner')) + .cookie-banner-content !{translate('cookie_banner_info', {}, [{ name: 'a', attrs: { href: '/legal#Cookies' }}])} .cookie-banner-actions button( type='button' class='btn btn-link btn-sm' data-ol-cookie-banner-set-consent='essential' - ) Essential cookies only + ) #{translate('essential_cookies_only')} button( type='button' class='btn btn-primary btn-sm' data-ol-cookie-banner-set-consent='all' - ) Accept all cookies + ) #{translate('accept_all_cookies')} diff --git a/services/web/app/views/general/post-gateway.pug b/services/web/app/views/general/post-gateway.pug index c6bbc92d01..86f379ac1b 100644 --- a/services/web/app/views/general/post-gateway.pug +++ b/services/web/app/views/general/post-gateway.pug @@ -4,7 +4,7 @@ block vars - var suppressNavbar = true - var suppressFooter = true - var suppressSkipToContent = true - - var suppressCookieBanner = true + - var suppressPugCookieBanner = true block content .content.content-alt diff --git a/services/web/app/views/layout-marketing.pug b/services/web/app/views/layout-marketing.pug index b54c30f033..26e4eb539d 100644 --- a/services/web/app/views/layout-marketing.pug +++ b/services/web/app/views/layout-marketing.pug @@ -24,7 +24,7 @@ block body else include layout/fat-footer - if typeof suppressCookieBanner == 'undefined' + if typeof suppressPugCookieBanner == 'undefined' include _cookie_banner if bootstrapVersion === 5 diff --git a/services/web/app/views/layout-react.pug b/services/web/app/views/layout-react.pug index 94ff3ba247..e9c4c932c4 100644 --- a/services/web/app/views/layout-react.pug +++ b/services/web/app/views/layout-react.pug @@ -69,5 +69,5 @@ block body else include layout/fat-footer-react-bootstrap-5 - if typeof suppressCookieBanner === 'undefined' + if typeof suppressPugCookieBanner === 'undefined' include _cookie_banner diff --git a/services/web/app/views/layout-website-redesign.pug b/services/web/app/views/layout-website-redesign.pug index 61ed83043b..aa7fea9f07 100644 --- a/services/web/app/views/layout-website-redesign.pug +++ b/services/web/app/views/layout-website-redesign.pug @@ -27,7 +27,7 @@ block body else include layout/fat-footer-website-redesign - if typeof suppressCookieBanner == 'undefined' + if typeof suppressPugCookieBanner == 'undefined' include _cookie_banner block contactModal diff --git a/services/web/app/views/project/editor/new_from_template.pug b/services/web/app/views/project/editor/new_from_template.pug index c84288a21a..a5dc3ff33c 100644 --- a/services/web/app/views/project/editor/new_from_template.pug +++ b/services/web/app/views/project/editor/new_from_template.pug @@ -2,7 +2,7 @@ extends ../../layout-marketing block vars - var suppressFooter = true - - var suppressCookieBanner = true + - var suppressPugCookieBanner = true - var suppressSkipToContent = true block content diff --git a/services/web/app/views/project/ide-react-detached.pug b/services/web/app/views/project/ide-react-detached.pug index ca1a178bbf..fa695b1af5 100644 --- a/services/web/app/views/project/ide-react-detached.pug +++ b/services/web/app/views/project/ide-react-detached.pug @@ -7,7 +7,7 @@ block vars - var suppressNavbar = true - var suppressFooter = true - var suppressSkipToContent = true - - var suppressCookieBanner = true + - var suppressPugCookieBanner = true - metadata.robotsNoindexNofollow = true block content diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index 78103e75a6..47bff344b6 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -7,6 +7,7 @@ block vars - const suppressNavContentLinks = true - const suppressNavbar = true - const suppressFooter = true + - const suppressPugCookieBanner = true block append meta meta( diff --git a/services/web/app/views/project/token/access-react.pug b/services/web/app/views/project/token/access-react.pug index 80b91f1a99..6c01ad15b1 100644 --- a/services/web/app/views/project/token/access-react.pug +++ b/services/web/app/views/project/token/access-react.pug @@ -5,7 +5,7 @@ block entrypointVar block vars - var suppressFooter = true - - var suppressCookieBanner = true + - var suppressPugCookieBanner = true - var suppressSkipToContent = true block append meta diff --git a/services/web/app/views/project/token/sharing-updates.pug b/services/web/app/views/project/token/sharing-updates.pug index d1818be0af..2f67e5a3c1 100644 --- a/services/web/app/views/project/token/sharing-updates.pug +++ b/services/web/app/views/project/token/sharing-updates.pug @@ -5,7 +5,7 @@ block entrypointVar block vars - var suppressFooter = true - - var suppressCookieBanner = true + - var suppressPugCookieBanner = true - var suppressSkipToContent = true block append meta diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index ef2a9c6a2c..2775c04601 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -35,6 +35,7 @@ "about_to_remove_user_preamble": "", "about_to_trash_projects": "", "abstract": "", + "accept_all_cookies": "", "accept_and_continue": "", "accept_change": "", "accept_change_error_description": "", @@ -332,6 +333,8 @@ "continue_to": "", "continue_using_free_features": "", "continue_with_free_plan": "", + "cookie_banner": "", + "cookie_banner_info": "", "copied": "", "copy": "", "copy_code": "", @@ -544,6 +547,7 @@ "error_opening_document_detail": "", "error_performing_request": "", "error_processing_file": "", + "essential_cookies_only": "", "example_project": "", "existing_plan_active_until_term_end": "", "expand": "", diff --git a/services/web/frontend/js/features/cookie-banner/index.js b/services/web/frontend/js/features/cookie-banner/index.js deleted file mode 100644 index 3d9b2b8d6c..0000000000 --- a/services/web/frontend/js/features/cookie-banner/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import getMeta from '@/utils/meta' - -function loadGA() { - if (window.olLoadGA) { - window.olLoadGA() - } -} - -function setConsent(value) { - document.querySelector('.cookie-banner').classList.add('hidden') - const cookieDomain = getMeta('ol-ExposedSettings').cookieDomain - const oneYearInSeconds = 60 * 60 * 24 * 365 - const cookieAttributes = - '; path=/' + - '; domain=' + - cookieDomain + - '; max-age=' + - oneYearInSeconds + - '; SameSite=Lax; Secure' - if (value === 'all') { - document.cookie = 'oa=1' + cookieAttributes - loadGA() - window.dispatchEvent(new CustomEvent('cookie-consent', { detail: true })) - } else { - document.cookie = 'oa=0' + cookieAttributes - window.dispatchEvent(new CustomEvent('cookie-consent', { detail: false })) - } -} - -if ( - getMeta('ol-ExposedSettings').gaToken || - getMeta('ol-ExposedSettings').gaTokenV4 || - getMeta('ol-ExposedSettings').propensityId || - getMeta('ol-ExposedSettings').hotjarId -) { - document - .querySelectorAll('[data-ol-cookie-banner-set-consent]') - .forEach(el => { - el.addEventListener('click', function (e) { - e.preventDefault() - const consentType = el.getAttribute('data-ol-cookie-banner-set-consent') - setConsent(consentType) - }) - }) - - const oaCookie = document.cookie.split('; ').find(c => c.startsWith('oa=')) - if (!oaCookie) { - const cookieBannerEl = document.querySelector('.cookie-banner') - if (cookieBannerEl) { - cookieBannerEl.classList.remove('hidden') - } - } -} diff --git a/services/web/frontend/js/features/cookie-banner/index.ts b/services/web/frontend/js/features/cookie-banner/index.ts new file mode 100644 index 0000000000..2ea97e875a --- /dev/null +++ b/services/web/frontend/js/features/cookie-banner/index.ts @@ -0,0 +1,32 @@ +import { + CookieConsentValue, + cookieBannerRequired, + hasMadeCookieChoice, + setConsent, +} from '@/features/cookie-banner/utils' + +function toggleCookieBanner(hidden: boolean) { + const cookieBannerEl = document.querySelector('.cookie-banner') + if (cookieBannerEl) { + cookieBannerEl.classList.toggle('hidden', hidden) + } +} + +if (cookieBannerRequired()) { + document + .querySelectorAll('[data-ol-cookie-banner-set-consent]') + .forEach(el => { + el.addEventListener('click', function (e) { + e.preventDefault() + toggleCookieBanner(true) + const consentType = el.getAttribute( + 'data-ol-cookie-banner-set-consent' + ) as CookieConsentValue | null + setConsent(consentType) + }) + }) + + if (!hasMadeCookieChoice()) { + toggleCookieBanner(false) + } +} diff --git a/services/web/frontend/js/features/cookie-banner/utils.ts b/services/web/frontend/js/features/cookie-banner/utils.ts new file mode 100644 index 0000000000..5c045d4e71 --- /dev/null +++ b/services/web/frontend/js/features/cookie-banner/utils.ts @@ -0,0 +1,43 @@ +import getMeta from '@/utils/meta' + +export type CookieConsentValue = 'all' | 'essential' + +function loadGA() { + if (window.olLoadGA) { + window.olLoadGA() + } +} + +export function setConsent(value: CookieConsentValue | null) { + const cookieDomain = getMeta('ol-ExposedSettings').cookieDomain + const oneYearInSeconds = 60 * 60 * 24 * 365 + const cookieAttributes = + '; path=/' + + '; domain=' + + cookieDomain + + '; max-age=' + + oneYearInSeconds + + '; SameSite=Lax; Secure' + if (value === 'all') { + document.cookie = 'oa=1' + cookieAttributes + loadGA() + window.dispatchEvent(new CustomEvent('cookie-consent', { detail: true })) + } else { + document.cookie = 'oa=0' + cookieAttributes + window.dispatchEvent(new CustomEvent('cookie-consent', { detail: false })) + } +} + +export function cookieBannerRequired() { + const exposedSettings = getMeta('ol-ExposedSettings') + return Boolean( + exposedSettings.gaToken || + exposedSettings.gaTokenV4 || + exposedSettings.propensityId || + exposedSettings.hotjarId + ) +} + +export function hasMadeCookieChoice() { + return document.cookie.split('; ').some(c => c.startsWith('oa=')) +} diff --git a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx b/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx index 3d24f9845c..07319ffaf1 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx @@ -20,6 +20,7 @@ import Footer from '@/features/ui/components/bootstrap-5/footer/footer' import SidebarDsNav from '@/features/project-list/components/sidebar/sidebar-ds-nav' import SystemMessages from '@/shared/components/system-messages' import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg' +import CookieBanner from '@/shared/components/cookie-banner' export function ProjectListDsNav() { const navbarProps = getMeta('ol-navbar') @@ -125,6 +126,7 @@ export function ProjectListDsNav() {