mirror of https://github.com/xwiki-labs/cryptpad
Merge branch 'staging' into documents-toolbar
This commit is contained in:
commit
a1e2a03d03
|
@ -1,16 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
# EditorConfig is awesome: https://EditorConfig.org
|
|
||||||
|
|
||||||
# top-most EditorConfig file
|
|
||||||
root = true
|
|
||||||
|
|
||||||
# Unix-style newlines with a newline ending every file
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
charset = utf-8
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
module.exports = {
|
||||||
|
'env': {
|
||||||
|
'browser': true,
|
||||||
|
'es2021': true,
|
||||||
|
'node': true
|
||||||
|
},
|
||||||
|
'plugins': ['compat'],
|
||||||
|
'extends': ['eslint:recommended', 'plugin:compat/recommended'],
|
||||||
|
"globals": {
|
||||||
|
"define": "readonly",
|
||||||
|
},
|
||||||
|
'overrides': [
|
||||||
|
{
|
||||||
|
'env': {
|
||||||
|
'node': true
|
||||||
|
},
|
||||||
|
'files': [
|
||||||
|
'.eslintrc.{js,cjs}'
|
||||||
|
],
|
||||||
|
'parserOptions': {
|
||||||
|
'sourceType': 'script'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'parserOptions': {
|
||||||
|
'ecmaVersion': 'latest'
|
||||||
|
},
|
||||||
|
'rules': {
|
||||||
|
'indent': [
|
||||||
|
'off', // TODO enable this check
|
||||||
|
4
|
||||||
|
],
|
||||||
|
'linebreak-style': [
|
||||||
|
'error',
|
||||||
|
'unix'
|
||||||
|
],
|
||||||
|
'quotes': [
|
||||||
|
'off', // TODO enable this check
|
||||||
|
'single'
|
||||||
|
],
|
||||||
|
'semi': [
|
||||||
|
'error',
|
||||||
|
'always'
|
||||||
|
],
|
||||||
|
|
||||||
|
// TODO remove these exceptions from the eslint defaults
|
||||||
|
'no-irregular-whitespace': ['off'],
|
||||||
|
'no-unused-vars': ['warn'],
|
||||||
|
'no-self-assign': ['off'],
|
||||||
|
'no-empty': ['off'],
|
||||||
|
'no-useless-escape': ['off'],
|
||||||
|
'no-redeclare': ['off'],
|
||||||
|
'no-extra-boolean-cast': ['off'],
|
||||||
|
'no-global-assign': ['off'],
|
||||||
|
'no-prototype-builtins': ['off'],
|
||||||
|
}
|
||||||
|
};
|
12
.flowconfig
12
.flowconfig
|
@ -1,12 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
[ignore]
|
|
||||||
.*/components/.*
|
|
||||||
.*/node_modules/lesshint/*
|
|
||||||
[include]
|
|
||||||
|
|
||||||
[libs]
|
|
||||||
|
|
||||||
[options]
|
|
|
@ -55,7 +55,7 @@ body:
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: operating-system
|
id: operating-system
|
||||||
attributes:
|
attributes:
|
||||||
label: What opreating system are you using?
|
label: Which operating system are you using?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- Linux/BSD/UNIX
|
- Linux/BSD/UNIX
|
||||||
|
@ -89,6 +89,8 @@ body:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version of CryptPad are you running?
|
description: What version of CryptPad are you running?
|
||||||
options:
|
options:
|
||||||
|
- 5.7.0
|
||||||
|
- 5.6.0
|
||||||
- 5.5.0
|
- 5.5.0
|
||||||
- 5.4.1
|
- 5.4.1
|
||||||
- 5.4.0
|
- 5.4.0
|
||||||
|
@ -99,11 +101,6 @@ body:
|
||||||
- 5.0.0
|
- 5.0.0
|
||||||
- 4.14.1
|
- 4.14.1
|
||||||
- 4.14.0
|
- 4.14.0
|
||||||
- 4.13.0
|
|
||||||
- 4.12.1
|
|
||||||
- 4.12.0
|
|
||||||
- 4.11.0
|
|
||||||
- 4.10.0
|
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
|
@ -27,3 +27,6 @@ block/
|
||||||
logs/
|
logs/
|
||||||
privileged.conf
|
privileged.conf
|
||||||
config/config.js
|
config/config.js
|
||||||
|
config/sso.js
|
||||||
|
lib/plugins/*
|
||||||
|
!lib/plugins/README.md
|
||||||
|
|
26
.jshintrc
26
.jshintrc
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"laxcomma": true,
|
|
||||||
"laxbreak": true,
|
|
||||||
"sub": true,
|
|
||||||
"curly": true,
|
|
||||||
"eqeqeq": true,
|
|
||||||
"iterator": true,
|
|
||||||
"latedef": true,
|
|
||||||
"nocomma": true,
|
|
||||||
"shadow": false,
|
|
||||||
"undef": true,
|
|
||||||
"unused": true,
|
|
||||||
"futurehostile":true,
|
|
||||||
"browser": true,
|
|
||||||
"esversion": 6,
|
|
||||||
"predef": [
|
|
||||||
"console",
|
|
||||||
"define",
|
|
||||||
"require",
|
|
||||||
"module",
|
|
||||||
"__dirname"
|
|
||||||
],
|
|
||||||
"globals": {
|
|
||||||
"self": true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,6 +39,10 @@ Files: www/lib/mermaid/*
|
||||||
Copyright: (c) 2014 - 2021 Knut Sveidqvist
|
Copyright: (c) 2014 - 2021 Knut Sveidqvist
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|
||||||
|
Files: www/lib/calendar/moment.min.js
|
||||||
|
Copyright: (c) JS Foundation and other contributors
|
||||||
|
License: MIT
|
||||||
|
|
||||||
Files: www/lib/pdfjs/*
|
Files: www/lib/pdfjs/*
|
||||||
Copyright: 2017 Mozilla Foundation
|
Copyright: 2017 Mozilla Foundation
|
||||||
License: Apache-2.0
|
License: Apache-2.0
|
||||||
|
|
229
CHANGELOG.md
229
CHANGELOG.md
|
@ -4,6 +4,235 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# 5.7.0
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
This release includes some features that could not be included into 5.6.0, namely instance invitations and support for images in diagrams. It also includes bug fixes in the drive, calendar and many other places.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Instance administrators can now issue invitation links that can be used to create one account each, even if registration is closed on the instance. An optional User Directory can help keep track of the known accounts on the instance. This feature is designed for the needs of enterprise customers who use their own instance, hence allowing administrators access to more information than on a public-facing service [#1395](https://github.com/cryptpad/cryptpad/pull/1395)
|
||||||
|
- Diagram documents now support images [#1295](https://github.com/cryptpad/cryptpad/pull/1295)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fix access modal issues after password change [#1394](https://github.com/cryptpad/cryptpad/pull/1394)
|
||||||
|
- Drive
|
||||||
|
- Shared folder access list [#1388](https://github.com/cryptpad/cryptpad/pull/1388)
|
||||||
|
- File icons in drive [#1386](https://github.com/cryptpad/cryptpad/pull/1386)
|
||||||
|
- Emptying trash with multiple folders and files fails [#1344](https://github.com/cryptpad/cryptpad/issues/1344)
|
||||||
|
- Shared folder and drive, read-only link issue [#1238](https://github.com/cryptpad/cryptpad/issues/1238)
|
||||||
|
- Loss of access to a shared folder after a double password change [#1365](https://github.com/cryptpad/cryptpad/issues/1365)
|
||||||
|
- Files
|
||||||
|
- PDFjs rendering issue with Firefox 121 [#1393](https://github.com/cryptpad/cryptpad/pull/1393)
|
||||||
|
- Rich Text
|
||||||
|
- Fix richtext issues [#1392](https://github.com/cryptpad/cryptpad/pull/1392)
|
||||||
|
- Duplicated element in table of content (TOC) [#1336](https://github.com/cryptpad/cryptpad/issues/1336)
|
||||||
|
- Anchors don't work anymore [#1226](https://github.com/cryptpad/cryptpad/issues/1226)
|
||||||
|
- Rows and columns numbers in tables can't be modified anymore [#1358](https://github.com/cryptpad/cryptpad/issues/1358)
|
||||||
|
- Forms
|
||||||
|
- Fix issue with duplicating choice/checkbox grid questions [#1359](https://github.com/cryptpad/cryptpad/pull/1359)
|
||||||
|
- Date question datepicker/input field now displays correctly [#1357](https://github.com/cryptpad/cryptpad/pull/1357)
|
||||||
|
- Duplicated “Enter” event sent when navigating with keyboard [#1396](https://github.com/cryptpad/cryptpad/issues/1396)
|
||||||
|
- Kanban
|
||||||
|
- Kanban item export [#1360](https://github.com/cryptpad/cryptpad/pull/1360)
|
||||||
|
- Calendar
|
||||||
|
- Calendar datepicker on mobile now easily toggled [#1368](https://github.com/cryptpad/cryptpad/pull/1368)
|
||||||
|
- Behaviour change: keep the offset between start and end date constant when updating the start date (otherwise it was possible to create events that end before even starting that thus don’t appear in the calendar)
|
||||||
|
- Calendar yearly recurring event - wrong month name [#1398](https://github.com/cryptpad/cryptpad/issues/1398)
|
||||||
|
- Admin
|
||||||
|
- Encoding issues in broadcast messages [#1379](https://github.com/cryptpad/cryptpad/issues/1379)
|
||||||
|
- Deployment
|
||||||
|
- Fix Cryptpad is unhealthy on Docker [#1350](https://github.com/cryptpad/cryptpad/pull/1350) thanks to @llaumgui
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Bump follow-redirects from 1.15.3 to 1.15.4 [#1378](https://github.com/cryptpad/cryptpad/pull/1378)
|
||||||
|
|
||||||
|
|
||||||
|
## Upgrade notes
|
||||||
|
|
||||||
|
If you are upgrading from a version older than `5.6.0` please read the upgrade notes of all versions between yours and `5.6.0` to avoid configuration issues.
|
||||||
|
|
||||||
|
⚠️ Before proceeding note that this upgrade requires changes to the Nginx configuration, please see full diff below.
|
||||||
|
|
||||||
|
To upgrade:
|
||||||
|
|
||||||
|
1. Stop your server
|
||||||
|
2. Get the latest code with git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin --tags
|
||||||
|
git checkout 5.7.0
|
||||||
|
```
|
||||||
|
3. Update dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run install:components
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart your server
|
||||||
|
5. Review your instance's checkup page to ensure that you are passing all tests
|
||||||
|
|
||||||
|
### Nginx config changes
|
||||||
|
|
||||||
|
```diff
|
||||||
|
diff --git a/docs/example-advanced.nginx.conf b/docs/example-advanced.nginx.conf
|
||||||
|
index cb827b4b0..f2b32e959 100644
|
||||||
|
--- a/docs/example-advanced.nginx.conf
|
||||||
|
+++ b/docs/example-advanced.nginx.conf
|
||||||
|
@@ -14,6 +14,8 @@ server {
|
||||||
|
|
||||||
|
# Let's Encrypt webroot
|
||||||
|
include letsencrypt-webroot;
|
||||||
|
+ # Include mime.types to be able to support .mjs files (see "types" below)
|
||||||
|
+ include mime.types;
|
||||||
|
|
||||||
|
# CryptPad serves static assets over these two domains.
|
||||||
|
# `main_domain` is what users will enter in their address bar.
|
||||||
|
@@ -166,11 +168,6 @@ server {
|
||||||
|
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
|
||||||
|
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
|
||||||
|
|
||||||
|
- # draw.io uses inline script tags in it's index.html. The hashes are added here.
|
||||||
|
- if ($uri ~ ^\/components\/drawio\/src\/main\/webapp\/index.html.*$) {
|
||||||
|
- set $scriptSrc "'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: https://${main_domain}";
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
|
||||||
|
if ($unsafe) {
|
||||||
|
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
|
||||||
|
@@ -179,6 +176,11 @@ server {
|
||||||
|
# Finally, set all the rules you composed above.
|
||||||
|
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
|
||||||
|
|
||||||
|
+ # Add support for .mjs files used by pdfjs
|
||||||
|
+ types {
|
||||||
|
+ application/javascript mjs;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
|
||||||
|
# We prefer to serve static content from nginx directly and to leave the API server to handle
|
||||||
|
# the dynamic content that only it can manage. This is primarily an optimization
|
||||||
|
```
|
||||||
|
|
||||||
|
# 5.6.0
|
||||||
|
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
This release introduces support for integrating CryptPad instances with Single-Sign On authentication. It brings a lot of improvements and fixes to Form, Calendar, and other parts of CryptPad. This release begins to improve the accessibility of the toolbar towards full WCAG compliance which we hope to achieve in the near future.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Authentication
|
||||||
|
- This version paves the way for SSO authentication for a CryptPad instance via a plugin (est. release Jan. 2024) [#1320](https://github.com/cryptpad/cryptpad/pull/1320)
|
||||||
|
- New setting to make Two-Factor Authentication mandatory for all user accounts on an instance [#1341](https://github.com/cryptpad/cryptpad/pull/1341)
|
||||||
|
- Form
|
||||||
|
- New button to duplicate a question [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Calendar
|
||||||
|
- New description field for calendar events [#1299](https://github.com/cryptpad/cryptpad/pull/1299)
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
- Accessibility of toolbars and some drop-down menus [#1290](https://github.com/cryptpad/cryptpad/pull/1290)
|
||||||
|
- "+ New" drop-down menu in Drive and Team Drive #1191
|
||||||
|
- New `Ctrl + e` modal #1192
|
||||||
|
- Code contact request notifications as headings #1197
|
||||||
|
- DOM order of toolbar #1198
|
||||||
|
- Notifications menu not accessible via Keyboard #1201
|
||||||
|
- Sidebar "tabs" not accessible via keyboard #1203
|
||||||
|
- Implement keyboard navigation of toolbar menus #1209
|
||||||
|
- CryptDrive page needs a logical tab order #1151
|
||||||
|
- Elements not accessible using the keyboard #1162
|
||||||
|
- Calendar event modal date-picker is cut-off at some screen resolutions #1280
|
||||||
|
- Visible focus #1206
|
||||||
|
- Rich Text
|
||||||
|
- Improvements to the Rich Text toolbar and layout for mobile usage [#1296](https://github.com/cryptpad/cryptpad/pull/1296)
|
||||||
|
- Calendar
|
||||||
|
- Handling the move of repeating events from a calendar to another [#1308](https://github.com/cryptpad/cryptpad/pull/1308)
|
||||||
|
- Kanban
|
||||||
|
- Changed positioning of kanban tag container on smaller screens [#1307](https://github.com/cryptpad/cryptpad/pull/1307)
|
||||||
|
- New option to increase the number of teams slots for premium users only [#1315](https://github.com/cryptpad/cryptpad/pull/1315)
|
||||||
|
- Improve licensing information, CryptPad code now complies with the [REUSE](https://reuse.software/) specifications [#1300](https://github.com/cryptpad/cryptpad/pull/1300)
|
||||||
|
- Deployment
|
||||||
|
- Basic configuration for Apache HTTPd [#1332](https://github.com/cryptpad/cryptpad/pull/1332)
|
||||||
|
- Add Docker health check [#1287](https://github.com/cryptpad/cryptpad/pull/1287)
|
||||||
|
- Cleanup
|
||||||
|
- Old // XXX comments [#1334](https://github.com/cryptpad/cryptpad/pull/1334)
|
||||||
|
- Outdated/misplaced files [#1327](https://github.com/cryptpad/cryptpad/pull/1327)
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Fix browser autocomplete issues (password, numbers, etc.) [#1342](https://github.com/cryptpad/cryptpad/pull/1342)
|
||||||
|
- Drive
|
||||||
|
- Container height fills screen [#1304](https://github.com/cryptpad/cryptpad/pull/1304)
|
||||||
|
- Context menu on mobile [#1301](https://github.com/cryptpad/cryptpad/pull/1301)
|
||||||
|
- OnlyOffice applications
|
||||||
|
- Use correct mime type for .wasm files (export functionality) [#1288](https://github.com/cryptpad/cryptpad/pull/1288)
|
||||||
|
- Fix filter functionality in Sheets [#1319](https://github.com/cryptpad/cryptpad/issues/1319)
|
||||||
|
- Form
|
||||||
|
- Fix an error upon importing a template in forms [#1316](https://github.com/cryptpad/cryptpad/pull/1316)
|
||||||
|
- Can now set form closing date/time on mobile [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Can now edit time options for poll questions on mobile [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Dates in CSV exports of forms are now in ISO (not timestamp) format [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Page breaks are no longer visible in conditional sections when condition is not met [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Final submission page now has margins [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Question blocks on mobile are now only draggable at the top of the block to make scrolling possible [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
|
||||||
|
- Whiteboard
|
||||||
|
- Fix a few export-related issues [#1328](https://github.com/cryptpad/cryptpad/pull/1328)
|
||||||
|
- Calendar
|
||||||
|
- Reformat `www/calendar/export.js` [#1314](https://github.com/cryptpad/cryptpad/pull/1314)
|
||||||
|
- Fix a bug with stopping the recurrence of a calendar event [#1312](https://github.com/cryptpad/cryptpad/pull/1312)
|
||||||
|
- Calendar creates itself twice when navigating with the keyboard [#1250](https://github.com/cryptpad/cryptpad/issues/1250)
|
||||||
|
- Fix timezone in Daylight Saving Time issues [#1317](https://github.com/cryptpad/cryptpad/pull/1317)
|
||||||
|
- Translations
|
||||||
|
- Revise the translation of `zh` [#1329](https://github.com/cryptpad/cryptpad/pull/1329)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Added [Moment.js](http://momentjs.com/) for improved handling of dates in Calendar (added as part of [#1317](https://github.com/cryptpad/cryptpad/pull/1317))
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
We [fixed an issue with the Systemd service file and logging](https://github.com/cryptpad/cryptpad/commit/078095c3e25d39707bdaab7ec066ceed6cb7158b), you'll need to add the following lines to your `cryptpad.service` before continuing by following the upgrade notes below.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
# Restart service after 10 seconds if node service crashes
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
+ # Proper logging to journald
|
||||||
|
+ StandardOutput=journal
|
||||||
|
+ StandardError=journal+console
|
||||||
|
|
||||||
|
User=cryptpad
|
||||||
|
Group=cryptpad
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upgrade notes
|
||||||
|
|
||||||
|
If you are upgrading from a version older than `5.5.0` please read the upgrade notes of all versions between yours and `5.5.0` to avoid configuration issues.
|
||||||
|
|
||||||
|
To upgrade:
|
||||||
|
|
||||||
|
1. Reload the Systemd daemon, required due to the changes in the **Deployment** section
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Stop your server
|
||||||
|
3. Get the latest code with git
|
||||||
|
```bash
|
||||||
|
git fetch origin --tags
|
||||||
|
git checkout 5.6.0
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart your server
|
||||||
|
5. Review your instance's checkup page to ensure that you are passing all tests
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 5.5.0
|
# 5.5.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
|
@ -61,7 +61,7 @@ further defined and clarified by project maintainers.
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at contact@cryptpad.fr. All
|
reported by contacting the project team at contact@cryptpad.org. All
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -12,7 +12,7 @@ WORKDIR /cryptpad
|
||||||
# Copy CryptPad source code to the container
|
# Copy CryptPad source code to the container
|
||||||
COPY . /cryptpad
|
COPY . /cryptpad
|
||||||
|
|
||||||
RUN sed -i "s@//httpAddress: '::'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
|
RUN sed -i "s@//httpAddress: 'localhost'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
|
||||||
RUN sed -i "s@installMethod: 'unspecified'@installMethod: 'docker'@" /cryptpad/config/config.example.js
|
RUN sed -i "s@installMethod: 'unspecified'@installMethod: 'docker'@" /cryptpad/config/config.example.js
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
@ -26,6 +26,11 @@ FROM node:lts-slim
|
||||||
RUN groupadd cryptpad -g 4001
|
RUN groupadd cryptpad -g 4001
|
||||||
RUN useradd cryptpad -u 4001 -g 4001 -d /cryptpad
|
RUN useradd cryptpad -u 4001 -g 4001 -d /cryptpad
|
||||||
|
|
||||||
|
# Install wget for healthcheck
|
||||||
|
RUN apt-get update && apt-get install --no-install-recommends -y wget && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy cryptpad with installed modules
|
# Copy cryptpad with installed modules
|
||||||
COPY --from=build --chown=cryptpad /cryptpad /cryptpad
|
COPY --from=build --chown=cryptpad /cryptpad /cryptpad
|
||||||
USER cryptpad
|
USER cryptpad
|
||||||
|
@ -48,6 +53,9 @@ VOLUME /cryptpad/datastore
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/bash", "/cryptpad/docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/bash", "/cryptpad/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
HEALTHCHECK --interval=1m CMD wget --no-verbose --tries=1 http://localhost:3000/ -q -O /dev/null || exit 1
|
||||||
|
|
||||||
# Ports
|
# Ports
|
||||||
EXPOSE 3000 3001 3003
|
EXPOSE 3000 3001 3003
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Considering the amount of resources necessary to backport security or bug fixes to previous, unsupported CryptPad versions, it's not something we do.
|
||||||
|
However, we quickly release new minor versions in case of need.
|
||||||
|
|
||||||
|
Please keep up with the latest release published here: https://github.com/cryptpad/cryptpad/releases
|
||||||
|
|
||||||
|
Note that every GitHub release page has an RSS compatible feed that you can subscribe on to be informed of every new release.
|
||||||
|
|
||||||
|
We do also communicate about this topic on:
|
||||||
|
- [Our blog](https://blog.cryptpad.org)
|
||||||
|
- [Our Matrix public space](https://matrix.to/#/#cryptpad:matrix.xwiki.com)
|
||||||
|
- [Our Mastodon account](https://fosstodon.org/@cryptpad)
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Vulnerabilities can be reported using the GitHub Security interface. You can also send us an email at security@cryptpad.org
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB |
|
@ -74,12 +74,12 @@ module.exports = {
|
||||||
// httpSafeOrigin: "https://some-other-domain.xyz",
|
// httpSafeOrigin: "https://some-other-domain.xyz",
|
||||||
|
|
||||||
/* httpAddress specifies the address on which the nodejs server
|
/* httpAddress specifies the address on which the nodejs server
|
||||||
* should be accessible. By default it will listen on 127.0.0.1
|
* should be accessible. By default it will listen on localhost
|
||||||
* (IPv4 localhost on most systems). If you want it to listen on
|
* (IPv4 & IPv6 if enabled). If you want it to listen on
|
||||||
* all addresses, including IPv6, set this to '::'.
|
* a specific address, specify it here. e.g '192.168.0.1'
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
//httpAddress: '::',
|
//httpAddress: 'localhost',
|
||||||
|
|
||||||
/* httpPort specifies on which port the nodejs server should listen.
|
/* httpPort specifies on which port the nodejs server should listen.
|
||||||
* By default it will serve content over port 3000, which is suitable
|
* By default it will serve content over port 3000, which is suitable
|
||||||
|
@ -117,6 +117,28 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
// maxWorkers: 4,
|
// maxWorkers: 4,
|
||||||
|
|
||||||
|
/* =====================
|
||||||
|
* Sessions
|
||||||
|
* ===================== */
|
||||||
|
|
||||||
|
/* Accounts can be protected with an OTP (One Time Password) system
|
||||||
|
* to add a second authentication layer. Such accounts use a session
|
||||||
|
* with a given lifetime after which they are logged out and need
|
||||||
|
* to be re-authenticated. You can configure the lifetime of these
|
||||||
|
* sessions here.
|
||||||
|
*
|
||||||
|
* defaults to 7 days
|
||||||
|
*/
|
||||||
|
//otpSessionExpiration: 7*24, // hours
|
||||||
|
|
||||||
|
/* Registered users can be forced to protect their account
|
||||||
|
* with a Multi-factor Authentication (MFA) tool like a TOTP
|
||||||
|
* authenticator application.
|
||||||
|
*
|
||||||
|
* defaults to false
|
||||||
|
*/
|
||||||
|
//enforceMFA: false,
|
||||||
|
|
||||||
/* =====================
|
/* =====================
|
||||||
* Admin
|
* Admin
|
||||||
* ===================== */
|
* ===================== */
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
//const fs = require('node:fs');
|
||||||
|
module.exports = {
|
||||||
|
// Enable SSO login on this instance
|
||||||
|
enabled: false,
|
||||||
|
// Block registration for non-SSO users on this instance
|
||||||
|
enforced: false,
|
||||||
|
// Allow users to add an additional CryptPad password to their SSO account
|
||||||
|
cpPassword: false,
|
||||||
|
// You can also force your SSO users to add a CryptPad password
|
||||||
|
forceCpPassword: false,
|
||||||
|
// List of SSO providers
|
||||||
|
list: [
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
name: 'google',
|
||||||
|
type: 'oidc',
|
||||||
|
url: 'https://accounts.google.com',
|
||||||
|
client_id: "{your_client_id}",
|
||||||
|
client_secret: "{your_client_secret}",
|
||||||
|
jwt_alg: 'RS256' (optional)
|
||||||
|
}, {
|
||||||
|
name: 'samltest',
|
||||||
|
type: 'saml',
|
||||||
|
url: 'https://samltest.id/idp/profile/SAML2/Redirect/SSO',
|
||||||
|
issuer: 'your-cryptpad-issuer-id',
|
||||||
|
cert: String or fs.readFileSync("./your/cert/location", "utf-8"),
|
||||||
|
privateKey: fs.readFileSync("./your/private/key/location", "utf-8"),
|
||||||
|
signingCert: fs.readFileSync("./your/signing/cert/location", "utf-8"),
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
BIN
cryptofist.png
BIN
cryptofist.png
Binary file not shown.
Before Width: | Height: | Size: 197 KiB |
|
@ -8,13 +8,12 @@ CKEDITOR.editorConfig = function( config ) {
|
||||||
// https://dev.ckeditor.com/ticket/10907
|
// https://dev.ckeditor.com/ticket/10907
|
||||||
config.needsBrFiller= fixThings;
|
config.needsBrFiller= fixThings;
|
||||||
config.needsNbspFiller= fixThings;
|
config.needsNbspFiller= fixThings;
|
||||||
|
|
||||||
config.disableObjectResizing = true;
|
config.disableObjectResizing = true;
|
||||||
|
|
||||||
config.removeButtons= 'Source,Maximize';
|
config.removeButtons= 'Source,Maximize';
|
||||||
// magicline plugin inserts html crap into the document which is not part of the
|
// magicline plugin inserts html crap into the document which is not part of the
|
||||||
// document itself and causes problems when it's sent across the wire and reflected back
|
// document itself and causes problems when it's sent across the wire and reflected back
|
||||||
config.removePlugins= 'resize,elementspath';
|
config.removePlugins= 'resize,elementspath,liststyle';
|
||||||
config.resize_enabled= false; //bottom-bar
|
config.resize_enabled= false; //bottom-bar
|
||||||
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount,comments';
|
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount,comments';
|
||||||
config.toolbarGroups= [
|
config.toolbarGroups= [
|
||||||
|
@ -35,7 +34,7 @@ CKEDITOR.editorConfig = function( config ) {
|
||||||
config.mathJaxLib = '/pad/mathjax/MathJax.js?config=TeX-AMS_HTML';
|
config.mathJaxLib = '/pad/mathjax/MathJax.js?config=TeX-AMS_HTML';
|
||||||
config.font_defaultLabel = 'Arial';
|
config.font_defaultLabel = 'Arial';
|
||||||
config.fontSize_defaultLabel = '16';
|
config.fontSize_defaultLabel = '16';
|
||||||
|
config.accessibility = 'true';
|
||||||
config.keystrokes = [
|
config.keystrokes = [
|
||||||
[ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ],
|
[ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ],
|
||||||
[ CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ],
|
[ CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ],
|
||||||
|
@ -53,7 +52,13 @@ CKEDITOR.editorConfig = function( config ) {
|
||||||
[ CKEDITOR.CTRL + 73 /*I*/, 'italic' ],
|
[ CKEDITOR.CTRL + 73 /*I*/, 'italic' ],
|
||||||
[ CKEDITOR.CTRL + 85 /*U*/, 'underline' ],
|
[ CKEDITOR.CTRL + 85 /*U*/, 'underline' ],
|
||||||
|
|
||||||
[ CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ]
|
[CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ],
|
||||||
|
[37 /* Left Arrow */, 'focusPreviousButton'],
|
||||||
|
[39 /* Right Arrow */, 'focusNextButton'],
|
||||||
|
//enter
|
||||||
|
[13, 'clickFocusedButton'],
|
||||||
|
//space bar
|
||||||
|
[32, 'clickFocusedButton']
|
||||||
];
|
];
|
||||||
|
|
||||||
//skin: 'moono-cryptpad,/pad/themes/moono-cryptpad/'
|
//skin: 'moono-cryptpad,/pad/themes/moono-cryptpad/'
|
||||||
|
|
|
@ -8,6 +8,7 @@ define([
|
||||||
'/components/chainpad-crypto/crypto.js',
|
'/components/chainpad-crypto/crypto.js',
|
||||||
'/common/common-util.js',
|
'/common/common-util.js',
|
||||||
'/common/outer/network-config.js',
|
'/common/outer/network-config.js',
|
||||||
|
'/common/common-login.js',
|
||||||
'/common/common-credential.js',
|
'/common/common-credential.js',
|
||||||
'/components/chainpad/chainpad.dist.js',
|
'/components/chainpad/chainpad.dist.js',
|
||||||
'/common/common-realtime.js',
|
'/common/common-realtime.js',
|
||||||
|
@ -24,14 +25,14 @@ define([
|
||||||
|
|
||||||
'/components/tweetnacl/nacl-fast.min.js',
|
'/components/tweetnacl/nacl-fast.min.js',
|
||||||
'/components/scrypt-async/scrypt-async.min.js', // better load speed
|
'/components/scrypt-async/scrypt-async.min.js', // better load speed
|
||||||
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
|
], function ($, Listmap, Crypto, Util, NetConfig, Login, Cred, ChainPad, Realtime, Constants, UI,
|
||||||
Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
|
Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
|
||||||
var Exports = {
|
var Exports = {
|
||||||
Cred: Cred,
|
Cred: Cred,
|
||||||
Block: Block,
|
Block: Block,
|
||||||
// this is depended on by non-customizable files
|
// this is depended on by non-customizable files
|
||||||
// be careful when modifying login.js
|
// be careful when modifying login.js
|
||||||
requiredBytes: 192,
|
requiredBytes: Login.requiredBytes,
|
||||||
};
|
};
|
||||||
|
|
||||||
var Nacl = window.nacl;
|
var Nacl = window.nacl;
|
||||||
|
@ -44,511 +45,33 @@ define([
|
||||||
redirectTo = newPad.href;
|
redirectTo = newPad.href;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (window.location.hash) {
|
if (window.location.hash) { setRedirectTo(); }
|
||||||
setRedirectTo();
|
|
||||||
}
|
|
||||||
|
|
||||||
var allocateBytes = Exports.allocateBytes = function (bytes) {
|
Exports.ssoAuth = function (provider, cb) {
|
||||||
var dispense = Cred.dispenser(bytes);
|
var keys = Nacl.sign.keyPair();
|
||||||
|
var inviteToken = window.location.hash.slice(1);
|
||||||
var opt = {};
|
localStorage.CP_sso_auth = JSON.stringify({
|
||||||
|
s: Nacl.util.encodeBase64(keys.secretKey),
|
||||||
// dispense 18 bytes of entropy for your encryption key
|
p: Nacl.util.encodeBase64(keys.publicKey),
|
||||||
var encryptionSeed = dispense(18);
|
token: inviteToken
|
||||||
// 16 bytes for a deterministic channel key
|
|
||||||
var channelSeed = dispense(16);
|
|
||||||
// 32 bytes for a curve key
|
|
||||||
var curveSeed = dispense(32);
|
|
||||||
|
|
||||||
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
|
|
||||||
opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
|
|
||||||
opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
|
|
||||||
|
|
||||||
// 32 more for a signing key
|
|
||||||
var edSeed = opt.edSeed = dispense(32);
|
|
||||||
|
|
||||||
// 64 more bytes to seed an additional signing key
|
|
||||||
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
|
|
||||||
opt.blockHash = Block.getBlockHash(blockKeys);
|
|
||||||
|
|
||||||
// derive a private key from the ed seed
|
|
||||||
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
|
|
||||||
|
|
||||||
opt.edPrivate = Nacl.util.encodeBase64(signingKeypair.secretKey);
|
|
||||||
opt.edPublic = Nacl.util.encodeBase64(signingKeypair.publicKey);
|
|
||||||
|
|
||||||
var keys = opt.keys = Crypto.createEditCryptor(null, encryptionSeed);
|
|
||||||
|
|
||||||
// 24 bytes of base64
|
|
||||||
keys.editKeyStr = keys.editKeyStr.replace(/\//g, '-');
|
|
||||||
|
|
||||||
// 32 bytes of hex
|
|
||||||
var channelHex = opt.channelHex = Util.uint8ArrayToHex(channelSeed);
|
|
||||||
|
|
||||||
// should never happen
|
|
||||||
if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
|
|
||||||
|
|
||||||
var channel64 = Util.hexToBase64(channelHex);
|
|
||||||
|
|
||||||
// we still generate a v1 hash because this function needs to deterministically
|
|
||||||
// derive the same values as it always has. New accounts will generate their own
|
|
||||||
// userHash values
|
|
||||||
opt.userHash = '/1/edit/' + [channel64, opt.keys.editKeyStr].join('/') + '/';
|
|
||||||
|
|
||||||
return opt;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var loginOptionsFromBlock = Exports.loginOptionsFromBlock = function (blockInfo) {
|
|
||||||
var opt = {};
|
|
||||||
var parsed = Hash.getSecrets('pad', blockInfo.User_hash);
|
|
||||||
opt.channelHex = parsed.channel;
|
|
||||||
opt.keys = parsed.keys;
|
|
||||||
opt.edPublic = blockInfo.edPublic;
|
|
||||||
return opt;
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadUserObject = Exports.loadUserObject = function (opt, _cb) {
|
|
||||||
var cb = Util.once(Util.mkAsync(_cb));
|
|
||||||
var config = {
|
|
||||||
websocketURL: NetConfig.getWebsocketURL(),
|
|
||||||
channel: opt.channelHex,
|
|
||||||
data: {},
|
|
||||||
validateKey: opt.keys.validateKey, // derived validation key
|
|
||||||
crypto: Crypto.createEncryptor(opt.keys),
|
|
||||||
logLevel: 1,
|
|
||||||
classic: true,
|
|
||||||
ChainPad: ChainPad,
|
|
||||||
owners: [opt.edPublic]
|
|
||||||
};
|
|
||||||
|
|
||||||
var rt = opt.rt = Listmap.create(config);
|
|
||||||
rt.proxy
|
|
||||||
.on('ready', function () {
|
|
||||||
setTimeout(function () { cb(void 0, rt); });
|
|
||||||
})
|
|
||||||
.on('error', function (info) {
|
|
||||||
cb(info.type, {reason: info.message});
|
|
||||||
})
|
|
||||||
.on('disconnect', function (info) {
|
|
||||||
cb('E_DISCONNECT', info);
|
|
||||||
});
|
});
|
||||||
};
|
ServerCommand(keys, {
|
||||||
|
command: 'SSO_AUTH',
|
||||||
var isProxyEmpty = function (proxy) {
|
provider: provider,
|
||||||
var l = Object.keys(proxy).length;
|
register: true
|
||||||
return l === 0 || (l === 2 && proxy._events && proxy.on);
|
|
||||||
};
|
|
||||||
|
|
||||||
var setMergeAnonDrive = function () {
|
|
||||||
Exports.mergeAnonDrive = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, onOTP, cb) {
|
|
||||||
if (typeof(cb) !== 'function') { return; }
|
|
||||||
|
|
||||||
// Usernames are all lowercase. No going back on this one
|
|
||||||
uname = uname.toLowerCase();
|
|
||||||
|
|
||||||
// validate inputs
|
|
||||||
if (!Cred.isValidUsername(uname)) { return void cb('INVAL_USER'); }
|
|
||||||
if (!Cred.isValidPassword(passwd)) { return void cb('INVAL_PASS'); }
|
|
||||||
if (isRegister && !Cred.isLongEnoughPassword(passwd)) {
|
|
||||||
return void cb('PASS_TOO_SHORT');
|
|
||||||
}
|
|
||||||
|
|
||||||
// results...
|
|
||||||
var res = {
|
|
||||||
register: isRegister,
|
|
||||||
};
|
|
||||||
|
|
||||||
var RT, blockKeys, blockHash, blockUrl, Pinpad, rpc, userHash;
|
|
||||||
|
|
||||||
nThen(function (waitFor) {
|
|
||||||
// derive a predefined number of bytes from the user's inputs,
|
|
||||||
// and allocate them in a deterministic fashion
|
|
||||||
Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
|
|
||||||
res.opt = allocateBytes(bytes);
|
|
||||||
blockHash = res.opt.blockHash;
|
|
||||||
blockKeys = res.opt.blockKeys;
|
|
||||||
}));
|
|
||||||
}).nThen(function (waitFor) {
|
|
||||||
// the allocated bytes can be used either in a legacy fashion,
|
|
||||||
// or in such a way that a previously unused byte range determines
|
|
||||||
// the location of a layer of indirection which points users to
|
|
||||||
// an encrypted block, from which they can recover the location of
|
|
||||||
// the rest of their data
|
|
||||||
|
|
||||||
// determine where a block for your set of keys would be stored
|
|
||||||
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
|
|
||||||
|
|
||||||
var TOTP_prompt = function (err, cb) {
|
|
||||||
onOTP(function (code) {
|
|
||||||
ServerCommand(res.opt.blockKeys.sign, {
|
|
||||||
command: 'TOTP_VALIDATE',
|
|
||||||
code: code,
|
|
||||||
// TODO optionally allow the user to specify a lifetime for this session?
|
|
||||||
// this will require a little bit of server work
|
|
||||||
// and more UI/UX:
|
|
||||||
// ie. just a simple "remember me" checkbox?
|
|
||||||
// allow them to specify a lifetime for the session?
|
|
||||||
// "log me out after one day"?
|
|
||||||
}, cb);
|
}, cb);
|
||||||
}, false, err);
|
};
|
||||||
|
Exports.ssoLogin = function () {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var done = waitFor();
|
Exports.allocateBytes = Login.allocateBytes;
|
||||||
var responseToDecryptedBlock = function (response, cb) {
|
Exports.loadUserObject = Login.loadUserObject;
|
||||||
response.arrayBuffer().then(arraybuffer => {
|
|
||||||
arraybuffer = new Uint8Array(arraybuffer);
|
var setMergeAnonDrive = function (value) {
|
||||||
var decryptedBlock = Block.decrypt(arraybuffer, blockKeys);
|
Exports.mergeAnonDrive = Boolean(value);
|
||||||
if (!decryptedBlock) {
|
|
||||||
console.error("BLOCK DECRYPTION ERROR");
|
|
||||||
return void cb("BLOCK_DECRYPTION_ERROR");
|
|
||||||
}
|
|
||||||
cb(void 0, decryptedBlock);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var TOTP_response;
|
|
||||||
nThen(function (w) {
|
|
||||||
Util.getBlock(blockUrl, {
|
|
||||||
// request the block without credentials
|
|
||||||
}, w(function (err, response) {
|
|
||||||
if (err === 401) {
|
|
||||||
return void console.log("Block requires 2FA");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err === 404 && response && response.reason) {
|
|
||||||
waitFor.abort();
|
|
||||||
w.abort();
|
|
||||||
/*
|
|
||||||
// the following block prevent users from re-using an old password
|
|
||||||
if (isRegister) { return void cb('HAS_PLACEHOLDER'); }
|
|
||||||
*/
|
|
||||||
return void cb('DELETED_USER', response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some other error?
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
w.abort();
|
|
||||||
return void done();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the block was returned without requiring authentication
|
|
||||||
// then we can abort the subsequent steps of this nested nThen
|
|
||||||
w.abort();
|
|
||||||
|
|
||||||
// decrypt the response and continue the normal procedure with its payload
|
|
||||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
|
||||||
if (err) {
|
|
||||||
// if a block was present but you were not able to decrypt it...
|
|
||||||
console.error(err);
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb(err);
|
|
||||||
}
|
|
||||||
res.blockInfo = decryptedBlock;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}).nThen(function (w) {
|
|
||||||
// if you're here then you need to request a JWT
|
|
||||||
var done = w();
|
|
||||||
var tries = 3;
|
|
||||||
var ask = function () {
|
|
||||||
if (!tries) {
|
|
||||||
w.abort();
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb('TOTP_ATTEMPTS_EXHAUSTED');
|
|
||||||
}
|
|
||||||
tries--;
|
|
||||||
TOTP_prompt(tries !== 2, function (err, response) {
|
|
||||||
// ask again until your number of tries are exhausted
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
console.log("Normal failure. Asking again...");
|
|
||||||
return void ask();
|
|
||||||
}
|
|
||||||
if (!response || !response.bearer) {
|
|
||||||
console.log(response);
|
|
||||||
console.log("Unexpected failure. No bearer token. Asking again");
|
|
||||||
return void ask();
|
|
||||||
}
|
|
||||||
console.log("Successfully retrieved a bearer token");
|
|
||||||
res.TOTP_token = TOTP_response = response;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
ask();
|
|
||||||
}).nThen(function (w) {
|
|
||||||
Util.getBlock(blockUrl, TOTP_response, function (err, response) {
|
|
||||||
if (err) {
|
|
||||||
w.abort();
|
|
||||||
console.error(err);
|
|
||||||
return void cb('BLOCK_ERROR_3');
|
|
||||||
}
|
|
||||||
|
|
||||||
responseToDecryptedBlock(response, function (err, decryptedBlock) {
|
|
||||||
if (err) {
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb(err);
|
|
||||||
}
|
|
||||||
res.blockInfo = decryptedBlock;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).nThen(function (waitFor) {
|
|
||||||
// we assume that if there is a block, it was created in a valid manner
|
|
||||||
// so, just proceed to the next block which handles that stuff
|
|
||||||
if (res.blockInfo) { return; }
|
|
||||||
|
|
||||||
var opt = res.opt;
|
|
||||||
|
|
||||||
// load the user's object using the legacy credentials
|
|
||||||
loadUserObject(opt, waitFor(function (err, rt) {
|
|
||||||
if (err) {
|
|
||||||
waitFor.abort();
|
|
||||||
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
|
|
||||||
return void cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a proxy is marked as deprecated, it is because someone had a non-owned drive
|
|
||||||
// but changed their password, and couldn't delete their old data.
|
|
||||||
// if they are here, they have entered their old credentials, so we should not
|
|
||||||
// allow them to proceed. In time, their old drive should get deleted, since
|
|
||||||
// it will should be pinned by anyone's drive.
|
|
||||||
if (rt.proxy[Constants.deprecatedKey]) {
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb('NO_SUCH_USER', res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRegister && isProxyEmpty(rt.proxy)) {
|
|
||||||
// If they are trying to register,
|
|
||||||
// and the proxy is empty, then there is no 'legacy user' either
|
|
||||||
// so we should just shut down this session and disconnect.
|
|
||||||
//rt.network.disconnect();
|
|
||||||
return; // proceed to the next async block
|
|
||||||
}
|
|
||||||
|
|
||||||
// they tried to just log in but there's no such user
|
|
||||||
// and since we're here at all there is no modern-block
|
|
||||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
|
||||||
//rt.network.disconnect(); // clean up after yourself
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb('NO_SUCH_USER', res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// they tried to register, but those exact credentials exist
|
|
||||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
|
||||||
//rt.network.disconnect();
|
|
||||||
waitFor.abort();
|
|
||||||
Feedback.send('LOGIN', true);
|
|
||||||
return void cb('ALREADY_REGISTERED', res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if you are here, then there is no block, the user is trying
|
|
||||||
// to log in. The proxy is **not** empty. All values assigned here
|
|
||||||
// should have been deterministically created using their credentials
|
|
||||||
// so setting them is just a precaution to keep things in good shape
|
|
||||||
res.proxy = rt.proxy;
|
|
||||||
res.realtime = rt.realtime;
|
|
||||||
res.network = rt.network;
|
|
||||||
|
|
||||||
// they're registering...
|
|
||||||
res.userHash = opt.userHash;
|
|
||||||
res.userName = uname;
|
|
||||||
|
|
||||||
// export their signing key
|
|
||||||
res.edPrivate = opt.edPrivate;
|
|
||||||
res.edPublic = opt.edPublic;
|
|
||||||
|
|
||||||
// export their encryption key
|
|
||||||
res.curvePrivate = opt.curvePrivate;
|
|
||||||
res.curvePublic = opt.curvePublic;
|
|
||||||
|
|
||||||
if (shouldImport) { setMergeAnonDrive(); }
|
|
||||||
|
|
||||||
// don't proceed past this async block.
|
|
||||||
waitFor.abort();
|
|
||||||
|
|
||||||
// We have to call whenRealtimeSyncs asynchronously here because in the current
|
|
||||||
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
|
|
||||||
// asynchronously.
|
|
||||||
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
|
|
||||||
// `contentUpdate` so that we have an update userDoc in chainpad.
|
|
||||||
setTimeout(function () {
|
|
||||||
Realtime.whenRealtimeSyncs(rt.realtime, function () {
|
|
||||||
// the following stages are there to initialize a new drive
|
|
||||||
// if you are registering
|
|
||||||
LocalStore.login(res.userHash, undefined, res.userName, function () {
|
|
||||||
setTimeout(function () { cb(void 0, res); });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}).nThen(function (waitFor) { // MODERN REGISTRATION / LOGIN
|
|
||||||
var opt;
|
|
||||||
if (res.blockInfo) {
|
|
||||||
opt = loginOptionsFromBlock(res.blockInfo);
|
|
||||||
userHash = res.blockInfo.User_hash;
|
|
||||||
//console.error(opt, userHash);
|
|
||||||
} else {
|
|
||||||
console.log("allocating random bytes for a new user object");
|
|
||||||
opt = allocateBytes(Nacl.randomBytes(Exports.requiredBytes));
|
|
||||||
// create a random v2 hash, since we don't need backwards compatibility
|
|
||||||
userHash = opt.userHash = Hash.createRandomHash('drive');
|
|
||||||
var secret = Hash.getSecrets('drive', userHash);
|
|
||||||
opt.keys = secret.keys;
|
|
||||||
opt.channelHex = secret.channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// according to the location derived from the credentials which you entered
|
|
||||||
loadUserObject(opt, waitFor(function (err, rt) {
|
|
||||||
if (err) {
|
|
||||||
waitFor.abort();
|
|
||||||
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
|
|
||||||
return void cb('MODERN_REGISTRATION_INIT');
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.error(JSON.stringify(rt.proxy));
|
|
||||||
|
|
||||||
// export the realtime object you checked
|
|
||||||
RT = rt;
|
|
||||||
|
|
||||||
var proxy = rt.proxy;
|
|
||||||
if (isRegister && !isProxyEmpty(proxy) && (!proxy.edPublic || !proxy.edPrivate)) {
|
|
||||||
console.error("INVALID KEYS");
|
|
||||||
console.log(JSON.stringify(proxy));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.proxy = rt.proxy;
|
|
||||||
res.realtime = rt.realtime;
|
|
||||||
res.network = rt.network;
|
|
||||||
|
|
||||||
// they're registering...
|
|
||||||
res.userHash = userHash;
|
|
||||||
res.userName = uname;
|
|
||||||
|
|
||||||
// somehow they have a block present, but nothing in the user object it specifies
|
|
||||||
// this shouldn't happen, but let's send feedback if it does
|
|
||||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
|
||||||
// this really shouldn't happen, but let's handle it anyway
|
|
||||||
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
|
|
||||||
|
|
||||||
//rt.network.disconnect(); // clean up after yourself
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb('NO_SUCH_USER', res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// they tried to register, but those exact credentials exist
|
|
||||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
|
||||||
//rt.network.disconnect();
|
|
||||||
waitFor.abort();
|
|
||||||
res.blockHash = blockHash;
|
|
||||||
if (shouldImport) {
|
|
||||||
setMergeAnonDrive();
|
|
||||||
}
|
|
||||||
|
|
||||||
return void cb('ALREADY_REGISTERED', res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRegister && !isProxyEmpty(rt.proxy)) {
|
|
||||||
waitFor.abort();
|
|
||||||
if (shouldImport) {
|
|
||||||
setMergeAnonDrive();
|
|
||||||
}
|
|
||||||
var l = Util.find(rt.proxy, ['settings', 'general', 'language']);
|
|
||||||
var LS_LANG = "CRYPTPAD_LANG";
|
|
||||||
if (l) {
|
|
||||||
localStorage.setItem(LS_LANG, l);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.TOTP_token && res.TOTP_token.bearer) {
|
|
||||||
LocalStore.setSessionToken(res.TOTP_token.bearer);
|
|
||||||
}
|
|
||||||
return void LocalStore.login(undefined, blockHash, uname, function () {
|
|
||||||
cb(void 0, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRegister && isProxyEmpty(rt.proxy)) {
|
|
||||||
proxy.edPublic = opt.edPublic;
|
|
||||||
proxy.edPrivate = opt.edPrivate;
|
|
||||||
proxy.curvePublic = opt.curvePublic;
|
|
||||||
proxy.curvePrivate = opt.curvePrivate;
|
|
||||||
proxy.login_name = uname;
|
|
||||||
proxy[Constants.displayNameKey] = uname;
|
|
||||||
if (shouldImport) {
|
|
||||||
setMergeAnonDrive();
|
|
||||||
} else {
|
|
||||||
proxy.version = 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
Feedback.send('REGISTRATION', true);
|
|
||||||
} else {
|
|
||||||
Feedback.send('LOGIN', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(waitFor(function () {
|
|
||||||
Realtime.whenRealtimeSyncs(rt.realtime, waitFor());
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}).nThen(function (waitFor) {
|
|
||||||
require(['/common/pinpad.js'], waitFor(function (_Pinpad) {
|
|
||||||
console.log("loaded rpc module");
|
|
||||||
Pinpad = _Pinpad;
|
|
||||||
}));
|
|
||||||
}).nThen(function (waitFor) {
|
|
||||||
// send an RPC to store the block which you created.
|
|
||||||
console.log("initializing rpc interface");
|
|
||||||
|
|
||||||
Pinpad.create(RT.network, Block.keysToRPCFormat(res.opt.blockKeys), waitFor(function (e, _rpc) {
|
|
||||||
if (e) {
|
|
||||||
waitFor.abort();
|
|
||||||
console.error(e); // INVALID_KEYS
|
|
||||||
return void cb('RPC_CREATION_ERROR');
|
|
||||||
}
|
|
||||||
rpc = _rpc;
|
|
||||||
console.log("rpc initialized");
|
|
||||||
}));
|
|
||||||
}).nThen(function (waitFor) {
|
|
||||||
console.log("creating request to publish a login block");
|
|
||||||
|
|
||||||
// Finally, create the login block for the object you just created.
|
|
||||||
var toPublish = {};
|
|
||||||
|
|
||||||
toPublish[Constants.userHashKey] = userHash;
|
|
||||||
toPublish.edPublic = RT.proxy.edPublic;
|
|
||||||
|
|
||||||
Block.writeLoginBlock({
|
|
||||||
blockKeys: blockKeys,
|
|
||||||
content: toPublish
|
|
||||||
}, waitFor(function (e) {
|
|
||||||
if (e) {
|
|
||||||
console.error(e);
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb(e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}).nThen(function (waitFor) {
|
|
||||||
// confirm that the block was actually written before considering registration successful
|
|
||||||
Util.fetch(blockUrl, waitFor(function (err /*, block */) {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
waitFor.abort();
|
|
||||||
return void cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("blockInfo available at:", blockHash);
|
|
||||||
LocalStore.login(undefined, blockHash, uname, function () {
|
|
||||||
cb(void 0, res);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Exports.redirect = function () {
|
Exports.redirect = function () {
|
||||||
if (redirectTo) {
|
if (redirectTo) {
|
||||||
var h = redirectTo;
|
var h = redirectTo;
|
||||||
|
@ -569,14 +92,17 @@ define([
|
||||||
};
|
};
|
||||||
|
|
||||||
var hashing;
|
var hashing;
|
||||||
Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, onOTP, testing, test) {
|
|
||||||
|
Exports.loginOrRegisterUI = function (config) {
|
||||||
|
let { uname, token, shouldImport, cb } = config;
|
||||||
if (hashing) { return void console.log("hashing is already in progress"); }
|
if (hashing) { return void console.log("hashing is already in progress"); }
|
||||||
hashing = true;
|
hashing = true;
|
||||||
|
|
||||||
|
setMergeAnonDrive(shouldImport);
|
||||||
|
|
||||||
var proceed = function (result) {
|
var proceed = function (result) {
|
||||||
hashing = false;
|
hashing = false;
|
||||||
// NOTE: test is also use as a cb for the install page
|
if (cb && typeof cb === "function" && cb(result)) { return; }
|
||||||
if (test && typeof test === "function" && test(result)) { return; }
|
|
||||||
LocalStore.clearLoginToken();
|
LocalStore.clearLoginToken();
|
||||||
Realtime.whenRealtimeSyncs(result.realtime, function () {
|
Realtime.whenRealtimeSyncs(result.realtime, function () {
|
||||||
Exports.redirect();
|
Exports.redirect();
|
||||||
|
@ -594,11 +120,12 @@ define([
|
||||||
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
|
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
|
||||||
// after hashing the password
|
// after hashing the password
|
||||||
window.setTimeout(function () {
|
window.setTimeout(function () {
|
||||||
Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, onOTP, function (err, result) {
|
Login.loginOrRegister(config, function (err, result) {
|
||||||
var proxy;
|
var proxy = {};
|
||||||
if (result) { proxy = result.proxy; }
|
if (result) { proxy = result.proxy; }
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
|
console.warn(err);
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case 'NO_SUCH_USER':
|
case 'NO_SUCH_USER':
|
||||||
UI.removeLoadingScreen(function () {
|
UI.removeLoadingScreen(function () {
|
||||||
|
@ -668,6 +195,9 @@ define([
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'E_RESTRICTED':
|
case 'E_RESTRICTED':
|
||||||
|
if (token) {
|
||||||
|
return UI.errorLoadingScreen(Messages.register_invalidToken);
|
||||||
|
}
|
||||||
UI.errorLoadingScreen(Messages.register_registrationIsClosed);
|
UI.errorLoadingScreen(Messages.register_registrationIsClosed);
|
||||||
break;
|
break;
|
||||||
default: // UNHANDLED ERROR
|
default: // UNHANDLED ERROR
|
||||||
|
@ -677,8 +207,6 @@ define([
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (testing) { return void proceed(result); }
|
|
||||||
|
|
||||||
if (!(proxy.curvePrivate && proxy.curvePublic &&
|
if (!(proxy.curvePrivate && proxy.curvePublic &&
|
||||||
proxy.edPrivate && proxy.edPublic)) {
|
proxy.edPrivate && proxy.edPublic)) {
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ define([
|
||||||
return h('a', attrs, [icon, text]);
|
return h('a', attrs, [icon, text]);
|
||||||
};
|
};
|
||||||
|
|
||||||
Pages.versionString = "5.5.0";
|
Pages.versionString = "5.7.0";
|
||||||
|
|
||||||
var customURLs = Pages.customURLs = {};
|
var customURLs = Pages.customURLs = {};
|
||||||
(function () {
|
(function () {
|
||||||
|
|
|
@ -11,13 +11,17 @@ define([
|
||||||
], function (h, UI, Msg, Pages, Config) {
|
], function (h, UI, Msg, Pages, Config) {
|
||||||
return function () {
|
return function () {
|
||||||
document.title = Msg.login_login;
|
document.title = Msg.login_login;
|
||||||
|
|
||||||
|
var ssoEnabled = (Config.sso && Config.sso.list && Config.sso.list.length) ?'': '.cp-hidden';
|
||||||
|
var ssoEnforced = (Config.sso && Config.sso.force) ? '.cp-hidden' : '';
|
||||||
|
|
||||||
return [h('div#cp-main', [
|
return [h('div#cp-main', [
|
||||||
Pages.infopageTopbar(),
|
Pages.infopageTopbar(),
|
||||||
h('div.container.cp-container', [
|
h('div.container.cp-container', [
|
||||||
h('div.row.cp-page-title', h('h1', Msg.login_login)),
|
h('div.row.cp-page-title', h('h1', Msg.login_login)),
|
||||||
h('div.row', [
|
h('div.row', [
|
||||||
h('div.col-md-3'),
|
h('div.col-md-3'+ssoEnforced),
|
||||||
h('div#userForm.form-group.hidden.col-md-6', [
|
h('div#userForm.form-group.col-md-6'+ssoEnforced, [
|
||||||
h('div.cp-login-instance', Msg._getKey('login_instance', [ Pages.Instance.name ])),
|
h('div.cp-login-instance', Msg._getKey('login_instance', [ Pages.Instance.name ])),
|
||||||
h('div.big-container', [
|
h('div.big-container', [
|
||||||
h('div.input-container', [
|
h('div.input-container', [
|
||||||
|
@ -48,17 +52,22 @@ define([
|
||||||
]),
|
]),
|
||||||
h('div.extra', [
|
h('div.extra', [
|
||||||
(Config.restrictRegistration?
|
(Config.restrictRegistration?
|
||||||
undefined:
|
h('div'):
|
||||||
h('a#register', {
|
h('a#register', {
|
||||||
href: "/register/",
|
href: "/register/",
|
||||||
}, Msg.login_register)
|
}, Msg.login_register)
|
||||||
),
|
),
|
||||||
h('button.login', Msg.login_login)
|
h('button.login', Msg.login_login),
|
||||||
])
|
|
||||||
]),
|
]),
|
||||||
h('div.col-md-3')
|
|
||||||
]),
|
]),
|
||||||
h('div.row', [
|
h('div.col-md-3'+ssoEnforced),
|
||||||
|
h('div.col-md-3'+ssoEnabled),
|
||||||
|
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
|
||||||
|
h('div.cp-login-sso', Msg.sso_login_description)
|
||||||
|
]),
|
||||||
|
h('div.col-md-3'+ssoEnabled),
|
||||||
|
]),
|
||||||
|
h('div.row.cp-login-encryption', [
|
||||||
h('div.col-md-3'),
|
h('div.col-md-3'),
|
||||||
h('div.col-md-6', Msg.register_warning_note),
|
h('div.col-md-6', Msg.register_warning_note),
|
||||||
h('div.col-md-3'),
|
h('div.col-md-3'),
|
||||||
|
|
|
@ -14,6 +14,9 @@ define([
|
||||||
document.title = Msg.register_header;
|
document.title = Msg.register_header;
|
||||||
var tos = $(UI.createCheckbox('accept-terms')).find('.cp-checkmark-label').append(Msg.register_acceptTerms).parent()[0];
|
var tos = $(UI.createCheckbox('accept-terms')).find('.cp-checkmark-label').append(Msg.register_acceptTerms).parent()[0];
|
||||||
|
|
||||||
|
var ssoEnabled = (Config.sso && Config.sso.list && Config.sso.list.length) ?'': '.cp-hidden';
|
||||||
|
var ssoEnforced = (Config.sso && Config.sso.force) ? '.cp-hidden' : '';
|
||||||
|
|
||||||
var termsLink = Pages.customURLs.terms;
|
var termsLink = Pages.customURLs.terms;
|
||||||
$(tos).find('a').attr({
|
$(tos).find('a').attr({
|
||||||
href: termsLink,
|
href: termsLink,
|
||||||
|
@ -33,27 +36,29 @@ define([
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Config.restrictRegistration) {
|
|
||||||
return frame([
|
|
||||||
h('div.cp-restricted-registration', [
|
|
||||||
h('p', Msg.register_registrationIsClosed),
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var termsCheck;
|
var termsCheck;
|
||||||
if (termsLink) {
|
if (termsLink) {
|
||||||
termsCheck = h('div.checkbox-container', tos);
|
termsCheck = h('div.checkbox-container', tos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var closed = Config.restrictRegistration;
|
||||||
|
if (closed) {
|
||||||
|
$('body').addClass('cp-register-closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return frame([
|
return frame([
|
||||||
|
h('div.cp-restricted-registration', [
|
||||||
|
h('p', Msg.register_registrationIsClosed),
|
||||||
|
]),
|
||||||
h('div.row.cp-register-det', [
|
h('div.row.cp-register-det', [
|
||||||
h('div#data.hidden.col-md-6', [
|
h('div#data.hidden.col-md-6', [
|
||||||
h('h2', Msg.register_notes_title),
|
h('h2', Msg.register_notes_title),
|
||||||
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
|
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
|
||||||
]),
|
]),
|
||||||
|
h('div.col-md-3.cp-closed-filler'+ssoEnabled, h('div')),
|
||||||
h('div.cp-reg-form.col-md-6', [
|
h('div.cp-reg-form.col-md-6', [
|
||||||
h('div#userForm.form-group.hidden', [
|
h('div#userForm.form-group'+ssoEnforced, [
|
||||||
h('div.cp-register-instance', [
|
h('div.cp-register-instance', [
|
||||||
Msg._getKey('register_instance', [Pages.Instance.name]),
|
Msg._getKey('register_instance', [Pages.Instance.name]),
|
||||||
h('br'),
|
h('br'),
|
||||||
|
@ -95,9 +100,13 @@ define([
|
||||||
UI.createCheckbox('import-recent', Msg.register_importRecent, true)
|
UI.createCheckbox('import-recent', Msg.register_importRecent, true)
|
||||||
]),
|
]),
|
||||||
termsCheck,
|
termsCheck,
|
||||||
h('button#register', Msg.login_register)
|
h('button#register', Msg.login_register),
|
||||||
])
|
|
||||||
]),
|
]),
|
||||||
|
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
|
||||||
|
h('div.cp-register-sso', Msg.sso_register_description)
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
h('div.col-md-3.cp-closed-filler'+ssoEnabled),
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
define([
|
||||||
|
'/api/config',
|
||||||
|
'jquery',
|
||||||
|
'/common/hyperscript.js',
|
||||||
|
'/common/common-interface.js',
|
||||||
|
'/customize/messages.js',
|
||||||
|
'/customize/pages.js'
|
||||||
|
], function (Config, $, h, UI, Msg, Pages) {
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
document.title = Msg.ssoauth_header;
|
||||||
|
|
||||||
|
var frame = function (content) {
|
||||||
|
return [
|
||||||
|
h('div#cp-main', [
|
||||||
|
Pages.infopageTopbar(),
|
||||||
|
h('div.container.cp-container', [
|
||||||
|
h('div.row.cp-page-title', h('h1', Msg.ssoauth_header)),
|
||||||
|
].concat(content)),
|
||||||
|
Pages.infopageFooter(),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return frame([
|
||||||
|
h('div.row', [
|
||||||
|
h('div.hidden.col-md-3'),
|
||||||
|
h('div#userForm.form-group.col-md-6.cp-ssoauth-pw', [
|
||||||
|
h('p.cp-isregister.cp-login-instance', Msg.ssoauth_form_hint_register),
|
||||||
|
h('p.cp-islogin.cp-login-instance', Msg.ssoauth_form_hint_login),
|
||||||
|
h('input.form-control#password', {
|
||||||
|
type: 'password',
|
||||||
|
placeholder: Msg.login_password,
|
||||||
|
}),
|
||||||
|
h('input.form-control.cp-isregister#passwordconfirm', {
|
||||||
|
type: 'password',
|
||||||
|
placeholder: Msg.login_confirm,
|
||||||
|
}),
|
||||||
|
h('div.cp-ssoauth-button.extra',
|
||||||
|
h('div'),
|
||||||
|
h('button.login#cp-ssoauth-button', Msg.continue)
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
h('div.hidden.col-md-3'),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,26 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
# Customizing CryptPad
|
# Customizing CryptPad
|
||||||
|
|
||||||
In order allow a variety of features to be changed and to allow site-specific changes
|
This tutorial is part of our documentation, you can find it in our administrator guide: https://docs.cryptpad.org/en/admin_guide/customization.html
|
||||||
to CryptPad apps while still keeping the git repository pristine, this directory exists
|
|
||||||
to allow a set of hooks to be run.
|
|
||||||
|
|
||||||
The server is configured to load files from the `/customize/` path preferentially from
|
|
||||||
`cryptpad/customize/`, and to fall back to `cryptpad/customize.dist/` if they are not found
|
|
||||||
|
|
||||||
If you wish to customize cryptpad, please **copy**
|
|
||||||
`/customize.dist/` to `/customize` and then edit it there, this way you will still be able
|
|
||||||
to pull from (and make pull requests to (!) the git repository.
|
|
||||||
|
|
||||||
## Files you may be interested in
|
|
||||||
|
|
||||||
* index.html is the main page
|
|
||||||
* main.js contains javascript for the home page
|
|
||||||
* application_config.js allows you to modify settings used by the various applications
|
|
||||||
* messages.js contains functions for applying translations to various pages
|
|
||||||
* look inside `/translations/` for the rest of the files which contain translated strings
|
|
||||||
* `/share/` implements an iframe RPC which allows multiple domains to access the same localStorage
|
|
||||||
* `/src/` contains source files for html and css (in the form of html templates and .less stylesheets)
|
|
||||||
|
|
||||||
All other content which is placed in this directory will be referencable at the `/customize/`
|
|
||||||
URL location.
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
@import (reference) "./browser.less";
|
@import (reference) "./browser.less";
|
||||||
@import (reference) './leftside-menu.less';
|
@import (reference) './leftside-menu.less';
|
||||||
@import (reference) "./tools.less";
|
@import (reference) "./tools.less";
|
||||||
|
@ -48,7 +54,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
//align-items: center;
|
//align-items: center;
|
||||||
//justify-content: center;
|
//justify-content: center;
|
||||||
|
@ -661,6 +669,17 @@
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.cp-app-drive-element-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
}
|
||||||
|
.cp-app-drive-element-name-text {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.cp-app-drive-element-state {
|
.cp-app-drive-element-state {
|
||||||
left: 3px;
|
left: 3px;
|
||||||
}
|
}
|
||||||
|
@ -672,7 +691,7 @@
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
background: @cp_drive-thumb-bg;
|
background: @cp_drive-thumb-bg;
|
||||||
& ~ .fa, & ~ .cptools {
|
& ~ .fa, & ~ .cptools {
|
||||||
display: inline;
|
display: none;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
|
@ -695,6 +714,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
div.cp-app-drive-content-list {
|
div.cp-app-drive-content-list {
|
||||||
|
.cp-app-drive-element-icon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
.cp-app-drive-element-grid {
|
.cp-app-drive-element-grid {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.cp-dropdown-content {
|
.cp-dropdown-content {
|
||||||
|
list-style-type: none;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: @cp_dropdown-bg;
|
background-color: @cp_dropdown-bg;
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
.cp-loading-container {
|
.cp-loading-container {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
height: 236px;
|
min-height: 236px;
|
||||||
max-height: calc(100vh - 20px);
|
max-height: calc(100vh - 20px);
|
||||||
margin: 50px;
|
margin: 50px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -106,6 +106,7 @@
|
||||||
color: @cp_loading-fg;
|
color: @cp_loading-fg;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: none;
|
display: none;
|
||||||
|
overflow-y: auto;
|
||||||
a {
|
a {
|
||||||
color: @cp_loading-link;
|
color: @cp_loading-link;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import (reference) "../include/infopages.less";
|
||||||
|
@import (reference) "../include/colortheme-all.less";
|
||||||
|
@import (reference) "../include/alertify.less";
|
||||||
|
@import (reference) "../include/forms.less";
|
||||||
|
|
||||||
|
.login_main() {
|
||||||
|
#userForm, #ssoForm {
|
||||||
|
.cp-shadow();
|
||||||
|
background-color: @cp_static-card-bg;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: @infopages-radius-L;
|
||||||
|
.cp-login-instance, .cp-login-sso-description {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.form-control {
|
||||||
|
border-radius: @infopages-radius;
|
||||||
|
color: @cryptpad_text_col;
|
||||||
|
background-color: @cp_forms-bg;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
&:focus {
|
||||||
|
border-color: @cryptpad_color_brand;
|
||||||
|
}
|
||||||
|
.tools_placeholder-color();
|
||||||
|
}
|
||||||
|
.checkbox-container {
|
||||||
|
color: @cryptpad_text_col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ssoForm {
|
||||||
|
margin-top: 10px;
|
||||||
|
button {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
.cp-login-sso {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cp-default-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.extra {
|
||||||
|
margin-top: 1em;
|
||||||
|
button.login {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cp-loading-missing-mfa {
|
||||||
|
.cp-settings-qr-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
.cp-settings-qr-code {
|
||||||
|
input {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cp-settings-qr {
|
||||||
|
img {
|
||||||
|
border: 10px solid white;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
margin: 10px 10px 10px 0;
|
||||||
|
}
|
||||||
|
.cp-password-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
justify-content: flex-start;
|
||||||
|
input {
|
||||||
|
flex-shrink: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
width: 100%;
|
||||||
|
font-weight: unset;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Properties modal
|
// Properties modal
|
||||||
.cp-app-prop {
|
.cp-app-prop {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
@import (reference) "../include/alertify.less";
|
@import (reference) "../include/alertify.less";
|
||||||
@import (reference) "../include/checkmark.less";
|
@import (reference) "../include/checkmark.less";
|
||||||
@import (reference) "../include/forms.less";
|
@import (reference) "../include/forms.less";
|
||||||
|
@import (reference) "../include/login.less";
|
||||||
|
|
||||||
&.cp-page-login {
|
&.cp-page-login {
|
||||||
.infopages_main();
|
.infopages_main();
|
||||||
|
@ -25,42 +26,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.cp-container {
|
.cp-container {
|
||||||
#userForm {
|
.cp-hidden {
|
||||||
.cp-shadow();
|
display: none !important;
|
||||||
background-color: @cp_static-card-bg;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 10px 10px 10px;
|
|
||||||
border-radius: @infopages-radius-L;
|
|
||||||
.cp-login-instance {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.form-control {
|
|
||||||
border-radius: @infopages-radius;
|
|
||||||
color: @cryptpad_text_col;
|
|
||||||
background-color: @cp_forms-bg;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
&:focus {
|
|
||||||
border-color: @cryptpad_color_brand;
|
|
||||||
}
|
|
||||||
.tools_placeholder-color();
|
|
||||||
}
|
|
||||||
.checkbox-container {
|
|
||||||
color: @cryptpad_text_col;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.login_main();
|
||||||
.align-items-center {
|
.align-items-center {
|
||||||
box-shadow: 0 5px 15px @cp_shadow-color;
|
box-shadow: 0 5px 15px @cp_shadow-color;
|
||||||
background: @cryptpad_color_white;
|
background: @cryptpad_color_white;
|
||||||
}
|
}
|
||||||
.extra {
|
|
||||||
margin-top: 1em;
|
|
||||||
button.login {
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.cp-default-label {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cp-login-encryption {
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cp-password-form {
|
.cp-password-form {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
@import (reference) "../include/alertify.less";
|
@import (reference) "../include/alertify.less";
|
||||||
@import (reference) "../include/checkmark.less";
|
@import (reference) "../include/checkmark.less";
|
||||||
@import (reference) "../include/forms.less";
|
@import (reference) "../include/forms.less";
|
||||||
|
@import (reference) "../include/login.less";
|
||||||
|
|
||||||
&.cp-page-register {
|
&.cp-page-register {
|
||||||
.infopages_main();
|
.infopages_main();
|
||||||
|
@ -17,6 +18,20 @@
|
||||||
.alertify_main();
|
.alertify_main();
|
||||||
.checkmark_main(20px);
|
.checkmark_main(20px);
|
||||||
|
|
||||||
|
&:not(.cp-register-closed) {
|
||||||
|
.cp-restricted-registration {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.cp-closed-filler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.cp-register-closed {
|
||||||
|
div.cp-register-det {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cp-container {
|
.cp-container {
|
||||||
.form-group {
|
.form-group {
|
||||||
.cp-register-instance {
|
.cp-register-instance {
|
||||||
|
@ -91,32 +106,25 @@
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#userForm {
|
.cp-hidden {
|
||||||
padding: 15px;
|
display: none !important;
|
||||||
background-color: @cp_static-card-bg;
|
}
|
||||||
|
.login_main();
|
||||||
|
#userForm, #ssoForm {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin-bottom: 100px;
|
max-width: 100%;
|
||||||
border-radius: @infopages-radius-L;
|
padding: 15px;
|
||||||
.cp-shadow();
|
|
||||||
.form-control {
|
|
||||||
border-radius: @infopages-radius;
|
|
||||||
color: @cryptpad_text_col;
|
|
||||||
background-color: @cp_forms-bg;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
&:focus {
|
|
||||||
border-color: @cryptpad_color_brand;
|
|
||||||
}
|
|
||||||
.tools_placeholder-color();
|
|
||||||
}
|
|
||||||
.checkbox-container {
|
.checkbox-container {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
color: @cryptpad_text_col;
|
|
||||||
}
|
}
|
||||||
button#register {
|
button#register {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#ssoForm {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.cp-register-notes {
|
.cp-register-notes {
|
||||||
ul.cp-notes-list {
|
ul.cp-notes-list {
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import (reference) "../include/infopages.less";
|
||||||
|
@import (reference) "../include/colortheme-all.less";
|
||||||
|
@import (reference) "../include/alertify.less";
|
||||||
|
@import (reference) "../include/forms.less";
|
||||||
|
@import (reference) "../include/login.less";
|
||||||
|
|
||||||
|
&.cp-page-ssoauth {
|
||||||
|
.infopages_main();
|
||||||
|
.forms_main();
|
||||||
|
.alertify_main();
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
.extra {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.cp-ssoauth-pw {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cp-container {
|
||||||
|
.login_main();
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cp-register {
|
||||||
|
.cp-islogin {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#passwordconfirm {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.cp-login {
|
||||||
|
.cp-isregister {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#password {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,8 @@ $(function () {
|
||||||
require([ '/install/main.js' ], function () {});
|
require([ '/install/main.js' ], function () {});
|
||||||
} else if (/^\/recovery\//.test(pathname)) {
|
} else if (/^\/recovery\//.test(pathname)) {
|
||||||
require([ '/recovery/main.js' ], function () {});
|
require([ '/recovery/main.js' ], function () {});
|
||||||
|
} else if (/^\/ssoauth\//.test(pathname)) {
|
||||||
|
require([ '/ssoauth/main.js' ], function () {});
|
||||||
} else if (/^\/login\//.test(pathname)) {
|
} else if (/^\/login\//.test(pathname)) {
|
||||||
require([ '/login/main.js' ], function () {});
|
require([ '/login/main.js' ], function () {});
|
||||||
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
|
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {
|
||||||
|
|
|
@ -6,32 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
Translations can now be made using [Weblate](https://weblate.cryptpad.org). We may still accept PRs for the internal translation files, but we won't provide support for this. See the state of the translated languages:
|
This tutorial is part of our documentation, you can find it in our how to contribute guide: https://docs.cryptpad.org/en/how_to_contribute.html#translate-cryptpad
|
||||||
|
|
||||||
![](https://weblate.cryptpad.org/widgets/cryptpad/-/app/multi-auto.svg)
|
|
||||||
|
|
||||||
## Request a new language
|
|
||||||
|
|
||||||
* New languages require some minor changes in the code which we have to apply manually. If you want to translate CryptPad into a new language, please send us an email at weblate@cryptpad.fr
|
|
||||||
* Please note that members of the current development team are only fluent in English and French, so additional languages will require ongoing contributions in order to stay up to date.
|
|
||||||
* If a language becomes too outdated, we will consider dropping it from the available language until it is fixed.
|
|
||||||
* Please refrain from asking for a new language if you are not able to provide a sufficiently translated version and maintain it or to find other users that can do so.
|
|
||||||
|
|
||||||
## Become a reviewer
|
|
||||||
|
|
||||||
* We need reviewers for all the languages except English and French. Their job will be to approve/reject/discuss the translations made by other contributors to this language, using the Weblate UI.
|
|
||||||
* If you want to become a reviewer for a language, please write us an email at weblate@cryptpad.fr
|
|
||||||
* Please tell us if you want to stop being a reviewer (even momentarily) so that we can find a replacement as soon as posible
|
|
||||||
|
|
||||||
## Translate an existing language
|
|
||||||
|
|
||||||
* All translations can be done using the Weblate UI. For better help about how to use the tool, please check the [Weblate documentation](https://docs.weblate.org/en/latest/).
|
|
||||||
* Our Weblate instance is configured to always require approval for changes.
|
|
||||||
|
|
||||||
### Update an existing translation
|
|
||||||
|
|
||||||
* Existing translations that have already been approved can't be changed directly. Users can only make suggestions, and the reviewers will have to approve the changes if they agree.
|
|
||||||
|
|
||||||
### Translate a new key
|
|
||||||
|
|
||||||
* Untranslated strings can be changed by any user and the translation can be edited by others as long as it has not been approved by a reviewer.
|
|
|
@ -14,6 +14,10 @@ Restart=always
|
||||||
# Restart service after 10 seconds if node service crashes
|
# Restart service after 10 seconds if node service crashes
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
|
|
||||||
|
# Proper logging to journald
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal+console
|
||||||
|
|
||||||
User=cryptpad
|
User=cryptpad
|
||||||
Group=cryptpad
|
Group=cryptpad
|
||||||
# modify to match your working directory
|
# modify to match your working directory
|
||||||
|
|
|
@ -14,6 +14,8 @@ server {
|
||||||
|
|
||||||
# Let's Encrypt webroot
|
# Let's Encrypt webroot
|
||||||
include letsencrypt-webroot;
|
include letsencrypt-webroot;
|
||||||
|
# Include mime.types to be able to support .mjs files (see "types" below)
|
||||||
|
include mime.types;
|
||||||
|
|
||||||
# CryptPad serves static assets over these two domains.
|
# CryptPad serves static assets over these two domains.
|
||||||
# `main_domain` is what users will enter in their address bar.
|
# `main_domain` is what users will enter in their address bar.
|
||||||
|
@ -166,11 +168,6 @@ server {
|
||||||
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
|
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
|
||||||
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
|
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
|
||||||
|
|
||||||
# draw.io uses inline script tags in it's index.html. The hashes are added here.
|
|
||||||
if ($uri ~ ^\/components\/drawio\/src\/main\/webapp\/index.html.*$) {
|
|
||||||
set $scriptSrc "'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: https://${main_domain}";
|
|
||||||
}
|
|
||||||
|
|
||||||
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
|
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
|
||||||
if ($unsafe) {
|
if ($unsafe) {
|
||||||
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
|
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
|
||||||
|
@ -179,6 +176,11 @@ server {
|
||||||
# Finally, set all the rules you composed above.
|
# Finally, set all the rules you composed above.
|
||||||
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
|
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
|
||||||
|
|
||||||
|
# Add support for .mjs files used by pdfjs
|
||||||
|
types {
|
||||||
|
application/javascript mjs;
|
||||||
|
}
|
||||||
|
|
||||||
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
|
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
|
||||||
# We prefer to serve static content from nginx directly and to leave the API server to handle
|
# We prefer to serve static content from nginx directly and to leave the API server to handle
|
||||||
# the dynamic content that only it can manage. This is primarily an optimization
|
# the dynamic content that only it can manage. This is primarily an optimization
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# This file is included strictly as an example of how Apache httpd can be
|
||||||
|
# configured to work with CryptPad. If you are using CryptPad in production
|
||||||
|
# and require professional support please contact sales@cryptpad.fr
|
||||||
|
|
||||||
|
# This configuration requires mod_ssl, mod_socache_shmcb, mod_proxy,
|
||||||
|
# mod_proxy_http and mod_headers
|
||||||
|
|
||||||
|
Listen 443
|
||||||
|
|
||||||
|
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||||
|
SSLProxyCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||||
|
SSLHonorCipherOrder off
|
||||||
|
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
|
||||||
|
SSLProxyProtocol all -SSLv3 -TLSv1 -TLSv1.1
|
||||||
|
SSLSessionCache "shmcb:logs/ssl_scache(512000)"
|
||||||
|
SSLSessionCacheTimeout 86400
|
||||||
|
SSLSessionTickets off
|
||||||
|
SSLUseStapling on
|
||||||
|
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
|
||||||
|
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName cryptpad.your-domain.com
|
||||||
|
ServerAlias sandbox.your-domain.com
|
||||||
|
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/cert.pem
|
||||||
|
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
|
||||||
|
BrowserMatch "MSIE [2-5]" \
|
||||||
|
nokeepalive ssl-unclean-shutdown \
|
||||||
|
downgrade-1.0 force-response-1.0
|
||||||
|
Protocols h2 http/1.1
|
||||||
|
LimitRequestBody 157286400
|
||||||
|
ProxyPass / http://localhost:3000/ upgrade=websocket
|
||||||
|
ProxyPassReverse / http://localhost:3000/
|
||||||
|
</VirtualHost>
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
const WebSocketServer = require('ws').Server;
|
const WebSocketServer = require('ws').Server;
|
||||||
const NetfluxSrv = require('chainpad-server');
|
const NetfluxSrv = require('chainpad-server');
|
||||||
const Decrees = require("./decrees");
|
const Decrees = require("./decrees");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6, node: true */
|
|
||||||
const nThen = require('nthen');
|
const nThen = require('nthen');
|
||||||
const Pins = require('./pins');
|
const Pins = require('./pins');
|
||||||
const Util = require("./common-util");
|
const Util = require("./common-util");
|
||||||
|
@ -13,6 +12,9 @@ const Core = require("./commands/core");
|
||||||
const Metadata = require("./commands/metadata");
|
const Metadata = require("./commands/metadata");
|
||||||
const Meta = require("./metadata");
|
const Meta = require("./metadata");
|
||||||
const Logger = require("./log");
|
const Logger = require("./log");
|
||||||
|
const plugins = require("./plugin-manager");
|
||||||
|
|
||||||
|
let SSOUtils = plugins.SSO && plugins.SSO.utils;
|
||||||
|
|
||||||
const Path = require("path");
|
const Path = require("path");
|
||||||
const Fse = require("fs-extra");
|
const Fse = require("fs-extra");
|
||||||
|
@ -200,6 +202,12 @@ COMMANDS.start = (edPublic, blockId, reason) => {
|
||||||
}
|
}
|
||||||
Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor());
|
Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor());
|
||||||
}));
|
}));
|
||||||
|
if (!SSOUtils) { return; }
|
||||||
|
SSOUtils.deleteAccount(Env, blockId, waitFor((err) => {
|
||||||
|
if (err) {
|
||||||
|
return Log.error('MODERATION_ACCOUNT_BLOCK_SSO', err, waitFor());
|
||||||
|
}
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
}).nThen((waitFor) => {
|
}).nThen((waitFor) => {
|
||||||
var report = {
|
var report = {
|
||||||
|
@ -289,6 +297,12 @@ COMMANDS.restore = (edPublic) => {
|
||||||
}
|
}
|
||||||
Log.info('MODERATION_ACCOUNT_BLOCK_RESTORE', safeKey, waitFor());
|
Log.info('MODERATION_ACCOUNT_BLOCK_RESTORE', safeKey, waitFor());
|
||||||
}));
|
}));
|
||||||
|
if (!SSOUtils) { return; }
|
||||||
|
SSOUtils.restoreAccount(Env, blockId, waitFor(function (err) {
|
||||||
|
if (err) {
|
||||||
|
return Log.error('MODERATION_ACCOUNT_BLOCK_RESTORE_SSO', err, waitFor());
|
||||||
|
}
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
}).nThen((waitFor) => {
|
}).nThen((waitFor) => {
|
||||||
deleteReport(Env, safeKey, waitFor((err) => {
|
deleteReport(Env, safeKey, waitFor((err) => {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
const Block = require("../commands/block");
|
const Block = require("../commands/block");
|
||||||
const MFA = require("../storage/mfa");
|
const MFA = require("../storage/mfa");
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
|
const Sessions = require("../storage/sessions");
|
||||||
|
|
||||||
const Commands = module.exports;
|
const Commands = module.exports;
|
||||||
|
|
||||||
|
@ -55,8 +56,16 @@ const writeBlock = Commands.WRITE_BLOCK = function (Env, body, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
writeBlock.complete = function (Env, body, cb) {
|
writeBlock.complete = function (Env, body, cb) {
|
||||||
const { content } = body;
|
const { publicKey, content, session } = body;
|
||||||
Block.writeLoginBlock(Env, content, cb);
|
Block.writeLoginBlock(Env, content, (err) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
|
||||||
|
if (!session) { return void cb(); }
|
||||||
|
|
||||||
|
const proof = Util.tryParse(content.registrationProof);
|
||||||
|
const oldKey = proof && proof[0];
|
||||||
|
Sessions.update(Env, publicKey, oldKey, session, "", cb);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove a login block IFF
|
// Remove a login block IFF
|
||||||
|
@ -73,8 +82,8 @@ const removeBlock = Commands.REMOVE_BLOCK = function (Env, body, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
removeBlock.complete = function (Env, body, cb) {
|
removeBlock.complete = function (Env, body, cb) {
|
||||||
const { publicKey, reason } = body;
|
const { publicKey, edPublic, reason } = body;
|
||||||
Block.removeLoginBlock(Env, publicKey, reason, cb);
|
Block.removeLoginBlock(Env, publicKey, reason, edPublic, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ const MFA = require("../storage/mfa");
|
||||||
const Sessions = require("../storage/sessions");
|
const Sessions = require("../storage/sessions");
|
||||||
const BlockStore = require("../storage/block");
|
const BlockStore = require("../storage/block");
|
||||||
const Block = require("../commands/block");
|
const Block = require("../commands/block");
|
||||||
|
const config = require("../load-config");
|
||||||
|
|
||||||
const Commands = module.exports;
|
const Commands = module.exports;
|
||||||
|
|
||||||
|
@ -61,21 +62,40 @@ var decode32 = S => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Decide expire time
|
|
||||||
// Allow user settings?
|
// Allow user settings?
|
||||||
var EXPIRATION = 7 * 24 * 3600 * 1000; // Sessions are valid 7 days
|
var EXPIRATION = (config.otpSessionExpiration || 7 * 24) * 3600 * 1000;
|
||||||
|
|
||||||
// Create a session with a token for the given public key
|
// Create a session with a token for the given public key
|
||||||
const makeSession = (Env, publicKey, cb) => {
|
const makeSession = (Env, publicKey, oldKey, ssoSession, cb) => {
|
||||||
const sessionId = Sessions.randomId();
|
const sessionId = ssoSession || Sessions.randomId();
|
||||||
|
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||||
|
|
||||||
|
// For password change, we need to get the sso session associated to the old block key
|
||||||
|
// In other cases (login and totp_setup), the sso session is associated to the current block
|
||||||
|
oldKey = oldKey || publicKey; // use the current block if no old key
|
||||||
|
|
||||||
|
let isUpdate = false;
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
|
if (!ssoSession || !SSOUtils) { return; }
|
||||||
|
// If we have an session token, confirm this is an sso account
|
||||||
|
SSOUtils.readBlock(Env, oldKey, w((err) => {
|
||||||
|
if (err === 'ENOENT') { return; } // No sso block, no need to update the session
|
||||||
|
if (err) {
|
||||||
|
w.abort();
|
||||||
|
return void cb('TOTP_VALIDATE_READ_SSO');
|
||||||
|
}
|
||||||
|
// We have an existing session for an SSO account: update the existing session
|
||||||
|
isUpdate = true;
|
||||||
|
}));
|
||||||
|
}).nThen(function (w) {
|
||||||
// store the token
|
// store the token
|
||||||
Sessions.write(Env, publicKey, sessionId, JSON.stringify({
|
let sessionData = {
|
||||||
mfa: {
|
mfa: {
|
||||||
type: 'otp',
|
type: 'otp',
|
||||||
exp: (+new Date()) + EXPIRATION
|
exp: (+new Date()) + EXPIRATION
|
||||||
}
|
}
|
||||||
}), w(function (err) {
|
};
|
||||||
|
var then = w(function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
|
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
|
||||||
error: Util.serializeError(err),
|
error: Util.serializeError(err),
|
||||||
|
@ -86,7 +106,12 @@ const makeSession = (Env, publicKey, cb) => {
|
||||||
return void cb("SESSION_WRITE_ERROR");
|
return void cb("SESSION_WRITE_ERROR");
|
||||||
}
|
}
|
||||||
// else continue
|
// else continue
|
||||||
}));
|
});
|
||||||
|
if (isUpdate) {
|
||||||
|
Sessions.update(Env, publicKey, oldKey, sessionId, JSON.stringify(sessionData), then);
|
||||||
|
} else {
|
||||||
|
Sessions.write(Env, publicKey, sessionId, JSON.stringify(sessionData), then);
|
||||||
|
}
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
cb(void 0, {
|
cb(void 0, {
|
||||||
bearer: sessionId,
|
bearer: sessionId,
|
||||||
|
@ -207,7 +232,7 @@ const TOTP_SETUP = Commands.TOTP_SETUP = function (Env, body, cb) {
|
||||||
// There's still a little bit more to do and it could still fail.
|
// There's still a little bit more to do and it could still fail.
|
||||||
TOTP_SETUP.complete = function (Env, body, cb) {
|
TOTP_SETUP.complete = function (Env, body, cb) {
|
||||||
// the OTP code should have already been validated
|
// the OTP code should have already been validated
|
||||||
var { publicKey, secret, contact } = body;
|
var { publicKey, secret, contact, session } = body;
|
||||||
|
|
||||||
// the device from which they configure MFA settings
|
// the device from which they configure MFA settings
|
||||||
// is assumed to be safe, so we'll respond with a JWT token
|
// is assumed to be safe, so we'll respond with a JWT token
|
||||||
|
@ -257,7 +282,7 @@ TOTP_SETUP.complete = function (Env, body, cb) {
|
||||||
// we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret.
|
// we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret.
|
||||||
// we attempt to create a session as a matter of convenience - so if it fails
|
// we attempt to create a session as a matter of convenience - so if it fails
|
||||||
// that just means they'll be forced to authenticate
|
// that just means they'll be forced to authenticate
|
||||||
makeSession(Env, publicKey, cb);
|
makeSession(Env, publicKey, null, session, cb);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -305,12 +330,12 @@ So, we should:
|
||||||
2. send them the token
|
2. send them the token
|
||||||
|
|
||||||
*/
|
*/
|
||||||
var { publicKey } = body;
|
var { publicKey, session } = body;
|
||||||
makeSession(Env, publicKey, cb);
|
makeSession(Env, publicKey, null, session, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Same as TOTP_VALIDATE but without making a session at the end
|
// Same as TOTP_VALIDATE but without making a session at the end
|
||||||
const check = Commands.TOTP_CHECK = function (Env, body, cb) {
|
const check = Commands.TOTP_MFA_CHECK = function (Env, body, cb) {
|
||||||
var { publicKey, auth } = body;
|
var { publicKey, auth } = body;
|
||||||
const code = auth;
|
const code = auth;
|
||||||
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||||
|
@ -436,7 +461,8 @@ const writeBlock = Commands.TOTP_WRITE_BLOCK = function (Env, body, cb) {
|
||||||
|
|
||||||
|
|
||||||
writeBlock.complete = function (Env, body, cb) {
|
writeBlock.complete = function (Env, body, cb) {
|
||||||
const { publicKey, content } = body;
|
const { publicKey, content, session } = body;
|
||||||
|
let oldKey;
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// Write new block
|
// Write new block
|
||||||
Block.writeLoginBlock(Env, content, w((err) => {
|
Block.writeLoginBlock(Env, content, w((err) => {
|
||||||
|
@ -448,7 +474,7 @@ writeBlock.complete = function (Env, body, cb) {
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
// Copy MFA settings
|
// Copy MFA settings
|
||||||
const proof = Util.tryParse(content.registrationProof);
|
const proof = Util.tryParse(content.registrationProof);
|
||||||
const oldKey = proof && proof[0];
|
oldKey = proof && proof[0];
|
||||||
if (!oldKey) {
|
if (!oldKey) {
|
||||||
w.abort();
|
w.abort();
|
||||||
return void cb('INVALID_ANCESTOR');
|
return void cb('INVALID_ANCESTOR');
|
||||||
|
@ -456,7 +482,7 @@ writeBlock.complete = function (Env, body, cb) {
|
||||||
MFA.copy(Env, oldKey, publicKey, w());
|
MFA.copy(Env, oldKey, publicKey, w());
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
// Create a session for the current user
|
// Create a session for the current user
|
||||||
makeSession(Env, publicKey, cb);
|
makeSession(Env, publicKey, oldKey, session, cb);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -487,10 +513,10 @@ const removeBlock = Commands.TOTP_REMOVE_BLOCK = function (Env, body, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
removeBlock.complete = function (Env, body, cb) {
|
removeBlock.complete = function (Env, body, cb) {
|
||||||
const { publicKey, reason } = body;
|
const { publicKey, edPublic, reason } = body;
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// Remove the block
|
// Remove the block
|
||||||
Block.removeLoginBlock(Env, publicKey, reason, w((err) => {
|
Block.removeLoginBlock(Env, publicKey, reason, edPublic, w((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
w.abort();
|
w.abort();
|
||||||
return void cb(err);
|
return void cb(err);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
var Netflux = require("netflux-websocket");
|
var Netflux = require("netflux-websocket");
|
||||||
var WebSocket = require("ws"); // jshint ignore:line
|
var WebSocket = require("ws");
|
||||||
var nThen = require("nthen");
|
var nThen = require("nthen");
|
||||||
|
|
||||||
var Util = require("../../www/common/common-util");
|
var Util = require("../../www/common/common-util");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
/* globals process */
|
/* globals process */
|
||||||
const nThen = require("nthen");
|
const nThen = require("nthen");
|
||||||
const getFolderSize = require("get-folder-size");
|
const getFolderSize = require("get-folder-size");
|
||||||
|
@ -12,12 +11,12 @@ const Decrees = require("../decrees");
|
||||||
const Pinning = require("./pin-rpc");
|
const Pinning = require("./pin-rpc");
|
||||||
const Core = require("./core");
|
const Core = require("./core");
|
||||||
const Channel = require("./channel");
|
const Channel = require("./channel");
|
||||||
|
const Invitation = require("./invitation");
|
||||||
|
const Users = require("./users");
|
||||||
const BlockStore = require("../storage/block");
|
const BlockStore = require("../storage/block");
|
||||||
const MFA = require("../storage/mfa");
|
const MFA = require("../storage/mfa");
|
||||||
const ArchiveAccount = require('../archive-account');
|
const ArchiveAccount = require('../archive-account');
|
||||||
/* jshint ignore:start */
|
|
||||||
const { Worker } = require('node:worker_threads');
|
const { Worker } = require('node:worker_threads');
|
||||||
/* jshint ignore:end */
|
|
||||||
|
|
||||||
var Fs = require("fs");
|
var Fs = require("fs");
|
||||||
|
|
||||||
|
@ -83,7 +82,7 @@ var getActiveSessions = function (Env, Server, cb) {
|
||||||
};
|
};
|
||||||
|
|
||||||
var shutdown = function (Env, Server, cb) {
|
var shutdown = function (Env, Server, cb) {
|
||||||
if (true) {
|
if (true) { // eslint-disable-line no-constant-condition
|
||||||
return void cb('E_NOT_IMPLEMENTED');
|
return void cb('E_NOT_IMPLEMENTED');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,7 +436,15 @@ the server adds two pieces of information to the supplied decree:
|
||||||
|
|
||||||
if (!changed) { return void cb(); }
|
if (!changed) { return void cb(); }
|
||||||
Env.Log.info('ADMIN_DECREE', decree);
|
Env.Log.info('ADMIN_DECREE', decree);
|
||||||
Decrees.write(Env, decree, cb);
|
let _err;
|
||||||
|
nThen((waitFor) => {
|
||||||
|
Decrees.write(Env, decree, waitFor((err) => {
|
||||||
|
_err = err;
|
||||||
|
}));
|
||||||
|
setTimeout(waitFor(), 300); // NOTE: 300 because cache update may take up to 250ms
|
||||||
|
}).nThen(function () {
|
||||||
|
cb(_err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// CryptPad_AsyncStore.rpc.send('ADMIN', ['SET_LAST_EVICTION', 0], console.log)
|
// CryptPad_AsyncStore.rpc.send('ADMIN', ['SET_LAST_EVICTION', 0], console.log)
|
||||||
|
@ -459,6 +466,10 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) {
|
||||||
var instanceStatus = function (Env, Server, cb) {
|
var instanceStatus = function (Env, Server, cb) {
|
||||||
cb(void 0, {
|
cb(void 0, {
|
||||||
restrictRegistration: Env.restrictRegistration,
|
restrictRegistration: Env.restrictRegistration,
|
||||||
|
restrictSsoRegistration: Env.restrictSsoRegistration,
|
||||||
|
dontStoreSSOUsers: Env.dontStoreSSOUsers,
|
||||||
|
dontStoreInvitedUsers: Env.dontStoreInvitedUsers,
|
||||||
|
|
||||||
enableEmbedding: Env.enableEmbedding,
|
enableEmbedding: Env.enableEmbedding,
|
||||||
launchTime: Env.launchTime,
|
launchTime: Env.launchTime,
|
||||||
currentTime: +new Date(),
|
currentTime: +new Date(),
|
||||||
|
@ -495,6 +506,7 @@ var instanceStatus = function (Env, Server, cb) {
|
||||||
instanceJurisdiction: Env.instanceJurisdiction,
|
instanceJurisdiction: Env.instanceJurisdiction,
|
||||||
instanceName: Env.instanceName,
|
instanceName: Env.instanceName,
|
||||||
instanceNotice: Env.instanceNotice,
|
instanceNotice: Env.instanceNotice,
|
||||||
|
enforceMFA: Env.enforceMFA,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -726,6 +738,8 @@ var archiveBlock = function (Env, Server, cb, data) {
|
||||||
});
|
});
|
||||||
cb(err);
|
cb(err);
|
||||||
});
|
});
|
||||||
|
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||||
|
if (SSOUtils) { SSOUtils.deleteAccount(Env, key, () => {}); }
|
||||||
};
|
};
|
||||||
|
|
||||||
var restoreArchivedBlock = function (Env, Server, cb, data) {
|
var restoreArchivedBlock = function (Env, Server, cb, data) {
|
||||||
|
@ -740,6 +754,11 @@ var restoreArchivedBlock = function (Env, Server, cb, data) {
|
||||||
key: key,
|
key: key,
|
||||||
reason: reason || '',
|
reason: reason || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also restore SSO data
|
||||||
|
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||||
|
if (SSOUtils) { SSOUtils.restoreAccount(Env, key, () => {}); }
|
||||||
|
|
||||||
cb(err);
|
cb(err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -854,6 +873,48 @@ var getMetadataHistory = function (Env, Server, cb, data) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getKnownUsers = (Env, Server, cb) => {
|
||||||
|
Users.getAll(Env, cb);
|
||||||
|
};
|
||||||
|
var addKnownUser = (Env, Server, cb, data, unsafeKey) => {
|
||||||
|
var obj = Array.isArray(data) && data[1];
|
||||||
|
var edPublic = obj.edPublic;
|
||||||
|
var block = obj.block;
|
||||||
|
var alias = obj.alias;
|
||||||
|
var userData = {
|
||||||
|
edPublic,
|
||||||
|
block,
|
||||||
|
alias,
|
||||||
|
email: obj.email,
|
||||||
|
name: obj.name,
|
||||||
|
type: 'manual'
|
||||||
|
};
|
||||||
|
Users.add(Env, edPublic, userData, unsafeKey, cb);
|
||||||
|
};
|
||||||
|
var deleteKnownUser = (Env, Server, cb, data) => {
|
||||||
|
var id = Array.isArray(data) && data[1];
|
||||||
|
Users.delete(Env, id, cb);
|
||||||
|
};
|
||||||
|
var updateKnownUser = (Env, Server, cb, data) => {
|
||||||
|
var args = Array.isArray(data) && data[1];
|
||||||
|
var edPublic = args.edPublic;
|
||||||
|
var changes = args.changes;
|
||||||
|
Users.update(Env, edPublic, changes, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getInvitations = (Env, Server, cb) => {
|
||||||
|
Invitation.getAll(Env, cb);
|
||||||
|
};
|
||||||
|
var createInvitation = (Env, Server, cb, data, unsafeKey) => {
|
||||||
|
const args = Array.isArray(data) && data[1];
|
||||||
|
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
|
||||||
|
Invitation.create(Env, args.alias, args.email, cb, unsafeKey);
|
||||||
|
};
|
||||||
|
var deleteInvitation = (Env, Server, cb, data) => {
|
||||||
|
var id = Array.isArray(data) && data[1];
|
||||||
|
Invitation.delete(Env, id, cb);
|
||||||
|
};
|
||||||
|
|
||||||
var commands = {
|
var commands = {
|
||||||
ACTIVE_SESSIONS: getActiveSessions,
|
ACTIVE_SESSIONS: getActiveSessions,
|
||||||
ACTIVE_PADS: getActiveChannelCount,
|
ACTIVE_PADS: getActiveChannelCount,
|
||||||
|
@ -912,6 +973,15 @@ var commands = {
|
||||||
GET_USER_TOTAL_SIZE: getUserTotalSize,
|
GET_USER_TOTAL_SIZE: getUserTotalSize,
|
||||||
|
|
||||||
REMOVE_DOCUMENT: removeDocument,
|
REMOVE_DOCUMENT: removeDocument,
|
||||||
|
|
||||||
|
GET_ALL_INVITATIONS: getInvitations,
|
||||||
|
CREATE_INVITATION: createInvitation,
|
||||||
|
DELETE_INVITATION: deleteInvitation,
|
||||||
|
|
||||||
|
GET_ALL_USERS: getKnownUsers,
|
||||||
|
ADD_KNOWN_USER: addKnownUser,
|
||||||
|
DELETE_KNOWN_USER: deleteKnownUser,
|
||||||
|
UPDATE_KNOWN_USER: updateKnownUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
// addFirstAdmin is an anon_rpc command
|
// addFirstAdmin is an anon_rpc command
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
/* globals Buffer*/
|
/* globals Buffer*/
|
||||||
const Block = module.exports;
|
const Block = module.exports;
|
||||||
const Nacl = require("tweetnacl/nacl-fast");
|
const Nacl = require("tweetnacl/nacl-fast");
|
||||||
const nThen = require("nthen");
|
const nThen = require("nthen");
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
const BlockStore = require("../storage/block");
|
const BlockStore = require("../storage/block");
|
||||||
|
const Invitation = require("./invitation");
|
||||||
|
const Users = require("./users");
|
||||||
|
|
||||||
var isString = s => typeof(s) === 'string';
|
var isString = s => typeof(s) === 'string';
|
||||||
Block.isValidBlockId = id => {
|
Block.isValidBlockId = id => {
|
||||||
|
@ -109,13 +110,21 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
|
||||||
|
|
||||||
Block.writeLoginBlock = function (Env, msg, _cb) {
|
Block.writeLoginBlock = function (Env, msg, _cb) {
|
||||||
var cb = Util.once(Util.mkAsync(_cb));
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
const { publicKey, signature, ciphertext, registrationProof } = msg;
|
const { publicKey, signature, ciphertext, registrationProof, userData, inviteToken, isSSO } = msg;
|
||||||
|
|
||||||
var previousKey;
|
var previousKey;
|
||||||
var validatedBlock, path;
|
var validatedBlock, path;
|
||||||
|
var validatedInvite;
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
|
if (!inviteToken) { return; }
|
||||||
|
Invitation.check(Env, inviteToken, w((err, state) => {
|
||||||
|
if (err || !state) { return; } // Invalid token, don't abort, check registration proof
|
||||||
|
validatedInvite = true;
|
||||||
|
}));
|
||||||
|
}).nThen(function (w) {
|
||||||
if (!Env.restrictRegistration) { return; }
|
if (!Env.restrictRegistration) { return; }
|
||||||
if (!registrationProof) {
|
var ssoAllowed = isSSO && !Env.restrictSsoRegistration;
|
||||||
|
if (!(registrationProof || validatedInvite || ssoAllowed)) {
|
||||||
// we allow users with existing blocks to create new ones
|
// we allow users with existing blocks to create new ones
|
||||||
// call back with error if registration is restricted and no proof of an existing block was provided
|
// call back with error if registration is restricted and no proof of an existing block was provided
|
||||||
w.abort();
|
w.abort();
|
||||||
|
@ -124,6 +133,7 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
|
||||||
});
|
});
|
||||||
return cb("E_RESTRICTED");
|
return cb("E_RESTRICTED");
|
||||||
}
|
}
|
||||||
|
if (!registrationProof) { return; }
|
||||||
Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) {
|
Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) {
|
||||||
if (err || !provenKey) { // double check that a key was validated
|
if (err || !provenKey) { // double check that a key was validated
|
||||||
w.abort();
|
w.abort();
|
||||||
|
@ -162,8 +172,47 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
|
||||||
path: path,
|
path: path,
|
||||||
});
|
});
|
||||||
cb(err);
|
cb(err);
|
||||||
|
if (!err && registrationProof) {
|
||||||
|
Users.checkUpdate(Env, userData, publicKey, (err) => {
|
||||||
|
if (!err) { return; }
|
||||||
|
Env.Log.error('UPDATE_KNOWN_USER', {
|
||||||
|
userData,
|
||||||
|
publicKey
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validatedInvite) {
|
||||||
|
Invitation.use(Env, inviteToken, publicKey, userData, (err) => {
|
||||||
|
if (!err) { return; }
|
||||||
|
Env.Log.error('USE_INVITATION_LINK', {
|
||||||
|
inviteToken,
|
||||||
|
userData,
|
||||||
|
publicKey
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (isSSO && !Env.dontStoreSSOUsers && !registrationProof) {
|
||||||
|
let edPublic = Array.isArray(userData) && userData[1];
|
||||||
|
let name = Array.isArray(userData) && userData[0];
|
||||||
|
if (!edPublic) { return; }
|
||||||
|
let data = {
|
||||||
|
block: publicKey,
|
||||||
|
name,
|
||||||
|
edPublic,
|
||||||
|
type: 'sso',
|
||||||
|
alias: name
|
||||||
|
};
|
||||||
|
Users.add(Env, edPublic, data, null, (err) => {
|
||||||
|
if (err) {
|
||||||
|
Env.Log.error('INVITATION_ADD_USER', {
|
||||||
|
error: err,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -176,7 +225,7 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
|
||||||
information, we can just sign some constant and use that as proof.
|
information, we can just sign some constant and use that as proof.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
|
Block.removeLoginBlock = function (Env, publicKey, reason, edPublic, _cb) {
|
||||||
var cb = Util.once(Util.mkAsync(_cb));
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
|
||||||
BlockStore.archive(Env, publicKey, reason, function (err) {
|
BlockStore.archive(Env, publicKey, reason, function (err) {
|
||||||
|
@ -186,5 +235,25 @@ Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
|
||||||
});
|
});
|
||||||
cb(err);
|
cb(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (edPublic && reason !== 'PASSWORD_CHANGE') {
|
||||||
|
Users.delete(Env, edPublic, (err) => {
|
||||||
|
if (err) { Env.Log.error('KNOWN_USER_DELETION_ERROR', { error: err, key: edPublic }); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should also try to remove the SSO data. Errors will be logged
|
||||||
|
// but they don't have to be shown to the user. The account data
|
||||||
|
// is already deleted anyway.
|
||||||
|
|
||||||
|
// If this is NOT a password change, also delete sso user.
|
||||||
|
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
|
||||||
|
|
||||||
|
if (!SSOUtils) { return; }
|
||||||
|
if (reason !== 'PASSWORD_CHANGE') {
|
||||||
|
SSOUtils.deleteAccount(Env, publicKey, () => {});
|
||||||
|
} else {
|
||||||
|
SSOUtils.deleteBlock(Env, publicKey, () => {});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Channel = module.exports;
|
const Channel = module.exports;
|
||||||
|
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
/* globals process */
|
/* globals process */
|
||||||
const Core = module.exports;
|
const Core = module.exports;
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const Invitation = module.exports;
|
||||||
|
|
||||||
|
const Invite = require('../storage/invite');
|
||||||
|
const Util = require("../common-util");
|
||||||
|
const Users = require("./users");
|
||||||
|
|
||||||
|
const getUid = () => {
|
||||||
|
return Util.uid() + Util.uid() + Util.uid();
|
||||||
|
};
|
||||||
|
|
||||||
|
Invitation.getAll = (Env, cb) => {
|
||||||
|
Invite.getAll(Env, (err, data) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb(null, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invitation.create = (Env, alias, email, _cb, unsafeKey) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
const id = getUid();
|
||||||
|
const invitation = {
|
||||||
|
alias,
|
||||||
|
email,
|
||||||
|
createdBy: unsafeKey,
|
||||||
|
time: +new Date()
|
||||||
|
};
|
||||||
|
Invite.write(Env, id, invitation, (err) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb(null, id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invitation.delete = (Env, id, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
Invite.delete(Env, id, (err) => {
|
||||||
|
if (err && err !== 'ENOENT') { return void cb(err); }
|
||||||
|
cb(void 0, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invitation.check = (Env, id, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
Invite.read(Env, id, (err) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb(void 0, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invitation.use = (Env, id, blockId, userData, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
Invite.read(Env, id, (err, _data) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
|
||||||
|
let data = Util.clone(_data);
|
||||||
|
if (!Array.isArray(userData)) { userData = []; }
|
||||||
|
let name = userData[0];
|
||||||
|
let edPublic = userData[1];
|
||||||
|
data.block = blockId;
|
||||||
|
data.name = name;
|
||||||
|
data.edPublic = edPublic;
|
||||||
|
data.type = 'invite:' + id;
|
||||||
|
let adminKey = data.createdBy;
|
||||||
|
if (!Env.dontStoreInvitedUsers) {
|
||||||
|
Users.add(Env, edPublic, data, adminKey, (err) => {
|
||||||
|
if (err) {
|
||||||
|
Env.Log.error('INVITATION_ADD_USER', {
|
||||||
|
error: err,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Invite.delete(Env, id, (err) => {
|
||||||
|
if (err) {
|
||||||
|
Env.Log.error('INVITATION_DELETE_USE', {
|
||||||
|
error: err,
|
||||||
|
id: id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Data = module.exports;
|
const Data = module.exports;
|
||||||
|
|
||||||
const Meta = require("../metadata");
|
const Meta = require("../metadata");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Core = require("./core");
|
const Core = require("./core");
|
||||||
|
|
||||||
const Pinning = module.exports;
|
const Pinning = module.exports;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
/* globals Buffer*/
|
/* globals Buffer*/
|
||||||
const Quota = module.exports;
|
const Quota = module.exports;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Upload = module.exports;
|
const Upload = module.exports;
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
const Pinning = require("./pin-rpc");
|
const Pinning = require("./pin-rpc");
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const Users = module.exports;
|
||||||
|
|
||||||
|
const User = require('../storage/user');
|
||||||
|
const Util = require("../common-util");
|
||||||
|
|
||||||
|
Users.getAll = (Env, cb) => {
|
||||||
|
User.getAll(Env, (err, data) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb(null, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Users.add = (Env, edPublic, data, adminKey, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
data.createdBy = adminKey;
|
||||||
|
data.time = +new Date();
|
||||||
|
const safeKey = Util.escapeKeyCharacters(edPublic);
|
||||||
|
User.write(Env, safeKey, data, (err) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Users.delete = (Env, id, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
User.delete(Env, id, (err) => {
|
||||||
|
if (err && err !== 'ENOENT') { return void cb(err); }
|
||||||
|
cb(void 0, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Users.read = (Env, edPublic, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
User.read(Env, edPublic, (err, data) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
cb(void 0, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Users.update = (Env, edPublic, changes, _cb) => {
|
||||||
|
const cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
Users.read(Env, edPublic, (err, data) => {
|
||||||
|
if (err === 'ENOENT') { return void cb(); }
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
if (typeof(changes) !== "object") { return void cb('EINVAL'); }
|
||||||
|
// User exists, update their data
|
||||||
|
var aborted = Object.keys(changes || {}).some((key) => {
|
||||||
|
if (changes[key] === false) {
|
||||||
|
delete data[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (String(changes[key]).length > 300) {
|
||||||
|
cb('E_TOO_LONG');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
data[key] = changes[key];
|
||||||
|
});
|
||||||
|
if (aborted) { return; }
|
||||||
|
User.update(Env, edPublic, data, cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// On password change, update the block
|
||||||
|
Users.checkUpdate = (Env, userData, newBlock, cb) => {
|
||||||
|
if (!Array.isArray(userData)) { userData = []; }
|
||||||
|
let edPublic = userData[1];
|
||||||
|
if (!edPublic) { return void cb('INVALID_PUBLIC_KEY'); }
|
||||||
|
Users.read(Env, edPublic, (err, data) => {
|
||||||
|
if (err === 'ENOENT') { return void cb(); }
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
// User exists, update their block
|
||||||
|
data.block = newBlock;
|
||||||
|
User.update(Env, edPublic, data, cb);
|
||||||
|
});
|
||||||
|
};
|
|
@ -10,6 +10,7 @@ var Core = require("./commands/core");
|
||||||
IMPLEMENTED:
|
IMPLEMENTED:
|
||||||
|
|
||||||
RESTRICT_REGISTRATION(<boolean>)
|
RESTRICT_REGISTRATION(<boolean>)
|
||||||
|
RESTRICT_SSO_REGISTRATION(<boolean>)
|
||||||
UPDATE_DEFAULT_STORAGE(<number>)
|
UPDATE_DEFAULT_STORAGE(<number>)
|
||||||
|
|
||||||
// QUOTA MANAGEMENT
|
// QUOTA MANAGEMENT
|
||||||
|
@ -107,8 +108,14 @@ var makeBooleanSetter = function (attr) {
|
||||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
|
||||||
commands.ENABLE_EMBEDDING = makeBooleanSetter('enableEmbedding');
|
commands.ENABLE_EMBEDDING = makeBooleanSetter('enableEmbedding');
|
||||||
|
|
||||||
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENFORCE_MFA', [true]]], console.log)
|
||||||
|
commands.ENFORCE_MFA = makeBooleanSetter('enforceMFA');
|
||||||
|
|
||||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
|
||||||
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');
|
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');
|
||||||
|
commands.RESTRICT_SSO_REGISTRATION = makeBooleanSetter('restrictSsoRegistration');
|
||||||
|
commands.DISABLE_STORE_INVITED_USERS = makeBooleanSetter('dontStoreInvitedUsers');
|
||||||
|
commands.DISABLE_STORE_SSO_USERS = makeBooleanSetter('dontStoreSSOUsers');
|
||||||
|
|
||||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_EVICTION', [true]]], console.log)
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_EVICTION', [true]]], console.log)
|
||||||
commands.DISABLE_INTEGRATED_EVICTION = makeBooleanSetter('disableIntegratedEviction');
|
commands.DISABLE_INTEGRATED_EVICTION = makeBooleanSetter('disableIntegratedEviction');
|
||||||
|
@ -466,7 +473,7 @@ Decrees.load = function (Env, _cb) {
|
||||||
Decrees.write = function (Env, decree, _cb) {
|
Decrees.write = function (Env, decree, _cb) {
|
||||||
var path = Path.join(Env.paths.decree, 'decree.ndjson');
|
var path = Path.join(Env.paths.decree, 'decree.ndjson');
|
||||||
Env.scheduleDecree.ordered('', function (next) {
|
Env.scheduleDecree.ordered('', function (next) {
|
||||||
var cb = Util.both(_cb, next);
|
var cb = Util.both(Util.mkAsync(_cb), next);
|
||||||
Fs.appendFile(path, JSON.stringify(decree) + '\n', cb);
|
Fs.appendFile(path, JSON.stringify(decree) + '\n', cb);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,10 +52,6 @@ Default.padContentSecurity = function (Env) {
|
||||||
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
|
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
Default.diagramContentSecurity = function (Env) {
|
|
||||||
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
Default.httpHeaders = function (Env) {
|
Default.httpHeaders = function (Env) {
|
||||||
return {
|
return {
|
||||||
"X-XSS-Protection": "1; mode=block",
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
/* globals process */
|
/* globals process */
|
||||||
|
|
||||||
const Crypto = require('crypto');
|
const Crypto = require('crypto');
|
||||||
|
@ -18,6 +17,8 @@ const Package = require("../package.json");
|
||||||
const Default = require("./defaults");
|
const Default = require("./defaults");
|
||||||
const Path = require("path");
|
const Path = require("path");
|
||||||
|
|
||||||
|
const plugins = require('./plugin-manager');
|
||||||
|
|
||||||
const Nacl = require("tweetnacl/nacl-fast");
|
const Nacl = require("tweetnacl/nacl-fast");
|
||||||
|
|
||||||
var canonicalizeOrigin = function (s) {
|
var canonicalizeOrigin = function (s) {
|
||||||
|
@ -83,6 +84,7 @@ module.exports.create = function (config) {
|
||||||
const curve = Nacl.box.keyPair();
|
const curve = Nacl.box.keyPair();
|
||||||
|
|
||||||
const Env = {
|
const Env = {
|
||||||
|
plugins: plugins,
|
||||||
logFeedback: Boolean(config.logFeedback),
|
logFeedback: Boolean(config.logFeedback),
|
||||||
mainPages: config.mainPages || Default.mainPages(),
|
mainPages: config.mainPages || Default.mainPages(),
|
||||||
|
|
||||||
|
@ -227,6 +229,9 @@ module.exports.create = function (config) {
|
||||||
evictionReport: {},
|
evictionReport: {},
|
||||||
commandTimers: {},
|
commandTimers: {},
|
||||||
|
|
||||||
|
sso: config.sso,
|
||||||
|
enforceMFA: config.enforceMFA,
|
||||||
|
|
||||||
// initialized as undefined
|
// initialized as undefined
|
||||||
bearerSecret: void 0,
|
bearerSecret: void 0,
|
||||||
curvePrivate: curve.secretKey,
|
curvePrivate: curve.secretKey,
|
||||||
|
|
|
@ -200,6 +200,11 @@ var evictArchived = function (Env, cb) {
|
||||||
|
|
||||||
// but if it's been stored for the configured time...
|
// but if it's been stored for the configured time...
|
||||||
// expire it
|
// expire it
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
if (item.channel.length === 32) { removed++; }
|
||||||
|
else if (item.channel.length === 44) { accounts++; }
|
||||||
|
return void Log.info("EVICT_ARCHIVED_CHANNEL_DRY_RUN", item.channel, cb);
|
||||||
|
}
|
||||||
store.removeArchivedChannel(item.channel, w(function (err) {
|
store.removeArchivedChannel(item.channel, w(function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', {
|
return Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', {
|
||||||
|
@ -245,7 +250,11 @@ var evictArchived = function (Env, cb) {
|
||||||
Log.error("EVICT_BLOB_LIST_ARCHIVED_PROOF_ERROR", err);
|
Log.error("EVICT_BLOB_LIST_ARCHIVED_PROOF_ERROR", err);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
if (item && item.mtime > retentionTime) { return void next(); }
|
if (item && item.ctime > retentionTime) { return void next(); }
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
removed++;
|
||||||
|
return void Log.info("EVICT_ARCHIVED_BLOB_PROOF_DRY_RUN", item, next);
|
||||||
|
}
|
||||||
blobs.remove.archived.proof(item.safeKey, item.blobId, (function (err) {
|
blobs.remove.archived.proof(item.safeKey, item.blobId, (function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
Log.error("EVICT_ARCHIVED_BLOB_PROOF_ERROR", item);
|
Log.error("EVICT_ARCHIVED_BLOB_PROOF_ERROR", item);
|
||||||
|
@ -272,7 +281,11 @@ var evictArchived = function (Env, cb) {
|
||||||
Log.error("EVICT_BLOB_LIST_ARCHIVED_BLOBS_ERROR", err);
|
Log.error("EVICT_BLOB_LIST_ARCHIVED_BLOBS_ERROR", err);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
if (item && item.mtime > retentionTime) { return void next(); }
|
if (item && item.ctime > retentionTime) { return void next(); }
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
removed++;
|
||||||
|
return void Log.info("EVICT_ARCHIVED_BLOB_DRY_RUN", item, next);
|
||||||
|
}
|
||||||
blobs.remove.archived.blob(item.blobId, function (err) {
|
blobs.remove.archived.blob(item.blobId, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
Log.error("EVICT_ARCHIVED_BLOB_ERROR", item);
|
Log.error("EVICT_ARCHIVED_BLOB_ERROR", item);
|
||||||
|
@ -288,6 +301,7 @@ var evictArchived = function (Env, cb) {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Env.DRY_RUN) { Env.Log.info('DRY RUN'); }
|
||||||
nThen(loadStorage)
|
nThen(loadStorage)
|
||||||
.nThen(migrateIncorrectBlobs)
|
.nThen(migrateIncorrectBlobs)
|
||||||
.nThen(removeArchivedChannels)
|
.nThen(removeArchivedChannels)
|
||||||
|
@ -544,6 +558,9 @@ module.exports = function (Env, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the pin logs of inactive accounts if inactive account removal is configured
|
// remove the pin logs of inactive accounts if inactive account removal is configured
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
return void Log.info("EVICT_INACTIVE_ACCOUNT_DRY_RUN", id, next);
|
||||||
|
}
|
||||||
pinStore.archiveChannel(id, undefined, function (err) {
|
pinStore.archiveChannel(id, undefined, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return Log.error('EVICT_INACTIVE_ACCOUNT_PIN_LOG', err, next);
|
return Log.error('EVICT_INACTIVE_ACCOUNT_PIN_LOG', err, next);
|
||||||
|
@ -602,7 +619,12 @@ module.exports = function (Env, cb) {
|
||||||
// unless we address this race condition with this last-minute double-check
|
// unless we address this race condition with this last-minute double-check
|
||||||
if (item.mtime > inactiveTime) { return void next(); }
|
if (item.mtime > inactiveTime) { return void next(); }
|
||||||
|
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
removed++;
|
removed++;
|
||||||
|
return void Log.info("EVICT_ARCHIVE_BLOB_DRY_RUN", {
|
||||||
|
item: item,
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
blobs.archive.blob(item.blobId, 'INACTIVE', function (err) {
|
blobs.archive.blob(item.blobId, 'INACTIVE', function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return Log.error("EVICT_ARCHIVE_BLOB_ERROR", {
|
return Log.error("EVICT_ARCHIVE_BLOB_ERROR", {
|
||||||
|
@ -610,6 +632,7 @@ module.exports = function (Env, cb) {
|
||||||
item: item,
|
item: item,
|
||||||
}, next);
|
}, next);
|
||||||
}
|
}
|
||||||
|
removed++;
|
||||||
Log.info("EVICT_ARCHIVE_BLOB", {
|
Log.info("EVICT_ARCHIVE_BLOB", {
|
||||||
item: item,
|
item: item,
|
||||||
}, next);
|
}, next);
|
||||||
|
@ -658,6 +681,10 @@ module.exports = function (Env, cb) {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
removed++;
|
||||||
|
return void Log.info("EVICT_BLOB_PROOF_LONELY_DRY_RUN", item, next);
|
||||||
|
}
|
||||||
blobs.remove.proof(item.safeKey, item.blobId, function (err) {
|
blobs.remove.proof(item.safeKey, item.blobId, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return Log.error("EVICT_BLOB_PROOF_LONELY_ERROR", item, next);
|
return Log.error("EVICT_BLOB_PROOF_LONELY_ERROR", item, next);
|
||||||
|
@ -698,6 +725,9 @@ module.exports = function (Env, cb) {
|
||||||
// check if the database has any ephemeral channels
|
// check if the database has any ephemeral channels
|
||||||
// if it does it's because of a bug, and they should be removed
|
// if it does it's because of a bug, and they should be removed
|
||||||
if (item.channel.length === 34) {
|
if (item.channel.length === 34) {
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
return void Log.info("EVICT_EPHEMERAL_DRY_RUN", item.channel, cb);
|
||||||
|
}
|
||||||
return void store.removeChannel(item.channel, w(function (err) {
|
return void store.removeChannel(item.channel, w(function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', {
|
return Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', {
|
||||||
|
@ -728,6 +758,11 @@ module.exports = function (Env, cb) {
|
||||||
// else fall through to the archival
|
// else fall through to the archival
|
||||||
}));
|
}));
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
|
if (Env.DRY_RUN) {
|
||||||
|
archived++;
|
||||||
|
w.abort();
|
||||||
|
return void Log.info("EVICT_CHANNEL_ARCHIVAL_DRY_RUN", item.channel, cb);
|
||||||
|
}
|
||||||
return void store.archiveChannel(item.channel, 'INACTIVE', w(function (err) {
|
return void store.archiveChannel(item.channel, 'INACTIVE', w(function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', {
|
Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', {
|
||||||
|
@ -736,8 +771,8 @@ module.exports = function (Env, cb) {
|
||||||
}, w());
|
}, w());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel, w());
|
|
||||||
archived++;
|
archived++;
|
||||||
|
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel, w());
|
||||||
}));
|
}));
|
||||||
}).nThen(cb);
|
}).nThen(cb);
|
||||||
};
|
};
|
||||||
|
@ -754,6 +789,7 @@ module.exports = function (Env, cb) {
|
||||||
store.listChannels(handler, w(done), true); // using a hacky "fast mode" since we only need the channel id
|
store.listChannels(handler, w(done), true); // using a hacky "fast mode" since we only need the channel id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Env.DRY_RUN) { Env.Log.info('DRY RUN'); }
|
||||||
nThen(loadStorage)
|
nThen(loadStorage)
|
||||||
|
|
||||||
// iterate over all documents and add them to a bloom filter if they have been active
|
// iterate over all documents and add them to a bloom filter if they have been active
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
|
|
||||||
const nThen = require('nthen');
|
const nThen = require('nthen');
|
||||||
const RPC = require("./rpc");
|
const RPC = require("./rpc");
|
||||||
const HK = require("./hk-util.js");
|
const HK = require("./hk-util.js");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
/* global Buffer */
|
/* global Buffer */
|
||||||
var HK = module.exports;
|
var HK = module.exports;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
var Nacl = require("tweetnacl/nacl-fast");
|
var Nacl = require("tweetnacl/nacl-fast");
|
||||||
var Util = require('./common-util.js');
|
var Util = require('./common-util.js');
|
||||||
|
const plugins = require("./plugin-manager");
|
||||||
|
|
||||||
var Challenge = require("./storage/challenge.js");
|
var Challenge = require("./storage/challenge.js");
|
||||||
// C.read(Env, id, cb)
|
// C.read(Env, id, cb)
|
||||||
|
@ -66,17 +67,26 @@ var COMMANDS = {};
|
||||||
// and to authenticate new sessions once a TOTP secret has been associated with their account,
|
// and to authenticate new sessions once a TOTP secret has been associated with their account,
|
||||||
const NOAUTH = require("./challenge-commands/base.js");
|
const NOAUTH = require("./challenge-commands/base.js");
|
||||||
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
|
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
|
||||||
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK;
|
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK; // Account creation + password change
|
||||||
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
|
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
|
||||||
|
|
||||||
const TOTP = require("./challenge-commands/totp.js");
|
const TOTP = require("./challenge-commands/totp.js");
|
||||||
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
|
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
|
||||||
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
|
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
|
||||||
COMMANDS.TOTP_CHECK = TOTP.TOTP_CHECK;
|
COMMANDS.TOTP_MFA_CHECK = TOTP.TOTP_MFA_CHECK;
|
||||||
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
|
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
|
||||||
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK;
|
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK; // Password change only for now (v5.5.0)
|
||||||
COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK;
|
COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// SSO plugin may not be installed
|
||||||
|
const SSO = plugins.SSO && plugins.SSO.challenge;
|
||||||
|
COMMANDS.SSO_AUTH = SSO.SSO_AUTH;
|
||||||
|
COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB;
|
||||||
|
COMMANDS.SSO_WRITE_BLOCK = SSO.SSO_WRITE_BLOCK; // Account creation only
|
||||||
|
COMMANDS.SSO_UPDATE_BLOCK = SSO.SSO_UPDATE_BLOCK; // Password change
|
||||||
|
COMMANDS.SSO_VALIDATE = SSO.SSO_VALIDATE;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
||||||
|
|
||||||
|
@ -145,7 +155,7 @@ var handleCommand = function (Env, req, res) {
|
||||||
date: date,
|
date: date,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}, req);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Env.Log.error("CHALLENGE_COMMAND_THROWN_ERROR", {
|
Env.Log.error("CHALLENGE_COMMAND_THROWN_ERROR", {
|
||||||
error: Util.serializeError(err),
|
error: Util.serializeError(err),
|
||||||
|
@ -295,7 +305,7 @@ var handleResponse = function (Env, req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.status(200).json(content);
|
res.status(200).json(content);
|
||||||
});
|
}, req, res);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,19 @@ const Logger = require("./log");
|
||||||
const AuthCommands = require("./http-commands");
|
const AuthCommands = require("./http-commands");
|
||||||
const MFA = require("./storage/mfa");
|
const MFA = require("./storage/mfa");
|
||||||
const Sessions = require("./storage/sessions");
|
const Sessions = require("./storage/sessions");
|
||||||
|
const cookieParser = require("cookie-parser");
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
const BlobStore = require("./storage/blob");
|
const BlobStore = require("./storage/blob");
|
||||||
const BlockStore = require("./storage/block");
|
const BlockStore = require("./storage/block");
|
||||||
|
const plugins = require("./plugin-manager");
|
||||||
|
|
||||||
const DEFAULT_QUERY_TIMEOUT = 5000;
|
const DEFAULT_QUERY_TIMEOUT = 5000;
|
||||||
const PID = process.pid;
|
const PID = process.pid;
|
||||||
|
|
||||||
|
let SSOUtils = plugins.SSO && plugins.SSO.utils;
|
||||||
|
|
||||||
var Env = JSON.parse(process.env.Env);
|
var Env = JSON.parse(process.env.Env);
|
||||||
|
Env.plugins = plugins;
|
||||||
const response = Util.response(function (errLabel, info) {
|
const response = Util.response(function (errLabel, info) {
|
||||||
if (!Env.Log) { return; }
|
if (!Env.Log) { return; }
|
||||||
Env.Log.error(errLabel, info);
|
Env.Log.error(errLabel, info);
|
||||||
|
@ -64,6 +70,7 @@ EVENTS.ENV_UPDATE = function (data /*, cb */) {
|
||||||
try {
|
try {
|
||||||
Env = JSON.parse(data);
|
Env = JSON.parse(data);
|
||||||
Env.Log = Log;
|
Env.Log = Log;
|
||||||
|
Env.plugins = plugins;
|
||||||
Env.incrementBytesWritten = function () {};
|
Env.incrementBytesWritten = function () {};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
|
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
|
||||||
|
@ -127,8 +134,6 @@ var getHeaders = function (Env, type) {
|
||||||
var csp;
|
var csp;
|
||||||
if (type === 'office') {
|
if (type === 'office') {
|
||||||
csp = Default.padContentSecurity(Env);
|
csp = Default.padContentSecurity(Env);
|
||||||
} else if (type === 'diagram') {
|
|
||||||
csp = Default.diagramContentSecurity(Env);
|
|
||||||
} else {
|
} else {
|
||||||
csp = Default.contentSecurity(Env);
|
csp = Default.contentSecurity(Env);
|
||||||
}
|
}
|
||||||
|
@ -151,8 +156,6 @@ var setHeaders = function (req, res) {
|
||||||
type = 'office';
|
type = 'office';
|
||||||
} else if (/^\/api\/(broadcast|config)/.test(req.url)) {
|
} else if (/^\/api\/(broadcast|config)/.test(req.url)) {
|
||||||
type = 'api';
|
type = 'api';
|
||||||
} else if (/^\/components\/drawio\/src\/main\/webapp\/index.html.*$/.test(req.url)) {
|
|
||||||
type = 'diagram';
|
|
||||||
} else {
|
} else {
|
||||||
type = 'standard';
|
type = 'standard';
|
||||||
}
|
}
|
||||||
|
@ -165,6 +168,11 @@ const Express = require("express");
|
||||||
Express.static.mime.define({'application/wasm': ['wasm']});
|
Express.static.mime.define({'application/wasm': ['wasm']});
|
||||||
var app = Express();
|
var app = Express();
|
||||||
|
|
||||||
|
app.use(bodyParser.urlencoded({
|
||||||
|
extended: true
|
||||||
|
}));
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
if (!Env.logFeedback) { return; }
|
if (!Env.logFeedback) { return; }
|
||||||
|
|
||||||
|
@ -182,7 +190,8 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
|
||||||
|
|
||||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||||
|
|
||||||
var proxyTarget = new URL('', `ws:${Env.httpAddress}`);
|
var httpAddress = Env.httpAddress === '::' ? 'localhost' : Env.httpAddress;
|
||||||
|
var proxyTarget = new URL('', `ws:${httpAddress}`);
|
||||||
proxyTarget.port = Env.websocketPort;
|
proxyTarget.port = Env.websocketPort;
|
||||||
|
|
||||||
const wsProxy = createProxyMiddleware({
|
const wsProxy = createProxyMiddleware({
|
||||||
|
@ -200,6 +209,30 @@ const wsProxy = createProxyMiddleware({
|
||||||
|
|
||||||
app.use('/cryptpad_websocket', wsProxy);
|
app.use('/cryptpad_websocket', wsProxy);
|
||||||
|
|
||||||
|
app.use('/ssoauth', (req, res, next) => {
|
||||||
|
if (SSOUtils && req && req.body && req.body.SAMLResponse) {
|
||||||
|
req.method = 'GET';
|
||||||
|
|
||||||
|
let token = Util.uid();
|
||||||
|
let smres = req.body.SAMLResponse;
|
||||||
|
return SSOUtils.writeRequest(Env, {
|
||||||
|
id: token,
|
||||||
|
type: 'saml',
|
||||||
|
content: smres
|
||||||
|
}, (err) => {
|
||||||
|
if (err) {
|
||||||
|
Log.error('E_SSO_WRITE_REQ', err);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
}
|
||||||
|
let value = `samltoken="${token}"; SameSite=Strict; HttpOnly`;
|
||||||
|
res.setHeader('Set-Cookie', value);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
app.use('/blob', function (req, res, next) {
|
app.use('/blob', function (req, res, next) {
|
||||||
/* Head requests are used to check the size of a blob.
|
/* Head requests are used to check the size of a blob.
|
||||||
|
@ -265,6 +298,7 @@ app.use(function (req, res, next) {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// serve custom app content from the customize directory
|
// serve custom app content from the customize directory
|
||||||
// useful for testing pages customized with opengraph data
|
// useful for testing pages customized with opengraph data
|
||||||
app.use(Express.static(Path.resolve('./customize/www')));
|
app.use(Express.static(Path.resolve('./customize/www')));
|
||||||
|
@ -303,7 +337,7 @@ app.use('/block/', function (req, res, next) {
|
||||||
|
|
||||||
var authorization = req.headers.authorization;
|
var authorization = req.headers.authorization;
|
||||||
|
|
||||||
var mfa_params;
|
var mfa_params, sso_params;
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// First, check whether the block id in question has any MFA settings stored
|
// First, check whether the block id in question has any MFA settings stored
|
||||||
MFA.read(Env, name, w(function (err, content) {
|
MFA.read(Env, name, w(function (err, content) {
|
||||||
|
@ -312,8 +346,7 @@ app.use('/block/', function (req, res, next) {
|
||||||
// in either case you can abort and fall through
|
// in either case you can abort and fall through
|
||||||
// allowing the static webserver to handle either case
|
// allowing the static webserver to handle either case
|
||||||
if (err && err.code === 'ENOENT') {
|
if (err && err.code === 'ENOENT') {
|
||||||
w.abort();
|
return;
|
||||||
return void next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we're not expecting other errors. the sensible thing is to fail
|
// we're not expecting other errors. the sensible thing is to fail
|
||||||
|
@ -344,10 +377,31 @@ app.use('/block/', function (req, res, next) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Same for SSO settings
|
||||||
|
if (!SSOUtils) { return; }
|
||||||
|
SSOUtils.readBlock(Env, name, w(function (err, content) {
|
||||||
|
if (err && (err.code === 'ENOENT' || err === 'ENOENT')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
Log.error('GET_BLOCK_METADATA', err);
|
||||||
|
return void res.status(500).json({
|
||||||
|
code: 500,
|
||||||
|
error: "UNEXPECTED_ERROR",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sso_params = content;
|
||||||
|
}));
|
||||||
|
}).nThen(function (w) {
|
||||||
|
if (!mfa_params && !sso_params) {
|
||||||
|
w.abort();
|
||||||
|
next();
|
||||||
|
}
|
||||||
}).nThen(function (w) {
|
}).nThen(function (w) {
|
||||||
// We should only be able to reach this logic
|
// We should only be able to reach this logic
|
||||||
// if we successfully loaded and parsed some JSON
|
// if we successfully loaded and parsed some JSON
|
||||||
// representing the user's MFA settings.
|
// representing the user's MFA and/or SSO settings.
|
||||||
|
|
||||||
// Failures at this point relate to insufficient or incorrect authorization.
|
// Failures at this point relate to insufficient or incorrect authorization.
|
||||||
// This function standardizes how we reject such requests.
|
// This function standardizes how we reject such requests.
|
||||||
|
@ -359,18 +413,19 @@ app.use('/block/', function (req, res, next) {
|
||||||
var no = function () {
|
var no = function () {
|
||||||
w.abort();
|
w.abort();
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
method: mfa_params.method,
|
sso: Boolean(sso_params),
|
||||||
|
method: mfa_params && mfa_params.method,
|
||||||
code: 401
|
code: 401
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// if you are here it is because this block is protected by MFA.
|
// if you are here it is because this block is protected by MFA or SSO.
|
||||||
// they will need to provide a JSON Web Token, so we can reject them outright
|
// they will need to provide a JSON Web Token, so we can reject them outright
|
||||||
// if one is not present in their authorization header
|
// if one is not present in their authorization header
|
||||||
if (!authorization) { return void no(); }
|
if (!authorization) { return void no(); }
|
||||||
|
|
||||||
// The authorization header should be of the form
|
// The authorization header should be of the form
|
||||||
// "Authorization: Bearer <JWT>"
|
// "Authorization: Bearer <SessionId>"
|
||||||
// We can reject the request if it is malformed.
|
// We can reject the request if it is malformed.
|
||||||
let token = authorization.replace(/^Bearer\s+/, '').trim();
|
let token = authorization.replace(/^Bearer\s+/, '').trim();
|
||||||
if (!token) { return void no(); }
|
if (!token) { return void no(); }
|
||||||
|
@ -379,14 +434,18 @@ app.use('/block/', function (req, res, next) {
|
||||||
if (err) {
|
if (err) {
|
||||||
Log.error('SESSION_READ_ERROR', err);
|
Log.error('SESSION_READ_ERROR', err);
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
method: mfa_params.method,
|
sso: Boolean(sso_params),
|
||||||
|
method: mfa_params && mfa_params.method,
|
||||||
code: 401,
|
code: 401,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = Util.tryParse(contentStr);
|
let content = Util.tryParse(contentStr);
|
||||||
|
|
||||||
if (content.mfa && content.mfa.exp && ((+new Date()) > content.mfa.exp)) {
|
if (mfa_params && !content.mfa) { return void no(); }
|
||||||
|
if (sso_params && !content.sso) { return void no(); }
|
||||||
|
|
||||||
|
if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
|
||||||
Log.error("OTP_SESSION_EXPIRED", content.mfa);
|
Log.error("OTP_SESSION_EXPIRED", content.mfa);
|
||||||
Sessions.delete(Env, name, token, function (err) {
|
Sessions.delete(Env, name, token, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -398,12 +457,20 @@ app.use('/block/', function (req, res, next) {
|
||||||
return void no();
|
return void no();
|
||||||
}
|
}
|
||||||
|
|
||||||
// we could also check whether the content of the file matches the token,
|
|
||||||
// but clients don't have any influence over the reference and can only
|
|
||||||
// request to create tokens that are scoped to a public key they control.
|
|
||||||
// I don' think there's any practical benefit to such a check.
|
|
||||||
|
|
||||||
// So, interpret the existence of a file in that location as the continued
|
if (content.sso && content.sso.exp && (+new Date()) > content.sso.exp) {
|
||||||
|
Log.error("SSO_SESSION_EXPIRED", content.sso);
|
||||||
|
Sessions.delete(Env, name, token, function (err) {
|
||||||
|
if (err) {
|
||||||
|
Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.info('SSO_SESSION_DELETE_EXPIRED', err);
|
||||||
|
});
|
||||||
|
return void no();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpret the existence of a file in that location as the continued
|
||||||
// validity of the session. Fall through and let the built-in webserver
|
// validity of the session. Fall through and let the built-in webserver
|
||||||
// handle the 404 or serving the file.
|
// handle the 404 or serving the file.
|
||||||
next();
|
next();
|
||||||
|
@ -481,6 +548,13 @@ var makeRouteCache = function (template, cacheName) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ssoList = Env.sso && Env.sso.enabled && Array.isArray(Env.sso.list) &&
|
||||||
|
Env.sso.list.map(function (obj) { return obj.name; }) || [];
|
||||||
|
const ssoCfg = (SSOUtils && ssoList.length) ? {
|
||||||
|
force: (Env.sso && Env.sso.enforced && 1) || 0,
|
||||||
|
password: (Env.sso && Env.sso.cpPassword && (Env.sso.forceCpPassword ? 2 : 1)) || 0,
|
||||||
|
list: ssoList
|
||||||
|
} : false;
|
||||||
var serveConfig = makeRouteCache(function () {
|
var serveConfig = makeRouteCache(function () {
|
||||||
return [
|
return [
|
||||||
'define(function(){',
|
'define(function(){',
|
||||||
|
@ -501,12 +575,15 @@ var serveConfig = makeRouteCache(function () {
|
||||||
maxUploadSize: Env.maxUploadSize,
|
maxUploadSize: Env.maxUploadSize,
|
||||||
premiumUploadSize: Env.premiumUploadSize,
|
premiumUploadSize: Env.premiumUploadSize,
|
||||||
restrictRegistration: Env.restrictRegistration,
|
restrictRegistration: Env.restrictRegistration,
|
||||||
|
restrictSsoRegistration: Env.restrictSsoRegistration,
|
||||||
httpSafeOrigin: Env.httpSafeOrigin,
|
httpSafeOrigin: Env.httpSafeOrigin,
|
||||||
enableEmbedding: Env.enableEmbedding,
|
enableEmbedding: Env.enableEmbedding,
|
||||||
fileHost: Env.fileHost,
|
fileHost: Env.fileHost,
|
||||||
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
||||||
listMyInstance: Env.listMyInstance,
|
listMyInstance: Env.listMyInstance,
|
||||||
accounts_api: Env.accounts_api,
|
accounts_api: Env.accounts_api,
|
||||||
|
sso: ssoCfg,
|
||||||
|
enforceMFA: Env.enforceMFA
|
||||||
}, null, '\t'),
|
}, null, '\t'),
|
||||||
'});'
|
'});'
|
||||||
].join(';\n');
|
].join(';\n');
|
||||||
|
|
|
@ -46,5 +46,12 @@ if (!isPositiveNumber(config.premiumUploadSize) || config.premiumUploadSize < co
|
||||||
delete config.premiumUploadSize;
|
delete config.premiumUploadSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.sso = {};
|
||||||
|
try {
|
||||||
|
config.sso = require("../config/sso");
|
||||||
|
} catch (e) {
|
||||||
|
//console.log("SSO config not found");
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
var Store = require("./storage/file");
|
var Store = require("./storage/file");
|
||||||
var Util = require("./common-util");
|
var Util = require("./common-util");
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
|
|
||||||
var Pins = module.exports;
|
var Pins = module.exports;
|
||||||
|
|
||||||
const Fs = require("fs");
|
const Fs = require("fs");
|
||||||
|
@ -90,7 +88,6 @@ var createLineHandler = Pins.createLineHandler = function (ref, errorHandler) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ref.surplus = ref.index;
|
ref.surplus = ref.index;
|
||||||
//jshint -W086
|
|
||||||
// fallthrough
|
// fallthrough
|
||||||
}
|
}
|
||||||
case 'PIN': {
|
case 'PIN': {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const plugins = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let pluginsDir = fs.readdirSync(__dirname + '/plugins');
|
||||||
|
pluginsDir.forEach((name) => {
|
||||||
|
if (name=== "README.md") { return; }
|
||||||
|
try {
|
||||||
|
let plugin = require(`./plugins/${name}/index`);
|
||||||
|
plugins[plugin.name] = plugin.modules;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') { console.error(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = plugins;
|
|
@ -4,5 +4,4 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
-->
|
-->
|
||||||
|
|
||||||
* compare your conf against `cryptpad/docs/example.nginx.conf`
|
# CryptPad's plugins directory
|
||||||
*
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Util = require("./common-util");
|
const Util = require("./common-util");
|
||||||
|
|
||||||
const Core = require("./commands/core");
|
const Core = require("./commands/core");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Stats = module.exports;
|
const Stats = module.exports;
|
||||||
|
|
||||||
var truthyStringOrNothing = function (s) {
|
var truthyStringOrNothing = function (s) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ Feel free to migrate all of these to a relational DB at some point in the future
|
||||||
|
|
||||||
const Basic = module.exports;
|
const Basic = module.exports;
|
||||||
const Fs = require("node:fs");
|
const Fs = require("node:fs");
|
||||||
|
const Fse = require("fs-extra");
|
||||||
const Path = require("node:path");
|
const Path = require("node:path");
|
||||||
|
|
||||||
var pathError = (cb) => {
|
var pathError = (cb) => {
|
||||||
|
@ -70,4 +71,17 @@ Basic.deleteDir = function (Env, path, cb) {
|
||||||
Fs.rm(path, { recursive: true, force: true }, cb);
|
Fs.rm(path, { recursive: true, force: true }, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Basic.archive = function (Env, path, archivePath, cb) {
|
||||||
|
Fse.move(path, archivePath, {
|
||||||
|
overwrite: true,
|
||||||
|
}, (err) => {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Basic.restore = function (Env, archivePath, path, cb) {
|
||||||
|
Fse.move(archivePath, path, {
|
||||||
|
//overwrite: true,
|
||||||
|
}, (err) => {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -153,8 +153,12 @@ var clearActivity = function (Env, blobId, cb) {
|
||||||
};
|
};
|
||||||
var updateActivity = function (Env, blobId, cb) {
|
var updateActivity = function (Env, blobId, cb) {
|
||||||
var path = makeActivityPath(Env, blobId);
|
var path = makeActivityPath(Env, blobId);
|
||||||
|
var blobPath = makeBlobPath(Env, blobId);
|
||||||
|
isFile(blobPath, (err, state) => {
|
||||||
|
if (err || !state) { return void cb(); }
|
||||||
var s_data = String(+new Date());
|
var s_data = String(+new Date());
|
||||||
Fs.writeFile(path, s_data, cb);
|
Fs.writeFile(path, s_data, cb);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var archiveActivity = function (Env, blobId, cb) {
|
var archiveActivity = function (Env, blobId, cb) {
|
||||||
|
@ -464,7 +468,7 @@ var makeWalker = function (n, handleChild, done) {
|
||||||
// do no more than 20 jobs at a time
|
// do no more than 20 jobs at a time
|
||||||
var tasks = Semaphore.create(n);
|
var tasks = Semaphore.create(n);
|
||||||
|
|
||||||
var recurse = function (path) {
|
var recurse = function (path, dir) {
|
||||||
tasks.take(function (give) {
|
tasks.take(function (give) {
|
||||||
var next = give(W());
|
var next = give(W());
|
||||||
|
|
||||||
|
@ -477,7 +481,19 @@ var makeWalker = function (n, handleChild, done) {
|
||||||
}
|
}
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
w.abort();
|
w.abort();
|
||||||
return void handleChild(void 0, path, next);
|
if (/\.activity$/.test(path)) {
|
||||||
|
// NOTE: some activity files were created for deleted blobs due to
|
||||||
|
// a bug. We're going to detect them here in order to be able to clean
|
||||||
|
// them.
|
||||||
|
if (!dir.includes(Path.basename(path.replace(/\.activity$/, '')))) {
|
||||||
|
return void handleChild(void 0, path, next, true);
|
||||||
|
}
|
||||||
|
// Ignore valid activity files
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
// Ignore placeholder files
|
||||||
|
if (/\.placeholder$/.test(path)) { return next(); }
|
||||||
|
return void handleChild(void 0, path, next, false);
|
||||||
}
|
}
|
||||||
// fall through
|
// fall through
|
||||||
}));
|
}));
|
||||||
|
@ -487,7 +503,7 @@ var makeWalker = function (n, handleChild, done) {
|
||||||
if (err) { return next(); }
|
if (err) { return next(); }
|
||||||
// everything is fine and it's a directory...
|
// everything is fine and it's a directory...
|
||||||
dir.forEach(function (d) {
|
dir.forEach(function (d) {
|
||||||
recurse(Path.join(path, d));
|
recurse(Path.join(path, d), dir);
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -502,7 +518,8 @@ var listProofs = function (root, handler, cb) {
|
||||||
Fs.readdir(root, function (err, dir) {
|
Fs.readdir(root, function (err, dir) {
|
||||||
if (err) { return void cb(err); }
|
if (err) { return void cb(err); }
|
||||||
|
|
||||||
var walk = makeWalker(20, function (err, path, next) {
|
var walk = makeWalker(20, function (err, path, next, loneActivity) {
|
||||||
|
if (loneActivity) { return void next(); }
|
||||||
// path is the path to a child node on the filesystem
|
// path is the path to a child node on the filesystem
|
||||||
|
|
||||||
// next handles the next job in a queue
|
// next handles the next job in a queue
|
||||||
|
@ -537,12 +554,20 @@ var listProofs = function (root, handler, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getActivityStat = function (path, base, cb) {
|
||||||
|
var suffix = base ? '' : '.activity';
|
||||||
|
Fs.stat(path+suffix, function (err, stats) {
|
||||||
|
if (err && err.code === 'ENOENT' && !base) { return getActivityStat(path, true, cb); }
|
||||||
|
cb(err, stats);
|
||||||
|
});
|
||||||
|
};
|
||||||
var listBlobs = function (root, handler, cb) {
|
var listBlobs = function (root, handler, cb) {
|
||||||
// iterate over files
|
// iterate over files
|
||||||
Fs.readdir(root, function (err, dir) {
|
Fs.readdir(root, function (err, dir) {
|
||||||
if (err) { return void cb(err); }
|
if (err) { return void cb(err); }
|
||||||
var walk = makeWalker(20, function (err, path, next) {
|
var walk = makeWalker(20, function (err, path, next, loneActivity) {
|
||||||
Fs.stat(path, function (err, stats) {
|
if (loneActivity) { return void next(); }
|
||||||
|
getActivityStat(path, false, function (err, stats) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return void handler(err, void 0, next);
|
return void handler(err, void 0, next);
|
||||||
}
|
}
|
||||||
|
@ -565,6 +590,30 @@ var listBlobs = function (root, handler, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var cleanLoneActivity = function (root, cb) {
|
||||||
|
// iterate over files
|
||||||
|
Fs.readdir(root, function (err, dir) {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
var walk = makeWalker(20, function (err, path, next, loneActivity) {
|
||||||
|
if (!loneActivity) { return void next(); }
|
||||||
|
Fs.unlink(path, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return console.error('ERROR', path, err);
|
||||||
|
}
|
||||||
|
console.log('DELETED', path);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}, function () {
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
|
||||||
|
dir.forEach(function (d) {
|
||||||
|
if (d.length !== 2) { return; }
|
||||||
|
walk(Path.join(root, d));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
BlobStore.create = function (config, _cb) {
|
BlobStore.create = function (config, _cb) {
|
||||||
var cb = Util.once(Util.mkAsync(_cb));
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
if (typeof(config.getSession) !== 'function') {
|
if (typeof(config.getSession) !== 'function') {
|
||||||
|
@ -651,6 +700,10 @@ BlobStore.create = function (config, _cb) {
|
||||||
removeArchivedProof(Env, safeKey, blobId, cb);
|
removeArchivedProof(Env, safeKey, blobId, cb);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
loneActivity: function (_cb) {
|
||||||
|
var cb = Util.once(Util.mkAsync(_cb));
|
||||||
|
cleanLoneActivity(Env.blobPath, cb);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
archive: {
|
archive: {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Block = module.exports;
|
const Block = module.exports;
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
const Path = require("path");
|
const Path = require("path");
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*@flow*/
|
/*@flow*/
|
||||||
/* jshint esversion: 6 */
|
|
||||||
/* globals Buffer */
|
/* globals Buffer */
|
||||||
var Fs = require("fs");
|
var Fs = require("fs");
|
||||||
var Fse = require("fs-extra");
|
var Fse = require("fs-extra");
|
||||||
|
@ -268,7 +267,7 @@ var getMetadataAtPath = function (Env, path, _cb) {
|
||||||
// if you can't parse, that's bad
|
// if you can't parse, that's bad
|
||||||
return void cb("INVALID_METADATA");
|
return void cb("INVALID_METADATA");
|
||||||
}
|
}
|
||||||
readMore();
|
readMore(); // eslint-disable-line no-unreachable
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const Basic = require("./basic.js");
|
||||||
|
const Path = require("node:path");
|
||||||
|
const nThen = require('nthen');
|
||||||
|
const Util = require('../common-util');
|
||||||
|
|
||||||
|
const Invite = module.exports;
|
||||||
|
/* This module manages storage used to implement instance invitations when registration
|
||||||
|
is closed. This "database" will store individual invitation and their state.
|
||||||
|
|
||||||
|
An invitation is created with a random uid and an alias (username, email, etc.)
|
||||||
|
Once it is used by the user, their newly created blockId is added which will mark
|
||||||
|
it as completed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pathFromId = function (Env, id) {
|
||||||
|
if (!id || typeof(id) !== 'string') { return void console.error('INVITE_BAD_ID', id); }
|
||||||
|
return Path.join(Env.paths.base, "invitations", id.slice(0, 2), id);
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.read = function (Env, id, cb) {
|
||||||
|
var path = pathFromId(Env, id);
|
||||||
|
Basic.read(Env, path, (err, data) => {
|
||||||
|
if (err) { return void cb(err.code); }
|
||||||
|
cb(void 0, Util.tryParse(data));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.getAll = function (Env, cb) {
|
||||||
|
let invitations = {};
|
||||||
|
|
||||||
|
|
||||||
|
nThen((waitFor) => {
|
||||||
|
let dirPath = Path.join(Env.paths.base, "invitations");
|
||||||
|
Basic.readDir(Env, dirPath, waitFor((err, prefixes) => {
|
||||||
|
if (err && err.code === 'ENOENT') { return void cb(void 0, {}); }
|
||||||
|
if (err) { waitFor.abort(); return void cb(err.code); }
|
||||||
|
prefixes.forEach((prefix) => {
|
||||||
|
var dirPath2 = Path.join(Env.paths.base, "invitations", prefix);
|
||||||
|
Basic.readDir(Env, dirPath2, waitFor((err, files) => {
|
||||||
|
if (err) { waitFor.abort(); return void cb(err.code); }
|
||||||
|
files.forEach((id) => {
|
||||||
|
Invite.read(Env, id, waitFor((err, data) => {
|
||||||
|
invitations[id] = data || { error: err };
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}).nThen(() => {
|
||||||
|
cb(null, invitations);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.write = function (Env, id, data, cb) {
|
||||||
|
var path = pathFromId(Env, id);
|
||||||
|
Basic.write(Env, path, JSON.stringify(data), (err) => {
|
||||||
|
if (err) { return void cb(err.code); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.delete = function (Env, id, cb) {
|
||||||
|
var path = pathFromId(Env, id);
|
||||||
|
Basic.delete(Env, path, (err) => {
|
||||||
|
if (err) { return void cb(err.code); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Invite.update = function (Env, id, data, cb) {
|
||||||
|
Invite.delete(Env, id, (err) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
Invite.write(Env, id, data, cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,19 @@ Sessions.delete = function (Env, id, ref, cb) {
|
||||||
Basic.delete(Env, path, cb);
|
Basic.delete(Env, path, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Sessions.update = function (Env, id, oldId, ref, dataStr, cb) {
|
||||||
|
var data = Util.tryParse(dataStr);
|
||||||
|
Sessions.read(Env, oldId, ref, (err, oldData) => {
|
||||||
|
let content = Util.tryParse(oldData) || {};
|
||||||
|
Object.keys(data || {}).forEach((type) => {
|
||||||
|
content[type] = data[type];
|
||||||
|
});
|
||||||
|
Sessions.delete(Env, oldId, ref, () => {
|
||||||
|
Sessions.write(Env, id, ref, JSON.stringify(content), cb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Sessions.deleteUser = function (Env, id, cb) {
|
Sessions.deleteUser = function (Env, id, cb) {
|
||||||
if (!id || typeof(id) !== 'string') { return; }
|
if (!id || typeof(id) !== 'string') { return; }
|
||||||
id = Util.escapeKeyCharacters(id);
|
id = Util.escapeKeyCharacters(id);
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const Basic = require("./basic");
|
||||||
|
const Path = require("node:path");
|
||||||
|
const Util = require("../common-util");
|
||||||
|
|
||||||
|
const SSO = module.exports;
|
||||||
|
/* This module manages storage related to Single Sign-On (SSO) settings.
|
||||||
|
|
||||||
|
A first part (sso-requests) contains temporary files for sso authentication with a remote service
|
||||||
|
A second part (sso-users) is a database of accounts registered via SSO (SSO id ==> block seed)
|
||||||
|
A third part (sso-blocks) is a database of blocks that are sso-protected (block id ==> SSO id...)
|
||||||
|
|
||||||
|
The path for requests is based on the "authentication request" token depending on the type of SSO.
|
||||||
|
The path for the user database is based on their persistent identifier (id) from the SSO.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var pathFromId = function (Env, id, subPath) {
|
||||||
|
if (!id || typeof(id) !== 'string') { return; }
|
||||||
|
id = Util.escapeKeyCharacters(id);
|
||||||
|
return Path.join(Env.paths.base, subPath, id.slice(0, 2), `${id}.json`);
|
||||||
|
};
|
||||||
|
var reqPathFromId = function (Env, id) {
|
||||||
|
return pathFromId(Env, id, 'sso_request');
|
||||||
|
};
|
||||||
|
var blockPathFromId = function (Env, id) {
|
||||||
|
return pathFromId(Env, id, 'sso_block');
|
||||||
|
};
|
||||||
|
|
||||||
|
var userPathFromId = function (Env, id, provider) {
|
||||||
|
if (!id || typeof(id) !== 'string') { return; }
|
||||||
|
if (!provider || typeof(provider) !== 'string') { return; }
|
||||||
|
id = Util.escapeKeyCharacters(id);
|
||||||
|
return Path.join(Env.paths.base, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
|
||||||
|
};
|
||||||
|
|
||||||
|
var blockArchivePath = function (Env, id) {
|
||||||
|
return Path.join(Env.paths.archive, 'sso_block', id.slice(0, 2), `${id}.json`);
|
||||||
|
};
|
||||||
|
var userArchivePath = function (Env, id, provider) {
|
||||||
|
return Path.join(Env.paths.archive, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Req = SSO.request = {};
|
||||||
|
|
||||||
|
Req.read = function (Env, id, cb) {
|
||||||
|
var path = reqPathFromId(Env, id);
|
||||||
|
Basic.read(Env, path, cb);
|
||||||
|
};
|
||||||
|
Req.write = function (Env, id, data, cb) {
|
||||||
|
var path = reqPathFromId(Env, id);
|
||||||
|
Basic.write(Env, path, data, cb);
|
||||||
|
};
|
||||||
|
Req.delete = function (Env, id, cb) {
|
||||||
|
var path = reqPathFromId(Env, id);
|
||||||
|
Basic.delete(Env, path, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const User = SSO.user = {};
|
||||||
|
|
||||||
|
User.read = function (Env, provider, id, cb) {
|
||||||
|
var path = userPathFromId(Env, id, provider);
|
||||||
|
Basic.read(Env, path, cb);
|
||||||
|
};
|
||||||
|
User.write = function (Env, provider, id, data, cb) {
|
||||||
|
var path = userPathFromId(Env, id, provider);
|
||||||
|
Basic.write(Env, path, data, cb);
|
||||||
|
};
|
||||||
|
User.delete = function (Env, provider, id, cb) {
|
||||||
|
var path = userPathFromId(Env, id, provider);
|
||||||
|
Basic.delete(Env, path, cb);
|
||||||
|
};
|
||||||
|
User.archive = function (Env, provider, id, cb) {
|
||||||
|
var path = userPathFromId(Env, id, provider);
|
||||||
|
var archivePath = userArchivePath(Env, id, provider);
|
||||||
|
Basic.archive(Env, path, archivePath, cb);
|
||||||
|
};
|
||||||
|
User.restore = function (Env, provider, id, cb) {
|
||||||
|
var path = userPathFromId(Env, id, provider);
|
||||||
|
var archivePath = userArchivePath(Env, id, provider);
|
||||||
|
Basic.restore(Env, archivePath, path, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Block = SSO.block = {};
|
||||||
|
|
||||||
|
Block.read = function (Env, id, cb) {
|
||||||
|
var path = blockPathFromId(Env, id);
|
||||||
|
Basic.read(Env, path, cb);
|
||||||
|
};
|
||||||
|
Block.write = function (Env, id, data, cb) {
|
||||||
|
var path = blockPathFromId(Env, id);
|
||||||
|
Basic.write(Env, path, data, cb);
|
||||||
|
};
|
||||||
|
Block.delete = function (Env, id, cb) {
|
||||||
|
var path = blockPathFromId(Env, id);
|
||||||
|
Basic.delete(Env, path, cb);
|
||||||
|
};
|
||||||
|
Block.archive = function (Env, id, cb) {
|
||||||
|
var path = blockPathFromId(Env, id);
|
||||||
|
var archivePath = blockArchivePath(Env, id);
|
||||||
|
Basic.archive(Env, path, archivePath, cb);
|
||||||
|
};
|
||||||
|
Block.restore = function (Env, id, cb) {
|
||||||
|
var path = blockPathFromId(Env, id);
|
||||||
|
var archivePath = blockArchivePath(Env, id);
|
||||||
|
Basic.restore(Env, archivePath, path, cb);
|
||||||
|
};
|
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const Basic = require("./basic.js");
|
||||||
|
const Path = require("node:path");
|
||||||
|
const nThen = require('nthen');
|
||||||
|
const Util = require('../common-util');
|
||||||
|
|
||||||
|
const User = module.exports;
|
||||||
|
/* This module manages storage used to implement user management. "Known users" can
|
||||||
|
be added here in order to store their public key, their block ID and an alias
|
||||||
|
used to recognize them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pathFromId = function (Env, id) {
|
||||||
|
if (!id || typeof(id) !== 'string') { return void console.error('KNWONUSER_BAD_ID', id); }
|
||||||
|
return Path.join(Env.paths.base, "users", id.slice(0, 2), id);
|
||||||
|
};
|
||||||
|
|
||||||
|
User.read = function (Env, id, cb) {
|
||||||
|
var path = pathFromId(Env, id);
|
||||||
|
Basic.read(Env, path, (err, data) => {
|
||||||
|
if (err) { return void cb(err.code); }
|
||||||
|
cb(void 0, Util.tryParse(data));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
User.getAll = function (Env, cb) {
|
||||||
|
let users = {};
|
||||||
|
|
||||||
|
|
||||||
|
nThen((waitFor) => {
|
||||||
|
let dirPath = Path.join(Env.paths.base, "users");
|
||||||
|
Basic.readDir(Env, dirPath, waitFor((err, prefixes) => {
|
||||||
|
if (err && err.code === 'ENOENT') { return void cb(void 0, {}); }
|
||||||
|
if (err) { waitFor.abort(); return void cb(err.code); }
|
||||||
|
prefixes.forEach((prefix) => {
|
||||||
|
var dirPath2 = Path.join(Env.paths.base, "users", prefix);
|
||||||
|
Basic.readDir(Env, dirPath2, waitFor((err, files) => {
|
||||||
|
if (err) { waitFor.abort(); return void cb(err.code); }
|
||||||
|
files.forEach((id) => {
|
||||||
|
User.read(Env, id, waitFor((err, data) => {
|
||||||
|
users[id] = data || { error: err };
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}).nThen(() => {
|
||||||
|
cb(null, users);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
User.write = function (Env, id, data, cb) {
|
||||||
|
var path = pathFromId(Env, id);
|
||||||
|
Basic.write(Env, path, JSON.stringify(data), (err) => {
|
||||||
|
if (err) { return void cb(err.code); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
User.delete = function (Env, id, cb) {
|
||||||
|
var path = pathFromId(Env, id);
|
||||||
|
Basic.delete(Env, path, (err) => {
|
||||||
|
if (err) { return void cb(err.code); }
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
User.update = function (Env, id, data, cb) {
|
||||||
|
User.delete(Env, id, (err) => {
|
||||||
|
if (err) { return void cb(err); }
|
||||||
|
User.write(Env, id, data, cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
/* global Buffer */
|
/* global Buffer */
|
||||||
|
|
||||||
const ToPull = require('stream-to-pull-stream');
|
const ToPull = require('stream-to-pull-stream');
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
/* globals process, Buffer */
|
/* globals process, Buffer */
|
||||||
|
|
||||||
const HK = require("../hk-util");
|
const HK = require("../hk-util");
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6 */
|
|
||||||
/* global process */
|
/* global process */
|
||||||
const Util = require("../common-util");
|
const Util = require("../common-util");
|
||||||
const nThen = require('nthen');
|
const nThen = require('nthen');
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "cryptpad",
|
"name": "cryptpad",
|
||||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
"description": "realtime collaborative visual editor with zero knowledge server",
|
||||||
"version": "5.5.0",
|
"version": "5.7.0",
|
||||||
"license": "AGPL-3.0+",
|
"license": "AGPL-3.0+",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -13,58 +13,62 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcrowe/minibloom": "^0.2.0",
|
"@mcrowe/minibloom": "^0.2.0",
|
||||||
|
"@node-saml/node-saml": "^4.0.5",
|
||||||
|
"alertify.js": "1.0.11",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"bootstrap": "^4.0.0",
|
||||||
|
"bootstrap-tokenfield": "^0.12.0",
|
||||||
|
"chainpad": "^5.2.6",
|
||||||
"chainpad-crypto": "^0.2.5",
|
"chainpad-crypto": "^0.2.5",
|
||||||
|
"chainpad-listmap": "^1.0.0",
|
||||||
|
"chainpad-netflux": "^1.0.0",
|
||||||
"chainpad-server": "^5.1.0",
|
"chainpad-server": "^5.1.0",
|
||||||
|
"ckeditor": "npm:ckeditor4@~4.22.1",
|
||||||
|
"codemirror": "^5.19.0",
|
||||||
|
"components-font-awesome": "^4.6.3",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"croppie": "^2.5.0",
|
||||||
|
"dragula": "3.7.2",
|
||||||
|
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+4",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
|
"file-saver": "1.3.1",
|
||||||
"fs-extra": "^7.0.0",
|
"fs-extra": "^7.0.0",
|
||||||
"get-folder-size": "^2.0.1",
|
"get-folder-size": "^2.0.1",
|
||||||
"netflux-websocket": "^1.0.0",
|
"html2canvas": "^1.4.0",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
|
"hyper-json": "~1.4.0",
|
||||||
|
"jquery": "3.6.0",
|
||||||
|
"json.sortify": "~2.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"jszip": "3.10.1",
|
||||||
|
"localforage": "^1.5.2",
|
||||||
|
"marked": "^4.3.0",
|
||||||
|
"mathjax": "3.0.5",
|
||||||
|
"netflux-websocket": "^1.0.0",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
"nthen": "0.1.8",
|
"nthen": "0.1.8",
|
||||||
|
"open-sans-fontface": "^1.4.0",
|
||||||
|
"openid-client": "^5.4.2",
|
||||||
|
"pako": "^2.1.0",
|
||||||
"prompt-confirm": "^2.0.4",
|
"prompt-confirm": "^2.0.4",
|
||||||
"pull-stream": "^3.6.1",
|
"pull-stream": "^3.6.1",
|
||||||
|
"require-css": "0.1.10",
|
||||||
|
"requirejs": "2.3.5",
|
||||||
|
"requirejs-plugins": "^1.0.2",
|
||||||
"saferphore": "0.0.1",
|
"saferphore": "0.0.1",
|
||||||
|
"scrypt-async": "1.2.0",
|
||||||
|
"sortablejs": "^1.6.0",
|
||||||
"sortify": "^1.0.4",
|
"sortify": "^1.0.4",
|
||||||
"stream-to-pull-stream": "^1.7.2",
|
"stream-to-pull-stream": "^1.7.2",
|
||||||
"thirty-two": "^1.0.2",
|
"thirty-two": "^1.0.2",
|
||||||
"tweetnacl": "~0.12.2",
|
"tweetnacl": "~0.12.2",
|
||||||
"ulimit": "0.0.2",
|
"ulimit": "0.0.2",
|
||||||
"ws": "^3.3.1",
|
"ws": "^3.3.1",
|
||||||
|
|
||||||
"alertify.js": "1.0.11",
|
|
||||||
"bootstrap": "^4.0.0",
|
|
||||||
"bootstrap-tokenfield": "^0.12.0",
|
|
||||||
"chainpad": "^5.2.6",
|
|
||||||
"chainpad-listmap": "^1.0.0",
|
|
||||||
"chainpad-netflux": "^1.0.0",
|
|
||||||
"ckeditor": "npm:ckeditor4@~4.22.1",
|
|
||||||
"codemirror": "^5.19.0",
|
|
||||||
"components-font-awesome": "^4.6.3",
|
|
||||||
"croppie": "^2.5.0",
|
|
||||||
"file-saver": "1.3.1",
|
|
||||||
"hyper-json": "~1.4.0",
|
|
||||||
"jquery": "3.6.0",
|
|
||||||
"json.sortify": "~2.1.0",
|
|
||||||
"jszip": "3.10.1",
|
|
||||||
"dragula": "3.7.2",
|
|
||||||
"html2canvas": "^1.4.0",
|
|
||||||
"localforage": "^1.5.2",
|
|
||||||
"marked": "^4.3.0",
|
|
||||||
"mathjax": "3.0.5",
|
|
||||||
"open-sans-fontface": "^1.4.0",
|
|
||||||
"require-css": "0.1.10",
|
|
||||||
"requirejs": "2.3.5",
|
|
||||||
"requirejs-plugins": "^1.0.2",
|
|
||||||
"scrypt-async": "1.2.0",
|
|
||||||
"sortablejs": "^1.6.0",
|
|
||||||
"drawio": "cryptpad/drawio-npm#npm",
|
|
||||||
"pako": "^2.1.0",
|
|
||||||
"x2js": "^3.4.4"
|
"x2js": "^3.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jshint": "^2.13.4",
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-compat": "^4.2.0",
|
||||||
"lesshint": "6.3.7"
|
"lesshint": "6.3.7"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
@ -82,9 +86,8 @@
|
||||||
"offline": "FRESH=1 OFFLINE=1 node server.js",
|
"offline": "FRESH=1 OFFLINE=1 node server.js",
|
||||||
"offlinedev": "DEV=1 OFFLINE=1 node server.js",
|
"offlinedev": "DEV=1 OFFLINE=1 node server.js",
|
||||||
"package": "PACKAGE=1 node server.js",
|
"package": "PACKAGE=1 node server.js",
|
||||||
"lint": "jshint --config .jshintrc --exclude-path .jshintignore . && ./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
|
"lint": "eslint . && ./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
|
||||||
"lint:js": "jshint --config .jshintrc --exclude-path .jshintignore .",
|
"lint:js": "eslint .",
|
||||||
"lint:server": "jshint --config .jshintrc lib",
|
|
||||||
"lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
|
"lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
|
||||||
"lint:translations": "node ./scripts/translations/lint-translations.js",
|
"lint:translations": "node ./scripts/translations/lint-translations.js",
|
||||||
"unused-translations": "node ./scripts/translations/unused-translations.js",
|
"unused-translations": "node ./scripts/translations/unused-translations.js",
|
||||||
|
@ -94,5 +97,6 @@
|
||||||
"build": "node scripts/build.js",
|
"build": "node scripts/build.js",
|
||||||
"clear": "node scripts/clear.js",
|
"clear": "node scripts/clear.js",
|
||||||
"installtoken": "node scripts/install.js"
|
"installtoken": "node scripts/install.js"
|
||||||
}
|
},
|
||||||
|
"browserslist": ["> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ You can find `Dockerfile`, `docker-compose.yml` and `docker-entrypoint.sh` files
|
||||||
|
|
||||||
Previously, Docker images were community maintained, had their own repository and weren't official supported. We changed that with v5.4.0 during July 2023. Thanks to @promasu for all the work on the community images.
|
Previously, Docker images were community maintained, had their own repository and weren't official supported. We changed that with v5.4.0 during July 2023. Thanks to @promasu for all the work on the community images.
|
||||||
|
|
||||||
# Security
|
# Privacy / Security
|
||||||
|
|
||||||
CryptPad offers a variety of collaborative tools that encrypt your data in your browser
|
CryptPad offers a variety of collaborative tools that encrypt your data in your browser
|
||||||
before it is sent to the server and your collaborators. In the event that the server is
|
before it is sent to the server and your collaborators. In the event that the server is
|
||||||
|
@ -95,7 +95,7 @@ This project is tested with [BrowserStack](https://www.browserstack.com/).
|
||||||
This software is and will always be available under the GNU Affero General Public License as
|
This software is and will always be available under the GNU Affero General Public License as
|
||||||
published by the Free Software Foundation, either version 3 of the License, or (at your option)
|
published by the Free Software Foundation, either version 3 of the License, or (at your option)
|
||||||
any later version. If you wish to use this technology in a proprietary product, please contact
|
any later version. If you wish to use this technology in a proprietary product, please contact
|
||||||
sales@xwiki.com.
|
sales@cryptpad.org
|
||||||
|
|
||||||
[Tor browser]: https://www.torproject.org/download/
|
[Tor browser]: https://www.torproject.org/download/
|
||||||
[active attack]: https://en.wikipedia.org/wiki/Attack_(computing)#Types_of_attack
|
[active attack]: https://en.wikipedia.org/wiki/Attack_(computing)#Types_of_attack
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6, node: true */
|
|
||||||
const Fs = require('fs');
|
const Fs = require('fs');
|
||||||
const nThen = require('nthen');
|
const nThen = require('nthen');
|
||||||
const Nacl = require('tweetnacl/nacl-fast');
|
const Nacl = require('tweetnacl/nacl-fast');
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some .activity file were created for deleted blob due to a bug.
|
||||||
|
* This script can be run once to remove these invalid activity file.
|
||||||
|
**/
|
||||||
|
var config = require("../lib/load-config");
|
||||||
|
var BlobStore = require("../lib/storage/blob");
|
||||||
|
|
||||||
|
config.getSession = function () {};
|
||||||
|
BlobStore.create(config, function (err, store) {
|
||||||
|
if (err) { return console.error('ERROR', err); }
|
||||||
|
console.log('Cleaning lone .activity files...');
|
||||||
|
store.remove.loneActivity(function (err) {
|
||||||
|
if (err) { return console.error('ERROR', err); }
|
||||||
|
console.log('Done');
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6, node: true */
|
|
||||||
const nThen = require("nthen");
|
const nThen = require("nthen");
|
||||||
const Pins = require("../lib/pins");
|
const Pins = require("../lib/pins");
|
||||||
const Assert = require("assert");
|
const Assert = require("assert");
|
||||||
|
|
|
@ -50,5 +50,6 @@ Fse.rmSync(oldComponentsPath, { recursive: true, force: true });
|
||||||
].forEach(l => {
|
].forEach(l => {
|
||||||
const source = Path.join("node_modules", l);
|
const source = Path.join("node_modules", l);
|
||||||
const destination = Path.join(componentsPath, l);
|
const destination = Path.join(componentsPath, l);
|
||||||
|
Fs.rmSync(destination, { recursive: true, force: true });
|
||||||
Fs.cpSync(source, destination, { recursive: true });
|
Fs.cpSync(source, destination, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,10 @@ var config = require("../lib/load-config");
|
||||||
|
|
||||||
var Env = Environment.create(config);
|
var Env = Environment.create(config);
|
||||||
|
|
||||||
|
// Set DRY_RUN to true to run the script without deleting anything. A log file
|
||||||
|
// will be created.
|
||||||
|
Env.DRY_RUN = false;
|
||||||
|
|
||||||
var loadPremiumAccounts = function (Env, cb) {
|
var loadPremiumAccounts = function (Env, cb) {
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// load premium accounts
|
// load premium accounts
|
||||||
|
|
|
@ -15,6 +15,10 @@ var config = require("../lib/load-config");
|
||||||
|
|
||||||
var Env = Environment.create(config);
|
var Env = Environment.create(config);
|
||||||
|
|
||||||
|
// Set DRY_RUN to true to run the script without deleting anything. A log file
|
||||||
|
// will be created.
|
||||||
|
Env.DRY_RUN = false;
|
||||||
|
|
||||||
var loadPremiumAccounts = function (Env, cb) {
|
var loadPremiumAccounts = function (Env, cb) {
|
||||||
nThen(function (w) {
|
nThen(function (w) {
|
||||||
// load premium accounts
|
// load premium accounts
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6, node: true */
|
|
||||||
|
|
||||||
const Nacl = require('tweetnacl/nacl-fast');
|
const Nacl = require('tweetnacl/nacl-fast');
|
||||||
|
|
||||||
const keyPair = Nacl.box.keyPair();
|
const keyPair = Nacl.box.keyPair();
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 6, node: true */
|
|
||||||
const Fs = require('fs');
|
const Fs = require('fs');
|
||||||
const Path = require("path");
|
const Path = require("path");
|
||||||
const Semaphore = require('saferphore');
|
const Semaphore = require('saferphore');
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
// jshint esversion: 6, browser: false, node: true
|
|
||||||
// This file is for automated testing, it should probably not be invoked for any other purpose.
|
// This file is for automated testing, it should probably not be invoked for any other purpose.
|
||||||
// It will:
|
// It will:
|
||||||
// 1. npm install
|
// 1. npm install
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Pins = require("../../lib/pins");
|
const Pins = require("../../lib/pins");
|
||||||
|
|
||||||
var stats = {
|
var stats = {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/*jshint esversion: 6 */
|
|
||||||
const Plan = require("../../lib/plan");
|
const Plan = require("../../lib/plan");
|
||||||
|
|
||||||
var rand_delay = function (f) {
|
var rand_delay = function (f) {
|
||||||
|
|
|
@ -53,7 +53,7 @@ special_rules.fr = function (s) {
|
||||||
ignore instances where the following character is a '/'
|
ignore instances where the following character is a '/'
|
||||||
because this is probably a URL (http(s)://)
|
because this is probably a URL (http(s)://)
|
||||||
*/
|
*/
|
||||||
return /\S[:;\?\!][^\/]{1,}/.test(s);
|
return /\S[:;\?\!][^\/]{1,}/.test(s.replace(/mailto:/g, " :"));
|
||||||
};
|
};
|
||||||
|
|
||||||
var noop = function () {};
|
var noop = function () {};
|
||||||
|
|
|
@ -167,7 +167,7 @@ nThen(function (w) {
|
||||||
var throttledEnvChange = Util.throttle(function () {
|
var throttledEnvChange = Util.throttle(function () {
|
||||||
Env.Log.info('WORKER_ENV_UPDATE', 'Updating HTTP workers with latest state');
|
Env.Log.info('WORKER_ENV_UPDATE', 'Updating HTTP workers with latest state');
|
||||||
broadcast('ENV_UPDATE', Environment.serialize(Env));
|
broadcast('ENV_UPDATE', Environment.serialize(Env));
|
||||||
}, 250);
|
}, 250); // NOTE: changing this value will impact lib/commands/admin-rpc.js#adminDecree callback
|
||||||
|
|
||||||
var throttledCacheFlush = Util.throttle(function () {
|
var throttledCacheFlush = Util.throttle(function () {
|
||||||
Env.Log.info('WORKER_CACHE_FLUSH', 'Instructing HTTP workers to flush cache');
|
Env.Log.info('WORKER_CACHE_FLUSH', 'Instructing HTTP workers to flush cache');
|
||||||
|
|
|
@ -37,12 +37,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cp-admin-setlimit-form, .cp-admin-broadcast-form {
|
.cp-sidebarlayout-element {
|
||||||
label {
|
label:not(.cp-admin-label) {
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
max-width: 400px;
|
max-width: 25rem;
|
||||||
}
|
}
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -246,6 +246,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cp-admin-users {
|
||||||
|
.cp-admin-store-invited, .cp-admin-store-sso {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cp-admin-broadcast-form {
|
.cp-admin-broadcast-form {
|
||||||
input.flatpickr-input {
|
input.flatpickr-input {
|
||||||
width: 307.875px !important; // same width as flatpickr calendar
|
width: 307.875px !important; // same width as flatpickr calendar
|
||||||
|
|
|
@ -61,8 +61,8 @@ define([
|
||||||
'general': [ // Msg.admin_cat_general
|
'general': [ // Msg.admin_cat_general
|
||||||
'cp-admin-flush-cache',
|
'cp-admin-flush-cache',
|
||||||
'cp-admin-update-limit',
|
'cp-admin-update-limit',
|
||||||
'cp-admin-registration',
|
|
||||||
'cp-admin-enableembeds',
|
'cp-admin-enableembeds',
|
||||||
|
'cp-admin-forcemfa',
|
||||||
'cp-admin-email',
|
'cp-admin-email',
|
||||||
|
|
||||||
'cp-admin-instance-info-notice',
|
'cp-admin-instance-info-notice',
|
||||||
|
@ -72,6 +72,11 @@ define([
|
||||||
'cp-admin-jurisdiction',
|
'cp-admin-jurisdiction',
|
||||||
'cp-admin-notice',
|
'cp-admin-notice',
|
||||||
],
|
],
|
||||||
|
'users': [ // Msg.admin_cat_quota
|
||||||
|
'cp-admin-registration',
|
||||||
|
'cp-admin-invitation',
|
||||||
|
'cp-admin-users',
|
||||||
|
],
|
||||||
'quota': [ // Msg.admin_cat_quota
|
'quota': [ // Msg.admin_cat_quota
|
||||||
'cp-admin-defaultlimit',
|
'cp-admin-defaultlimit',
|
||||||
'cp-admin-setlimit',
|
'cp-admin-setlimit',
|
||||||
|
@ -131,7 +136,7 @@ define([
|
||||||
// Convert to camlCase for translation keys
|
// Convert to camlCase for translation keys
|
||||||
var safeKey = keyToCamlCase(key);
|
var safeKey = keyToCamlCase(key);
|
||||||
var $div = $('<div>', {'class': 'cp-admin-' + key + ' cp-sidebarlayout-element'});
|
var $div = $('<div>', {'class': 'cp-admin-' + key + ' cp-sidebarlayout-element'});
|
||||||
$('<label>', {'id': 'cp-admin-' + key}).text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
|
$('<label>', {'id': 'cp-admin-' + key, 'class':'cp-admin-label'}).text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
|
||||||
$('<span>', {'class': 'cp-sidebarlayout-description'})
|
$('<span>', {'class': 'cp-sidebarlayout-description'})
|
||||||
.text(Messages['admin_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
|
.text(Messages['admin_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
|
||||||
if (addButton) {
|
if (addButton) {
|
||||||
|
@ -1203,6 +1208,25 @@ define([
|
||||||
return tableObj.table;
|
return tableObj.table;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getBlockId = (val) => {
|
||||||
|
var url;
|
||||||
|
try {
|
||||||
|
url = new URL(val, ApiConfig.httpUnsafeOrigin);
|
||||||
|
} catch (err) { }
|
||||||
|
var getKey = function () {
|
||||||
|
var parts = val.split('/');
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
};
|
||||||
|
var isValidBlockURL = function (url) {
|
||||||
|
if (!url) { return; }
|
||||||
|
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
|
||||||
|
};
|
||||||
|
if (isValidBlockURL(url)) {
|
||||||
|
return getKey();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
create['block-metadata'] = function () {
|
create['block-metadata'] = function () {
|
||||||
var key = 'block-metadata';
|
var key = 'block-metadata';
|
||||||
var $div = makeBlock(key, true); // Msg.admin_blockMetadataHint.admin_blockMetadataTitle
|
var $div = makeBlock(key, true); // Msg.admin_blockMetadataHint.admin_blockMetadataTitle
|
||||||
|
@ -1235,21 +1259,10 @@ define([
|
||||||
key: '',
|
key: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
var url;
|
var key = getBlockId(val);
|
||||||
try {
|
if (key) {
|
||||||
url = new URL(val, ApiConfig.httpUnsafeOrigin);
|
|
||||||
} catch (err) { }
|
|
||||||
var getKey = function () {
|
|
||||||
var parts = val.split('/');
|
|
||||||
return parts[parts.length - 1];
|
|
||||||
};
|
|
||||||
var isValidBlockURL = function (url) {
|
|
||||||
if (!url) { return; }
|
|
||||||
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
|
|
||||||
};
|
|
||||||
if (isValidBlockURL(url)) {
|
|
||||||
state.valid = true;
|
state.valid = true;
|
||||||
state.key = getKey();
|
state.key = key;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
@ -1482,7 +1495,11 @@ Example
|
||||||
};
|
};
|
||||||
|
|
||||||
// Msg.admin_registrationHint, .admin_registrationTitle
|
// Msg.admin_registrationHint, .admin_registrationTitle
|
||||||
create['registration'] = makeAdminCheckbox({
|
// Msg.admin_registrationSsoTitle
|
||||||
|
create['registration'] = function () {
|
||||||
|
var refresh = function () {};
|
||||||
|
|
||||||
|
var $div = makeAdminCheckbox({
|
||||||
key: 'registration',
|
key: 'registration',
|
||||||
getState: function () {
|
getState: function () {
|
||||||
return APP.instanceStatus.restrictRegistration;
|
return APP.instanceStatus.restrictRegistration;
|
||||||
|
@ -1498,11 +1515,454 @@ Example
|
||||||
}
|
}
|
||||||
APP.updateStatus(function () {
|
APP.updateStatus(function () {
|
||||||
setState(APP.instanceStatus.restrictRegistration);
|
setState(APP.instanceStatus.restrictRegistration);
|
||||||
|
refresh();
|
||||||
flushCacheNotice();
|
flushCacheNotice();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
var $sso = makeAdminCheckbox({
|
||||||
|
key: 'registration-sso',
|
||||||
|
getState: function () {
|
||||||
|
return APP.instanceStatus.restrictSsoRegistration;
|
||||||
},
|
},
|
||||||
|
query: function (val, setState) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'ADMIN_DECREE',
|
||||||
|
data: ['RESTRICT_SSO_REGISTRATION', [val]]
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
console.error(e, response);
|
||||||
|
}
|
||||||
|
APP.updateStatus(function () {
|
||||||
|
setState(APP.instanceStatus.restrictSsoRegistration);
|
||||||
|
flushCacheNotice();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
|
||||||
|
if (ssoEnabled) {
|
||||||
|
$sso.find('#cp-admin-registration-sso').hide();
|
||||||
|
$sso.find('> span.cp-sidebarlayout-description').hide();
|
||||||
|
$div.append($sso);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh = () => {
|
||||||
|
var closed = APP.instanceStatus.restrictRegistration;
|
||||||
|
if (closed) {
|
||||||
|
$sso.show();
|
||||||
|
} else {
|
||||||
|
$sso.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
|
||||||
|
return $div;
|
||||||
|
};
|
||||||
|
|
||||||
|
create['invitation'] = function () {
|
||||||
|
var key = 'invitation';
|
||||||
|
var $div = makeBlock(key); // Msg.admin_invitationHint, admin_invitationTitle
|
||||||
|
|
||||||
|
var list = h('table.cp-admin-all-limits');
|
||||||
|
var input = h('input#cp-admin-invitation-alias');
|
||||||
|
var inputEmail = h('input#cp-admin-invitation-email');
|
||||||
|
var button = h('button.btn.btn-primary', Messages.admin_invitationCreate);
|
||||||
|
var $b = $(button);
|
||||||
|
|
||||||
|
|
||||||
|
var refreshInvite = function () {};
|
||||||
|
var refresh = h('button.btn.btn-secondary', Messages.oo_refresh);
|
||||||
|
Util.onClickEnter($(refresh), function () {
|
||||||
|
refreshInvite();
|
||||||
|
});
|
||||||
|
|
||||||
|
var add = h('div', [
|
||||||
|
h('label', { for: 'cp-admin-invitation-alias' }, Messages.admin_invitationAlias),
|
||||||
|
input,
|
||||||
|
h('label', { for: 'cp-admin-invitation-email' }, Messages.admin_invitationEmail),
|
||||||
|
inputEmail,
|
||||||
|
h('nav', [button, refresh])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var metadataMgr = common.getMetadataMgr();
|
||||||
|
var privateData = metadataMgr.getPrivateData();
|
||||||
|
|
||||||
|
var deleteInvite = function (id) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'DELETE_INVITATION',
|
||||||
|
data: id
|
||||||
|
}, function (e, response) {
|
||||||
|
$b.prop('disabled', false);
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
return void console.error(e, response);
|
||||||
|
}
|
||||||
|
refreshInvite();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var $list = $(list);
|
||||||
|
refreshInvite = function () {
|
||||||
|
$list.empty();
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'GET_ALL_INVITATIONS',
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
|
||||||
|
console.error(e, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(response)) { return; }
|
||||||
|
var all = response[0];
|
||||||
|
Object.keys(all).forEach(function (key, i) {
|
||||||
|
if (!i) { // First item: add header to table
|
||||||
|
var trHead = h('tr', [
|
||||||
|
h('th', Messages.admin_invitationLink),
|
||||||
|
h('th', Messages.admin_invitationAlias),
|
||||||
|
h('th', Messages.admin_invitationEmail),
|
||||||
|
h('th', Messages.admin_documentCreationTime),
|
||||||
|
h('th')
|
||||||
|
]);
|
||||||
|
$list.append(trHead);
|
||||||
|
}
|
||||||
|
var data = all[key];
|
||||||
|
var url = privateData.origin + Hash.hashToHref(key, 'register');
|
||||||
|
|
||||||
|
var del = h('button.btn.btn-danger', [
|
||||||
|
h('i.fa.fa-trash'),
|
||||||
|
h('span', Messages.kanban_delete)
|
||||||
|
]);
|
||||||
|
var $del = $(del);
|
||||||
|
Util.onClickEnter($del, function () {
|
||||||
|
$del.attr('disabled', 'disabled');
|
||||||
|
UI.confirm(Messages.admin_invitationDeleteConfirm, function (yes) {
|
||||||
|
$del.attr('disabled', '');
|
||||||
|
if (!yes) { return; }
|
||||||
|
deleteInvite(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var copy = h('button.btn.btn-secondary', [
|
||||||
|
h('i.fa.fa-clipboard'),
|
||||||
|
h('span', Messages.admin_invitationCopy)
|
||||||
|
]);
|
||||||
|
Util.onClickEnter($(copy), function () {
|
||||||
|
Clipboard.copy(url, () => {
|
||||||
|
UI.log(Messages.genericCopySuccess);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var line = h('tr', [
|
||||||
|
h('td', UI.dialog.selectable(url)),
|
||||||
|
h('td', data.alias),
|
||||||
|
h('td', data.email),
|
||||||
|
h('td', new Date(data.time).toLocaleString()),
|
||||||
|
//h('td', data.createdBy),
|
||||||
|
h('td', [
|
||||||
|
copy,
|
||||||
|
del
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
$list.append(line);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
refreshInvite();
|
||||||
|
|
||||||
|
$b.on('click', () => {
|
||||||
|
var alias = $(input).val().trim();
|
||||||
|
if (!alias) { return void UI.warn(Messages.error); } // FIXME better error message
|
||||||
|
$b.prop('disabled', true);
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'CREATE_INVITATION',
|
||||||
|
data: {
|
||||||
|
alias,
|
||||||
|
email: $(inputEmail).val()
|
||||||
|
}
|
||||||
|
}, function (e, response) {
|
||||||
|
$b.prop('disabled', false);
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
return void console.error(e, response);
|
||||||
|
}
|
||||||
|
$(input).val('').focus();
|
||||||
|
$(inputEmail).val('');
|
||||||
|
refreshInvite();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$div.append([add, list]);
|
||||||
|
return $div;
|
||||||
|
};
|
||||||
|
|
||||||
|
create['users'] = function () {
|
||||||
|
var key = 'users';
|
||||||
|
var $div = makeBlock(key); // Msg.admin_usersHint, admin_usersTitle
|
||||||
|
|
||||||
|
var list = h('table.cp-admin-all-limits');
|
||||||
|
var userAlias = h('input#cp-admin-users-alias');
|
||||||
|
var userEmail = h('input#cp-admin-users-email');
|
||||||
|
var userEdPublic = h('input#cp-admin-users-key');
|
||||||
|
var userBlock = h('input#cp-admin-users-block');
|
||||||
|
var button = h('button.btn.btn-primary', Messages.admin_usersAdd);
|
||||||
|
var $b = $(button);
|
||||||
|
|
||||||
|
var refreshUsers = function () {};
|
||||||
|
|
||||||
|
var refresh = h('button.btn.btn-secondary', Messages.oo_refresh);
|
||||||
|
Util.onClickEnter($(refresh), function () {
|
||||||
|
refreshUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
var add = h('div', [
|
||||||
|
h('label', { for: 'cp-admin-users-alias' }, Messages.admin_invitationAlias),
|
||||||
|
userAlias,
|
||||||
|
h('label', { for: 'cp-admin-users-email' }, Messages.admin_invitationEmail),
|
||||||
|
userEmail,
|
||||||
|
h('label', { for: 'cp-admin-users-key' }, Messages.admin_limitUser),
|
||||||
|
userEdPublic,
|
||||||
|
h('label', { for: 'cp-admin-users-block' }, Messages.admin_usersBlock),
|
||||||
|
userBlock,
|
||||||
|
h('nav', [button, refresh])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var $invited = makeAdminCheckbox({
|
||||||
|
key: 'store-invited',
|
||||||
|
getState: function () {
|
||||||
|
return !APP.instanceStatus.dontStoreInvitedUsers;
|
||||||
|
},
|
||||||
|
query: function (val, setState) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'ADMIN_DECREE',
|
||||||
|
data: ['DISABLE_STORE_INVITED_USERS', [!val]]
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
console.error(e, response);
|
||||||
|
}
|
||||||
|
APP.updateStatus(function () {
|
||||||
|
setState(!APP.instanceStatus.dontStoreInvitedUsers);
|
||||||
|
flushCacheNotice();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
$invited.find('#cp-admin-store-invited').hide();
|
||||||
|
$invited.find('> span.cp-sidebarlayout-description').hide();
|
||||||
|
$div.append($invited);
|
||||||
|
var $sso = makeAdminCheckbox({
|
||||||
|
key: 'store-sso',
|
||||||
|
getState: function () {
|
||||||
|
return !APP.instanceStatus.dontStoreSSOUsers;
|
||||||
|
},
|
||||||
|
query: function (val, setState) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'ADMIN_DECREE',
|
||||||
|
data: ['DISABLE_STORE_SSO_USERS', [!val]]
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
console.error(e, response);
|
||||||
|
}
|
||||||
|
APP.updateStatus(function () {
|
||||||
|
setState(!APP.instanceStatus.dontStoreSSOUsers);
|
||||||
|
flushCacheNotice();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
|
||||||
|
if (ssoEnabled) {
|
||||||
|
$sso.find('#cp-admin-store-sso').hide();
|
||||||
|
$sso.find('> span.cp-sidebarlayout-description').hide();
|
||||||
|
$div.append($sso);
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleteUser = function (id) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'DELETE_KNOWN_USER',
|
||||||
|
data: id
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
return void console.error(e, response);
|
||||||
|
}
|
||||||
|
refreshUsers();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var updateUser = function (key, changes) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'UPDATE_KNOWN_USER',
|
||||||
|
data: {
|
||||||
|
edPublic: key,
|
||||||
|
changes: changes
|
||||||
|
}
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
return void console.error(e, response);
|
||||||
|
}
|
||||||
|
refreshUsers();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var $list = $(list);
|
||||||
|
refreshUsers = function () {
|
||||||
|
$list.empty();
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'GET_ALL_USERS',
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
|
||||||
|
console.error(e, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(response)) { return; }
|
||||||
|
var all = response[0];
|
||||||
|
Object.keys(all).forEach(function (key, i) {
|
||||||
|
if (!i) { // First item: add header to table
|
||||||
|
var trHead = h('tr', [
|
||||||
|
h('th', Messages.admin_invitationAlias),
|
||||||
|
h('th', Messages.admin_invitationEmail),
|
||||||
|
h('th', Messages.admin_limitUser),
|
||||||
|
h('th', Messages.admin_documentCreationTime),
|
||||||
|
h('th')
|
||||||
|
]);
|
||||||
|
$list.append(trHead);
|
||||||
|
}
|
||||||
|
var data = all[key];
|
||||||
|
var editUser = () => {};
|
||||||
|
var del = h('button.btn.btn-danger', [
|
||||||
|
h('i.fa.fa-trash'),
|
||||||
|
Messages.admin_usersRemove
|
||||||
|
]);
|
||||||
|
var $del = $(del);
|
||||||
|
Util.onClickEnter($del, function () {
|
||||||
|
$del.attr('disabled', 'disabled');
|
||||||
|
UI.confirm(Messages.admin_usersRemoveConfirm, function (yes) {
|
||||||
|
$del.attr('disabled', '');
|
||||||
|
if (!yes) { return; }
|
||||||
|
deleteUser(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var edit = h('button.btn.btn-secondary', [
|
||||||
|
h('i.fa.fa-pencil'),
|
||||||
|
h('span', Messages.tag_edit)
|
||||||
|
]);
|
||||||
|
Util.onClickEnter($(edit), function () {
|
||||||
|
editUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
var alias = h('td', data.alias);
|
||||||
|
var email = h('td', data.email);
|
||||||
|
var actions = h('td', [edit, del]);
|
||||||
|
var $alias = $(alias);
|
||||||
|
var $email = $(email);
|
||||||
|
var $actions = $(actions);
|
||||||
|
|
||||||
|
editUser = () => {
|
||||||
|
var aliasInput = h('input');
|
||||||
|
var emailInput = h('input');
|
||||||
|
$(aliasInput).val(data.alias);
|
||||||
|
$(emailInput).val(data.email);
|
||||||
|
var save = h('button.btn.btn-primary', Messages.settings_save);
|
||||||
|
var cancel = h('button.btn.btn-secondary', Messages.cancel);
|
||||||
|
Util.onClickEnter($(save), function () {
|
||||||
|
var aliasVal = $(aliasInput).val().trim();
|
||||||
|
if (!aliasVal) { return void UI.warn(Messages.error); }
|
||||||
|
var changes = {
|
||||||
|
alias: aliasVal,
|
||||||
|
email: $(emailInput).val().trim()
|
||||||
|
};
|
||||||
|
updateUser(key, changes);
|
||||||
|
});
|
||||||
|
Util.onClickEnter($(cancel), function () {
|
||||||
|
refreshUsers();
|
||||||
|
});
|
||||||
|
$alias.html('').append(aliasInput);
|
||||||
|
$email.html('').append(emailInput);
|
||||||
|
$actions.html('').append([save, cancel]);
|
||||||
|
console.warn(alias, email, $alias, $email, aliasInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
var infoButton = h('button.btn.primary.cp-report', {
|
||||||
|
style: 'margin-left: 10px; cursor: pointer;',
|
||||||
|
}, [
|
||||||
|
h('i.fa.fa-database'),
|
||||||
|
h('span', Messages.admin_diskUsageButton)
|
||||||
|
]);
|
||||||
|
$(infoButton).click(() => {
|
||||||
|
getAccountData(key, (err, data) => {
|
||||||
|
if (err) { return void console.error(err); }
|
||||||
|
var table = renderAccountData(data);
|
||||||
|
UI.alert(table, () => {
|
||||||
|
|
||||||
|
}, {
|
||||||
|
wide: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var line = h('tr', [
|
||||||
|
alias,
|
||||||
|
email,
|
||||||
|
h('td', [
|
||||||
|
h('code', key),
|
||||||
|
infoButton
|
||||||
|
]),
|
||||||
|
h('td', new Date(data.time).toLocaleString()),
|
||||||
|
//h('td', data.createdBy),
|
||||||
|
actions
|
||||||
|
]);
|
||||||
|
$list.append(line);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
refreshUsers();
|
||||||
|
|
||||||
|
$b.on('click', () => {
|
||||||
|
var alias = $(userAlias).val().trim();
|
||||||
|
if (!alias) { return void UI.warn(Messages.error); }
|
||||||
|
$b.prop('disabled', true);
|
||||||
|
|
||||||
|
var done = () => { $b.prop('disabled', false); };
|
||||||
|
// TODO Get "block" from pin log?
|
||||||
|
|
||||||
|
var keyStr = $(userEdPublic).val().trim();
|
||||||
|
var edPublic = keyStr && Keys.canonicalize(keyStr);
|
||||||
|
if (!edPublic) {
|
||||||
|
done();
|
||||||
|
return void UI.warn(Messages.admin_invalKey);
|
||||||
|
}
|
||||||
|
var block = getBlockId($(userBlock).val());
|
||||||
|
|
||||||
|
var obj = {
|
||||||
|
alias,
|
||||||
|
email: $(userEmail).val(),
|
||||||
|
block: block,
|
||||||
|
edPublic: edPublic,
|
||||||
|
};
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'ADD_KNOWN_USER',
|
||||||
|
data: obj
|
||||||
|
}, function (e, response) {
|
||||||
|
done();
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
return void console.error(e, response);
|
||||||
|
}
|
||||||
|
$(userAlias).val('').focus();
|
||||||
|
$(userEmail).val('');
|
||||||
|
$(userBlock).val('');
|
||||||
|
$(userEdPublic).val('');
|
||||||
|
refreshUsers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$div.append([add, list]);
|
||||||
|
return $div;
|
||||||
|
};
|
||||||
|
|
||||||
// Msg.admin_enableembedsHint, .admin_enableembedsTitle
|
// Msg.admin_enableembedsHint, .admin_enableembedsTitle
|
||||||
create['enableembeds'] = makeAdminCheckbox({
|
create['enableembeds'] = makeAdminCheckbox({
|
||||||
|
@ -1527,6 +1987,29 @@ Example
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Msg.admin_forcemfaHint, .admin_forcemfaTitle
|
||||||
|
create['forcemfa'] = makeAdminCheckbox({
|
||||||
|
key: 'forcemfa',
|
||||||
|
getState: function () {
|
||||||
|
return APP.instanceStatus.enforceMFA;
|
||||||
|
},
|
||||||
|
query: function (val, setState) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'ADMIN_DECREE',
|
||||||
|
data: ['ENFORCE_MFA', [val]]
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
console.error(e, response);
|
||||||
|
}
|
||||||
|
APP.updateStatus(function () {
|
||||||
|
setState(APP.instanceStatus.enforceMFA);
|
||||||
|
flushCacheNotice();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
create['email'] = function () {
|
create['email'] = function () {
|
||||||
var key = 'email';
|
var key = 'email';
|
||||||
var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle
|
var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle
|
||||||
|
@ -1889,19 +2372,23 @@ Example
|
||||||
var key = 'setlimit';
|
var key = 'setlimit';
|
||||||
var $div = makeBlock(key); // Msg.admin_setlimitHint, .admin_setlimitTitle
|
var $div = makeBlock(key); // Msg.admin_setlimitHint, .admin_setlimitTitle
|
||||||
|
|
||||||
var user = h('input.cp-setlimit-key', { id: 'user-input' });
|
var user = h('input.cp-setlimit-key#cp-admin-setlimit-user');
|
||||||
var $key = $(user);
|
var $key = $(user);
|
||||||
var limit = h('input.cp-setlimit-quota', { type: 'number', min: 0, value: 0, id: 'limit-input' });
|
var limit = h('input.cp-setlimit-quota#cp-admin-setlimit-value', {
|
||||||
var note = h('input.cp-setlimit-note', { id: 'note-input' });
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
var note = h('input.cp-setlimit-note#cp-admin-setlimit-note');
|
||||||
var remove = h('button.btn.btn-danger', Messages.fc_remove);
|
var remove = h('button.btn.btn-danger', Messages.fc_remove);
|
||||||
var set = h('button.btn.btn-primary', Messages.admin_setlimitButton);
|
var set = h('button.btn.btn-primary', Messages.admin_setlimitButton);
|
||||||
|
|
||||||
var form = h('div.cp-admin-setlimit-form', [
|
var form = h('div.cp-admin-setlimit-form', [
|
||||||
h('label', { for: 'user-input' }, Messages.admin_limitUser),
|
h('label', { for: 'cp-admin-setlimit-user' }, Messages.admin_limitUser),
|
||||||
user,
|
user,
|
||||||
h('label', { for: 'limit-input' }, Messages.admin_limitMB),
|
h('label', { for: 'cp-admin-setlimit-value' }, Messages.admin_limitMB),
|
||||||
limit,
|
limit,
|
||||||
h('label', { for: 'note-input' }, Messages.admin_limitSetNote),
|
h('label', { for: 'cp-admin-setlimit-note' }, Messages.admin_limitSetNote),
|
||||||
note,
|
note,
|
||||||
h('nav', [set, remove])
|
h('nav', [set, remove])
|
||||||
]);
|
]);
|
||||||
|
@ -3421,6 +3908,7 @@ Example
|
||||||
var SIDEBAR_ICONS = {
|
var SIDEBAR_ICONS = {
|
||||||
general: 'fa fa-user-o',
|
general: 'fa fa-user-o',
|
||||||
stats: 'fa fa-line-chart',
|
stats: 'fa fa-line-chart',
|
||||||
|
users: 'fa fa-address-card-o',
|
||||||
quota: 'fa fa-hdd-o',
|
quota: 'fa fa-hdd-o',
|
||||||
support: 'fa fa-life-ring',
|
support: 'fa fa-life-ring',
|
||||||
broadcast: 'fa fa-bullhorn',
|
broadcast: 'fa fa-bullhorn',
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
/* jshint esversion: 7 */
|
|
||||||
define([], function () {
|
define([], function () {
|
||||||
// Based on https://gist.github.com/bellbind/871b145110c458e83077a718aef9fa0e
|
// Based on https://gist.github.com/bellbind/871b145110c458e83077a718aef9fa0e
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ define(['/api/config'], function (ApiConfig) {
|
||||||
// Inform the user that we won't navigate and that the 'bounce tab' will be closed.
|
// Inform the user that we won't navigate and that the 'bounce tab' will be closed.
|
||||||
// our linter warns when it sees 'javascript:' because it doesn't distinguish between
|
// our linter warns when it sees 'javascript:' because it doesn't distinguish between
|
||||||
// detecting this pattern and using it, so we ignore this line
|
// detecting this pattern and using it, so we ignore this line
|
||||||
if (['javascript:', 'vbscript:', 'data:', 'blob:'].includes(target.protocol)) { // jshint ignore:line
|
if (['javascript:', 'vbscript:', 'data:', 'blob:'].includes(target.protocol)) {
|
||||||
window.alert(Messages._getKey('bounce_danger', [target.href]));
|
window.alert(Messages._getKey('bounce_danger', [target.href]));
|
||||||
return void reject();
|
return void reject();
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,9 +162,12 @@
|
||||||
color: @cryptpad_text_col;
|
color: @cryptpad_text_col;
|
||||||
border-radius: @variables_radius_L;
|
border-radius: @variables_radius_L;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
|
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox):not(.fa-map-marker) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.tui-full-calendar-icon {
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
.tui-full-calendar-popup-detail-item {
|
.tui-full-calendar-popup-detail-item {
|
||||||
a {
|
a {
|
||||||
color: @cryptpad_color_link;
|
color: @cryptpad_color_link;
|
||||||
|
@ -250,7 +253,7 @@
|
||||||
}
|
}
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
background: @cp_forms-bg;
|
background: @cp_forms-bg !important;
|
||||||
color: @cryptpad_text_col;
|
color: @cryptpad_text_col;
|
||||||
border: 1px solid @cp_forms-border;
|
border: 1px solid @cp_forms-border;
|
||||||
border-radius: @variables_radius;
|
border-radius: @variables_radius;
|
||||||
|
|
|
@ -23,6 +23,7 @@ define([
|
||||||
'/calendar/export.js',
|
'/calendar/export.js',
|
||||||
'/calendar/recurrence.js',
|
'/calendar/recurrence.js',
|
||||||
'/lib/datepicker/flatpickr.js',
|
'/lib/datepicker/flatpickr.js',
|
||||||
|
'tui-date-picker',
|
||||||
|
|
||||||
'/common/inner/share.js',
|
'/common/inner/share.js',
|
||||||
'/common/inner/access.js',
|
'/common/inner/access.js',
|
||||||
|
@ -64,17 +65,13 @@ define([
|
||||||
Export,
|
Export,
|
||||||
Rec,
|
Rec,
|
||||||
Flatpickr,
|
Flatpickr,
|
||||||
|
DatePicker,
|
||||||
Share, Access, Properties,
|
Share, Access, Properties,
|
||||||
diffMk,
|
diffMk,
|
||||||
SFCodeMirror,
|
SFCodeMirror,
|
||||||
CodeMirror
|
CodeMirror
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// XXX New translation keys
|
|
||||||
Messages.calendar_rec_change_first = "You moved the first repeating event to different calendar. You can only apply this change to all repeated events."; // XXX New translation key
|
|
||||||
Messages.calendar_rec_change = "You moved a repeating event to different calendar. You can only apply this change to this event or all repeated events."; // XXX New translation key
|
|
||||||
Messages.calendar_desc = "Description"; // XXX maybe rename in `description`?
|
|
||||||
Messages.calendar_description = "Description:{0}{1}"; // XXX
|
|
||||||
|
|
||||||
var SaveAs = window.saveAs;
|
var SaveAs = window.saveAs;
|
||||||
var APP = window.APP = {
|
var APP = window.APP = {
|
||||||
|
@ -124,6 +121,8 @@ define([
|
||||||
var startDate = event.start._date;
|
var startDate = event.start._date;
|
||||||
var endDate = event.end._date;
|
var endDate = event.end._date;
|
||||||
|
|
||||||
|
var timeZone;
|
||||||
|
try { timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) {}
|
||||||
var data = {
|
var data = {
|
||||||
id: Util.uid(),
|
id: Util.uid(),
|
||||||
calendarId: event.calendarId,
|
calendarId: event.calendarId,
|
||||||
|
@ -135,6 +134,7 @@ define([
|
||||||
end: +endDate,
|
end: +endDate,
|
||||||
reminders: reminders,
|
reminders: reminders,
|
||||||
body: eventBody,
|
body: eventBody,
|
||||||
|
timeZone: timeZone,
|
||||||
recurrenceRule: event.recurrenceRule
|
recurrenceRule: event.recurrenceRule
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -306,9 +306,9 @@ define([
|
||||||
obj.title = obj.title || "";
|
obj.title = obj.title || "";
|
||||||
obj.location = obj.location || "";
|
obj.location = obj.location || "";
|
||||||
obj.body = obj.body || "";
|
obj.body = obj.body || "";
|
||||||
if (obj.isAllDay && obj.startDay) { obj.start = +Flatpickr.parseDate((obj.startDay)); }
|
if (obj.isAllDay && obj.startDay) { obj.start = +DatePicker.parseDate((obj.startDay)); }
|
||||||
if (obj.isAllDay && obj.endDay) {
|
if (obj.isAllDay && obj.endDay) {
|
||||||
var endDate = Flatpickr.parseDate(obj.endDay);
|
var endDate = DatePicker.parseDate(obj.endDay);
|
||||||
endDate.setHours(23);
|
endDate.setHours(23);
|
||||||
endDate.setMinutes(59);
|
endDate.setMinutes(59);
|
||||||
endDate.setSeconds(59);
|
endDate.setSeconds(59);
|
||||||
|
@ -408,13 +408,13 @@ define([
|
||||||
str = `<a href="${l}" id="${uid}">${str}</a>`;
|
str = `<a href="${l}" id="${uid}">${str}</a>`;
|
||||||
APP.nextLocationUid = uid;
|
APP.nextLocationUid = uid;
|
||||||
}
|
}
|
||||||
|
let location_icon = h('i.fa.fa-map-marker.tui-full-calendar-icon', { 'aria-label': Messages.calendar_loc }, []);
|
||||||
return Messages._getKey('calendar_location', [str]);
|
return location_icon.outerHTML + str;
|
||||||
},
|
},
|
||||||
popupDetailBody: function(schedule) {
|
popupDetailBody: function(schedule) {
|
||||||
var str = schedule.body;
|
var str = schedule.body;
|
||||||
delete APP.eventBody;
|
delete APP.eventBody;
|
||||||
return Messages._getKey('calendar_description', ['<br />', diffMk.render(str, true)]);
|
return diffMk.render(str, true);
|
||||||
},
|
},
|
||||||
popupIsAllDay: function() { return Messages.calendar_allDay; },
|
popupIsAllDay: function() { return Messages.calendar_allDay; },
|
||||||
titlePlaceholder: function() { return Messages.calendar_title; },
|
titlePlaceholder: function() { return Messages.calendar_title; },
|
||||||
|
@ -1012,13 +1012,12 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
|
||||||
makeLeftside(cal, $(leftside));
|
makeLeftside(cal, $(leftside));
|
||||||
|
|
||||||
cal.on('beforeCreateSchedule', function(event) {
|
cal.on('beforeCreateSchedule', function(event) {
|
||||||
event.recurrenceRule = APP.recurrenceRule; // XXX Not sure about the consistency of data structures
|
event.recurrenceRule = APP.recurrenceRule;
|
||||||
newEvent(event, function (err) {
|
newEvent(event, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return void UI.warn(err);
|
return void UI.warn(err);
|
||||||
}
|
}
|
||||||
//cal.createSchedules([schedule]); XXX Remove these occurrences elsewhere
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cal.on('beforeUpdateSchedule', function(event) {
|
cal.on('beforeUpdateSchedule', function(event) {
|
||||||
|
@ -1127,7 +1126,6 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return void UI.warn(err);
|
return void UI.warn(err);
|
||||||
}
|
}
|
||||||
//cal.updateSchedule(old.id, old.calendarId, changes);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1309,23 +1307,20 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
|
||||||
updateDateRange();
|
updateDateRange();
|
||||||
updateRecurring();
|
updateRecurring();
|
||||||
});
|
});
|
||||||
$(goDate).click(function () {
|
|
||||||
var f = Flatpickr(goDate, {
|
var f = Flatpickr(goDate, {
|
||||||
enableTime: false,
|
enableTime: false,
|
||||||
defaultDate: APP.calendar.getDate()._date,
|
defaultDate: APP.calendar.getDate()._date,
|
||||||
|
clickOpens: false,
|
||||||
//dateFormat: dateFormat,
|
//dateFormat: dateFormat,
|
||||||
onChange: function (date) {
|
onChange: function (date) {
|
||||||
date[0].setHours(12);
|
date[0].setHours(12);
|
||||||
f.destroy();
|
|
||||||
APP.moveToDate(+date[0]);
|
APP.moveToDate(+date[0]);
|
||||||
updateDateRange();
|
updateDateRange();
|
||||||
updateRecurring();
|
updateRecurring();
|
||||||
},
|
},
|
||||||
onClose: function () {
|
|
||||||
setTimeout(f.destroy);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
f.open();
|
$(goDate).click(function () {
|
||||||
|
return f.isOpen ? f.close() : f.open();
|
||||||
});
|
});
|
||||||
APP.toolbar.$bottomL.append(h('div.cp-calendar-browse', [
|
APP.toolbar.$bottomL.append(h('div.cp-calendar-browse', [
|
||||||
goLeft, goToday, goRight, goDate
|
goLeft, goToday, goRight, goDate
|
||||||
|
@ -1382,6 +1377,8 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
|
||||||
}
|
}
|
||||||
if (m) {
|
if (m) {
|
||||||
m = m.map(function (n) {
|
m = m.map(function (n) {
|
||||||
|
tmp.setDate(15);
|
||||||
|
tmp.setHours(12);
|
||||||
tmp.setMonth(n-1);
|
tmp.setMonth(n-1);
|
||||||
return tmp.toLocaleDateString(getDateLanguage(), { month: 'long' });
|
return tmp.toLocaleDateString(getDateLanguage(), { month: 'long' });
|
||||||
});
|
});
|
||||||
|
@ -1468,7 +1465,7 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
|
||||||
dayStr,
|
dayStr,
|
||||||
monthStr
|
monthStr
|
||||||
]),
|
]),
|
||||||
last: last ? '-1' + dayStr : undefined,
|
last: last ? '-1' + dayCode : undefined,
|
||||||
lastStr: Messages._getKey('calendar_rec_'+key+'_nth', [
|
lastStr: Messages._getKey('calendar_rec_'+key+'_nth', [
|
||||||
Messages['calendar_nth_last'],
|
Messages['calendar_nth_last'],
|
||||||
dayStr,
|
dayStr,
|
||||||
|
@ -1670,7 +1667,7 @@ APP.recurrenceRule = {
|
||||||
minDate: date,
|
minDate: date,
|
||||||
//dateFormat: dateFormat,
|
//dateFormat: dateFormat,
|
||||||
onChange: function () {
|
onChange: function () {
|
||||||
//endPickr.set('minDate', startPickr.parseDate(s.value));
|
//endPickr.set('minDate', DatePicker.parseDate(s.value));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var endDate = new Date(+date);
|
var endDate = new Date(+date);
|
||||||
|
@ -1848,7 +1845,7 @@ APP.recurrenceRule = {
|
||||||
if (until === "count") {
|
if (until === "count") {
|
||||||
rec.count = $(radioCount).find('input[type="number"]').val();
|
rec.count = $(radioCount).find('input[type="number"]').val();
|
||||||
} else if (until === "date") {
|
} else if (until === "date") {
|
||||||
var _date = Flatpickr.parseDate(pickr.value);
|
var _date = DatePicker.parseDate(pickr.value);
|
||||||
_date.setDate(_date.getDate()+1);
|
_date.setDate(_date.getDate()+1);
|
||||||
rec.until = +_date - 1;
|
rec.until = +_date - 1;
|
||||||
}
|
}
|
||||||
|
@ -2182,7 +2179,7 @@ APP.recurrenceRule = {
|
||||||
var $button = $el.find('.tui-full-calendar-section-button-save');
|
var $button = $el.find('.tui-full-calendar-section-button-save');
|
||||||
|
|
||||||
var $startDate = $el.find('#tui-full-calendar-schedule-start-date');
|
var $startDate = $el.find('#tui-full-calendar-schedule-start-date');
|
||||||
var startDate = Flatpickr.parseDate($startDate.val());
|
var startDate = DatePicker.parseDate($startDate.val());
|
||||||
|
|
||||||
var divRec = getRecurrenceInput(startDate);
|
var divRec = getRecurrenceInput(startDate);
|
||||||
$button.before(divRec);
|
$button.before(divRec);
|
||||||
|
|
|
@ -582,6 +582,27 @@ define([
|
||||||
return r;
|
return r;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var fixTimeZone = function (evTimeZone, origin, target) {
|
||||||
|
var getOffset = function (date, tz) {
|
||||||
|
// Get an ISO string using Canadian local format
|
||||||
|
let iso = date.toLocaleString('en-CA', { timeZone:tz, hour12: false }).replace(', ', 'T');
|
||||||
|
iso += '.' + date.getMilliseconds().toString().padStart(3, '0');
|
||||||
|
|
||||||
|
// Get a UTC version of this time
|
||||||
|
let utcDate = new Date(iso + 'Z');
|
||||||
|
|
||||||
|
// Return the difference in timestamps, as minutes (60*1000)
|
||||||
|
return -(utcDate - date);
|
||||||
|
};
|
||||||
|
|
||||||
|
var myTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
var offset = getOffset(origin, evTimeZone) - getOffset(target, evTimeZone);
|
||||||
|
var myOffset = getOffset(origin, myTimeZone) - getOffset(target, myTimeZone);
|
||||||
|
|
||||||
|
return myOffset - offset;
|
||||||
|
};
|
||||||
|
|
||||||
Rec.getRecurring = function (months, events) {
|
Rec.getRecurring = function (months, events) {
|
||||||
if (window.CP_DEV_MODE) { debug = console.warn; }
|
if (window.CP_DEV_MODE) { debug = console.warn; }
|
||||||
|
|
||||||
|
@ -739,6 +760,11 @@ define([
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this event
|
// Add this event
|
||||||
|
if (_origin.timeZone && !_ev.isAllDay) {
|
||||||
|
var offset = fixTimeZone(_origin.timeZone, _start, _evS);
|
||||||
|
_ev.start += offset;
|
||||||
|
_ev.end += offset;
|
||||||
|
}
|
||||||
toAdd.push(_ev);
|
toAdd.push(_ev);
|
||||||
if (newrule) {
|
if (newrule) {
|
||||||
useNewRule();
|
useNewRule();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue