diff --git a/services/web/.storybook/preview.tsx b/services/web/.storybook/preview.tsx index fa6905d7d7..320caac144 100644 --- a/services/web/.storybook/preview.tsx +++ b/services/web/.storybook/preview.tsx @@ -13,7 +13,6 @@ import en from '../../../services/web/locales/en.json' function resetMeta() { window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' }) - window.metaAttributesCache.set('ol-chatEnabled', true) window.metaAttributesCache.set('ol-ExposedSettings', { adminEmail: 'placeholder@example.com', appName: 'Overleaf', diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 08307c2ab9..cb2e3c7025 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -728,14 +728,6 @@ const _ProjectController = { ? 'project/ide-react-detached' : 'project/ide-react' - let chatEnabled - if (Features.hasFeature('saas')) { - chatEnabled = - Features.hasFeature('chat') && req.capabilitySet.has('chat') - } else { - chatEnabled = Features.hasFeature('chat') - } - const isOverleafAssistBundleEnabled = splitTestAssignments['overleaf-assist-bundle']?.variant === 'enabled' @@ -841,7 +833,7 @@ const _ProjectController = { isTokenMember, isInvitedMember ), - chatEnabled, + capabilities: [...req.capabilitySet], projectHistoryBlobsEnabled: Features.hasFeature( 'project-history-blobs' ), diff --git a/services/web/app/src/Features/User/UserAuditLogHandler.js b/services/web/app/src/Features/User/UserAuditLogHandler.js index b1d404303e..87cd810161 100644 --- a/services/web/app/src/Features/User/UserAuditLogHandler.js +++ b/services/web/app/src/Features/User/UserAuditLogHandler.js @@ -8,6 +8,7 @@ function _canHaveNoIpAddressId(operation, info) { if (operation === 'must-reset-password-set') return true if (operation === 'remove-email' && info.script) return true if (operation === 'release-managed-user' && info.script) return true + if (operation === 'unlink-dropbox' && info.batch) return true return false } diff --git a/services/web/app/src/Features/User/UserPagesController.mjs b/services/web/app/src/Features/User/UserPagesController.mjs index 29fc505a7c..d353ca88e3 100644 --- a/services/web/app/src/Features/User/UserPagesController.mjs +++ b/services/web/app/src/Features/User/UserPagesController.mjs @@ -176,6 +176,7 @@ async function settingsPage(req, res) { gitBridgeEnabled: Settings.enableGitBridge, isSaas: Features.hasFeature('saas'), memberOfSSOEnabledGroups, + capabilities: [...req.capabilitySet], }) } diff --git a/services/web/app/src/models/GroupPolicy.js b/services/web/app/src/models/GroupPolicy.js index e975834008..55728a2415 100644 --- a/services/web/app/src/models/GroupPolicy.js +++ b/services/web/app/src/models/GroupPolicy.js @@ -27,6 +27,9 @@ const GroupPolicySchema = new Schema( // User can't use the chat feature userCannotUseChat: Boolean, + + // User can't use the Dropbox feature + userCannotUseDropbox: Boolean, }, { minimize: false } ) diff --git a/services/web/app/views/project/editor/_meta.pug b/services/web/app/views/project/editor/_meta.pug index 2c0af8dec8..4c49fc4cc7 100644 --- a/services/web/app/views/project/editor/_meta.pug +++ b/services/web/app/views/project/editor/_meta.pug @@ -12,7 +12,7 @@ meta(name="ol-isRestrictedTokenMember" data-type="boolean" content=isRestrictedT meta(name="ol-maxDocLength" data-type="json" content=maxDocLength) meta(name="ol-maxReconnectGracefullyIntervalMs" data-type="json" content=maxReconnectGracefullyIntervalMs) meta(name="ol-wikiEnabled" data-type="boolean" content=settings.proxyLearn) -meta(name="ol-chatEnabled" data-type="boolean" content=chatEnabled) +meta(name="ol-capabilities" data-type="json" content=capabilities) meta(name="ol-projectHistoryBlobsEnabled" data-type="boolean" content=projectHistoryBlobsEnabled) meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl) meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled) diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 4f939a41ca..4ac35bef71 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -32,6 +32,7 @@ block append meta meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled) meta(name="ol-isSaas" data-type="boolean" content=isSaas) meta(name="ol-memberOfSSOEnabledGroups" data-type="json" content=memberOfSSOEnabledGroups) + meta(name="ol-capabilities" data-type="json" content=capabilities) block content main.content.content-alt#main-content diff --git a/services/web/frontend/js/features/chat/context/chat-context.tsx b/services/web/frontend/js/features/chat/context/chat-context.tsx index 9feca60579..2ba0ff5f5d 100644 --- a/services/web/frontend/js/features/chat/context/chat-context.tsx +++ b/services/web/frontend/js/features/chat/context/chat-context.tsx @@ -193,7 +193,7 @@ export const ChatContext = createContext< >(undefined) export const ChatProvider: FC = ({ children }) => { - const chatEnabled = getMeta('ol-chatEnabled') + const chatEnabled = getMeta('ol-capabilities')?.includes('chat') const clientId = useRef() if (clientId.current === undefined) { diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx index 4304768c48..87bcbc0aac 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/toolbar-header.tsx @@ -80,7 +80,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({ openShareModal: () => void trackChangesVisible: boolean | undefined }) { - const chatEnabled = getMeta('ol-chatEnabled') + const chatEnabled = getMeta('ol-capabilities')?.includes('chat') const { t } = useTranslation() const shouldDisplayPublishButton = hasPublishPermissions && PublishButton diff --git a/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx b/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx index b0a65e12bb..93382d613a 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx @@ -47,7 +47,8 @@ export const MainLayout: FC = () => { handlePaneExpand: handleChatExpand, } = useChatPane() - const chatEnabled = getMeta('ol-chatEnabled') && !isRestrictedTokenMember + const chatEnabled = + getMeta('ol-capabilities')?.includes('chat') && !isRestrictedTokenMember const { t } = useTranslation() diff --git a/services/web/frontend/js/features/ide-redesign/components/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail.tsx index 825dd701b5..f3c741155c 100644 --- a/services/web/frontend/js/features/ide-redesign/components/rail.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/rail.tsx @@ -156,7 +156,7 @@ export const RailLayout = () => { component: , indicator: , title: t('chat'), - hide: !getMeta('ol-chatEnabled'), + hide: !getMeta('ol-capabilities')?.includes('chat'), }, { key: 'errors', diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index ffe1903fa5..8e1b5bdf61 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -85,7 +85,7 @@ export interface Meta { 'ol-cannot-link-other-third-party-sso': boolean 'ol-cannot-reactivate-subscription': boolean 'ol-cannot-use-ai': boolean - 'ol-chatEnabled': boolean + 'ol-capabilities': Array<'dropbox' | 'chat'> 'ol-compileSettings': { reducedTimeoutWarning: string compileTimeout: number diff --git a/services/web/migrations/20250604112908_add_dropbox_policy_to_group_policy.mjs b/services/web/migrations/20250604112908_add_dropbox_policy_to_group_policy.mjs new file mode 100644 index 0000000000..47cc644554 --- /dev/null +++ b/services/web/migrations/20250604112908_add_dropbox_policy_to_group_policy.mjs @@ -0,0 +1,23 @@ +const tags = ['saas'] + +const migrate = async client => { + const { db } = client + await db.grouppolicies.updateMany( + {}, + { $set: { userCannotUseDropbox: false } } + ) +} + +const rollback = async client => { + const { db } = client + await db.grouppolicies.updateMany( + {}, + { $unset: { userCannotUseDropbox: '' } } + ) +} + +export default { + tags, + migrate, + rollback, +} diff --git a/services/web/test/frontend/features/chat/components/chat-pane.test.jsx b/services/web/test/frontend/features/chat/components/chat-pane.test.jsx index ee7c391c33..f990a8c6ce 100644 --- a/services/web/test/frontend/features/chat/components/chat-pane.test.jsx +++ b/services/web/test/frontend/features/chat/components/chat-pane.test.jsx @@ -19,7 +19,6 @@ describe('', function () { beforeEach(function () { window.metaAttributesCache.set('ol-user', user) - window.metaAttributesCache.set('ol-chatEnabled', true) window.metaAttributesCache.set('ol-preventCompileOnLoad', true) }) diff --git a/services/web/test/frontend/features/chat/context/chat-context.test.jsx b/services/web/test/frontend/features/chat/context/chat-context.test.jsx index 36d5846555..a930ba3a9c 100644 --- a/services/web/test/frontend/features/chat/context/chat-context.test.jsx +++ b/services/web/test/frontend/features/chat/context/chat-context.test.jsx @@ -27,7 +27,6 @@ describe('ChatContext', function () { stubMathJax() window.metaAttributesCache.set('ol-user', user) - window.metaAttributesCache.set('ol-chatEnabled', true) window.metaAttributesCache.set('ol-preventCompileOnLoad', true) this.stub = sinon.stub(chatClientIdGenerator, 'generate').returns(uuidValue) diff --git a/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.jsx b/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.jsx index 84b1e680ef..be7894fc73 100644 --- a/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.jsx +++ b/services/web/test/frontend/features/editor-navigation-toolbar/components/toolbar-header.test.jsx @@ -27,7 +27,6 @@ describe('', function () { } beforeEach(function () { - window.metaAttributesCache.set('ol-chatEnabled', true) window.metaAttributesCache.set('ol-preventCompileOnLoad', true) }) diff --git a/services/web/test/frontend/helpers/editor-providers.jsx b/services/web/test/frontend/helpers/editor-providers.jsx index 645b29fb64..1fe143a8e3 100644 --- a/services/web/test/frontend/helpers/editor-providers.jsx +++ b/services/web/test/frontend/helpers/editor-providers.jsx @@ -85,6 +85,8 @@ export function EditorProviders({ merge({}, defaultUserSettings, userSettings) ) + window.metaAttributesCache.set('ol-capabilities', ['chat', 'dropbox']) + const scope = merge( { user, diff --git a/services/web/test/frontend/helpers/reset-meta.ts b/services/web/test/frontend/helpers/reset-meta.ts index f5a979828a..e59e62342d 100644 --- a/services/web/test/frontend/helpers/reset-meta.ts +++ b/services/web/test/frontend/helpers/reset-meta.ts @@ -2,6 +2,7 @@ export function resetMeta() { window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-projectHistoryBlobsEnabled', true) window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' }) + window.metaAttributesCache.set('ol-capabilities', ['chat', 'dropbox']) window.metaAttributesCache.set('ol-ExposedSettings', { appName: 'Overleaf', maxEntitiesPerProject: 10, diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index 7745ece8fa..0acd900b90 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -300,6 +300,7 @@ describe('ProjectController', function () { translate() {}, }, ip: '192.170.18.1', + capabilitySet: new Set(['chat']), } this.res = { locals: { @@ -1085,34 +1086,12 @@ describe('ProjectController', function () { this.ProjectController.loadEditor(this.req, this.res) }) - describe('chatEnabled flag', function () { - it('should be set to false when the feature is disabled', function (done) { + describe('capabilitySet', function () { + it('should be passed as an array when loading the editor', function (done) { this.Features.hasFeature = sinon.stub().withArgs('chat').returns(false) this.res.render = (pageName, opts) => { - expect(opts.chatEnabled).to.be.false - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) - - it('should be set to false when the feature is enabled but the capability is not available', function (done) { - this.Features.hasFeature = sinon.stub().withArgs('chat').returns(false) - this.req.capabilitySet = new Set() - - this.res.render = (pageName, opts) => { - expect(opts.chatEnabled).to.be.false - done() - } - this.ProjectController.loadEditor(this.req, this.res) - }) - - it('should be set to true when the feature is enabled and the capability is available', function (done) { - this.Features.hasFeature = sinon.stub().withArgs('chat').returns(true) - this.req.capabilitySet = new Set(['chat']) - - this.res.render = (pageName, opts) => { - expect(opts.chatEnabled).to.be.true + expect(opts.capabilities).to.deep.equal(['chat']) done() } this.ProjectController.loadEditor(this.req, this.res)