Compare commits

..

72 Commits

Author SHA1 Message Date
Girish Ramakrishnan a00b7281a7 Fixup changelog 2020-07-17 10:43:22 -07:00
Girish Ramakrishnan ddeee0c970 Add note that links expire in 24 hours 2020-07-16 15:17:51 -07:00
Johannes Zellner 8aad71efd0 Add more feature flags 2020-07-16 18:14:25 +02:00
Johannes Zellner 2028f6b984 Do not reassign ubunt_codename in base image init 2020-07-16 16:42:15 +02:00
Girish Ramakrishnan bff4999d27 mail: add mailbox count route 2020-07-15 15:48:30 -07:00
Johannes Zellner d429015f83 Add more 3.4.0 changes 2020-07-15 14:57:06 +02:00
Johannes Zellner e2628e2d43 Use latest filemanager addon
Fixes dot- and json-files
2020-07-14 17:16:41 +02:00
Girish Ramakrishnan 05dcbee7e3 backups: add b2 provider
part of #508
2020-07-13 14:52:35 -07:00
Johannes Zellner a81919262e Use addon with chown functionality 2020-07-13 18:48:42 +02:00
Girish Ramakrishnan b14b5f141b Hide nginx version 2020-07-13 09:27:57 -07:00
Girish Ramakrishnan 1259d11173 Add back provider field into getStatus 2020-07-13 08:46:05 -07:00
Johannes Zellner 0a7b132be8 Remove or increase timeouts for filemanager 2020-07-13 17:05:22 +02:00
Girish Ramakrishnan ed9210eede Add mandatory 2FA flag
part of #716
2020-07-10 10:25:04 -07:00
Girish Ramakrishnan 9ee6aa54c6 avatar is not part of the profile lock
this is because avatar is not exposed via LDAP anyways. it's purely
a personal dashboard thing.
2020-07-10 09:43:42 -07:00
Girish Ramakrishnan 7cfc455cd3 make tests pass again
also disable column statistics on ubuntu 20
2020-07-10 09:33:35 -07:00
Johannes Zellner a481ceac8c Allow larger file uploads for filemanager 2020-07-10 18:23:55 +02:00
Girish Ramakrishnan 8c7eff4e24 user: add routes to set/clear avatar 2020-07-10 07:23:38 -07:00
Girish Ramakrishnan c6c584ff74 user: move avatar handling into model code 2020-07-10 07:01:15 -07:00
Johannes Zellner ba50eb121d Use new sftp addon 2020-07-10 14:13:16 +02:00
Johannes Zellner aa8ebbd7ea Add filemanager proxy routes 2020-07-10 14:10:52 +02:00
Girish Ramakrishnan 64bc9c6dbe disable profile view for all users to avoid confusion 2020-07-09 21:54:09 -07:00
Girish Ramakrishnan bba9963b7c Add directoryConfig feature flag
Fixes #704
2020-07-09 21:51:22 -07:00
Girish Ramakrishnan 6ea2aa4a54 return profileLocked in config route
part of #704
2020-07-09 17:28:44 -07:00
Girish Ramakrishnan 3c3f81365b add route to get/set directory config
part of #704
2020-07-09 17:12:07 -07:00
Girish Ramakrishnan 3adeed381b setup account based on directory config
part of #704
2020-07-09 16:39:34 -07:00
Girish Ramakrishnan 0f5b7278b8 add directory config setting
part of #704
2020-07-09 16:02:58 -07:00
Girish Ramakrishnan f94ff49fb9 users: replace modifiedAt with ts 2020-07-09 16:02:49 -07:00
Girish Ramakrishnan d512a9c30d rename function 2020-07-09 16:02:43 -07:00
Girish Ramakrishnan 0c5113ed5b email is never used in account setup 2020-07-09 15:37:35 -07:00
Girish Ramakrishnan 2469f4cdff rename function to sendPasswordResetByIdentifier 2020-07-09 15:37:35 -07:00
Girish Ramakrishnan 9c53bfb7fb Do not show LDAP logs, it spams a lot 2020-07-07 11:16:47 -07:00
Girish Ramakrishnan 8b8144588d list must search members 2020-07-05 11:44:46 -07:00
Girish Ramakrishnan 77553da4c1 mail: add search param for mailbox and mailing list api 2020-07-05 11:23:53 -07:00
Girish Ramakrishnan cbcf943691 mail: parameterize the query 2020-07-05 10:48:08 -07:00
Girish Ramakrishnan 725a19e5b5 mail: Add pagination to lists API
Fixes #712
2020-07-05 10:48:04 -07:00
Girish Ramakrishnan f9115f902a Do not send alive status
we used to do this for managed hosting to track scaling but we don't
need this info anymore
2020-07-03 19:13:27 -07:00
Girish Ramakrishnan e4faf26d74 5.3.4 changes
(cherry picked from commit 77785097c1)
2020-07-03 14:23:20 -07:00
Girish Ramakrishnan 1c96fbb533 Fixes for tests 2020-07-03 13:47:56 -07:00
Girish Ramakrishnan 3dc163c33d database: rework connection logic 2020-07-03 13:14:00 -07:00
Girish Ramakrishnan edae94cf2e Bump max_connection for postgres addon to 200 2020-07-02 15:47:26 -07:00
Girish Ramakrishnan d1ff8e9d6b Fix crash when mysql crashes 2020-07-02 15:10:05 -07:00
Girish Ramakrishnan 70743bd285 database: Fix event emitter warning
the connection object gets reused after release. this means that we keep
attaching the 'error' event and not unlistening.

