13 Commits

Author SHA1 Message Date
Borja
d4992914c2 Add figure generator from text functionality (#29742)
GitOrigin-RevId: 94c65c567d59e3228dba63395bd46fe0c57fab02
2025-11-24 09:07:16 +00:00
Andrew Rumble
e2cb424695 Merge pull request #29849 from overleaf/ar-add-missing-prism-certificate-to-web
[web] Add the missing intermediate certificate to web service

GitOrigin-RevId: 293009ae2da30f698aa0d38dc4cd902b0d8ab5e5
2025-11-24 09:07:00 +00:00
Malik Glossop
abff6fccd4 Merge pull request #29847 from overleaf/mg-revert-js-yaml-change
Revert "[web] Update js-yaml to patched security version including ov…

GitOrigin-RevId: ecbb05915bdc4c21fd9e6fd6a8f74012853f1322
2025-11-24 09:06:55 +00:00
Malik Glossop
016778295a Merge pull request #29733 from overleaf/mg-transfer-ownership
Fix transfer ownership permissions

GitOrigin-RevId: b6d09704361507085e3eae8dc9240a36ae47c70e
2025-11-24 09:06:50 +00:00
Malik Glossop
68c9d1931d Merge pull request #29765 from overleaf/mg-s-patch-js-yaml
Update js-yaml to patched security version including overrides

GitOrigin-RevId: 364f4dd8fe3cb0a5c486bdf3921b42c49772e4c2
2025-11-24 09:06:45 +00:00
Tim Down
00f6a1e0f9 CIAM registration form buttons, inputs and fixes (#29740)
* Many fixes to CIAM registration form, including Phosphor icons

* Unify layout between Pug and React, fixes for spacing and mobile screen sizes

* Pug lint fix

* Make CIAM footer links underlined

* Add CIAM error notification styling

* Merge duplicate style rules

* Remove outdated comment

* Fix ordering of en.json

* Move aria-label to buttons

* Move full stop into translation string

* Remove dummy password strength indicator

* CIAM spacing and label fixes

* Header logo fixes from review

* Add aria-hidden to error icon

GitOrigin-RevId: 87c8181566f0878256b8010f95f115ec25c7ceb9
2025-11-24 09:06:40 +00:00
Mathias Jakobsen
f2a05b1a2e Merge pull request #29793 from overleaf/mj-custom-rail-icon
[web] Add custom sparkles icon for workbench rail tab

GitOrigin-RevId: b332c90532e75b41b494906e021bcb49ff358024
2025-11-24 09:06:28 +00:00
Mathias Jakobsen
4a4b82cec1 Merge pull request #29797 from overleaf/mj-rail-tab-hiding-refactor
[web] Refactor tab hiding

GitOrigin-RevId: 29b4d1e67348a51e3c575ab2dda6e0931a90d504
2025-11-24 09:06:23 +00:00
Anna Claire Fields
9d2f5b3cde Merge pull request #29803 from overleaf/acf-admin-privileges-matrix
Admin privileges matrix

GitOrigin-RevId: 926c8053ab00292ee6fc0f04e0e429f307081f5e
2025-11-24 09:06:07 +00:00
Anna Claire Fields
f954796709 Merge pull request #29687 from overleaf/acf-update-validator-package
Upgrade validator to 13.15.0 to fix security vulnerabilities

GitOrigin-RevId: ddafde16f9783454c332124e88dae4f164eab4f3
2025-11-24 09:05:58 +00:00
Anna Claire Fields
49e8d0c551 Merge pull request #29695 from overleaf/change-auto-compile-rate-limit
Change auto compile rate limit

GitOrigin-RevId: 9f689c4811ad03ebbbc8cf0abb5f0ac867356873
2025-11-24 09:05:53 +00:00
Brian Gough
1f356754de Merge pull request #29801 from overleaf/bg-history-refactor-backup-worker-shutdown
refactor history-v1-backup worker shutdown

GitOrigin-RevId: 9666a99b00b30e98844e7dd25932f1590d0879b3
2025-11-24 09:05:49 +00:00
Brian Gough
e822ac0ee0 Merge pull request #29775 from overleaf/bg-history-use-configurable-concurrency-in-backup-worker
allow configurable concurrency for history-v1 backup worker

GitOrigin-RevId: 59c734b013f99e215cc84688142cb0fbe45b064b
2025-11-24 09:05:44 +00:00
38 changed files with 941 additions and 291 deletions

202
package-lock.json generated
View File

@@ -13191,7 +13191,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz",
"integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@lit/reactive-element": {
@@ -15599,9 +15598,9 @@
]
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
@@ -15620,6 +15619,55 @@
"react-dom": ">= 16.8"
}
},
"node_modules/@phosphor-icons/webcomponents": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz",
"integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==",
"license": "MIT",
"dependencies": {
"lit": "^3"
}
},
"node_modules/@phosphor-icons/webcomponents/node_modules/@lit/reactive-element": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz",
"integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.4.0"
}
},
"node_modules/@phosphor-icons/webcomponents/node_modules/lit": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz",
"integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^2.1.0",
"lit-element": "^4.2.0",
"lit-html": "^3.3.0"
}
},
"node_modules/@phosphor-icons/webcomponents/node_modules/lit-element": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz",
"integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.4.0",
"@lit/reactive-element": "^2.1.0",
"lit-html": "^3.3.0"
}
},
"node_modules/@phosphor-icons/webcomponents/node_modules/lit-html": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz",
"integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -20579,8 +20627,7 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@types/unist": {
"version": "2.0.11",
@@ -20595,9 +20642,10 @@
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
},
"node_modules/@types/validator": {
"version": "13.7.15",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz",
"integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ=="
"version": "13.15.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
"license": "MIT"
},
"node_modules/@types/warning": {
"version": "3.0.3",
@@ -28216,7 +28264,8 @@
"node_modules/dottie": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
"license": "MIT"
},
"node_modules/downshift": {
"version": "9.0.9",
@@ -34416,7 +34465,8 @@
"integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
"engines": [
"node >= 0.4.0"
]
],
"license": "MIT"
},
"node_modules/inflight": {
"version": "1.0.6",
@@ -36076,9 +36126,9 @@
}
},
"node_modules/json-refs/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -39307,11 +39357,12 @@
}
},
"node_modules/moment-timezone": {
"version": "0.5.40",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz",
"integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==",
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"license": "MIT",
"dependencies": {
"moment": ">= 2.9.0"
"moment": "^2.29.4"
},
"engines": {
"node": "*"
@@ -41235,9 +41286,9 @@
}
},
"node_modules/path-loader/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -41267,9 +41318,9 @@
}
},
"node_modules/path-loader/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -45552,9 +45603,10 @@
}
},
"node_modules/retry-as-promised": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz",
"integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA=="
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
"integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
"license": "MIT"
},
"node_modules/retry-request": {
"version": "4.2.2",
@@ -46182,31 +46234,32 @@
}
},
"node_modules/sequelize": {
"version": "6.31.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.0.tgz",
"integrity": "sha512-nCPVtv+QydBmb3Us2jCNAr1Dx3gST83VZxxrUQn/JAVFCOrmYOgUaPUz5bevummyNf30zfHsZhIKYAOD3ULfTA==",
"version": "6.37.7",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
"integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/sequelize"
}
],
"license": "MIT",
"dependencies": {
"@types/debug": "^4.1.7",
"@types/validator": "^13.7.1",
"debug": "^4.3.3",
"dottie": "^2.0.2",
"inflection": "^1.13.2",
"@types/debug": "^4.1.8",
"@types/validator": "^13.7.17",
"debug": "^4.3.4",
"dottie": "^2.0.6",
"inflection": "^1.13.4",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment-timezone": "^0.5.35",
"pg-connection-string": "^2.5.0",
"retry-as-promised": "^7.0.3",
"semver": "^7.3.5",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"pg-connection-string": "^2.6.1",
"retry-as-promised": "^7.0.4",
"semver": "^7.5.4",
"sequelize-pool": "^7.1.0",
"toposort-class": "^1.0.1",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"validator": "^13.9.0",
"wkx": "^0.5.0"
},
"engines": {
@@ -46301,28 +46354,39 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
"integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/sequelize/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"node_modules/sequelize/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"yallist": "^4.0.0"
"ms": "^2.1.3"
},
"engines": {
"node": ">=10"
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/sequelize/node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/sequelize/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -46334,15 +46398,11 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/sequelize/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -48923,9 +48983,9 @@
}
},
"node_modules/swagger-tools/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -49983,7 +50043,8 @@
"node_modules/toposort-class": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
"integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
"integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
"license": "MIT"
},
"node_modules/touch": {
"version": "3.1.0",
@@ -51212,9 +51273,10 @@
}
},
"node_modules/validator": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
"version": "13.15.23",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz",
"integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
@@ -52991,6 +53053,7 @@
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
"integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -53411,9 +53474,9 @@
"license": "MIT"
},
"node_modules/z-schema/node_modules/validator": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz",
"integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==",
"version": "13.15.20",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
@@ -53562,7 +53625,7 @@
"pg-copy-streams": "^2.2.2",
"promptly": "^3.0.3",
"recurly": "^4.0.1",
"sequelize": "^6.31.0",
"sequelize": "^6.37.7",
"yargs": "^17.0.0"
},
"devDependencies": {
@@ -55649,6 +55712,7 @@
"@overleaf/stream-utils": "*",
"@overleaf/validation-tools": "*",
"@phosphor-icons/react": "^2.1.7",
"@phosphor-icons/webcomponents": "^2.1.5",
"@slack/webhook": "^7.0.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.7.0",

View File

@@ -33,7 +33,8 @@
"swagger-tools@0.10.4": {
"path-to-regexp": "3.3.0",
"body-parser": "1.20.3",
"multer": "2.0.2"
"multer": "2.0.2",
"validator": "13.15.20"
},
"request@2.88.2": {
"tough-cookie": "5.1.2",

View File

@@ -8,7 +8,7 @@ import express from 'express'
import logger from '@overleaf/logger'
import Metrics from '@overleaf/metrics'
import { expressify } from '@overleaf/promise-utils'
import { drainQueue, healthCheck } from './storage/scripts/backup_worker.mjs'
import { healthCheck } from './storage/scripts/backup_worker.mjs'
const app = express()
logger.initialize('history-v1-backup-worker')
@@ -39,7 +39,6 @@ app.use((err, req, res, next) => {
async function triggerGracefulShutdown(server, signal) {
logger.info({ signal }, 'graceful shutdown: started shutdown sequence')
await drainQueue()
server.close(function () {
logger.info({ signal }, 'graceful shutdown: closed server')
setTimeout(() => {

View File

@@ -93,8 +93,10 @@ process.on('SIGINT', handleSignal)
process.on('SIGTERM', handleSignal)
function handleSignal() {
gracefulShutdownInitiated = true
logger.info({}, 'graceful shutdown initiated, draining queue')
if (!gracefulShutdownInitiated) {
gracefulShutdownInitiated = true
logger.info({}, 'graceful shutdown: waiting for backups to complete')
}
}
async function retry(fn, times, delayMs) {
@@ -1071,6 +1073,45 @@ async function main() {
}
}
/**
* Close all database connections gracefully
* @returns {Promise<void>}
*/
export async function closeConnections() {
/** @type {Error[]} */
const errors = []
try {
await knex.destroy()
console.log('Postgres connection closed')
} catch (err) {
console.error('Error closing Postgres connection:', err)
errors.push(/** @type {Error} */ (err))
}
try {
await client.close()
console.log('MongoDB connection closed')
} catch (err) {
console.error('Error closing MongoDB connection:', err)
errors.push(/** @type {Error} */ (err))
}
try {
await redis.disconnect()
console.log('Redis connection closed')
} catch (err) {
console.error('Error closing Redis connection:', err)
errors.push(/** @type {Error} */ (err))
}
if (errors.length > 0) {
throw new Error(
`Failed to close ${errors.length} connection(s): ${errors.map(e => e.message).join(', ')}`
)
}
}
// Only run command-line interface when script is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
main()
@@ -1083,30 +1124,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
console.error('Error backing up project:', err)
process.exit(1)
})
.finally(() => {
knex
.destroy()
.then(() => {
console.log('Postgres connection closed')
})
.catch(err => {
console.error('Error closing Postgres connection:', err)
})
client
.close()
.then(() => {
console.log('MongoDB connection closed')
})
.catch(err => {
console.error('Error closing MongoDB connection:', err)
})
redis
.disconnect()
.then(() => {
console.log('Redis connection closed')
})
.catch(err => {
console.error('Error closing Redis connection:', err)
})
.finally(async () => {
await closeConnections()
})
}

View File

@@ -6,9 +6,11 @@ import {
backupProject,
initializeProjects,
configureBackup,
closeConnections,
} from './backup.mjs'
const CONCURRENCY = 15
const JOB_CONCURRENCY = parseInt(process.env.JOB_CONCURRENCY, 10) || 15
const UPLOAD_CONCURRENCY = parseInt(process.env.UPLOAD_CONCURRENCY, 10) || 50
const WARN_THRESHOLD = 2 * 60 * 60 * 1000 // warn if projects are older than this
const redisOptions = config.get('redis.queue')
const JOB_TIME_BUCKETS = [10, 100, 500, 1000, 5000, 10000, 30000, 60000] // milliseconds
@@ -17,7 +19,20 @@ const LAG_TIME_BUCKETS_HRS = [
] // hours
// Configure backup settings to match worker concurrency
configureBackup({ concurrency: 50, useSecondary: true })
configureBackup({ concurrency: UPLOAD_CONCURRENCY, useSecondary: true })
let gracefulShutdownInitiated = false
process.on('SIGINT', handleSignal)
process.on('SIGTERM', handleSignal)
async function handleSignal() {
if (!gracefulShutdownInitiated) {
gracefulShutdownInitiated = true
logger.info({}, 'graceful shutdown: stopping backup worker')
await drainQueue()
}
}
// Create a Bull queue named 'backup'
const backupQueue = new Queue('backup', {
@@ -61,15 +76,15 @@ backupQueue.on('lock-extension-failed', (job, err) => {
})
backupQueue.on('paused', () => {
logger.info('queue paused')
logger.info({}, 'queue paused')
})
backupQueue.on('resumed', () => {
logger.info('queue resumed')
logger.info({}, 'queue resumed')
})
// Process jobs
backupQueue.process(CONCURRENCY, async job => {
backupQueue.process(JOB_CONCURRENCY, async job => {
const { projectId, startDate, endDate } = job.data
if (projectId) {
@@ -137,10 +152,10 @@ async function runInit(startDate, endDate) {
}
export async function drainQueue() {
logger.info({ queue: backupQueue.name }, 'pausing queue')
await backupQueue.pause(true) // pause this worker and wait for jobs to finish
logger.info({ queue: backupQueue.name }, 'closing queue')
await backupQueue.close()
logger.info({ queue: backupQueue.name }, 'closing database connections')
await closeConnections()
}
export async function healthCheck() {

View File

@@ -18,6 +18,9 @@ RUN mkdir -p /overleaf/services/web/data/dumpFolder \
&& chmod -R 0755 /overleaf/services/web/data \
&& chown -R node:node /overleaf/services/web/data
# Add intermediate certificate for prism.optica.org
COPY services/web/certs/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt.pem /usr/local/share/ca-certificates/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt
RUN update-ca-certificates
# the deps image is used for caching npm ci
FROM base AS deps-prod
@@ -127,6 +130,7 @@ CMD ["node", "--expose-gc", "app.mjs"]
FROM pug AS app
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt
COPY --from=webpack-no-sourcemaps /overleaf/services/web/public /overleaf/services/web/public
USER node
CMD ["node", "--expose-gc", "app.mjs"]

View File

@@ -12,6 +12,7 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import OError from '@overleaf/o-error'
import TagsHandler from '../Tags/TagsHandler.mjs'
import { promiseMapWithLimit } from '@overleaf/promise-utils'
import LimitationsManager from '../Subscription/LimitationsManager.mjs'
export default {
promises: {
@@ -124,16 +125,20 @@ async function transferOwnership(projectId, newOwnerId, options = {}) {
{ previousOwnerId, newOwnerId }
)
// Determine which permissions to give old owner based on
// new owner's existing permissions
const newPermissions =
_getUserPermissions(newOwner, project) || PrivilegeLevels.READ_ONLY
// Transfer ownership
await _transferOwnership(projectId, previousOwnerId, newOwnerId)
await _transferOwnership(
// Determine which permissions to give old owner (after transfer, so we use new owner's limits)
const { privilegeLevel, pendingPrivilegeLevel } =
await _determinePrivilegeLevelForPreviousOwner(projectId)
// Add the previous owner back to the project with determined permissions
await CollaboratorsHandler.promises.addUserIdToProject(
projectId,
previousOwnerId,
newOwnerId,
newPermissions
previousOwnerId,
privilegeLevel,
pendingPrivilegeLevel
)
// Flush project to TPDS
@@ -189,26 +194,34 @@ function _userIsCollaborator(user, project) {
return Boolean(_getUserPermissions(user, project))
}
async function _transferOwnership(
projectId,
previousOwnerId,
newOwnerId,
newPermissions
) {
async function _determinePrivilegeLevelForPreviousOwner(projectId) {
// Try to give READ_AND_WRITE if space available based on new owner's limits
const canAddEditor =
await LimitationsManager.promises.canAddXEditCollaborators(projectId, 1)
if (canAddEditor) {
return { privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
}
// Collaborator limit is reached for editor and reviewer so fall back to read-only
// Add pending editor status so they are automatically upgraded when possible
return {
privilegeLevel: PrivilegeLevels.READ_ONLY,
pendingPrivilegeLevel: { pendingEditor: true },
}
}
async function _transferOwnership(projectId, previousOwnerId, newOwnerId) {
// Remove new owner from collaborators list
await CollaboratorsHandler.promises.removeUserFromProject(
projectId,
newOwnerId
)
// Update project ownership
await Project.updateOne(
{ _id: projectId },
{ $set: { owner_ref: newOwnerId } }
).exec()
await CollaboratorsHandler.promises.addUserIdToProject(
projectId,
newOwnerId,
previousOwnerId,
newPermissions
)
}
async function _sendEmails(project, previousOwner, newOwner) {

View File

@@ -455,6 +455,7 @@ const _ProjectController = {
'compile-timeout-remove-info',
'ai-workbench',
'compile-timeout-target-plans',
'writefull-figure-generator',
].filter(Boolean)
const getUserValues = async userId =>

View File

@@ -6,6 +6,7 @@ import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs'
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.mjs'
import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
import SystemMessageManager from '../SystemMessages/SystemMessageManager.mjs'
import Modules from '../../infrastructure/Modules.js'
const AdminController = {
_sendDisconnectAllUsersMessage: delay => {
@@ -30,16 +31,23 @@ const AdminController = {
)
}
SystemMessageManager.getMessagesFromDB(function (error, systemMessages) {
if (error) {
return next(error)
SystemMessageManager.getMessagesFromDB(
async function (error, systemMessages) {
if (error) {
return next(error)
}
const privilegesMatrixResults = await Modules.promises.hooks.fire(
'getPrivilegesMatrix'
)
const privilegesMatrix = privilegesMatrixResults[0] || null
res.render('admin/index', {
title: 'System Admin',
openSockets,
systemMessages,
privilegesMatrix,
})
}
res.render('admin/index', {
title: 'System Admin',
openSockets,
systemMessages,
})
})
)
},
disconnectAllUsers: (req, res) => {

View File

@@ -91,8 +91,8 @@ const rateLimiters = {
duration: 60,
}),
compileProjectHttp: new RateLimiter('compile-project-http', {
points: 800,
duration: 60 * 60,
points: 200,
duration: 10 * 60,
}),
confirmEmail: new RateLimiter('confirm-email', {
points: 10,

View File

@@ -2,13 +2,15 @@ include terms_of_service
include recaptcha
mixin ciamLogo
a.brand.overleaf-ds-logo(href='/' aria-label='Overleaf')
header.ciam-logo
a.brand.overleaf-ds-logo(href='/')
span.visually-hidden Overleaf
mixin ciamCardSeparator
hr.ciam-card-separator
mixin ciamCardFooter
.ciam-card-footer
section.ciam-card-footer
+ciamCardSeparator
.ciam-footer-ds-logo
img(
@@ -25,3 +27,16 @@ mixin ciamTermsOfServiceAgreement
mixin ciamRecaptchaConditions
p
+recaptchaConditionsContent
mixin ciamCustomFormDangerMessage(key)
div(
class='notification ciam-notification notification-type-error'
hidden
data-ol-custom-form-message=key
role='alert'
aria-live='polite'
)
.notification-icon
ph-warning-circle(aria-hidden='true')
.notification-content.text-left
block

View File

@@ -16,6 +16,7 @@ block content
+bookmarkable-tabset-header('system-messages', 'System Messages', true)
+bookmarkable-tabset-header('open-sockets', 'Open Sockets')
+bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
+bookmarkable-tabset-header('privileges-matrix', 'Privileges Matrix')
if hasFeature('saas')
+bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
@@ -71,6 +72,41 @@ block content
button.btn.btn-danger(type='submit') Reopen Editor
p.small Will reopen the editor after closing.
.tab-pane(role='tabpanel' id='privileges-matrix')
p This matrix shows the access levels for different admin roles across various user management privileges. The matrix is automatically generated from the code-defined roles and capabilities.
if hasFeature('saas') && privilegesMatrix
.privileges-matrix-container
table.table.table-bordered.table-striped.privileges-matrix-table
thead
tr
th Webpage and privilege
th(colspan=privilegesMatrix.roles.length) Access Level Needed For Team
tr
th
each role in privilegesMatrix.roles
th= role.displayName
tbody
- let currentSection = null
each row in privilegesMatrix.privileges
- const privilege = row.privilege
- if (currentSection !== privilege.section)
- currentSection = privilege.section
tr.section-header
td(colspan=privilegesMatrix.roles.length + 1)
strong= privilege.section
tr
td.privilege-label= privilege.label
each role in privilegesMatrix.roles
- const accessLevel = row.access[role.id]
td.access-cell(class=`access-${accessLevel}`)
span.access-badge(class=`badge-${accessLevel}`)
if accessLevel === 'yes'
| Yes
else
| No
else
p The privileges matrix is not available in this environment.
if hasFeature('saas')
.tab-pane(role='tabpanel' id='tpds')
h3 Flush project to TPDS

View File

@@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh
bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV
cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy
FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc
3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8
osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT
zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud
EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G
A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG
NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH
Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC
ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG
9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t
wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS
slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R
bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4
chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN
JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==
-----END CERTIFICATE-----

View File

@@ -665,6 +665,7 @@
"full_doc_history": "",
"full_width": "",
"future_payments": "",
"generate_from_text": "",
"generate_from_text_or_image": "",
"generate_tables_and_equations": "",
"generate_token": "",

View File

@@ -0,0 +1,4 @@
// These are used in the CIAM registration form
import '@phosphor-icons/webcomponents/PhBank'
import '@phosphor-icons/webcomponents/PhEye'
import '@phosphor-icons/webcomponents/PhEyeSlash'

View File

@@ -1,24 +1,42 @@
import { materialIcon } from '@/features/utils/material-icon'
import classNames from 'classnames'
import '@phosphor-icons/webcomponents/PhWarningCircle'
function dsErrorIcon() {
const icon = document.createElement('ph-warning-circle')
icon.className = 'ciam-form-text-icon'
icon.ariaHidden = 'true'
return icon
}
export default function inputValidator(
inputEl: HTMLInputElement | HTMLTextAreaElement
) {
const isDsBranded = inputEl.classList.contains('form-control-ds')
const messageEl = document.createElement('div')
messageEl.className =
inputEl.getAttribute('data-ol-validation-message-classes') ||
'small text-danger mt-2 form-text'
classNames(
'small text-danger mt-2 form-text',
{ 'form-text-ds': isDsBranded }
)
messageEl.hidden = true
const messageInnerEl = messageEl.appendChild(document.createElement('span'))
messageInnerEl.className = 'form-text-inner'
messageInnerEl.className = classNames('form-text-inner', {
'form-text-inner-ds': isDsBranded,
})
const messageTextNode = document.createTextNode('')
const iconEl = materialIcon('error')
const iconEl = isDsBranded ? dsErrorIcon() : materialIcon('error')
messageInnerEl.append(iconEl)
messageInnerEl.append(messageTextNode)
inputEl.insertAdjacentElement('afterend', messageEl)
const inputContainerEl =
inputEl.closest('.form-complex-input-container') || inputEl
inputContainerEl.insertAdjacentElement('afterend', messageEl)
// Hide messages until the user leaves the input field or submits the form.
let canDisplayErrorMessages = false

View File

@@ -2,6 +2,7 @@ import { DropdownMenu } from '@/shared/components/dropdown/dropdown-menu'
import { RailTabKey } from '../../contexts/rail-context'
import { RailElement } from '../../utils/rail-types'
import RailTab from './rail-tab'
import { shouldIncludeRailTab } from '../../utils/rail-utils'
export default function RailOverflowDropdown({
tabs,
@@ -15,7 +16,7 @@ export default function RailOverflowDropdown({
return (
<DropdownMenu className="ide-rail-overflow-dropdown">
{tabs
.filter(({ hide }) => !hide)
.filter(shouldIncludeRailTab)
.map(({ icon, key, indicator, title, disabled }) => (
<RailTab
open={isOpen && selectedTab === key}

View File

@@ -6,6 +6,7 @@ import usePreviousValue from '@/shared/hooks/use-previous-value'
import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
import { Tab } from 'react-bootstrap'
import { RailElement } from '../../utils/rail-types'
import { shouldIncludeRailTab } from '../../utils/rail-utils'
export default function RailPanel({
isReviewPanelOpen,
@@ -52,9 +53,7 @@ export default function RailPanel({
>
<Tab.Content className="ide-rail-tab-content">
{railTabs
.filter(({ hide }) => {
return typeof hide === 'function' ? !hide() : !hide
})
.filter(shouldIncludeRailTab)
.map(({ key, component, mountOnFirstLoad }) => (
<Tab.Pane
eventKey={key}

View File

@@ -1,15 +1,14 @@
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import MaterialIcon from '@/shared/components/material-icon'
import classNames from 'classnames'
import { forwardRef, ReactElement } from 'react'
import { NavLink } from 'react-bootstrap'
import { RailElement } from '../../utils/rail-types'
const RailTab = forwardRef<
HTMLAnchorElement,
{
icon: AvailableUnfilledIcon
icon: RailElement['icon']
eventKey: string
open: boolean
indicator?: ReactElement
@@ -31,26 +30,43 @@ const RailTab = forwardRef<
})}
disabled={disabled}
>
{open ? (
<MaterialIcon
className="ide-rail-tab-link-icon"
type={icon}
accessibilityLabel={title}
/>
) : (
<MaterialIcon
className="ide-rail-tab-link-icon"
type={icon}
accessibilityLabel={title}
unfilled
/>
)}
<RailTabIcon icon={icon} title={title} open={open} />
{indicator}
</NavLink>
</OLTooltip>
)
})
const RailTabIcon = ({
icon,
title,
open,
}: {
icon: RailElement['icon']
title: string
open: boolean
}) => {
if (typeof icon === 'string') {
return open ? (
<MaterialIcon
type={icon}
className="ide-rail-tab-link-icon"
accessibilityLabel={title}
/>
) : (
<MaterialIcon
type={icon}
className="ide-rail-tab-link-icon"
unfilled
accessibilityLabel={title}
/>
)
} else {
const Component = icon
return <Component open={open} title={title} />
}
}
RailTab.displayName = 'RailTab'
export default RailTab

View File

@@ -30,6 +30,7 @@ import EditorTourRailTooltip from '../editor-tour/editor-tour-rail-tooltip'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import EditorTourThemeTooltip from '../editor-tour/editor-tour-theme-tooltip'
import EditorTourSwitchBackTooltip from '../editor-tour/editor-tour-switch-back-tooltip'
import { shouldIncludeRailTab } from '../../utils/rail-utils'
const moduleRailEntries = (
importOverleafModules('railEntries') as {
@@ -174,7 +175,7 @@ export const RailLayout = () => {
useEffect(() => {
const validTabKeys = railTabs
.filter(tab => (typeof tab.hide === 'function' ? !tab.hide() : !tab.hide))
.filter(shouldIncludeRailTab)
.map(tab => tab.key)
if (!validTabKeys.includes(selectedTab) && isOpen) {
// If the selected tab is no longer valid (e.g. due to permissions changes),
@@ -224,9 +225,7 @@ export const RailLayout = () => {
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
{tabsInRail
.filter(({ hide }) =>
typeof hide === 'function' ? !hide() : !hide
)
.filter(shouldIncludeRailTab)
.map(({ icon, key, indicator, title, disabled, ref }) => (
<RailTab
open={isOpen && selectedTab === key}

View File

@@ -1,9 +1,11 @@
import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
import { RailTabKey } from '../contexts/rail-context'
import { ReactElement } from 'react'
import { FC, ReactElement } from 'react'
export type CustomRailTabIcon = FC<{ open: boolean; title: string }>
export type RailElement = {
icon: AvailableUnfilledIcon
icon: AvailableUnfilledIcon | CustomRailTabIcon
key: RailTabKey
component: ReactElement | null
indicator?: ReactElement

View File

@@ -0,0 +1,8 @@
import { RailElement } from './rail-types'
export function shouldIncludeRailTab({ hide }: RailElement): boolean {
if (typeof hide === 'function') {
return !hide()
}
return !hide
}

View File

@@ -7,14 +7,20 @@ import { useTranslation } from 'react-i18next'
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
import { useCodeMirrorViewContext } from '../codemirror-context'
import { insertFigure } from '../../extensions/toolbar/commands'
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
import getMeta from '@/utils/meta'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { ToolbarButton } from './toolbar-button'
import { useEditorContext } from '@/shared/context/editor-context'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
const { t } = useTranslation()
const view = useCodeMirrorViewContext()
const { writefullInstance } = useEditorContext()
const { write } = usePermissionsContext()
const openFigureModal = useCallback(
(source: FigureModalSource, sourceName: string) => {
emitToolbarEvent(view, `toolbar-figure-modal-${sourceName}`)
@@ -32,6 +38,10 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
hasLinkUrlFeature,
} = getMeta('ol-ExposedSettings')
const hasGenerateFromTextFeature =
writefullInstance !== null &&
isSplitTestEnabled('writefull-figure-generator')
if (!write) {
return (
<ToolbarButton
@@ -88,6 +98,27 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
{t('from_url')}
</OLListGroupItem>
)}
{hasGenerateFromTextFeature && (
<OLListGroupItem
onClick={() => {
writefullInstance!.openFigureGenerator()
}}
>
<img
alt="sparkle"
className="ol-cm-toolbar-ai-sparkle-gradient"
src={sparkle}
aria-hidden="true"
/>
<img
alt="sparkle"
className="ol-cm-toolbar-ai-sparkle-white"
src={sparkleWhite}
aria-hidden="true"
/>
<span>{t('generate_from_text')}</span>
</OLListGroupItem>
)}
</ToolbarButtonMenu>
)
})

View File

@@ -16,6 +16,7 @@ const en = {
'do-not-know': 'Dont know',
equation: 'equation',
table: 'table',
figure: 'figure',
or: 'or',
close: 'Close',
submit: 'Submit',
@@ -228,7 +229,7 @@ const en = {
'create-modal.accept-tos_equation': 'Before you generate an equation',
'create-modal.image-picker-placeholder':
'Drop an image of the __name__ here',
'create-modal.enter-prompt': 'Enter your own prompt/Paste an image here:',
'create-modal.enter-prompt': 'Enter your prompt to generate the __name__:',
'create-modal.enter-prompt-or-paste-image':
'Enter your prompt or paste an image with the __name__:',
'create-modal.drop-image': 'Or, drop an image of the __name__ here:',
@@ -390,6 +391,7 @@ const es = {
'do-not-know': 'No lo sé',
equation: 'equación',
table: 'tabla',
figure: 'figura',
or: 'o',
close: 'Cerrar',
submit: 'Enviar',
@@ -616,7 +618,7 @@ const es = {
'create-modal.accept-tos_equation': 'Antes de generar una ecuación',
'create-modal.image-picker-placeholder': 'Arrastra una imagen aquí',
'create-modal.enter-prompt':
'Introduce tu propio prompt/pega una imagen aquí:',
'Introduce tu propio prompt para generar la __name__:',
'create-modal.enter-prompt-or-paste-image':
'Introduce tu propio prompt o pega una imagen de la __name__:',
'create-modal.drop-image': 'O, arrastre una imagen de __name__ aquí:',

View File

@@ -2,6 +2,7 @@ import './utils/webpack-public-path'
import './infrastructure/error-reporter'
import './infrastructure/hotjar'
import './features/form-helpers/hydrate-form'
import './features/form-helpers/form-phosphor-icons'
import './features/form-helpers/password-visibility'
import './features/link-helpers/slow-link'
import './features/event-tracking'

View File

@@ -1,16 +1,14 @@
import React, { FC, ReactNode } from 'react'
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
type Props = { children: ReactNode }
const CiamLayout: FC<Props> = ({ children }: Props) => (
<div className="ciam-layout ciam-enabled">
<a
href="/"
aria-label="Overleaf"
className="brand"
style={{ backgroundImage: `url("${overleafLogo}")` }}
/>
<header className="ciam-logo">
<a href="/" className="brand overleaf-ds-logo">
<span className="visually-hidden">Overleaf</span>
</a>
</header>
<div className="ciam-container">
<main className="ciam-card" id="main-content">
{children}

View File

@@ -17,4 +17,5 @@ export interface WritefullAPI {
): void
openTableGenerator(): void
openEquationGenerator(): void
openFigureGenerator(): void
}

View File

@@ -1,33 +1,59 @@
@use 'sass:math';
.overleaf-ds-logo {
background-image: url('../../../frontend/js/shared/svgs/overleaf-a-ds-solution-mallard.svg');
}
.ciam-layout {
@include full-height-stacked-page;
display: flex;
flex-direction: column;
gap: var(--ds-spacing-400);
@include media-breakpoint-up(sm) {
gap: var(--ds-spacing-800);
}
}
.ciam-enabled {
@include ds-body-md-regular;
font-family: var(--ds-font-family-sans), sans-serif;
--password-visibility-toggle-width: calc(24px + 2 * var(--ds-spacing-250));
&,
h1 {
font-family: var(--ds-font-family-sans), sans-serif;
color: var(--ds-color-text-primary);
}
.ciam-container {
flex: 1 1 auto;
padding: var(--ds-spacing-350);
padding: 0 var(--ds-spacing-300);
}
.brand {
.ciam-logo {
padding: var(--ds-spacing-800) 0 0 0;
text-align: center;
@include media-breakpoint-up(sm) {
padding-left: var(--ds-spacing-800);
padding-right: var(--ds-spacing-800);
}
}
.ciam-logo .brand {
flex-shrink: 0;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
height: 64px;
width: 130px;
margin: var(--ds-spacing-350) auto;
height: 49px;
width: 107px;
margin: 0 auto;
display: block;
@include media-breakpoint-up(sm) {
margin: var(--ds-spacing-350) var(--ds-spacing-800);
height: 64px;
width: 130px;
margin: 9px 0; // Vertical margin isn't an exacting spacing value in the design
}
}
@@ -49,11 +75,32 @@
padding: var(--ds-spacing-800) var(--ds-spacing-400);
border-radius: var(--ds-border-radius-400);
max-width: 464px;
margin: var(--ds-spacing-400) auto;
margin: 0 auto;
@include media-breakpoint-up(sm) {
padding: var(--ds-spacing-1300);
}
.notification {
@include ds-body-sm-regular;
color: var(--ds-color-text-primary);
padding: 0 var(--ds-spacing-400);
border-width: 0;
border-radius: var(--ds-border-radius-200);
.notification-icon {
font-size: math.div(20em, 14);
}
.notification-content {
padding: var(--ds-spacing-400) 0;
}
&.notification-type-error {
background-color: var(--ds-color-red-50);
}
}
}
.ciam-disclaimers p {
@@ -77,6 +124,7 @@
p {
@include ds-body-sm-regular;
color: var(--ds-color-text-secondary);
margin-bottom: 0;
}
}
@@ -86,25 +134,31 @@
padding: var(--ds-spacing-200) 0;
}
.ciam-stepper {
margin: 0;
height: 4px;
border-radius: var(--ds-border-radius-full);
.step {
background: var(--ds-color-neutral-200);
}
}
footer {
.footer-links {
display: flex;
gap: var(--ds-spacing-600);
justify-content: center;
padding: var(--ds-spacing-350) 0;
margin: 0 auto var(--spacing-15) auto;
display: flex;
gap: var(--ds-spacing-600);
justify-content: center;
padding: var(--ds-spacing-300) 0;
margin: 0 auto;
@include media-breakpoint-up(sm) {
margin-left: var(--ds-spacing-800);
margin-right: var(--ds-spacing-800);
justify-content: start;
}
@include media-breakpoint-up(sm) {
margin-left: var(--ds-spacing-800);
margin-right: var(--ds-spacing-800);
justify-content: start;
}
a {
text-decoration: none;
@include ds-body-sm-regular;
}
a {
@include ds-body-sm-regular;
}
}
}

View File

@@ -1,13 +1,5 @@
@import 'ds-design-system';
.ciam-register-container {
@include full-viewport-height;
display: flex;
min-height: 100%;
flex-direction: column;
}
.ciam-register-columns {
display: flex;
flex-grow: 1;
@@ -15,10 +7,51 @@
.ciam-register-login-link {
text-align: center;
margin: var(--ds-spacing-200) 0;
margin: 0;
padding: var(--ds-spacing-200) 0;
}
.ciam-work-uni-sso {
color: var(--ds-color-text-secondary);
padding-top: var(--ds-spacing-200);
margin-bottom: var(--ds-spacing-400);
font-weight: var(--ds-font-weight-semibold);
}
.ciam-register-container {
display: flex;
flex-direction: column;
.login-register-or-text-container {
@include ds-body-xs-semibold;
gap: var(--ds-spacing-250);
padding: var(--ds-spacing-200) 0 0 0;
margin-bottom: var(--ds-spacing-400);
&::before,
&::after {
background-color: var(--ds-color-neutral-200);
}
}
.login-register-error-container {
padding-bottom: 0;
.notification {
margin-bottom: var(--ds-spacing-400);
}
}
}
.ciam-password-group {
margin-bottom: var(--ds-spacing-400);
}
.ciam-password-requirements-message {
@include ds-body-sm-regular;
color: var(--ds-color-text-secondary);
padding-top: var(--ds-spacing-200);
margin: 0;
}

View File

@@ -1,16 +1,17 @@
// TODO: Replace `fuchsia` by the correct colors.
.ciam-enabled {
.ciam-enabled,
.website-redesign:not(.application-page) .ciam-enabled .notification {
// Links
// used in services/web/frontend/stylesheets/base/links.scss
--link-color: var(--ds-color-text-secondary);
--link-hover-color: fuchsia;
--link-color: var(--ds-color-text-primary);
--link-hover-color: var(--ds-color-text-secondary);
--link-visited-color: var(--ds-color-text-secondary);
--link-text-decoration: underline;
--link-hover-text-decoration: none;
// TODO: validate that this is correct
--link-visited-color: var(--ds-color-text-secondary);
--link-color-dark: fuchsia;
--link-hover-color-dark: fuchsia;
--link-visited-color-dark: fuchsia;
--link-text-decoration: underline;
--link-hover-text-decoration: none;
}

View File

@@ -4,14 +4,26 @@
.form-control-ds,
.form-group-ds,
.form-text-ds,
.form-label-ds {
.form-label-ds,
.form-group-ds label,
.website-redesign .form-group-ds label,
.website-redesign .form-label-ds {
@include ds-body-sm-semibold;
--bs-body-font-family: var(--ds-font-family-sans), sans-serif;
--bs-success-rgb: 25, 117, 76; // #19754c
--bs-danger-rgb: 195, 9, 43; // #c3092b
--content-placeholder: var(--ds-color-text-disabled);
// Without this, it inherits the body's --bs-body-font-family which isn't the DS font
font-family: var(--ds-font-family-sans), sans-serif;
font-family:
var(--ds-font-family-sans), sans-serif; // Without this, it inherits the body's --bs-body-font-family which isn't the DS font
color: var(--ds-color-text-primary);
margin-bottom: var(--ds-spacing-100);
}
.form-group-ds {
margin-bottom: var(--ds-spacing-400);
}
input.form-control.form-control-ds {
@@ -83,3 +95,11 @@ input.form-control.form-control-ds {
font-size: math.div(20em, 14);
}
}
.form-complex-input-container {
position: relative;
}
.ciam-form-input-icon {
font-size: var(--ds-font-size-600);
}

View File

@@ -100,3 +100,76 @@
.expiration-label {
vertical-align: super;
}
.privileges-matrix-container {
overflow-x: auto;
margin-top: var(--spacing-04);
}
.privileges-matrix-table {
width: 100%;
min-width: 800px;
thead {
background-color: var(--bg-accent-01);
position: sticky;
top: 0;
z-index: 10;
th {
text-align: center;
font-weight: 600;
padding: var(--spacing-03);
border: 1px solid var(--border-color);
}
th:first-child {
text-align: left;
}
}
tbody {
.section-header {
background-color: var(--bg-accent-02);
font-weight: 600;
td {
padding: var(--spacing-03);
border: 1px solid var(--border-color);
}
}
.privilege-label {
padding: var(--spacing-03);
border: 1px solid var(--border-color);
text-align: left;
}
.access-cell {
text-align: center;
padding: var(--spacing-03);
border: 1px solid var(--border-color);
vertical-align: middle;
}
.access-badge {
display: inline-block;
padding: var(--spacing-01) var(--spacing-02);
border-radius: var(--border-radius-large);
font-weight: 500;
font-size: 0.875rem;
min-width: 40px;
text-align: center;
}
.badge-yes {
background-color: #28a745;
color: white;
}
.badge-no {
background-color: #8b4513;
color: white;
}
}
}

View File

@@ -93,6 +93,12 @@ body {
font-size: 20px;
}
.ide-rail-tab-img-icon {
width: 20px;
max-height: 32px;
object-fit: contain;
}
&.open-rail {
color: var(--ide-rail-link-active-color);
background-color: var(--ide-rail-link-active-background);

View File

@@ -1,25 +1,23 @@
.login-register-or-text-container {
padding: var(--spacing-08) 0 var(--spacing-05) 0;
margin: 0;
line-height: 1;
position: relative;
font-size: var(--font-size-02);
text-align: center;
:root {
--password-visibility-toggle-width: 35px;
}
&::before {
.login-register-or-text-container {
display: flex;
gap: var(--spacing-05);
padding: var(--spacing-08) 0 var(--spacing-05) 0;
align-items: center;
font-size: var(--font-size-02);
line-height: 1;
margin: 0;
&::before,
&::after {
content: '';
position: absolute;
display: block;
flex-grow: 1;
height: 1px;
background-color: var(--neutral-20);
left: 0;
right: 0;
top: calc(var(--spacing-08) + var(--spacing-08) / 4);
}
.login-register-or-text {
position: relative;
background-color: #fff;
padding: 0 var(--spacing-05);
}
}
@@ -39,7 +37,7 @@
.form-group-password-input {
input.form-control {
padding-right: 35px;
padding-right: var(--password-visibility-toggle-width);
}
}
@@ -47,8 +45,8 @@
position: absolute;
right: 0;
top: 0;
width: 35px; // TODO: Should this be calculated ?
height: 35px; // TODO: Should this be calculated ?
width: var(--password-visibility-toggle-width);
height: 100%;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -861,6 +861,7 @@
"gallery_page_title": "Gallery - Templates, Examples and Articles written in LaTeX",
"gallery_show_more_tags": "Show more",
"general": "General",
"generate_from_text": "From text",
"generate_from_text_or_image": "From text or image",
"generate_tables_and_equations": "Generate tables and equations from text and images. Try it for free in the Overleaf toolbar!",
"generate_token": "Generate token",
@@ -1415,6 +1416,7 @@
"more_project_collaborators": "<0>More</0> project <0>collaborators</0>",
"more_than_one_kind_of_snippet_was_requested": "The link to open this content on Overleaf included some invalid parameters. If this keeps happening for links on a particular site, please report this to them.",
"most_popular_uppercase": "Most popular",
"must_be_at_least_n_characters": "Must be at least __n__ characters. Avoid common passwords.",
"must_be_email_address": "Must be an email address.",
"must_be_purchased_online": "Must be purchased online",
"my_library": "My Library",
@@ -2533,8 +2535,10 @@
"try_to_compile_despite_errors": "Try to compile despite errors",
"turn_off": "Turn off",
"turn_off_link_sharing": "Turn off link sharing",
"turn_off_password_visibility": "Turn off password visibility",
"turn_on": "Turn on",
"turn_on_link_sharing": "Turn on link sharing",
"turn_on_password_visibility": "Turn on password visibility",
"tutorials": "Tutorials",
"uk": "Ukrainian",
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",

View File

@@ -104,6 +104,7 @@
"@overleaf/stream-utils": "*",
"@overleaf/validation-tools": "*",
"@phosphor-icons/react": "^2.1.7",
"@phosphor-icons/webcomponents": "^2.1.5",
"@slack/webhook": "^7.0.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.7.0",

View File

@@ -1,5 +1,6 @@
import { expect } from 'chai'
import UserHelper from './helpers/User.mjs'
import Features from '../../../app/src/infrastructure/Features.mjs'
const User = UserHelper.promises
@@ -53,11 +54,38 @@ describe('Project ownership transfer', function () {
})
it('adds the previous owner as a read/write collaborator', async function () {
// Skip this test in SaaS environments as limited collaborators are enforced
if (Features.hasFeature('saas')) {
this.skip()
}
const project = await this.collaboratorSession.getProject(this.projectId)
expect(project.collaberator_refs.map(x => x.toString())).to.have.members([
this.owner._id.toString(),
this.invitedAdmin._id.toString(),
])
expect(project.owner_ref.toString()).to.equal(
this.collaborator._id.toString()
)
expect(project.readOnly_refs.map(x => x.toString())).to.be.empty
})
it('adds the previous owner as a read only', async function () {
// Skip this test in non-SaaS environments as unlimited collaborators are allowed
if (!Features.hasFeature('saas')) {
this.skip()
}
const project = await this.collaboratorSession.getProject(this.projectId)
expect(project.collaberator_refs.map(x => x.toString())).to.have.members([
this.invitedAdmin._id.toString(),
])
expect(project.owner_ref.toString()).to.equal(
this.collaborator._id.toString()
)
expect(project.readOnly_refs.map(x => x.toString())).to.have.members([
this.owner._id.toString(),
])
})
it('lets the new owner open the project', async function () {

View File

@@ -80,6 +80,11 @@ describe('OwnershipTransferHandler', function () {
addProjectsToTag: sinon.stub().resolves(),
},
}
ctx.LimitationsManager = {
promises: {
canAddXEditCollaborators: sinon.stub().resolves(true),
},
}
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
@@ -136,6 +141,13 @@ describe('OwnershipTransferHandler', function () {
})
)
vi.doMock(
'../../../../app/src/Features/Subscription/LimitationsManager.mjs',
() => ({
default: ctx.LimitationsManager,
})
)
ctx.handler = (await import(MODULE_PATH)).default
})
@@ -195,30 +207,197 @@ describe('OwnershipTransferHandler', function () {
)
})
it('should transfer ownership of the project to a read-only collaborator', async function (ctx) {
it('should check collaborator limits after ownership transfer', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.readOnlyCollaborator._id
ctx.collaborator._id
)
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: ctx.project._id },
sinon.match({ $set: { owner_ref: ctx.readOnlyCollaborator._id } })
expect(ctx.ProjectModel.updateOne).to.have.been.calledBefore(
ctx.LimitationsManager.promises.canAddXEditCollaborators
)
expect(
ctx.LimitationsManager.promises.canAddXEditCollaborators
).to.have.been.calledBefore(
ctx.CollaboratorsHandler.promises.addUserIdToProject
)
})
it('gives old owner read-only permissions if new owner was previously a viewer', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.readOnlyCollaborator._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.readOnlyCollaborator._id,
ctx.user._id,
PrivilegeLevels.READ_ONLY
)
describe('when there are edit collaborator slots available', function () {
beforeEach(function (ctx) {
ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(true)
})
it('should give old owner read/write permissions when transferring to a read-only collaborator', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.readOnlyCollaborator._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.readOnlyCollaborator._id,
ctx.user._id,
PrivilegeLevels.READ_AND_WRITE,
undefined
)
expect(
ctx.LimitationsManager.promises.canAddXEditCollaborators
).to.have.been.calledWith(ctx.project._id, 1)
})
it('should give old owner read/write permissions when transferring to an editor', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.collaborator._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.collaborator._id,
ctx.user._id,
PrivilegeLevels.READ_AND_WRITE,
undefined
)
expect(
ctx.LimitationsManager.promises.canAddXEditCollaborators
).to.have.been.calledWith(ctx.project._id, 1)
})
it('should give old owner read/write permissions when transferring to a reviewer', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.reviewer._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.reviewer._id,
ctx.user._id,
PrivilegeLevels.READ_AND_WRITE,
undefined
)
expect(
ctx.LimitationsManager.promises.canAddXEditCollaborators
).to.have.been.calledWith(ctx.project._id, 1)
})
it('should give old owner read/write permissions when transferring to a non-collaborator', async function (ctx) {
ctx.project.collaberator_refs = []
ctx.project.readOnly_refs = []
ctx.project.reviewer_refs = []
const newOwner = {
_id: new ObjectId(),
email: 'admin@example.com',
}
ctx.UserGetter.promises.getUser
.withArgs(newOwner._id)
.resolves(newOwner)
await ctx.handler.promises.transferOwnership(
ctx.project._id,
newOwner._id,
{ allowTransferToNonCollaborators: true }
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
newOwner._id,
ctx.user._id,
PrivilegeLevels.READ_AND_WRITE,
undefined
)
expect(
ctx.LimitationsManager.promises.canAddXEditCollaborators
).to.have.been.calledWith(ctx.project._id, 1)
})
})
describe('when there are no edit collaborator slots available', function () {
beforeEach(function (ctx) {
ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(false)
})
it('should give old owner read-only with pending editor flag when transferring to an editor', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.collaborator._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.collaborator._id,
ctx.user._id,
PrivilegeLevels.READ_ONLY,
{ pendingEditor: true }
)
})
it('should give old owner read-only with pending editor flag when transferring to a reviewer', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.reviewer._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.reviewer._id,
ctx.user._id,
PrivilegeLevels.READ_ONLY,
{ pendingEditor: true }
)
})
it('should give old owner read-only with pending editor flag when transferring to a read-only collaborator', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.readOnlyCollaborator._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.readOnlyCollaborator._id,
ctx.user._id,
PrivilegeLevels.READ_ONLY,
{ pendingEditor: true }
)
})
it('should give old owner read-only with pending editor flag when transferring to a non-collaborator', async function (ctx) {
ctx.project.collaberator_refs = []
ctx.project.readOnly_refs = []
ctx.project.reviewer_refs = []
const newOwner = {
_id: new ObjectId(),
email: 'newowner@example.com',
}
ctx.UserGetter.promises.getUser
.withArgs(newOwner._id)
.resolves(newOwner)
await ctx.handler.promises.transferOwnership(
ctx.project._id,
newOwner._id,
{ allowTransferToNonCollaborators: true }
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
newOwner._id,
ctx.user._id,
PrivilegeLevels.READ_ONLY,
{ pendingEditor: true }
)
})
})
it('should do nothing if transferring back to the owner', async function (ctx) {
@@ -239,21 +418,6 @@ describe('OwnershipTransferHandler', function () {
).to.have.been.calledWith(ctx.project._id, ctx.collaborator._id)
})
it('should add the former project owner as a read/write collaborator', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.collaborator._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.collaborator._id,
ctx.user._id,
PrivilegeLevels.READ_AND_WRITE
)
})
it('should transfer ownership of the project to a reviewer', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
@@ -265,21 +429,6 @@ describe('OwnershipTransferHandler', function () {
)
})
it('gives old owner reviewer permissions if new owner was previously a reviewer', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,
ctx.reviewer._id
)
expect(
ctx.CollaboratorsHandler.promises.addUserIdToProject
).to.have.been.calledWith(
ctx.project._id,
ctx.reviewer._id,
ctx.user._id,
PrivilegeLevels.REVIEW
)
})
it('should flush the project to tpds', async function (ctx) {
await ctx.handler.promises.transferOwnership(
ctx.project._id,