--trace-warnings can be added to box.service to get the stack trace
2020-07-02 12:00:56 -07:00
Johannes Zellner 493f1505f0 Check also for mountpoint on filesystem with external disk 2020-07-02 19:08:27 +02:00
Girish Ramakrishnan 007e3b5eef Add changes 2020-07-01 14:29:40 -07:00
Johannes Zellner d9bf6c0933 also support uniqueMember property next to member for ldap groups 2020-07-01 17:08:17 +02:00
Johannes Zellner 324344d118 Reusue the single correct ldap.createClient call also in auth 2020-07-01 14:59:26 +02:00
Johannes Zellner 5cb71e9443 No need to return externalLdapConfig in getClient() 2020-07-01 14:52:11 +02:00
Johannes Zellner cca19f00c5 Fallback to mailPrimaryAddress in ldap sync 2020-07-01 14:34:41 +02:00
Girish Ramakrishnan 6648f41f3d nginx: [warn] the "ssl" directive is deprecated, use the "listen ... ssl" directive 2020-06-30 16:00:52 -07:00
Girish Ramakrishnan c1e6b47fd6 Fix sogo aliases
Fixes cloudron/sogo#18
2020-06-30 14:29:50 -07:00
Girish Ramakrishnan 0f103ccce1 Add ping capability (for statping) 2020-06-30 07:40:17 -07:00
Girish Ramakrishnan bc6e652293 5.3.3 changes 2020-06-29 19:52:08 -07:00
Girish Ramakrishnan 85b4f2dbdd print sudo command to check failures 2020-06-29 14:03:34 -07:00
Girish Ramakrishnan d47b83a63b Package lock mystery 2020-06-29 14:03:15 -07:00
Girish Ramakrishnan b2e9fa7e0d aschema: dd servicesConfigJson 2020-06-26 15:48:39 -07:00
Girish Ramakrishnan a9fb444622 Use nginx 1.18 for security fixes 2020-06-26 14:57:53 -07:00
Girish Ramakrishnan 33ba22a021 Put this in 5.3.2 itself 2020-06-26 10:41:32 -07:00
Girish Ramakrishnan 57de0282cd remove provider from trackBeginSetup 2020-06-26 09:55:39 -07:00
Girish Ramakrishnan 8568fd26d8 Fix failing test 2020-06-26 09:48:10 -07:00
Girish Ramakrishnan 84f41e08cf Add mlock capability to manifest (for vault app) 2020-06-26 09:27:35 -07:00
Johannes Zellner a96da20536 TODO is done for filesystem backend moutnpoint check 2020-06-26 17:57:26 +02:00
Johannes Zellner 5199a9342e Add missing ldap client error handling 2020-06-26 17:55:42 +02:00
Girish Ramakrishnan 893ecec0fa redis: Set maxmemory and maxmemory-policy 2020-06-26 08:54:47 -07:00
Girish Ramakrishnan e3da6419f5 Add 5.3.2 changes 2020-06-26 08:48:01 -07:00
Girish Ramakrishnan 0750d2ba50 More changes 2020-06-25 16:48:11 -07:00
Girish Ramakrishnan f1fcb65fbe Do not install sshfs. user will install it if they want
we don't use sshfs anywhere in our code ourselves
2020-06-25 12:21:49 -07:00
Girish Ramakrishnan 215aa65d5a Fix provider usage
* do not send to appstore anymore
* do not set in getStatus/getConfig
* provider is not needed when registering cloudron
2020-06-25 11:20:05 -07:00
Girish Ramakrishnan 85f67c13da remove unused registerWithLicense 2020-06-25 11:11:52 -07:00
Girish Ramakrishnan 6dcc478aeb add to changes 2020-06-25 09:20:37 -07:00
Johannes Zellner 3f2496db6f Support self-signed certs for external ldap/ad 2020-06-25 17:45:59 +02:00
Johannes Zellner 612f79f9e0 Copy over changes for 5.3.1 2020-06-25 14:22:44 +02:00
Johannes Zellner 90fb1cd735 We also need enableBackup property for app listing api 2020-06-25 12:31:00 +02:00
48 changed files with 868 additions and 750 deletions
+50
View File
@@ -1965,6 +1965,7 @@
* better nginx config for higher loads
* backups: add CIFS storage provider
* backups: add SSHFS storage provider
* backups: add NFS storage provider
* s3: use vhost style
* Fix crash when redis config was set
* Update schedule was unselected in the UI
@@ -1978,4 +1979,53 @@
* mail: make authentication case insensitive
* Fix timeout issues in postgresql and mysql addon
* Do not count stopped apps for memory use
* LDAP group synchronization
[5.3.1]
* better nginx config for higher loads
* backups: add CIFS storage provider
* backups: add SSHFS storage provider
* backups: add NFS storage provider
* s3: use vhost style
* Fix crash when redis config was set
* Update schedule was unselected in the UI
* cloudron-setup: --provider is now optional
* show warning for unstable updates
* add forumUrl to app manifest
* postgresql: add unaccent extension for peertube
* mail: Add Auto-Submitted header to NDRs
* backups: ensure that the latest backup of installed apps is always preserved
* add nginx logs
* mail: make authentication case insensitive
* Fix timeout issues in postgresql and mysql addon
* Do not count stopped apps for memory use
* LDAP group synchronization
[5.3.2]
* Do not install sshfs package
* 'provider' is not required anymore in various API calls
* redis: Set maxmemory and maxmemory-policy
* Add mlock capability to manifest (for vault app)
[5.3.3]
* Fix issue where some postinstall messages where causing angular to infinite loop
[5.3.4]
* Fix issue in database error handling
[5.4.0]
* Update nginx to 1.18 for various security fixes
* Add ping capability (for statping app)
* Fix bug where aliases were displayed incorrectly in SOGo
* Add univention as LDAP provider
* Bump max_connection for postgres addon to 200
* mail: Add pagination to mailing list API
* Allow admin to lock email and display name of users
* Allow admin to ensure all users have 2FA setup
* ami: fix regression where we didn't send provider as part of get status call
* nginx: hide version
* backups: add b2 provider
* Add filemanager webinterface
* Add darkmode
* Add note that password reset and invite links expire in 24 hours
+8 -12
View File
@@ -13,6 +13,9 @@ function die {
export DEBIAN_FRONTEND=noninteractive
readonly ubuntu_codename=$(lsb_release -cs)
readonly ubuntu_version=$(lsb_release -rs)
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
@@ -26,8 +29,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
ubuntu_codename=$(lsb_release -cs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
@@ -52,16 +53,11 @@ apt-get -y install \
unbound \
xfsprogs
if [[ "${ubuntu_version}" == "16.04" ]]; then
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
else
apt install -y nginx-full
fi
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
if (error) console.error(error);
callback(error);
});
};
+2 -1
View File
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS users(
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
@@ -88,6 +88,7 @@ CREATE TABLE IF NOT EXISTS apps(
taskId INTEGER, // current task
errorJson TEXT,
bindsJson TEXT, // bind mounts
servicesConfigJson TEXT, // app services configuration
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
+46 -46
View File
@@ -343,7 +343,7 @@
},
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
@@ -416,7 +416,7 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
@@ -488,7 +488,7 @@
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"resolved": false,
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
@@ -626,7 +626,7 @@
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
@@ -641,7 +641,7 @@
},
"bunyan": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"resolved": false,
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
@@ -741,9 +741,9 @@
}
},
"cloudron-manifestformat": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.3.0.tgz",
"integrity": "sha512-KMHTtR/oRnMzqTUzY1706xpYA1PrvmwIenC8HrigJhOYQdeMLv7egg1Eg0QIB1bfxKIhb+1Zc2i48HKE7MQGkg==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.5.0.tgz",
"integrity": "sha512-Xf1vOwCFT5h1MZQ9fC8EyfL2jfpVlShg5r7est/ZA+vSzcbvk2nQxPmpk4q4e6iDfr19B7iUw2b2X7mw5c1Dlg==",
"requires": {
"cron": "^1.8.2",
"java-packagename-regex": "^1.0.0",
@@ -772,7 +772,7 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
@@ -817,7 +817,7 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
@@ -1001,7 +1001,7 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
@@ -1043,7 +1043,7 @@
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"resolved": false,
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
@@ -1249,7 +1249,7 @@
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"resolved": false,
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"deep-eql": {
@@ -1529,7 +1529,7 @@
},
"ent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"resolved": false,
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
},
"error-ex": {
@@ -1633,7 +1633,7 @@
},
"expect.js": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
"resolved": false,
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
"dev": true
},
@@ -2420,7 +2420,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
@@ -2450,7 +2450,7 @@
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"resolved": false,
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
@@ -2529,12 +2529,12 @@
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isstream": {
@@ -2661,7 +2661,7 @@
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"resolved": false,
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
@@ -2669,7 +2669,7 @@
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"resolved": false,
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
@@ -2868,19 +2868,19 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -3093,7 +3093,7 @@
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
@@ -3104,7 +3104,7 @@
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"resolved": false,
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
@@ -3117,13 +3117,13 @@
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"resolved": false,
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
@@ -3411,7 +3411,7 @@
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"resolved": false,
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true,
"requires": {
@@ -3460,7 +3460,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
@@ -3611,7 +3611,7 @@
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"resolved": false,
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
@@ -3635,7 +3635,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
@@ -3725,7 +3725,7 @@
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"resolved": false,
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pretty-bytes": {
@@ -3793,7 +3793,7 @@
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
"dev": true
},
@@ -4061,7 +4061,7 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
@@ -4247,7 +4247,7 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
@@ -4307,7 +4307,7 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"smtp-connection": {
@@ -4401,7 +4401,7 @@
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"resolved": false,
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
@@ -4568,7 +4568,7 @@
},
"stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"resolved": false,
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
@@ -4876,7 +4876,7 @@
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"resolved": false,
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
@@ -4948,7 +4948,7 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utile": {
@@ -5003,7 +5003,7 @@
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"resolved": false,
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
@@ -5011,7 +5011,7 @@
"dependencies": {
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"resolved": false,
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
@@ -5021,7 +5021,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"resolved": false,
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
@@ -5044,7 +5044,7 @@
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
@@ -5131,7 +5131,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
+1 -1
View File
@@ -20,7 +20,7 @@
"async": "^2.6.3",
"aws-sdk": "^2.685.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.3.0",
"cloudron-manifestformat": "^5.5.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0",
+1 -7
View File
@@ -106,12 +106,6 @@ if [[ "${initBaseImage}" == "true" ]]; then
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
@@ -159,7 +153,7 @@ if [[ "${initBaseImage}" == "true" ]]; then
echo ""
fi
# NOTE: this install script only supports 4.2 and above
# The provider flag is still used for marketplace images
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
echo "${provider}" > /etc/cloudron/PROVIDER
+1 -1
View File
@@ -112,7 +112,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
if [[ -d /home/ubuntu ]]; then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
+3 -6
View File
@@ -58,9 +58,9 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
fi
readonly nginx_version=$(nginx -v)
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then
echo "==> installer: installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
@@ -92,9 +92,6 @@ if [[ ${try} -eq 10 ]]; then
exit 4
fi
echo "==> installer: ensure sshfs is installed"
apt install -y sshfs
echo "==> installer: downloading new addon images"
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
+1 -1
View File
@@ -14,7 +14,7 @@ WorkingDirectory=/home/yellowtent/box
Restart=always
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
+1 -1
View File
@@ -427,7 +427,7 @@ function removeInternalFields(app) {
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'alternateDomains', 'sso',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label');
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
}
function getIconUrlSync(app) {
+7 -136
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials: registerWithLoginCredentials,
registerWithLicense: registerWithLicense,
purchaseApp: purchaseApp,
unpurchaseApp: unpurchaseApp,
@@ -20,8 +19,6 @@ exports = module.exports = {
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
@@ -34,21 +31,14 @@ var apps = require('./apps.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
users = require('./users.js'),
util = require('util');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
let gFeatures = {
@@ -57,7 +47,10 @@ let gFeatures = {
externalLdap: false,
privateDockerRegistry: false,
branding: false,
support: false
support: false,
directoryConfig: false,
mailboxMaxCount: 5,
emailPremium: false
};
// attempt to load feature cache in case appstore would be down
@@ -233,111 +226,6 @@ function unpurchaseApp(appId, data, callback) {
});
}
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(error);
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(error);
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(error);
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(error);
loginEvents = result;
callback();
});
},
function (callback) {
users.count(function (error, result) {
if (error) return callback(error);
userCount = result;
callback();
});
},
function (callback) {
groups.count(function (error, result) {
if (error) return callback(error);
groupCount = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
userCount: userCount,
groupCount: groupCount,
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
};
var data = {
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: settings.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
});
}
function getBoxUpdate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -453,16 +341,14 @@ function registerCloudron(data, callback) {
// This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false;
function trackBeginSetup(provider) {
assert.strictEqual(typeof provider, 'string');
function trackBeginSetup() {
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
if (gBeginSetupAlreadyTracked) return;
gBeginSetupAlreadyTracked = true;
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
superagent.post(url).send({ provider }).timeout(30 * 1000).end(function (error, result) {
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
@@ -480,21 +366,6 @@ function trackFinishedSetup(domain) {
});
}
function registerWithLicense(license, domain, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
const provider = settings.provider();
const version = constants.VERSION;
registerCloudron({ license, domain, provider, version }, callback);
});
}
function registerWithLoginCredentials(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -514,7 +385,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION, purpose: options.purpose || '' }, callback);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
});
});
});
+1
View File
@@ -105,6 +105,7 @@ function api(provider) {
case 'exoscale-sos': return require('./storage/s3.js');
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'backblaze-b2': return require('./storage/s3.js');
case 'linode-objectstorage': return require('./storage/s3.js');
case 'ovh-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
+3 -2
View File
@@ -139,10 +139,11 @@ function getConfig(callback) {
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
features: appstore.getFeatures()
features: appstore.getFeatures(),
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
});
});
}
-8
View File
@@ -22,7 +22,6 @@ exports = module.exports = {
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
@@ -41,7 +40,6 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
@@ -73,12 +71,6 @@ function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
const randomMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // every hour on a random minute
onTick: appstore.sendAliveStatus,
start: true
});
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
+32 -82
View File
@@ -22,8 +22,7 @@ var assert = require('assert'),
once = require('once'),
util = require('util');
var gConnectionPool = null,
gDefaultConnection = null;
var gConnectionPool = null;
const gDatabase = {
hostname: '127.0.0.1',
@@ -43,66 +42,37 @@ function initialize(callback) {
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// https://github.com/mysqljs/mysql#pool-options
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
connectionLimit: 5,
host: gDatabase.hostname,
user: gDatabase.username,
password: gDatabase.password,
port: gDatabase.port,
database: gDatabase.name,
multipleStatements: false,
waitForConnections: true, // getConnection() will wait until a connection is avaiable
ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
});
gConnectionPool.on('connection', function (connection) {
// connection objects are re-used. so we have to attach to the event here (once) to prevent crash
// note the pool also has an 'acquire' event but that is called whenever we do a getConnection()
connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message} ${error.code}`));
connection.query('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
reconnect(callback);
callback(null);
}
function uninitialize(callback) {
if (gConnectionPool) {
gConnectionPool.end(callback);
gDefaultConnection = null;
gConnectionPool = null;
} else {
callback(null);
}
}
if (!gConnectionPool) return callback(null);
function reconnect(callback) {
callback = callback ? once(callback) : function () {};
debug('reconnect: connecting to database');
gConnectionPool.getConnection(function (error, connection) {
if (error) {
debug(`reconnect: db connection error. ${error.message} fatal:${error.fatal} code:${error.code}. Will retry in 10 seconds`);
return setTimeout(reconnect.bind(null, callback), 10000);
}
debug('reconnect: connected to database');
connection.on('error', function (error) {
// by design, we catch all normal errors by providing callbacks.
// this function should be invoked only when we have no callbacks pending and we have a fatal error
assert(error.fatal, 'Non-fatal error on connection object');
debug(`reconnect: db connection error. ${error.message} fatal:${error.fatal} code:${error.code}. Will retry in 10 seconds`);
gDefaultConnection = null;
// This is most likely an issue an can cause double callbacks from reconnect()
setTimeout(reconnect.bind(null, callback), 10000);
});
gDefaultConnection = connection;
callback(null);
});
gConnectionPool.end(callback);
gConnectionPool = null;
}
function clear(callback) {
@@ -115,39 +85,14 @@ function clear(callback) {
child_process.exec(cmd, callback);
}
function beginTransaction(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.'));
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error);
connection.beginTransaction(function (error) {
if (error) return callback(error);
return callback(null, connection);
});
});
}
function query() {
var args = Array.prototype.slice.call(arguments);
var callback = args[args.length - 1];
const args = Array.prototype.slice.call(arguments);
const callback = args[args.length - 1];
assert.strictEqual(typeof callback, 'function');
if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database'));
if (constants.TEST && !gConnectionPool) return callback(new BoxError(BoxError.DATABASE_ERROR, 'database.js not initialized'));
gDefaultConnection.query.apply(gDefaultConnection, args);
}
function rollback(connection, transactionError, callback) {
connection.rollback(function (error) {
if (error) debug('rollback: error when rolling back', error);
connection.release();
callback(transactionError);
});
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
}
function transaction(queries, callback) {
@@ -156,21 +101,26 @@ function transaction(queries, callback) {
callback = once(callback);
beginTransaction(function (error, connection) {
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error);
connection.on('error', callback);
const releaseConnection = (error) => { connection.release(); callback(error); };
async.mapSeries(queries, function iterator(query, done) {
connection.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return rollback(connection, error, callback);
connection.beginTransaction(function (error) {
if (error) return releaseConnection(error);
connection.commit(function (error) {
if (error) return rollback(connection, error, callback);
async.mapSeries(queries, function iterator(query, done) {
connection.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.release();
callback(null, results);
connection.commit(function (error) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.release();
callback(null, results);
});
});
});
});
@@ -194,7 +144,7 @@ function exportToFile(file, callback) {
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
// this option must not be set in production cloudrons which still use the old mysqldump
const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : '';
const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
+7 -6
View File
@@ -306,7 +306,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapDrop: [ 'NET_RAW' ] // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
CapAdd: [],
CapDrop: []
},
NetworkingConfig: {
EndpointsConfig: {
@@ -318,11 +319,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
};
var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd = [
'NET_ADMIN', 'NET_RAW'
];
}
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
containerOptions = _.extend(containerOptions, options);
+38 -16
View File
@@ -22,6 +22,7 @@ var assert = require('assert'),
debug = require('debug')('box:externalldap'),
groups = require('./groups.js'),
ldap = require('ldapjs'),
once = require('once'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
@@ -41,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
return {
username: ldapUser[ldapConfig.usernameField],
email: ldapUser.mail,
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
};
}
function validUserRequirements(user) {
if (!user.username || !user.email || !user.displayName) {
debug(`[LDAP user empty username/email/displayName] username=${user.username} email=${user.email} displayName=${user.displayName}`);
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
return false;
} else {
return true;
@@ -56,29 +57,46 @@ function validUserRequirements(user) {
}
// performs service bind if required
function getClient(externalLdapConfig, callback) {
function getClient(externalLdapConfig, doBindAuth, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof doBindAuth, 'boolean');
assert.strictEqual(typeof callback, 'function');
// ensure we only callback once since we also have to listen to client.error events
callback = once(callback);
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
var config = {
url: externalLdapConfig.url,
tlsOptions: {
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
}
};
var client;
try {
client = ldap.createClient({ url: externalLdapConfig.url });
client = ldap.createClient(config);
} catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
}
if (!externalLdapConfig.bindDn) return callback(null, client);
// ensure we don't just crash
client.on('error', function (error) {
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
});
// skip bind auth if none exist or if not wanted
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, client, externalLdapConfig);
callback(null, client);
});
}
@@ -87,7 +105,7 @@ function ldapGetByDN(externalLdapConfig, dn, callback) {
assert.strictEqual(typeof dn, 'string');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) {
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -124,7 +142,7 @@ function ldapUserSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) {
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -165,7 +183,7 @@ function ldapGroupSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) {
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -220,6 +238,7 @@ function testConfig(config, callback) {
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean'));
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean'));
if (config.syncGroups) {
if (!config.groupBaseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty'));
@@ -231,7 +250,7 @@ function testConfig(config, callback) {
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
}
getClient(config, function (error, client) {
getClient(config, true, function (error, client) {
if (error) return callback(error);
var opts = {
@@ -315,12 +334,15 @@ function verifyPassword(user, password, callback) {
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
let client = ldap.createClient({ url: externalLdapConfig.url });
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
getClient(externalLdapConfig, false, function (error, client) {
if (error) return callback(error);
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
});
});
});
@@ -496,7 +518,7 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
return callback();
}
var ldapGroupMembers = found.member || [];
var ldapGroupMembers = found.member || found.uniqueMember || [];
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
+3 -3
View File
@@ -17,11 +17,11 @@ exports = module.exports = {
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.2.1@sha256:430f3e8b700327d4afa03a7b4e10a8b5544f171e0946ead8cdc5b67ee32db8e4' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.3.0@sha256:4112cc31a09b465bdfbd715fedab4bc86898246b4615d789dfb1d2cb728e3872' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.2.0@sha256:205486ff0f6bf6854610572df401cf3651bc62baf28fd26e9c5632497f10c2cb' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.2.0@sha256:cfdcc1a54cf29818cac99eacedc2ecf04e44995be3d06beea11dcaa09d90ed8d' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.4@sha256:0e169b97a0584a76197d2bbc039d8698bf93f815588b3b43c251bd83dd545465' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.1.0@sha256:0c1fe4dd6121900624dcb383251ecb0084c3810e095064933de671409d8d6d7b' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.0@sha256:63cba3c70c91d440e1e436417ab33f655aa75c7bf936c0a5f4447e71e4ffbe4f' }
}
};
+1 -1
View File
@@ -346,7 +346,7 @@ function mailboxSearch(req, res, next) {
if (error) return callback(error);
aliases.forEach(function (a, idx) {
obj.attributes['mail' + idx] = `${a}@${mailbox.domain}`;
obj.attributes['mail' + idx] = `${a.name}@${a.domain}`;
});
// ensure all filter values are also lowercase
+53 -37
View File
@@ -1,52 +1,53 @@
'use strict';
exports = module.exports = {
getStatus: getStatus,
checkConfiguration: checkConfiguration,
getStatus,
checkConfiguration,
getDomains: getDomains,
getDomains,
getDomain: getDomain,
clearDomains: clearDomains,
getDomain,
clearDomains,
onDomainAdded: onDomainAdded,
onDomainRemoved: onDomainRemoved,
onDomainAdded,
onDomainRemoved,
onMailFqdnChanged,
removePrivateFields: removePrivateFields,
removePrivateFields,
setDnsRecords: setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
setDnsRecords,
validateName: validateName,
validateName,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
getMailAuth: getMailAuth,
restartMail,
handleCertChanged,
getMailAuth,
sendTestMail: sendTestMail,
sendTestMail,
listMailboxes: listMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
getMailboxCount,
listMailboxes,
removeMailboxes,
getMailbox,
addMailbox,
updateMailboxOwner,
removeMailbox,
getAliases: getAliases,
setAliases: setAliases,
getAliases,
setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
resolveList: resolveList,
getLists,
getList,
addList,
updateList,
removeList,
resolveList,
_readDkimPublicKeySync: readDkimPublicKeySync
};
@@ -1032,13 +1033,25 @@ function sendTestMail(domain, to, callback) {
});
}
function listMailboxes(domain, page, perPage, callback) {
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
mailboxdb.listMailboxes(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailboxCount(domain, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1165,11 +1178,14 @@ function setAliases(name, domain, aliases, callback) {
});
}
function getLists(domain, callback) {
function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, function (error, result) {
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
+5 -1
View File
@@ -8,7 +8,7 @@ be reset. If you did not request this reset, please ignore this message.
To reset your password, please visit the following page:
<%- resetLink %>
Please note that the password reset link will expire in 24 hours.
Powered by https://cloudron.io
@@ -29,6 +29,10 @@ Powered by https://cloudron.io
<a href="<%= resetLink %>">Click to reset your password</a>
</p>
<br/>
Please note that the password reset link will expire in 24 hours.
<br/>
<br/>
+4
View File
@@ -11,6 +11,7 @@ Follow the link to get started.
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
Please note that the invite link will expire in 24 hours.
Powered by https://cloudron.io
@@ -36,6 +37,9 @@ Powered by https://cloudron.io
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
<br/>
Please note that the invite link will expire in 24 hours.
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
+58 -34
View File
@@ -1,31 +1,32 @@
'use strict';
exports = module.exports = {
addMailbox: addMailbox,
addList: addList,
addMailbox,
addList,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
del: del,
updateMailboxOwner,
updateList,
del,
listMailboxes: listMailboxes,
getLists: getLists,
getMailboxCount,
listMailboxes,
getLists,
listAllMailboxes: listAllMailboxes,
listAllMailboxes,
get: get,
getMailbox: getMailbox,
getList: getList,
getAlias: getAlias,
get,
getMailbox,
getList,
getAlias,
getAliasesForName: getAliasesForName,
setAliasesForName: setAliasesForName,
getAliasesForName,
setAliasesForName,
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
getByOwnerId,
delByOwnerId,
delByDomain,
updateName: updateName,
updateName,
_clear: clear,
@@ -37,6 +38,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
@@ -203,20 +205,35 @@ function getMailbox(name, domain, callback) {
});
}
function listMailboxes(domain, page, perPage, callback) {
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results[0].total);
});
}
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ')';
query += 'ORDER BY name LIMIT ?,?';
results.forEach(function (result) { postProcess(result); });
database.query(query, [ exports.TYPE_MAILBOX, domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function listAllMailboxes(page, perPage, callback) {
@@ -224,8 +241,8 @@ function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX ], function (error, results) {
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ?,?`,
[ exports.TYPE_MAILBOX, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -234,18 +251,25 @@ function listAllMailboxes(page, perPage, callback) {
});
}
function getLists(domain, callback) {
function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
[ exports.TYPE_LIST, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
results.forEach(function (result) { postProcess(result); });
query += 'ORDER BY name LIMIT ?,?';
callback(null, results);
});
database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getList(name, domain, callback) {
+12 -5
View File
@@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
# http server
server {
listen 80;
server_tokens off; # hide version
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
@@ -42,18 +43,19 @@ server {
server {
<% if (vhost) { -%>
server_name <%= vhost %>;
listen 443 http2;
listen 443 ssl http2;
<% if (hasIPv6) { -%>
listen [::]:443 http2;
listen [::]:443 ssl http2;
<% } -%>
<% } else { -%>
listen 443 http2 default_server;
listen 443 ssl http2 default_server;
<% if (hasIPv6) { -%>
listen [::]:443 http2 default_server;
listen [::]:443 ssl http2 default_server;
<% } -%>
<% } -%>
ssl on;
server_tokens off; # hide version
# paths are relative to prefix and not to this file
ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>;
@@ -193,6 +195,11 @@ server {
client_max_body_size 0;
}
location ~ ^/api/v1/apps/.*/files/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/graphite-web/ {
+1 -1
View File
@@ -226,11 +226,11 @@ function getStatus(callback) {
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated,
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus));
});
});
+10 -23
View File
@@ -86,7 +86,7 @@ function logout(req, res) {
function passwordResetRequest(req, res, next) {
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
users.sendPasswordResetByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
@@ -102,6 +102,7 @@ function passwordReset(req, res, next) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
// if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
@@ -122,37 +123,23 @@ function passwordReset(req, res, next) {
function setupAccount(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken must be a non-empty string'));
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string'));
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
if (!req.body.displayName || typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
// only sent if profile is not locked
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
// if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
users.setupAccount(userObject, req.body, auditSource.fromRequest(req), function (error, accessToken) {
if (error) return next(BoxError.toHttpError(error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
users.setPassword(userObject, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { accessToken: result.accessToken }));
});
});
next(new HttpSuccess(201, { accessToken }));
});
});
}
+43
View File
@@ -0,0 +1,43 @@
'use strict';
exports = module.exports = {
proxy
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
docker = require('../docker.js'),
middleware = require('../middleware/index.js'),
HttpError = require('connect-lastmile').HttpError,
safe = require('safetydance'),
url = require('url');
function proxy(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
const appId = req.params.id;
req.clearTimeout();
docker.inspect('sftp', function (error, result) {
if (error)return next(BoxError.toHttpError(error));
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return next(new BoxError(BoxError.INACTIVE, 'Error getting IP of sftp service'));
req.url = req.originalUrl.replace(`/api/v1/apps/${appId}/files`, `/files/${appId}`);
const proxyOptions = url.parse(`https://${ip}:3000`);
proxyOptions.rejectUnauthorized = false;
const fileManagerProxy = middleware.proxy(proxyOptions);
fileManagerProxy(req, res, function (error) {
if (!error) return next();
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to filemanager server'));
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query filemanager server'));
next(new HttpError(500, error));
});
});
}
+1
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
cloudron: require('./cloudron.js'),
domains: require('./domains.js'),
eventlog: require('./eventlog.js'),
filemanager: require('./filemanager.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
mail: require('./mail.js'),
+44 -22
View File
@@ -1,33 +1,35 @@
'use strict';
exports = module.exports = {
getDomain: getDomain,
getDomain,
setDnsRecords: setDnsRecords,
setDnsRecords,
getStatus: getStatus,
getStatus,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
sendTestMail: sendTestMail,
sendTestMail,
listMailboxes: listMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
listMailboxes,
getMailbox,
addMailbox,
updateMailbox,
removeMailbox,
getAliases: getAliases,
setAliases: setAliases,
getAliases,
setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
getLists,
getList,
addList,
updateList,
removeList,
getMailboxCount
};
var assert = require('assert'),
@@ -159,13 +161,25 @@ function listMailboxes(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
mail.listMailboxes(req.params.domain, page, perPage, function (error, result) {
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
mail.listMailboxes(req.params.domain, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function getMailboxCount(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getMailboxCount(req.params.domain, function (error, count) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { count }));
});
}
function getMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
@@ -248,7 +262,15 @@ function setAliases(req, res, next) {
function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getLists(req.params.domain, function (error, result) {
const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
mail.getLists(req.params.domain, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { lists: result }));
+34 -21
View File
@@ -1,34 +1,41 @@
'use strict';
exports = module.exports = {
get: get,
update: update,
getAvatar: getAvatar,
setAvatar: setAvatar,
clearAvatar: clearAvatar,
changePassword: changePassword,
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication: enableTwoFactorAuthentication,
disableTwoFactorAuthentication: disableTwoFactorAuthentication
authorize,
get,
update,
getAvatar,
setAvatar,
clearAvatar,
changePassword,
setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication,
disableTwoFactorAuthentication,
};
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
users = require('../users.js'),
settings = require('../settings.js'),
_ = require('underscore');
function get(req, res, next) {
function authorize(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const emailHash = require('crypto').createHash('md5').update(req.user.email).digest('hex');
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return next(BoxError.toHttpError(error));
if (directoryConfig.lockUserProfiles) return next(new HttpError(403, 'admin has disallowed users from editing profiles'));
next();
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
next(new HttpSuccess(200, {
id: req.user.id,
@@ -39,7 +46,7 @@ function get(req, res, next) {
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
role: req.user.role,
source: req.user.source,
avatarUrl: fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id)) ? `${settings.adminOrigin()}/api/v1/profile/avatar/${req.user.id}` : `https://www.gravatar.com/avatar/${emailHash}.jpg`
avatarUrl: users.getAvatarUrlSync(req.user)
}));
}
@@ -65,21 +72,27 @@ function setAvatar(req, res, next) {
if (!req.files.avatar) return next(new HttpError(400, 'avatar is missing'));
if (!safe.fs.renameSync(req.files.avatar.path, path.join(paths.PROFILE_ICONS_DIR, req.user.id))) return next(new HttpError(500, safe.error));
users.setAvatar(req.user.id, req.files.avatar.path, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, {}));
});
}
function clearAvatar(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
safe.fs.unlinkSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id));
users.clearAvatar(req.user.id, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, {}));
});
}
function getAvatar(req, res) {
res.sendFile(path.join(paths.PROFILE_ICONS_DIR, req.params.identifier));
assert.strictEqual(typeof req.params.identifier, 'string');
res.sendFile(users.getAvatarFileSync(req.params.identifier));
}
function changePassword(req, res, next) {
+1 -1
View File
@@ -122,7 +122,7 @@ function getStatus(req, res, next) {
// check if Cloudron is not in setup state nor activated and let appstore know of the attempt
if (!status.activated && !status.setup.active && !status.restore.active) {
appstore.trackBeginSetup(status.provider);
appstore.trackBeginSetup();
}
});
}
+24
View File
@@ -234,6 +234,27 @@ function setRegistryConfig(req, res, next) {
});
}
function getDirectoryConfig(req, res, next) {
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, directoryConfig));
});
}
function setDirectoryConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.lockUserProfiles !== 'boolean') return next(new HttpError(400, 'lockUserProfiles is required'));
if (typeof req.body.mandatory2FA !== 'boolean') return next(new HttpError(400, 'mandatory2FA is required'));
settings.setDirectoryConfig(req.body, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function getSysinfoConfig(req, res, next) {
settings.getSysinfoConfig(function (error, sysinfoConfig) {
if (error) return next(BoxError.toHttpError(error));
@@ -270,6 +291,7 @@ function get(req, res, next) {
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return getDirectoryConfig(req, res, next);
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
@@ -291,6 +313,8 @@ function set(req, res, next) {
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return setDirectoryConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
}
}
+35 -11
View File
@@ -1,18 +1,20 @@
'use strict';
exports = module.exports = {
get: get,
update: update,
list: list,
create: create,
remove: remove,
changePassword: changePassword,
verifyPassword: verifyPassword,
createInvite: createInvite,
sendInvite: sendInvite,
setGroups: setGroups,
get,
update,
list,
create,
remove,
changePassword,
verifyPassword,
createInvite,
sendInvite,
setGroups,
setAvatar,
clearAvatar,
load: load
load
};
var assert = require('assert'),
@@ -192,3 +194,25 @@ function changePassword(req, res, next) {
next(new HttpSuccess(204));
});
}
function setAvatar(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (!req.files.avatar) return next(new HttpError(400, 'avatar is missing'));
users.setAvatar(req.resource.id, req.files.avatar.path, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function clearAvatar(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
users.clearAvatar(req.resource.id, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
+164 -164
View File
@@ -66,7 +66,6 @@ function initializeExpressSync() {
// the timeout middleware will respond with a 503. the request itself cannot be 'aborted' and will continue
// search for req.clearTimeout in route handlers to see places where this timeout is reset
.use(middleware.timeout(REQUEST_TIMEOUT, { respond: true }))
.use(json)
.use(urlencoded)
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
.use(router)
@@ -86,163 +85,164 @@ function initializeExpressSync() {
const authorizeUserManager = routes.accesscontrol.authorize(users.ROLE_USER_MANAGER);
// public routes
router.post('/api/v1/cloudron/setup', routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.provision.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.provision.activate);
router.get ('/api/v1/cloudron/status', routes.provision.getStatus);
router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // this is a public alias for /api/v1/branding/cloudron_avatar
router.post('/api/v1/cloudron/setup', json, routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
router.post('/api/v1/cloudron/restore', json, routes.provision.restore); // only available until activated
router.post('/api/v1/cloudron/activate', json, routes.provision.activate);
router.get ('/api/v1/cloudron/status', routes.provision.getStatus);
router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // this is a public alias for /api/v1/branding/cloudron_avatar
// login/logout routes
router.post('/api/v1/cloudron/login', password, routes.cloudron.login);
router.get ('/api/v1/cloudron/logout', routes.cloudron.logout); // this will invalidate the token if any and redirect to /login.html always
router.post('/api/v1/cloudron/password_reset_request', routes.cloudron.passwordResetRequest);
router.post('/api/v1/cloudron/password_reset', routes.cloudron.passwordReset);
router.post('/api/v1/cloudron/setup_account', routes.cloudron.setupAccount);
router.post('/api/v1/cloudron/login', json, password, routes.cloudron.login);
router.get ('/api/v1/cloudron/logout', routes.cloudron.logout); // this will invalidate the token if any and redirect to /login.html always
router.post('/api/v1/cloudron/password_reset_request', json, routes.cloudron.passwordResetRequest);
router.post('/api/v1/cloudron/password_reset', json, routes.cloudron.passwordReset);
router.post('/api/v1/cloudron/setup_account', json, routes.cloudron.setupAccount);
// developer routes
router.post('/api/v1/developer/login', password, routes.cloudron.login); // DEPRECATED we should use the regular /api/v1/cloudron/login
router.post('/api/v1/developer/login', json, password, routes.cloudron.login); // DEPRECATED we should use the regular /api/v1/cloudron/login
// cloudron routes
router.get ('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.update);
router.post('/api/v1/cloudron/prepare_dashboard_domain', token, authorizeAdmin, routes.cloudron.prepareDashboardDomain);
router.post('/api/v1/cloudron/set_dashboard_domain', token, authorizeAdmin, routes.cloudron.setDashboardAndMailDomain);
router.post('/api/v1/cloudron/renew_certs', token, authorizeAdmin, routes.cloudron.renewCerts);
router.post('/api/v1/cloudron/check_for_updates', token, authorizeAdmin, routes.cloudron.checkForUpdates);
router.get ('/api/v1/cloudron/reboot', token, authorizeAdmin, routes.cloudron.isRebootRequired);
router.post('/api/v1/cloudron/reboot', token, authorizeAdmin, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', token, authorizeAdmin, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/disks', token, authorizeAdmin, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/memory', token, authorizeAdmin, routes.cloudron.getMemory);
router.get ('/api/v1/cloudron/logs/:unit', token, authorizeAdmin, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream/:unit', token, authorizeAdmin, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/eventlog', token, authorizeAdmin, routes.eventlog.list);
router.get ('/api/v1/cloudron/eventlog/:eventId', token, authorizeAdmin, routes.eventlog.get);
router.post('/api/v1/cloudron/sync_external_ldap', token, authorizeAdmin, routes.cloudron.syncExternalLdap);
router.get ('/api/v1/cloudron/server_ip', token, authorizeAdmin, routes.cloudron.getServerIp);
router.get ('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', json, token, authorizeAdmin, routes.cloudron.update);
router.post('/api/v1/cloudron/prepare_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.prepareDashboardDomain);
router.post('/api/v1/cloudron/set_dashboard_domain', json, token, authorizeAdmin, routes.cloudron.setDashboardAndMailDomain);
router.post('/api/v1/cloudron/renew_certs', json, token, authorizeAdmin, routes.cloudron.renewCerts);
router.post('/api/v1/cloudron/check_for_updates', json, token, authorizeAdmin, routes.cloudron.checkForUpdates);
router.get ('/api/v1/cloudron/reboot', token, authorizeAdmin, routes.cloudron.isRebootRequired);
router.post('/api/v1/cloudron/reboot', json, token, authorizeAdmin, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', token, authorizeAdmin, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/disks', token, authorizeAdmin, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/memory', token, authorizeAdmin, routes.cloudron.getMemory);
router.get ('/api/v1/cloudron/logs/:unit', token, authorizeAdmin, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream/:unit', token, authorizeAdmin, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/eventlog', token, authorizeAdmin, routes.eventlog.list);
router.get ('/api/v1/cloudron/eventlog/:eventId', token, authorizeAdmin, routes.eventlog.get);
router.post('/api/v1/cloudron/sync_external_ldap', json, token, authorizeAdmin, routes.cloudron.syncExternalLdap);
router.get ('/api/v1/cloudron/server_ip', token, authorizeAdmin, routes.cloudron.getServerIp);
// tasks
router.get ('/api/v1/tasks', token, authorizeAdmin, routes.tasks.list);
router.get ('/api/v1/tasks/:taskId', token, authorizeAdmin, routes.tasks.get);
router.get ('/api/v1/tasks/:taskId/logs', token, authorizeAdmin, routes.tasks.getLogs);
router.get ('/api/v1/tasks/:taskId/logstream', token, authorizeAdmin, routes.tasks.getLogStream);
router.post('/api/v1/tasks/:taskId/stop', token, authorizeAdmin, routes.tasks.stopTask);
// task routes
router.get ('/api/v1/tasks', token, authorizeAdmin, routes.tasks.list);
router.get ('/api/v1/tasks/:taskId', token, authorizeAdmin, routes.tasks.get);
router.get ('/api/v1/tasks/:taskId/logs', token, authorizeAdmin, routes.tasks.getLogs);
router.get ('/api/v1/tasks/:taskId/logstream', token, authorizeAdmin, routes.tasks.getLogStream);
router.post('/api/v1/tasks/:taskId/stop', json, token, authorizeAdmin, routes.tasks.stopTask);
// notifications
router.get ('/api/v1/notifications', token, routes.notifications.verifyOwnership, routes.notifications.list);
router.get ('/api/v1/notifications/:notificationId', token, routes.notifications.verifyOwnership, routes.notifications.get);
router.post('/api/v1/notifications/:notificationId', token, routes.notifications.verifyOwnership, routes.notifications.ack);
// notification routes
router.get ('/api/v1/notifications', token, routes.notifications.verifyOwnership, routes.notifications.list);
router.get ('/api/v1/notifications/:notificationId', token, routes.notifications.verifyOwnership, routes.notifications.get);
router.post('/api/v1/notifications/:notificationId', json, token, routes.notifications.verifyOwnership, routes.notifications.ack);
// backups
router.get ('/api/v1/backups', token, authorizeAdmin, routes.backups.list);
router.post('/api/v1/backups/create', token, authorizeAdmin, routes.backups.startBackup);
router.post('/api/v1/backups/cleanup', token, authorizeAdmin, routes.backups.cleanup);
// backup routes
router.get ('/api/v1/backups', token, authorizeAdmin, routes.backups.list);
router.post('/api/v1/backups/create', token, authorizeAdmin, routes.backups.startBackup);
router.post('/api/v1/backups/cleanup', json, token, authorizeAdmin, routes.backups.cleanup);
// config route (for dashboard)
// config route (for dashboard). can return some private configuration unlike status
router.get ('/api/v1/config', token, routes.cloudron.getConfig);
// working off the user behind the provided token
router.get ('/api/v1/profile', token, routes.profile.get);
router.post('/api/v1/profile', token, routes.profile.update);
router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatar); // this is not scoped so it can used directly in img tag
router.post('/api/v1/profile/avatar', token, multipart, routes.profile.setAvatar);
router.del ('/api/v1/profile/avatar', token, routes.profile.clearAvatar);
router.post('/api/v1/profile/password', token, routes.users.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/profile/twofactorauthentication', token, routes.profile.setTwoFactorAuthenticationSecret);
router.post('/api/v1/profile/twofactorauthentication/enable', token, routes.profile.enableTwoFactorAuthentication);
router.post('/api/v1/profile/twofactorauthentication/disable', token, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
router.get ('/api/v1/profile', token, routes.profile.get);
router.post('/api/v1/profile', json, token, routes.profile.authorize, routes.profile.update);
router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatar); // this is not scoped so it can used directly in img tag
router.post('/api/v1/profile/avatar', json, token, multipart, routes.profile.setAvatar); // avatar is not exposed in LDAP. so it's personal and not locked
router.del ('/api/v1/profile/avatar', token, routes.profile.clearAvatar);
router.post('/api/v1/profile/password', json, token, routes.users.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/profile/twofactorauthentication', json, token, routes.profile.setTwoFactorAuthenticationSecret);
router.post('/api/v1/profile/twofactorauthentication/enable', json, token, routes.profile.enableTwoFactorAuthentication);
router.post('/api/v1/profile/twofactorauthentication/disable', json, token, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
router.get ('/api/v1/app_passwords', token, routes.appPasswords.list);
router.post('/api/v1/app_passwords', token, routes.appPasswords.add);
router.get ('/api/v1/app_passwords/:id', token, routes.appPasswords.get);
router.del ('/api/v1/app_passwords/:id', token, routes.appPasswords.del);
// app password routes
router.get ('/api/v1/app_passwords', token, routes.appPasswords.list);
router.post('/api/v1/app_passwords', json, token, routes.appPasswords.add);
router.get ('/api/v1/app_passwords/:id', token, routes.appPasswords.get);
router.del ('/api/v1/app_passwords/:id', token, routes.appPasswords.del);
// access tokens
router.get ('/api/v1/tokens', token, routes.tokens.getAll);
router.post('/api/v1/tokens', token, routes.tokens.add);
router.get ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.get);
router.del ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.del);
router.get ('/api/v1/tokens', token, routes.tokens.getAll);
router.post('/api/v1/tokens', json, token, routes.tokens.add);
router.get ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.get);
router.del ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.del);
// user routes
router.get ('/api/v1/users', token, authorizeUserManager, routes.users.list);
router.post('/api/v1/users', token, authorizeUserManager, routes.users.create);
router.get ('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.remove);
router.post('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.update);
router.post('/api/v1/users/:userId/password', token, authorizeUserManager, routes.users.load, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', token, authorizeUserManager, routes.users.load, routes.users.setGroups);
router.post('/api/v1/users/:userId/send_invite', token, authorizeUserManager, routes.users.load, routes.users.sendInvite);
router.post('/api/v1/users/:userId/create_invite', token, authorizeUserManager, routes.users.load, routes.users.createInvite);
router.get ('/api/v1/users', token, authorizeUserManager, routes.users.list);
router.post('/api/v1/users', json, token, authorizeUserManager, routes.users.create);
router.get ('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.remove);
router.post('/api/v1/users/:userId', json, token, authorizeUserManager, routes.users.load, routes.users.update);
router.post('/api/v1/users/:userId/password', json, token, authorizeUserManager, routes.users.load, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', json, token, authorizeUserManager, routes.users.load, routes.users.setGroups);
router.post('/api/v1/users/:userId/send_invite', json, token, authorizeUserManager, routes.users.load, routes.users.sendInvite);
router.post('/api/v1/users/:userId/create_invite', json, token, authorizeUserManager, routes.users.load, routes.users.createInvite);
router.post('/api/v1/users/:userId/avatar', json, token, authorizeUserManager, routes.users.load, multipart, routes.users.setAvatar);
router.del ('/api/v1/users/:userId/avatar', token, authorizeUserManager, routes.users.load, routes.users.clearAvatar);
// Group management
router.get ('/api/v1/groups', token, authorizeUserManager, routes.groups.list);
router.post('/api/v1/groups', token, authorizeUserManager, routes.groups.create);
router.get ('/api/v1/groups/:groupId', token, authorizeUserManager, routes.groups.get);
router.put ('/api/v1/groups/:groupId/members', token, authorizeUserManager, routes.groups.updateMembers);
router.post('/api/v1/groups/:groupId', token, authorizeUserManager, routes.groups.update);
router.del ('/api/v1/groups/:groupId', token, authorizeUserManager, routes.groups.remove);
router.get ('/api/v1/groups', token, authorizeUserManager, routes.groups.list);
router.post('/api/v1/groups', json, token, authorizeUserManager, routes.groups.create);
router.get ('/api/v1/groups/:groupId', token, authorizeUserManager, routes.groups.get);
router.put ('/api/v1/groups/:groupId/members', json, token, authorizeUserManager, routes.groups.updateMembers);
router.post('/api/v1/groups/:groupId', json, token, authorizeUserManager, routes.groups.update);
router.del ('/api/v1/groups/:groupId', token, authorizeUserManager, routes.groups.remove);
// appstore and subscription routes
router.post('/api/v1/appstore/register_cloudron', token, authorizeAdmin, routes.appstore.registerCloudron);
router.post('/api/v1/appstore/user_token', token, authorizeAdmin, routes.appstore.createUserToken);
router.get ('/api/v1/appstore/subscription', token, authorizeAdmin, routes.appstore.getSubscription);
router.get ('/api/v1/appstore/apps', token, authorizeAdmin, routes.appstore.getApps);
router.get ('/api/v1/appstore/apps/:appstoreId', token, authorizeAdmin, routes.appstore.getApp);
router.get ('/api/v1/appstore/apps/:appstoreId/versions/:versionId', token, authorizeAdmin, routes.appstore.getAppVersion);
router.post('/api/v1/appstore/register_cloudron', json, token, authorizeAdmin, routes.appstore.registerCloudron);
router.post('/api/v1/appstore/user_token', json, token, authorizeAdmin, routes.appstore.createUserToken);
router.get ('/api/v1/appstore/subscription', token, authorizeAdmin, routes.appstore.getSubscription);
router.get ('/api/v1/appstore/apps', token, authorizeAdmin, routes.appstore.getApps);
router.get ('/api/v1/appstore/apps/:appstoreId', token, authorizeAdmin, routes.appstore.getApp);
router.get ('/api/v1/appstore/apps/:appstoreId/versions/:versionId', token, authorizeAdmin, routes.appstore.getAppVersion);
// app routes
router.get ('/api/v1/apps', token, routes.apps.getApps);
router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.load, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', token, routes.apps.load, routes.apps.getAppIcon);
router.post('/api/v1/apps/install', token, authorizeAdmin, routes.apps.install);
router.post('/api/v1/apps/:id/uninstall', token, authorizeAdmin, routes.apps.load, routes.apps.uninstall);
router.post('/api/v1/apps/:id/configure/access_restriction', token, authorizeAdmin, routes.apps.load, routes.apps.setAccessRestriction);
router.post('/api/v1/apps/:id/configure/label', token, authorizeAdmin, routes.apps.load, routes.apps.setLabel);
router.post('/api/v1/apps/:id/configure/tags', token, authorizeAdmin, routes.apps.load, routes.apps.setTags);
router.post('/api/v1/apps/:id/configure/icon', token, authorizeAdmin, routes.apps.load, routes.apps.setIcon);
router.post('/api/v1/apps/:id/configure/memory_limit', token, authorizeAdmin, routes.apps.load, routes.apps.setMemoryLimit);
router.post('/api/v1/apps/:id/configure/cpu_shares', token, authorizeAdmin, routes.apps.load, routes.apps.setCpuShares);
router.post('/api/v1/apps/:id/configure/automatic_backup', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticBackup);
router.post('/api/v1/apps/:id/configure/automatic_update', token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticUpdate);
router.post('/api/v1/apps/:id/configure/reverse_proxy', token, authorizeAdmin, routes.apps.load, routes.apps.setReverseProxyConfig);
router.post('/api/v1/apps/:id/configure/cert', token, authorizeAdmin, routes.apps.load, routes.apps.setCertificate);
router.post('/api/v1/apps/:id/configure/debug_mode', token, authorizeAdmin, routes.apps.load, routes.apps.setDebugMode);
router.post('/api/v1/apps/:id/configure/mailbox', token, authorizeAdmin, routes.apps.load, routes.apps.setMailbox);
router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment);
router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir);
router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.load, routes.apps.setLocation);
router.post('/api/v1/apps/:id/configure/binds', token, authorizeAdmin, routes.apps.load, routes.apps.setBinds);
router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.load, routes.apps.repair);
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.load, routes.apps.update);
router.post('/api/v1/apps/:id/restore', token, authorizeAdmin, routes.apps.load, routes.apps.restore);
router.post('/api/v1/apps/:id/import', token, authorizeAdmin, routes.apps.load, routes.apps.importApp);
router.post('/api/v1/apps/:id/backup', token, authorizeAdmin, routes.apps.load, routes.apps.backup);
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.load, routes.apps.listBackups);
router.post('/api/v1/apps/:id/start', token, authorizeAdmin, routes.apps.load, routes.apps.start);
router.post('/api/v1/apps/:id/stop', token, authorizeAdmin, routes.apps.load, routes.apps.stop);
router.post('/api/v1/apps/:id/restart', token, authorizeAdmin, routes.apps.load, routes.apps.restart);
router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.load, routes.apps.getLogStream);
router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.load, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.load, routes.apps.exec);
router.post('/api/v1/apps/install', json, token, authorizeAdmin, routes.apps.install);
router.get ('/api/v1/apps', token, routes.apps.getApps);
router.get ('/api/v1/apps/:id', token, authorizeAdmin, routes.apps.load, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', token, routes.apps.load, routes.apps.getAppIcon);
router.post('/api/v1/apps/:id/uninstall', json, token, authorizeAdmin, routes.apps.load, routes.apps.uninstall);
router.post('/api/v1/apps/:id/configure/access_restriction', json, token, authorizeAdmin, routes.apps.load, routes.apps.setAccessRestriction);
router.post('/api/v1/apps/:id/configure/label', json, token, authorizeAdmin, routes.apps.load, routes.apps.setLabel);
router.post('/api/v1/apps/:id/configure/tags', json, token, authorizeAdmin, routes.apps.load, routes.apps.setTags);
router.post('/api/v1/apps/:id/configure/icon', json, token, authorizeAdmin, routes.apps.load, routes.apps.setIcon);
router.post('/api/v1/apps/:id/configure/memory_limit', json, token, authorizeAdmin, routes.apps.load, routes.apps.setMemoryLimit);
router.post('/api/v1/apps/:id/configure/cpu_shares', json, token, authorizeAdmin, routes.apps.load, routes.apps.setCpuShares);
router.post('/api/v1/apps/:id/configure/automatic_backup', json, token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticBackup);
router.post('/api/v1/apps/:id/configure/automatic_update', json, token, authorizeAdmin, routes.apps.load, routes.apps.setAutomaticUpdate);
router.post('/api/v1/apps/:id/configure/reverse_proxy', json, token, authorizeAdmin, routes.apps.load, routes.apps.setReverseProxyConfig);
router.post('/api/v1/apps/:id/configure/cert', json, token, authorizeAdmin, routes.apps.load, routes.apps.setCertificate);
router.post('/api/v1/apps/:id/configure/debug_mode', json, token, authorizeAdmin, routes.apps.load, routes.apps.setDebugMode);
router.post('/api/v1/apps/:id/configure/mailbox', json, token, authorizeAdmin, routes.apps.load, routes.apps.setMailbox);
router.post('/api/v1/apps/:id/configure/env', json, token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment);
router.post('/api/v1/apps/:id/configure/data_dir', json, token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir);
router.post('/api/v1/apps/:id/configure/location', json, token, authorizeAdmin, routes.apps.load, routes.apps.setLocation);
router.post('/api/v1/apps/:id/configure/binds', json, token, authorizeAdmin, routes.apps.load, routes.apps.setBinds);
router.post('/api/v1/apps/:id/repair', json, token, authorizeAdmin, routes.apps.load, routes.apps.repair);
router.post('/api/v1/apps/:id/update', json, token, authorizeAdmin, routes.apps.load, routes.apps.update);
router.post('/api/v1/apps/:id/restore', json, token, authorizeAdmin, routes.apps.load, routes.apps.restore);
router.post('/api/v1/apps/:id/import', json, token, authorizeAdmin, routes.apps.load, routes.apps.importApp);
router.post('/api/v1/apps/:id/backup', json, token, authorizeAdmin, routes.apps.load, routes.apps.backup);
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.load, routes.apps.listBackups);
router.post('/api/v1/apps/:id/start', json, token, authorizeAdmin, routes.apps.load, routes.apps.start);
router.post('/api/v1/apps/:id/stop', json, token, authorizeAdmin, routes.apps.load, routes.apps.stop);
router.post('/api/v1/apps/:id/restart', json, token, authorizeAdmin, routes.apps.load, routes.apps.restart);
router.get ('/api/v1/apps/:id/logstream', token, authorizeAdmin, routes.apps.load, routes.apps.getLogStream);
router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.load, routes.apps.getLogs);
router.post('/api/v1/apps/:id/clone', json, token, authorizeAdmin, routes.apps.load, routes.apps.clone);
router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.load, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', json, token, authorizeAdmin, multipart, routes.apps.load, routes.apps.uploadFile);
router.use ('/api/v1/apps/:id/files/*', token, authorizeAdmin, routes.filemanager.proxy);
router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.load, routes.apps.exec);
// websocket cannot do bearer authentication
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, users.ROLE_ADMIN), routes.apps.load, routes.apps.execWebSocket);
router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.load, routes.apps.clone);
router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.load, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.load, routes.apps.uploadFile);
router.get ('/api/v1/branding/:setting', token, authorizeOwner, routes.branding.get);
router.post('/api/v1/branding/:setting', token, authorizeOwner, (req, res, next) => {
// branding routes
router.get ('/api/v1/branding/:setting', token, authorizeOwner, routes.branding.get);
router.post('/api/v1/branding/:setting', json, token, authorizeOwner, (req, res, next) => {
return req.params.setting === 'cloudron_avatar' ? multipart(req, res, next) : next();
}, routes.branding.set);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/:setting', token, authorizeAdmin, routes.settings.get);
router.post('/api/v1/settings/backup_config', token, authorizeOwner, routes.settings.setBackupConfig);
router.post('/api/v1/settings/:setting', token, authorizeAdmin, routes.settings.set);
router.get ('/api/v1/settings/:setting', token, authorizeAdmin, routes.settings.get);
router.post('/api/v1/settings/backup_config', json, token, authorizeOwner, routes.settings.setBackupConfig);
router.post('/api/v1/settings/:setting', json, token, authorizeAdmin, routes.settings.set);
// email routes
router.get('/api/v1/mailserver/:pathname', token, (req, res, next) => {
@@ -253,48 +253,48 @@ function initializeExpressSync() {
authorizeAdmin(req, res, next);
}, routes.mailserver.proxy);
router.get ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.getDomain);
router.get ('/api/v1/mail/:domain/status', token, authorizeAdmin, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', token, authorizeAdmin, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', token, authorizeAdmin, routes.mail.setCatchAllAddress);
router.post('/api/v1/mail/:domain/relay', token, authorizeAdmin, routes.mail.setMailRelay);
router.post('/api/v1/mail/:domain/enable', token, authorizeAdmin, routes.mail.setMailEnabled);
router.post('/api/v1/mail/:domain/dns', token, authorizeAdmin, routes.mail.setDnsRecords);
router.post('/api/v1/mail/:domain/send_test_mail', token, authorizeAdmin, routes.mail.sendTestMail);
router.get ('/api/v1/mail/:domain/mailboxes', token, authorizeAdmin, routes.mail.listMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.getMailbox);
router.post('/api/v1/mail/:domain/mailboxes', token, authorizeAdmin, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.updateMailbox);
router.del ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.removeMailbox);
router.get ('/api/v1/mail/:domain/mailboxes/:name/aliases', token, authorizeAdmin, routes.mail.getAliases);
router.put ('/api/v1/mail/:domain/mailboxes/:name/aliases', token, authorizeAdmin, routes.mail.setAliases);
router.get ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.getDomain);
router.get ('/api/v1/mail/:domain/status', token, authorizeAdmin, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', json, token, authorizeAdmin, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', json, token, authorizeAdmin, routes.mail.setCatchAllAddress);
router.post('/api/v1/mail/:domain/relay', json, token, authorizeAdmin, routes.mail.setMailRelay);
router.post('/api/v1/mail/:domain/enable', json, token, authorizeAdmin, routes.mail.setMailEnabled);
router.post('/api/v1/mail/:domain/dns', json, token, authorizeAdmin, routes.mail.setDnsRecords);
router.post('/api/v1/mail/:domain/send_test_mail', json, token, authorizeAdmin, routes.mail.sendTestMail);
router.get ('/api/v1/mail/:domain/mailbox_count', token, authorizeAdmin, routes.mail.getMailboxCount);
router.get ('/api/v1/mail/:domain/mailboxes', token, authorizeAdmin, routes.mail.listMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.getMailbox);
router.post('/api/v1/mail/:domain/mailboxes', json, token, authorizeAdmin, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', json, token, authorizeAdmin, routes.mail.updateMailbox);
router.del ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.removeMailbox);
router.get ('/api/v1/mail/:domain/mailboxes/:name/aliases', token, authorizeAdmin, routes.mail.getAliases);
router.put ('/api/v1/mail/:domain/mailboxes/:name/aliases', json, token, authorizeAdmin, routes.mail.setAliases);
router.get ('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.getLists);
router.post('/api/v1/mail/:domain/lists', json, token, authorizeAdmin, routes.mail.addList);
router.get ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.getList);
router.post('/api/v1/mail/:domain/lists/:name', json, token, authorizeAdmin, routes.mail.updateList);
router.del ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.removeList);
router.get ('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.getLists);
router.post('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.addList);
router.get ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.getList);
router.post('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.updateList);
router.del ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.removeList);
// support
router.post('/api/v1/support/ticket', token, authorizeAdmin, routes.support.canCreateTicket, routes.support.createTicket);
router.get ('/api/v1/support/remote_support', token, authorizeAdmin, routes.support.getRemoteSupport);
router.post('/api/v1/support/remote_support', token, authorizeAdmin, routes.support.canEnableRemoteSupport, routes.support.enableRemoteSupport);
// support routes
router.post('/api/v1/support/ticket', json, token, authorizeAdmin, routes.support.canCreateTicket, routes.support.createTicket);
router.get ('/api/v1/support/remote_support', token, authorizeAdmin, routes.support.getRemoteSupport);
router.post('/api/v1/support/remote_support', json, token, authorizeAdmin, routes.support.canEnableRemoteSupport, routes.support.enableRemoteSupport);
// domain routes
router.post('/api/v1/domains', token, authorizeAdmin, routes.domains.add);
router.get ('/api/v1/domains', token, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.update);
router.del ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.del);
router.get ('/api/v1/domains/:domain/dns_check', token, authorizeAdmin, routes.domains.checkDnsRecords);
router.post('/api/v1/domains', json, token, authorizeAdmin, routes.domains.add);
router.get ('/api/v1/domains', token, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', json, token, authorizeAdmin, routes.domains.update);
router.del ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.del);
router.get ('/api/v1/domains/:domain/dns_check', token, authorizeAdmin, routes.domains.checkDnsRecords);
// addon routes
router.get ('/api/v1/services', token, authorizeAdmin, routes.services.getAll);
router.get ('/api/v1/services/:service', token, authorizeAdmin, routes.services.get);
router.post('/api/v1/services/:service', token, authorizeAdmin, routes.services.configure);
router.get ('/api/v1/services/:service/logs', token, authorizeAdmin, routes.services.getLogs);
router.get ('/api/v1/services/:service/logstream', token, authorizeAdmin, routes.services.getLogStream);
router.post('/api/v1/services/:service/restart', token, authorizeAdmin, routes.services.restart);
router.get ('/api/v1/services', token, authorizeAdmin, routes.services.getAll);
router.get ('/api/v1/services/:service', token, authorizeAdmin, routes.services.get);
router.post('/api/v1/services/:service', json, token, authorizeAdmin, routes.services.configure);
router.get ('/api/v1/services/:service/logs', token, authorizeAdmin, routes.services.getLogs);
router.get ('/api/v1/services/:service/logstream', token, authorizeAdmin, routes.services.getLogStream);
router.post('/api/v1/services/:service/restart', json, token, authorizeAdmin, routes.services.restart);
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)
+34 -1
View File
@@ -50,6 +50,9 @@ exports = module.exports = {
getFooter: getFooter,
setFooter: setFooter,
getDirectoryConfig: getDirectoryConfig,
setDirectoryConfig: setDirectoryConfig,
getAppstoreListingConfig: getAppstoreListingConfig,
setAppstoreListingConfig: setAppstoreListingConfig,
@@ -86,6 +89,7 @@ exports = module.exports = {
SYSINFO_CONFIG_KEY: 'sysinfo_config',
APPSTORE_LISTING_CONFIG_KEY: 'appstore_listing_config',
SUPPORT_CONFIG_KEY: 'support_config',
DIRECTORY_CONFIG_KEY: 'directory_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
@@ -157,6 +161,11 @@ let gDefaults = (function () {
result[exports.SYSINFO_CONFIG_KEY] = {
provider: 'generic'
};
result[exports.DIRECTORY_CONFIG_KEY] = {
lockUserProfiles: false,
mandatory2FA: false
};
result[exports.ADMIN_DOMAIN_KEY] = '';
result[exports.ADMIN_FQDN_KEY] = '';
result[exports.API_SERVER_ORIGIN_KEY] = 'https://api.cloudron.io';
@@ -573,6 +582,30 @@ function setSysinfoConfig(sysinfoConfig, callback) {
});
}
function getDirectoryConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.DIRECTORY_CONFIG_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.DIRECTORY_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
}
function setDirectoryConfig(directoryConfig, callback) {
assert.strictEqual(typeof directoryConfig, 'object');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.DIRECTORY_CONFIG_KEY, JSON.stringify(directoryConfig), function (error) {
if (error) return callback(error);
notifyChange(exports.DIRECTORY_CONFIG_KEY, directoryConfig);
callback(null);
});
}
function getAppstoreListingConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -696,7 +729,7 @@ function getAll(callback) {
result[exports.DEMO_KEY] = !!result[exports.DEMO_KEY];
// convert JSON objects
[exports.BACKUP_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY, exports.REGISTRY_CONFIG_KEY, exports.SYSINFO_CONFIG_KEY ].forEach(function (key) {
[exports.BACKUP_CONFIG_KEY, exports.DIRECTORY_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY, exports.REGISTRY_CONFIG_KEY, exports.SYSINFO_CONFIG_KEY ].forEach(function (key) {
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
});
+11 -10
View File
@@ -54,13 +54,6 @@ function checkPreconditions(apiConfig, dataLayout, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof callback, 'function');
// TODO check filesystem is mounted for sshfs and cifs so we don't write into the actual folder on disk
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS) {
const mounts = safe.fs.readFileSync('/proc/mounts', 'utf8');
const mountInfo = mounts.split('\n').filter(function (l) { return l.indexOf(apiConfig.mountPoint) !== -1; })[0];
if (!mountInfo) return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.mountPoint} is not mounted`));
}
let used = 0;
for (let localPath of dataLayout.localPaths()) {
debug(`checkPreconditions: getting disk usage of ${localPath}`);
@@ -71,9 +64,17 @@ function checkPreconditions(apiConfig, dataLayout, callback) {
debug(`checkPreconditions: ${used} bytes`);
df.file(getBackupPath(apiConfig)).then(function (diskUsage) {
df.file(getBackupPath(apiConfig)).then(function (result) {
// Check filesystem is mounted so we don't write into the actual folder on disk
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS) {
if (result.mountpoint !== apiConfig.mountPoint) return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.mountPoint} is not mounted`));
} else if (apiConfig.provider === PROVIDER_FILESYSTEM && apiConfig.externalDisk) {
if (result.mountpoint === '/') return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.backupFolder} is not mounted`));
}
const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards
if (diskUsage.available <= needed) return callback(new BoxError(BoxError.FS_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`));
if (result.available <= needed) return callback(new BoxError(BoxError.FS_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(result.available)}`));
callback(null);
}).catch(function (error) {
@@ -239,7 +240,7 @@ function testConfig(apiConfig, callback) {
const mounts = safe.fs.readFileSync('/proc/mounts', 'utf8');
const mountInfo = mounts.split('\n').filter(function (l) { return l.indexOf(apiConfig.mountPoint) !== -1; })[0];
if (!mountInfo) return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint is not mounted', { field: 'mountPoint' }));
if (!mountInfo) return callback(new BoxError(BoxError.BAD_FIELD, `${apiConfig.mountPoint} is not mounted`, { field: 'mountPoint' }));
if (apiConfig.provider === PROVIDER_SSHFS && !mountInfo.split(' ').find(i => i === 'fuse.sshfs')) return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be a "fuse.sshfs" filesystem', { field: 'mountPoint' }));
if (apiConfig.provider === PROVIDER_CIFS && !mountInfo.split(' ').find(i => i === 'cifs')) return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be a "cifs" filesystem', { field: 'mountPoint' }));
+2 -2
View File
@@ -12,7 +12,7 @@ let assert = require('assert'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
settings = require('./settings.js'),
safe = require('safetydance'),
shell = require('./shell.js');
// the logic here is also used in the cloudron-support tool
@@ -24,7 +24,7 @@ function sshInfo() {
if (constants.TEST) {
filePath = path.join(paths.baseDir(), 'authorized_keys');
user = process.getuid();
} else if (settings.provider() === 'ec2' || settings.provider() === 'lightsail' || settings.provider() === 'ami') {
} else if (safe.fs.existsSync('/home/ubuntu')) { // yellowtent user won't have access to anything deeper
filePath = '/home/ubuntu/.ssh/authorized_keys';
user = 'ubuntu';
} else {
-3
View File
@@ -29,7 +29,6 @@ describe('Apps', function () {
fallbackEmail: 'admin@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: '',
groupIds: [],
@@ -45,7 +44,6 @@ describe('Apps', function () {
fallbackEmail: 'safe@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: '',
groupIds: [],
@@ -61,7 +59,6 @@ describe('Apps', function () {
fallbackEmail: 'safe1@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: '',
groupIds: [ 'somegroup' ],
-48
View File
@@ -50,58 +50,10 @@ describe('Appstore', function () {
beforeEach(nock.cleanAll);
it('cannot send alive status without registering', function (done) {
appstore.sendAliveStatus(function (error) {
expect(error).to.be.ok();
expect(error.reason).to.equal(BoxError.LICENSE_ERROR); // missing token
done();
});
});
it('can set cloudron token', function (done) {
settingsdb.set(settings.CLOUDRON_TOKEN_KEY, APPSTORE_TOKEN, done);
});
it('can send alive status', function (done) {
var scope = nock(MOCK_API_SERVER_ORIGIN)
.post(`/api/v1/alive?accessToken=${APPSTORE_TOKEN}`, function (body) {
expect(body.version).to.be.a('string');
expect(body.adminFqdn).to.be.a('string');
expect(body.provider).to.be.a('string');
expect(body.backendSettings).to.be.an('object');
expect(body.backendSettings.backupConfig).to.be.an('object');
expect(body.backendSettings.backupConfig.provider).to.be.a('string');
expect(body.backendSettings.backupConfig.hardlinks).to.be.a('boolean');
expect(body.backendSettings.domainConfig).to.be.an('object');
expect(body.backendSettings.domainConfig.count).to.be.a('number');
expect(body.backendSettings.domainConfig.domains).to.be.an('array');
expect(body.backendSettings.mailConfig).to.be.an('object');
expect(body.backendSettings.mailConfig.outboundCount).to.be.a('number');
expect(body.backendSettings.mailConfig.inboundCount).to.be.a('number');
expect(body.backendSettings.mailConfig.catchAllCount).to.be.a('number');
expect(body.backendSettings.mailConfig.relayProviders).to.be.an('array');
expect(body.backendSettings.appAutoupdatePattern).to.be.a('string');
expect(body.backendSettings.boxAutoupdatePattern).to.be.a('string');
expect(body.backendSettings.sysinfoProvider).to.be.a('string');
expect(body.backendSettings.timeZone).to.be.a('string');
expect(body.machine).to.be.an('object');
expect(body.machine.cpus).to.be.an('array');
expect(body.machine.totalmem).to.be.an('number');
expect(body.events).to.be.an('object');
expect(body.events.lastLogin).to.be.an('number');
return true;
})
.reply(201, { cloudron: { id: CLOUDRON_ID }});
appstore.sendAliveStatus(function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('can purchase an app', function (done) {
var scope1 = nock(MOCK_API_SERVER_ORIGIN)
.post(`/api/v1/cloudronapps?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
-1
View File
@@ -73,7 +73,6 @@ var ADMIN = {
fallbackEmail: 'admin@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: '',
displayName: '',
role: 'owner',
+1 -1
View File
@@ -28,7 +28,7 @@ scripts=("${SOURCE_DIR}/src/scripts/clearvolume.sh" \
for script in "${scripts[@]}"; do
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
echo ""
echo "${script} does not have sudo access."
echo "${script} does not have sudo access. Try 'sudo -n ${script} --check'"
echo "You have to add the lines below to /etc/sudoers.d/yellowtent"
echo ""
echo "Defaults!${script} env_keep=\"HOME BOX_ENV\""
+1 -4
View File
@@ -34,7 +34,6 @@ var USER_0 = {
fallbackEmail: 'safer@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: '',
twoFactorAuthenticationEnabled: false,
@@ -53,7 +52,6 @@ var USER_1 = {
fallbackEmail: 'safer2@me.com',
salt: 'tata',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: '',
displayName: 'Herbert 1',
twoFactorAuthenticationEnabled: false,
@@ -72,7 +70,6 @@ var USER_2 = {
fallbackEmail: 'safer3@me.com',
salt: 'tata',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: '',
displayName: 'Herbert 2',
twoFactorAuthenticationEnabled: false,
@@ -1837,7 +1834,7 @@ describe('database', function () {
});
it('list mailboxes succeeds', function (done) {
mailboxdb.listMailboxes(DOMAIN_0.domain, 1, 10, function (error, mailboxes) {
mailboxdb.listMailboxes(DOMAIN_0.domain, null /* search */, 1, 10, function (error, mailboxes) {
expect(error).to.be(null);
expect(mailboxes.length).to.be(2);
expect(mailboxes[0].name).to.be('girish');
+4 -4
View File
@@ -21,7 +21,7 @@ describe('Dockerproxy', function () {
dockerProxy.start(function (error) {
expect(error).to.not.be.ok();
exec(`${DOCKER} run -d cloudron/base:1.0.0 "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
exec(`${DOCKER} run -d cloudron/base:2.0.0 "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
@@ -45,7 +45,7 @@ describe('Dockerproxy', function () {
// it('wait', function (done) {} );
it('can get info', function (done) {
exec(DOCKER + ' info', function (error, stdout, stderr) {
exec(DOCKER + ' info', function (error, stdout/*, stderr*/) {
expect(error).to.be(null);
expect(stdout).to.contain('Containers:');
// expect(stderr).to.be.empty(); // on some machines, i get 'No swap limit support'
@@ -54,7 +54,7 @@ describe('Dockerproxy', function () {
});
it('can create container', function (done) {
var cmd = `${DOCKER} run cloudron/base:1.0.0 "/bin/bash" "-c" "echo 'hello'"`;
var cmd = `${DOCKER} run cloudron/base:2.0.0 "/bin/bash" "-c" "echo 'hello'"`;
exec(cmd, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('hello');
@@ -64,7 +64,7 @@ describe('Dockerproxy', function () {
});
it('proxy overwrites the container network option', function (done) {
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail cloudron/base:1.0.0 "/bin/bash" "-c" "echo 'hello'"`;
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail cloudron/base:2.0.0 "/bin/bash" "-c" "echo 'hello'"`;
exec(cmd, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('hello');
-2
View File
@@ -36,7 +36,6 @@ var USER_0 = {
role: 'user',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: '',
source: '',
@@ -52,7 +51,6 @@ var USER_1 = { // this user has not signed up yet
role: 'user',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: '',
source: '',
+5 -5
View File
@@ -912,12 +912,12 @@ describe('User', function () {
});
});
describe('resetPasswordByIdentifier', function () {
describe('sendPasswordResetByIdentifier', function () {
before(createOwner);
after(cleanupUsers);
it('fails due to unkown email', function (done) {
users.resetPasswordByIdentifier('unknown@mail.com', function (error) {
users.sendPasswordResetByIdentifier('unknown@mail.com', function (error) {
expect(error).to.be.an(BoxError);
expect(error.reason).to.eql(BoxError.NOT_FOUND);
done();
@@ -925,7 +925,7 @@ describe('User', function () {
});
it('fails due to unkown username', function (done) {
users.resetPasswordByIdentifier('unknown', function (error) {
users.sendPasswordResetByIdentifier('unknown', function (error) {
expect(error).to.be.an(BoxError);
expect(error.reason).to.eql(BoxError.NOT_FOUND);
done();
@@ -933,14 +933,14 @@ describe('User', function () {
});
it('succeeds with email', function (done) {
users.resetPasswordByIdentifier(EMAIL, function (error) {
users.sendPasswordResetByIdentifier(EMAIL, function (error) {
expect(error).to.not.be.ok();
checkMails(1, done);
});
});
it('succeeds with username', function (done) {
users.resetPasswordByIdentifier(USERNAME, function (error) {
users.sendPasswordResetByIdentifier(USERNAME, function (error) {
expect(error).to.not.be.ok();
checkMails(1, done);
});
+3 -4
View File
@@ -28,7 +28,7 @@ var assert = require('assert'),
debug = require('debug')('box:userdb'),
mysql = require('mysql');
var USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName',
var USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'createdAt', 'resetToken', 'displayName',
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime' ].join(',');
var APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(',');
@@ -166,15 +166,14 @@ function add(userId, user, callback) {
assert.strictEqual(typeof user.fallbackEmail, 'string');
assert.strictEqual(typeof user.salt, 'string');
assert.strictEqual(typeof user.createdAt, 'string');
assert.strictEqual(typeof user.modifiedAt, 'string');
assert.strictEqual(typeof user.resetToken, 'string');
assert.strictEqual(typeof user.displayName, 'string');
assert.strictEqual(typeof user.source, 'string');
assert.strictEqual(typeof user.role, 'string');
assert.strictEqual(typeof callback, 'function');
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, createdAt, modifiedAt, resetToken, displayName, source, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const args = [ userId, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName, user.source, user.role ];
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, createdAt, resetToken, displayName, source, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const args = [ userId, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.createdAt, user.resetToken, user.displayName, user.source, user.role ];
database.query(query, args, function (error) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'email already exists'));
+96 -15
View File
@@ -16,9 +16,8 @@ exports = module.exports = {
getByResetToken: getByResetToken,
getByUsername: getByUsername,
getAdmins: getAdmins,
resetPasswordByIdentifier: resetPasswordByIdentifier,
setPassword: setPassword,
update: updateUser,
update: update,
createOwner: createOwner,
getOwner: getOwner,
createInvite: createInvite,
@@ -28,6 +27,14 @@ exports = module.exports = {
enableTwoFactorAuthentication: enableTwoFactorAuthentication,
disableTwoFactorAuthentication: disableTwoFactorAuthentication,
sendPasswordResetByIdentifier: sendPasswordResetByIdentifier,
setupAccount,
getAvatarUrlSync,
getAvatarFileSync,
setAvatar,
clearAvatar,
count: count,
AP_MAIL: 'mail',
@@ -54,14 +61,17 @@ let assert = require('assert'),
debug = require('debug')('box:user'),
eventlog = require('./eventlog.js'),
externalLdap = require('./externalldap.js'),
fs = require('fs'),
groups = require('./groups.js'),
hat = require('./hat.js'),
mailer = require('./mailer.js'),
path = require('path'),
paths = require('./paths.js'),
qrcode = require('qrcode'),
safe = require('safetydance'),
settings = require('./settings.js'),
speakeasy = require('speakeasy'),
tokens = require('./tokens.js'),
userdb = require('./userdb.js'),
uuid = require('uuid'),
validator = require('validator'),
@@ -182,7 +192,6 @@ function create(username, password, email, displayName, options, auditSource, ca
password: Buffer.from(derivedKey, 'binary').toString('hex'),
salt: salt.toString('hex'),
createdAt: now,
modifiedAt: now,
resetToken: '',
displayName: displayName,
source: source,
@@ -407,7 +416,7 @@ function getByUsername(username, callback) {
});
}
function updateUser(user, data, auditSource, callback) {
function update(user, data, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof data, 'object');
assert(auditSource && typeof auditSource === 'object');
@@ -484,13 +493,11 @@ function getAdmins(callback) {
});
}
function resetPasswordByIdentifier(identifier, callback) {
function sendPasswordResetByIdentifier(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var getter;
if (identifier.indexOf('@') === -1) getter = userdb.getByUsername;
else getter = userdb.getByEmail;
const getter = identifier.indexOf('@') === -1 ? userdb.getByUsername : userdb.getByEmail;
getter(identifier.toLowerCase(), function (error, result) {
if (error) return callback(error);
@@ -525,7 +532,6 @@ function setPassword(user, newPassword, callback) {
if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error));
let data = {
modifiedAt: (new Date()).toISOString(),
password: Buffer.from(derivedKey, 'binary').toString('hex'),
resetToken: ''
};
@@ -569,11 +575,12 @@ function getOwner(callback) {
});
}
function inviteLink(user) {
function inviteLink(user, directoryConfig) {
let link = `${settings.adminOrigin()}/setupaccount.html?resetToken=${user.resetToken}&email=${encodeURIComponent(user.email)}`;
if (user.username) link += `&username=${encodeURIComponent(user.username)}`;
if (user.displayName) link += `&displayName=${encodeURIComponent(user.displayName)}`;
if (directoryConfig.lockUserProfiles) link += '&profileLocked=true';
return link;
}
@@ -586,11 +593,16 @@ function createInvite(user, callback) {
const resetToken = hat(256), resetTokenCreationTime = new Date();
userdb.update(user.id, { resetToken, resetTokenCreationTime }, function (error) {
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return callback(error);
user.resetToken = resetToken;
callback(null, { resetToken, inviteLink: inviteLink(user) });
userdb.update(user.id, { resetToken, resetTokenCreationTime }, function (error) {
if (error) return callback(error);
user.resetToken = resetToken;
callback(null, { resetToken, inviteLink: inviteLink(user, directoryConfig) });
});
});
}
@@ -602,9 +614,43 @@ function sendInvite(user, options, callback) {
if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory'));
if (!user.resetToken) return callback(new BoxError(BoxError.CONFLICT, 'Must generate resetToken to send invitation'));
mailer.sendInvite(user, options.invitor || null, inviteLink(user));
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return callback(error);
callback(null);
mailer.sendInvite(user, options.invitor || null, inviteLink(user, directoryConfig));
callback(null);
});
}
function setupAccount(user, data, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof data, 'object');
assert(auditSource && typeof auditSource === 'object');
assert.strictEqual(typeof callback, 'function');
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return callback(error);
const updateFunc = (done) => {
if (directoryConfig.lockUserProfiles) return done();
update(user, _.pick(data, 'username', 'displayName'), auditSource, done);
};
updateFunc(function (error) {
if (error) return callback(error);
setPassword(user, data.password, function (error) { // setPassword clears the resetToken
if (error) return callback(error);
tokens.add(tokens.ID_WEBADMIN, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(error);
callback(null, result.accessToken);
});
});
});
});
}
function setTwoFactorAuthenticationSecret(userId, callback) {
@@ -753,3 +799,38 @@ function delAppPassword(id, callback) {
callback(null);
});
}
function getAvatarFileSync(id) {
assert.strictEqual(typeof id, 'string');
return path.join(paths.PROFILE_ICONS_DIR, id);
}
function getAvatarUrlSync(user) {
assert.strictEqual(typeof user, 'object');
if (fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, user.id))) return `${settings.adminOrigin()}/api/v1/profile/avatar/${user.id}`;
const emailHash = require('crypto').createHash('md5').update(user.email).digest('hex');
return `https://www.gravatar.com/avatar/${emailHash}.jpg`;
}
function setAvatar(id, filename, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
fs.rename(filename, path.join(paths.PROFILE_ICONS_DIR, id), function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, error.message));
callback();
});
}
function clearAvatar(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
safe.fs.unlinkSync(path.join(paths.PROFILE_ICONS_DIR, id));
callback();
}