Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7339c37b98 | |||
| 3176938ea0 | |||
| c3c77c5a97 | |||
| 32e6b9024c | |||
| 5a6ea33694 | |||
| 60bff95d9f | |||
| 0cc2838b8b | |||
| 0fc4f4bbff | |||
| 0b82146b3e | |||
| 4369b3046e | |||
| ac75b60f47 | |||
| d752ef5fad | |||
| c099d5d3fa | |||
| 6534297a5d | |||
| 2aa6350c94 | |||
| 8b4a399b8f | |||
| 177243b7f2 | |||
| c2ca827458 | |||
| 90d7dc893c | |||
| eeaaa95ca3 | |||
| 04be582573 | |||
| 0953787559 | |||
| 3bd8a58ea5 | |||
| 275181824f | |||
| f814ffb14f | |||
| 95ae948fce | |||
| 9debf1f6c6 | |||
| 0e583b5afe | |||
| fa47031a63 | |||
| 7fd1bb8597 | |||
| 8c5b550caa | |||
| 3d57c32853 | |||
| 898d928dd6 | |||
| c578a048dd | |||
| 2a475c1199 | |||
| 57e195883c | |||
| f2178d9b81 | |||
| df1ac43f40 | |||
| 39059c627b | |||
| d942c77ceb | |||
| c39240c518 | |||
| fd0e2782d8 | |||
| 36aaa0406e | |||
| 17ecb366af | |||
| 1a83281e16 | |||
| ec41e0eef5 | |||
| d4097ed4e0 | |||
| 8fa99fae1a | |||
| e9400e5dce | |||
| 372a17dc37 | |||
| 5ca60b2d3c | |||
| 1dc649b7a2 | |||
| 74437db740 | |||
| 70128458b2 | |||
| 900225957e | |||
| fd8f5e3c71 | |||
| 7382ea2b04 | |||
| 09163b8a2b | |||
| 953398c427 | |||
| 9f7406c235 | |||
| 2e427aa60e | |||
| ab80cc9ea1 | |||
| 321f11c644 | |||
| 47f85434db | |||
| 7717c7b1cd | |||
| 7618aa786c | |||
| f752cb368c | |||
| ca500e2165 | |||
| 371f81b980 | |||
| c68cca9a54 | |||
| 9194be06c3 | |||
| 9eb58cdfe5 | |||
| 99be89012d | |||
| 541fabcb2e | |||
| 915e04eb08 | |||
| 48896d4e50 | |||
| 29682c0944 | |||
| 346b1cb91c | |||
| e552821c01 | |||
| bac3ba101e | |||
| 87c46fe3ea | |||
| f9763b1ad3 | |||
| f1e6116b83 | |||
| 273948c3c7 | |||
| 9c073e7bee |
@@ -1281,3 +1281,39 @@
|
||||
* Preserve addon/database configuration across app updates and restores
|
||||
* ManageSieve port now offers STARTTLS
|
||||
|
||||
[2.3.1]
|
||||
* Add Name.com DNS provider
|
||||
* Fix issue where account setup page was crashing
|
||||
* Add advanced DNS configuration UI
|
||||
* Preserve addon/database configuration across app updates and restores
|
||||
* ManageSieve port now offers STARTTLS
|
||||
* Allow mailbox name to be set for apps
|
||||
* Rework the Email server UI
|
||||
* Add the ability to manually trigger a backup of an application
|
||||
* Enable/disable mail from validation within UI
|
||||
* Allow setting app visibility for non-SSO apps
|
||||
* Add Clone UI
|
||||
|
||||
[2.3.2]
|
||||
* Fix issue where multi-db apps were not provisioned correctly
|
||||
* Improve setup, restore views to have field labels
|
||||
|
||||
[2.4.0]
|
||||
* Use custom logging backend to have more control over log rotation
|
||||
* Make user explicitly confirm that fs backup dir is on external storage
|
||||
* Update node to 8.11.2
|
||||
* Update docker to 18.03.1
|
||||
* Fix docker exec terminal resize issue
|
||||
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
* Add backups view
|
||||
|
||||
[2.4.1]
|
||||
* Use custom logging backend to have more control over log rotation
|
||||
* Mail logs and box logs UI
|
||||
* Make user explicitly confirm that fs backup dir is on external storage
|
||||
* Update node to 8.11.2
|
||||
* Update docker to 18.03.1
|
||||
* Fix docker exec terminal resize issue
|
||||
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
* Add backups view
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016,2017 Cloudron UG
|
||||
Copyright (C) 2016,2017,2018 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -61,7 +61,7 @@ echo "==> Installing Docker"
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/docker.deb
|
||||
rm /tmp/docker.deb
|
||||
|
||||
Generated
+6065
-1748
File diff suppressed because it is too large
Load Diff
+28
-30
@@ -14,12 +14,12 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.7.1",
|
||||
"@google-cloud/storage": "^1.6.0",
|
||||
"@google-cloud/dns": "^0.7.2",
|
||||
"@google-cloud/storage": "^1.7.0",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.6.0",
|
||||
"aws-sdk": "^2.201.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.11.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
@@ -28,24 +28,23 @@
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.10.5",
|
||||
"db-migrate": "^0.11.1",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^3.1.0",
|
||||
"dockerode": "^2.5.4",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-cli": "^2.0.0",
|
||||
"express": "^4.16.2",
|
||||
"dockerode": "^2.5.5",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"hat": "0.0.3",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.2.0",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"mime": "^2.3.1",
|
||||
"moment-timezone": "^0.5.17",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.2",
|
||||
"multiparty": "^4.1.4",
|
||||
"mysql": "^2.15.0",
|
||||
"nodemailer": "^4.6.0",
|
||||
"nodemailer": "^4.6.5",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
@@ -55,40 +54,39 @@
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"password-generator": "^2.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.2.0",
|
||||
"recursive-readdir": "^2.2.1",
|
||||
"request": "^2.83.0",
|
||||
"s3-block-read-stream": "^0.2.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"request": "^2.87.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^5.5.0",
|
||||
"showdown": "^1.8.2",
|
||||
"showdown": "^1.8.6",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^3.8.1",
|
||||
"supererror": "^0.7.1",
|
||||
"tar-fs": "^1.16.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"superagent": "^3.8.3",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "^1.16.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.7.0",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.2.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^9.4.1",
|
||||
"ws": "^3.3.3"
|
||||
"validator": "^10.3.0",
|
||||
"ws": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.3.2",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "^5.0.1",
|
||||
"mocha": "^5.2.0",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"rimraf": "^2.6.2"
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
domain=""
|
||||
domainProvider=""
|
||||
domainConfigJson="{}"
|
||||
domainTlsProvider="letsencrypt-prod"
|
||||
adminUsername="superadmin"
|
||||
adminPassword="Secret123#"
|
||||
adminEmail="admin@server.local"
|
||||
appstoreUserId=""
|
||||
appstoreToken=""
|
||||
backupDir="/var/backups"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--domain-provider) domainProvider="$2"; shift 2;;
|
||||
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
|
||||
--admin-username) adminUsername="$2"; shift 2;;
|
||||
--admin-password) adminPassword="$2"; shift 2;;
|
||||
--admin-email) adminEmail="$2"; shift 2;;
|
||||
--appstore-user) appstoreUser="$2"; shift 2;;
|
||||
--appstore-token) appstoreToken="$2"; shift 2;;
|
||||
--backup-dir) backupDir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
|
||||
echo "=> Domain setup"
|
||||
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
|
||||
|
||||
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
|
||||
echo "DNS Setup Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
else
|
||||
echo "=> Skipping Domain setup"
|
||||
fi
|
||||
|
||||
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
|
||||
if [[ $(get_status "activated") == "false" ]]; then
|
||||
echo "=> Activating"
|
||||
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
|
||||
echo "Failed to activate with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "activated" "true"
|
||||
else
|
||||
echo "=> Skipping Activation"
|
||||
fi
|
||||
|
||||
echo "=> Getting token"
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
|
||||
echo "Failed to login with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
|
||||
|
||||
echo "=> Setting up App Store account with accessToken ${accessToken}"
|
||||
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
|
||||
|
||||
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
|
||||
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
|
||||
|
||||
chown -R yellowtent:yellowtent "${backupDir}"
|
||||
|
||||
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Done!"
|
||||
|
||||
@@ -34,8 +34,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v8.9.3" ]]; then
|
||||
echo "This script requires node 8.9.3"
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
echo "This script requires node 8.11.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+18
-8
@@ -35,11 +35,11 @@ while true; do
|
||||
done
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.03.0-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "1f7315b5723b849fe542fe973b0edb4164a0200e926d386ac14363a968f9e4fc" ]]; then
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
|
||||
echo "==> installer: docker binary download is corrupt"
|
||||
exit 5
|
||||
fi
|
||||
@@ -69,11 +69,11 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.0-ce" ]]; then
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v8.9.3" ]]; then
|
||||
mkdir -p /usr/local/node-8.9.3
|
||||
$curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
|
||||
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
mkdir -p /usr/local/node-8.11.2
|
||||
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
|
||||
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.5
|
||||
fi
|
||||
|
||||
@@ -93,6 +93,16 @@ if [[ ${try} -eq 10 ]]; then
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
|
||||
if [[ "$($CLOUDRON_SYSLOG_DIR/bin/cloudron-syslog --version)" != "1.0.1" ]]; then
|
||||
rm -rf "${CLOUDRON_SYSLOG_DIR}"
|
||||
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
|
||||
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@1.0.1; then break; fi
|
||||
echo "===> installer: Failed to install cloudron-syslog, trying again"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
if ! id "${USER}" 2>/dev/null; then
|
||||
useradd "${USER}" -m
|
||||
fi
|
||||
|
||||
+10
-1
@@ -120,6 +120,7 @@ echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable cloudron-firewall
|
||||
|
||||
@@ -132,6 +133,9 @@ systemctl enable --now cron
|
||||
# ensure unbound runs
|
||||
systemctl restart unbound
|
||||
|
||||
# ensure cloudron-syslog runs
|
||||
systemctl restart cloudron-syslog
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
@@ -146,6 +150,8 @@ echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
@@ -231,10 +237,13 @@ fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
|
||||
# do not chown the boxdata/mail directory; dovecot gets upset
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
|
||||
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# logrotate config for app logs
|
||||
|
||||
/home/yellowtent/platformdata/logs/*/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Cloudron Syslog
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
|
||||
WorkingDirectory=/usr/local/cloudron-syslog
|
||||
Environment="NODE_ENV=production"
|
||||
Restart=always
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+12
-9
@@ -28,7 +28,7 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
fs = require('fs'),
|
||||
hat = require('hat'),
|
||||
hat = require('./hat.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
@@ -442,13 +442,12 @@ function teardownRecvMail(app, options, callback) {
|
||||
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
|
||||
}
|
||||
|
||||
function mysqlDatabaseName(appId, prefix) {
|
||||
function mysqlDatabaseName(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
|
||||
var md5sum = crypto.createHash('md5'); // get rid of "-"
|
||||
md5sum.update(appId);
|
||||
var dbname = md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
|
||||
return prefix ? `${dbname}_` : dbname;
|
||||
return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
|
||||
}
|
||||
|
||||
function setupMySql(app, options, callback) {
|
||||
@@ -461,7 +460,7 @@ function setupMySql(app, options, callback) {
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id, options.multipleDatabases);
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
const password = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', dbname, password ];
|
||||
@@ -477,7 +476,7 @@ function setupMySql(app, options, callback) {
|
||||
];
|
||||
|
||||
if (options.multipleDatabases) {
|
||||
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: dbname });
|
||||
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${dbname}_` });
|
||||
} else {
|
||||
env = env.concat(
|
||||
{ name: 'MYSQL_URL', value: `mysql://${dbname}:${password}@mysql/${dbname}` },
|
||||
@@ -496,7 +495,7 @@ function teardownMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id, options.multipleDatabases);
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
@@ -520,7 +519,7 @@ function backupMySql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id, options.multipleDatabases);
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
|
||||
|
||||
docker.execContainer('mysql', cmd, { stdout: output }, callback);
|
||||
@@ -541,7 +540,7 @@ function restoreMySql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id, options.multipleDatabases);
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
|
||||
docker.execContainer('mysql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
@@ -767,6 +766,10 @@ function setupRedis(app, options, callback) {
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag="${redisName}" \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
|
||||
+64
-35
@@ -47,8 +47,7 @@ exports = module.exports = {
|
||||
_validateAccessRestriction: validateAccessRestriction
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
var appdb = require('./appdb.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
AppstoreError = require('./appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
@@ -66,6 +65,7 @@ var addons = require('./addons.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
os = require('os'),
|
||||
@@ -158,7 +158,7 @@ function validateHostname(location, domain, hostname) {
|
||||
function validatePortBindings(portBindings, tcpPorts) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
|
||||
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
|
||||
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
|
||||
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
||||
// for custom tcp ports
|
||||
var RESERVED_PORTS = [
|
||||
@@ -175,6 +175,7 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
2003, /* graphite (lo) */
|
||||
2004, /* graphite (lo) */
|
||||
2020, /* mail server */
|
||||
2514, /* cloudron-syslog (lo) */
|
||||
config.get('port'), /* app server (lo) */
|
||||
config.get('sysadminPort'), /* sysadmin app server (lo) */
|
||||
config.get('smtpPort'), /* internal smtp port (lo) */
|
||||
@@ -314,7 +315,6 @@ function getAppConfig(app) {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
domain: app.domain,
|
||||
fqdn: app.fqdn,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
@@ -325,8 +325,9 @@ function getAppConfig(app) {
|
||||
}
|
||||
|
||||
function removeInternalAppFields(app) {
|
||||
return _.pick(app, 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
|
||||
'location', 'domain', 'fqdn',
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
|
||||
'location', 'domain', 'fqdn', 'mailboxName',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime');
|
||||
}
|
||||
@@ -376,7 +377,13 @@ function get(appId, callback) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
callback(null, app);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -398,7 +405,13 @@ function getByIpAddress(ip, callback) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
callback(null, app);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -417,7 +430,13 @@ function getAll(callback) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
iteratorDone();
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
iteratorDone(null, app);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -460,6 +479,10 @@ function downloadManifest(appStoreId, manifest, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function mailboxNameForLocation(location, manifest) {
|
||||
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
}
|
||||
|
||||
function install(data, auditSource, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -549,7 +572,7 @@ function install(data, auditSource, callback) {
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
mailboxName: mailboxNameForLocation(location, manifest),
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt
|
||||
@@ -652,6 +675,11 @@ function configure(appId, data, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('mailboxName' in data) {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
@@ -681,8 +709,9 @@ function configure(appId, data, auditSource, callback) {
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
// make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
var oldName = app.mailboxName;
|
||||
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
|
||||
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
@@ -779,12 +808,6 @@ function update(appId, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function appLogFilter(app) {
|
||||
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
|
||||
|
||||
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
|
||||
}
|
||||
|
||||
function getLogs(appId, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
@@ -792,32 +815,34 @@ function getLogs(appId, options, callback) {
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
get(appId, function (error /*, app */) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
follow = !!options.follow,
|
||||
format = options.format || 'json';
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
var args = [ '--no-pager', '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
if (format == 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
args = args.concat(appLogFilter(app));
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
var cp = spawn('/bin/journalctl', args);
|
||||
var args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow', '--retry'); // same as -F. to make it work if file doesn't exist
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
|
||||
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: source || 'main'
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
source: appId
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
@@ -1095,7 +1120,11 @@ function exec(appId, options, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (options.rows && options.columns) {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
// there is a race where resizing too early results in a 404 "no such exec"
|
||||
// https://git.cloudron.io/cloudron/box/issues/549
|
||||
setTimeout(function () {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return callback(null, stream);
|
||||
@@ -1203,7 +1232,7 @@ function restoreInstalledApps(callback) {
|
||||
|
||||
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
|
||||
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
|
||||
+38
-11
@@ -5,6 +5,7 @@ exports = module.exports = {
|
||||
unpurchase: unpurchase,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
|
||||
@@ -18,7 +19,8 @@ exports = module.exports = {
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
@@ -89,6 +91,10 @@ function getSubscription(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function isFreePlan(subscription) {
|
||||
return !subscription || subscription.plan.id === 'free';
|
||||
}
|
||||
|
||||
function purchase(appId, appstoreId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
@@ -96,20 +102,41 @@ function purchase(appId, appstoreId, callback) {
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
function doThePurchase() {
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
// only check for app install count if on the free plan
|
||||
if (result.id !== 'free') return doThePurchase();
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
appdb.getAppStoreIds(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
|
||||
|
||||
// we only allow max of 2 app installations without a subscription
|
||||
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
|
||||
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
|
||||
|
||||
doThePurchase();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+26
-9
@@ -20,11 +20,6 @@ exports = module.exports = {
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
@@ -46,6 +41,7 @@ var addons = require('./addons.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -329,6 +325,16 @@ function removeIcon(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupLogs(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
|
||||
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForDnsPropagation(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -374,8 +380,14 @@ function install(app, callback) {
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
var addonsToRemove = !isRestoring
|
||||
? app.manifest.addons
|
||||
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
|
||||
// for restore case
|
||||
@@ -676,12 +688,15 @@ function uninstall(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app, app.location, app.domain),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
|
||||
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
|
||||
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
|
||||
cleanupLogs.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
|
||||
appdb.del.bind(null, app.id)
|
||||
], function seriesDone(error) {
|
||||
@@ -770,6 +785,8 @@ function startTask(appId, callback) {
|
||||
if (require.main === module) {
|
||||
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
|
||||
|
||||
// add a separator for the log file
|
||||
debug('------------------------------------------------------------');
|
||||
debug('Apptask for %s', process.argv[2]);
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ var apps = require('./apps.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:clients'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
hat = require('hat'),
|
||||
hat = require('./hat.js'),
|
||||
accesscontrol = require('./accesscontrol.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
|
||||
+46
-24
@@ -327,44 +327,66 @@ function checkDiskSpace(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(options, callback) {
|
||||
function getLogs(unit, options, callback) {
|
||||
assert.strictEqual(typeof unit, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var units = options.units || [],
|
||||
lines = options.lines || 100,
|
||||
var lines = options.lines || 100,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
assert(Array.isArray(units));
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
debug('Getting logs for %j', units);
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
var args = [ '--no-pager', '--lines=' + lines ];
|
||||
units.forEach(function (u) {
|
||||
if (u === 'box') args.push('--unit=box');
|
||||
else if (u === 'mail') args.push('CONTAINER_NAME=mail');
|
||||
});
|
||||
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
if (follow) args.push('--follow');
|
||||
debug('Getting logs for %s as %s', unit, format);
|
||||
|
||||
var cp = spawn('/bin/journalctl', args);
|
||||
var cp, transformStream;
|
||||
if (unit === 'box') {
|
||||
let args = [ '--no-pager', `--lines=${lines}` ];
|
||||
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
if (follow) args.push('--follow');
|
||||
args.push('--unit=box');
|
||||
args.push('--unit=cloudron-updater');
|
||||
cp = spawn('/bin/journalctl', args);
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
} else { // mail, mongodb, mysql, postgresql
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
|
||||
cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
source: unit
|
||||
}) + '\n';
|
||||
});
|
||||
}
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
|
||||
+2
-4
@@ -28,11 +28,9 @@ function maybeSend(callback) {
|
||||
var pendingAppUpdates = updateInfo.apps || {};
|
||||
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
appstore.getSubscription(function (error, subscription) {
|
||||
if (error) debug('Error getting subscription:', error);
|
||||
|
||||
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -46,7 +44,7 @@ function maybeSend(callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
hasSubscription: appstore.isFreePlan(subscription),
|
||||
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
+15
-3
@@ -34,10 +34,16 @@ function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
var data = {
|
||||
host: subdomain,
|
||||
type: type,
|
||||
answer: values[0],
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
superagent.post(`${NAMECOM_API}/domains/${zoneName}/records`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
@@ -49,7 +55,7 @@ function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
@@ -65,10 +71,16 @@ function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, ca
|
||||
var data = {
|
||||
host: subdomain,
|
||||
type: type,
|
||||
answer: values[0],
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
superagent.put(`${NAMECOM_API}/domains/${zoneName}/records/${recordId}`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
|
||||
@@ -179,6 +179,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
},
|
||||
HostConfig: {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
'tag': app.id,
|
||||
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
|
||||
'syslog-format': 'rfc5424'
|
||||
}
|
||||
},
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
|
||||
@@ -114,9 +114,11 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
if (zoneName.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = tld.getDomain(domain) || domain;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = hat;
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
function hat (bits) {
|
||||
return crypto.randomBytes(bits / 8).toString('hex');
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.10.0',
|
||||
'version': '48.11.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
|
||||
+9
-7
@@ -12,6 +12,8 @@ exports = module.exports = {
|
||||
|
||||
addDnsRecords: addDnsRecords,
|
||||
|
||||
validateName: validateName,
|
||||
|
||||
setMailFromValidation: setMailFromValidation,
|
||||
setCatchAllAddress: setCatchAllAddress,
|
||||
setMailRelay: setMailRelay,
|
||||
@@ -46,7 +48,6 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:mail'),
|
||||
dns = require('./native-dns.js'),
|
||||
@@ -92,10 +93,12 @@ function MailError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(MailError, Error);
|
||||
MailError.INTERNAL_ERROR = 'Internal Error';
|
||||
MailError.EXTERNAL_ERROR = 'External Error';
|
||||
MailError.BAD_FIELD = 'Bad Field';
|
||||
MailError.ALREADY_EXISTS = 'Already Exists';
|
||||
MailError.NOT_FOUND = 'Not Found';
|
||||
MailError.IN_USE = 'In Use';
|
||||
MailError.BILLING_REQUIRED = 'Billing Required';
|
||||
|
||||
function validateName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
@@ -357,11 +360,6 @@ const RBL_LIST = [
|
||||
'dns': 'spam.dnsbl.sorbs.net',
|
||||
'site': 'http://sorbs.net'
|
||||
},
|
||||
{
|
||||
'name': 'Spam Cannibal',
|
||||
'dns': 'bl.spamcannibal.org',
|
||||
'site': 'http://www.spamcannibal.org/cannibal.cgi'
|
||||
},
|
||||
{
|
||||
'name': 'SpamCop',
|
||||
'dns': 'bl.spamcop.net',
|
||||
@@ -562,6 +560,10 @@ function restartMail(callback) {
|
||||
const cmd = `docker run --restart=always -d --name="mail" \
|
||||
--net cloudron \
|
||||
--net-alias mail \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mail \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
@@ -989,7 +991,7 @@ function setAliases(name, domain, aliases, callback) {
|
||||
|
||||
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
|
||||
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`))
|
||||
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
|
||||
if (!aliasMatch) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
|
||||
return callback(new MailError(MailError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`));
|
||||
}
|
||||
|
||||
+16
-17
@@ -60,6 +60,19 @@ function splatchError(error) {
|
||||
return util.inspect(result, { depth: null, showHidden: true });
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
users.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
});
|
||||
}
|
||||
|
||||
// This will collect the most common details required for notification emails
|
||||
function getMailConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -157,20 +170,6 @@ function render(templateFile, params) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
users.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
adminEmails.push(admins[0].fallbackEmail);
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
});
|
||||
}
|
||||
|
||||
function mailUserEventToAdmins(user, event) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof event, 'string');
|
||||
@@ -203,7 +202,7 @@ function sendInvite(user, invitor) {
|
||||
var templateData = {
|
||||
user: user,
|
||||
webadminUrl: config.adminOrigin(),
|
||||
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
|
||||
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${user.email}`,
|
||||
invitor: invitor,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
@@ -240,7 +239,7 @@ function userAdded(user, inviteSent) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
|
||||
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${user.email}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
@@ -290,7 +289,7 @@ function passwordReset(user) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
resetLink: config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken,
|
||||
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${user.email}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
@@ -19,8 +19,8 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
|
||||
<center>
|
||||
<br/>
|
||||
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>.</h4>
|
||||
<h2>Setup your account and password.</h2>
|
||||
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>!</h4>
|
||||
<h2>Setup your account and password</h2>
|
||||
</center>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
@@ -29,6 +29,7 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="email" value="<%= email %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
@@ -58,9 +59,9 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted">© 2016-18 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
@@ -26,14 +26,15 @@ app.controller('Controller', [function () {}]);
|
||||
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="email" value="<%= email %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be atleast 8 characters</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
// From https://www.npmjs.com/package/password-generator
|
||||
|
||||
exports = module.exports = {
|
||||
generate: generate,
|
||||
validate: validate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
generatePassword = require('password-generator');
|
||||
|
||||
// http://www.w3resource.com/javascript/form/example4-javascript-form-validation-password.html
|
||||
// WARNING!!! if this is changed, the UI parts in the setup and account view have to be adjusted!
|
||||
var gPasswordTestRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/;
|
||||
|
||||
var UPPERCASE_RE = /([A-Z])/g;
|
||||
var LOWERCASE_RE = /([a-z])/g;
|
||||
var NUMBER_RE = /([\d])/g;
|
||||
var SPECIAL_CHAR_RE = /([\?\-])/g;
|
||||
|
||||
function isStrongEnough(password) {
|
||||
var uc = password.match(UPPERCASE_RE);
|
||||
var lc = password.match(LOWERCASE_RE);
|
||||
var n = password.match(NUMBER_RE);
|
||||
var sc = password.match(SPECIAL_CHAR_RE);
|
||||
|
||||
return uc && lc && n && sc;
|
||||
}
|
||||
|
||||
function generate() {
|
||||
var password = '';
|
||||
|
||||
while (!isStrongEnough(password)) password = generatePassword(8, false, /[\w\d\?\-]/);
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
function validate(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (!password.match(gPasswordTestRegExp)) return new Error('Password must be 8-30 character with at least one uppercase, one numeric and one special character');
|
||||
|
||||
return null;
|
||||
}
|
||||
+3
-1
@@ -33,5 +33,7 @@ exports = module.exports = {
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
|
||||
|
||||
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json')
|
||||
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json'),
|
||||
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs')
|
||||
};
|
||||
|
||||
+17
-1
@@ -13,7 +13,7 @@ var apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
fs = require('fs'),
|
||||
hat = require('hat'),
|
||||
hat = require('./hat.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
locker = require('./locker.js'),
|
||||
mail = require('./mail.js'),
|
||||
@@ -162,6 +162,10 @@ function startGraphite(callback) {
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
--dns 172.18.0.1 \
|
||||
@@ -191,6 +195,10 @@ function startMysql(callback) {
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mysql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
@@ -217,6 +225,10 @@ function startPostgresql(callback) {
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=postgresql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
@@ -243,6 +255,10 @@ function startMongodb(callback) {
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mongodb \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
|
||||
@@ -386,6 +386,7 @@ function renewAll(auditSource, callback) {
|
||||
|
||||
async.eachSeries(allApps, function (app, iteratorCallback) {
|
||||
ensureCertificate(app, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
|
||||
|
||||
// reconfigure for the case where we got a renewed cert after fallback
|
||||
|
||||
@@ -170,6 +170,8 @@ function configureApp(req, res, next) {
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
|
||||
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, auditSource(req), function (error) {
|
||||
|
||||
@@ -97,19 +97,18 @@ function feedback(req, res, next) {
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.unit, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
var units = req.query.units || 'all';
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
units: units.split(','),
|
||||
format: req.query.format
|
||||
};
|
||||
|
||||
cloudron.getLogs(options, function (error, logStream) {
|
||||
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -124,11 +123,11 @@ function getLogs(req, res, next) {
|
||||
}
|
||||
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.unit, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
var units = req.query.units || 'all';
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
@@ -136,11 +135,10 @@ function getLogStream(req, res, next) {
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true,
|
||||
units: units.split(','),
|
||||
format: req.query.format
|
||||
};
|
||||
|
||||
cloudron.getLogs(options, function (error, logStream) {
|
||||
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ function setMailEnabled(req, res, next) {
|
||||
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
|
||||
+17
-11
@@ -30,7 +30,7 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
DatabaseError = require('../databaseerror.js'),
|
||||
debug = require('debug')('box:routes/oauth2'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
hat = require('hat'),
|
||||
hat = require('../hat.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('../middleware/index.js'),
|
||||
oauth2orize = require('oauth2orize'),
|
||||
@@ -327,7 +327,7 @@ function passwordResetRequestSite(req, res) {
|
||||
function passwordResetRequest(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'Missing identifier'));
|
||||
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'Missing identifier')); // email or username
|
||||
|
||||
debug('passwordResetRequest: email or username %s.', req.body.identifier);
|
||||
|
||||
@@ -352,6 +352,7 @@ function renderAccountSetupSite(res, req, userObject, error) {
|
||||
error: error,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token || req.body.resetToken,
|
||||
email: req.query.email || req.body.email,
|
||||
title: 'Password Setup'
|
||||
});
|
||||
}
|
||||
@@ -359,9 +360,10 @@ function renderAccountSetupSite(res, req, userObject, error) {
|
||||
// -> GET /api/v1/session/account/setup.html
|
||||
function accountSetupSite(req, res) {
|
||||
if (!req.query.reset_token) return sendError(req, res, 'Missing Reset Token');
|
||||
if (!req.query.email) return sendError(req, res, 'Missing Email');
|
||||
|
||||
users.getByResetToken(req.query.reset_token, function (error, userObject) {
|
||||
if (error) return sendError(req, res, 'Invalid Reset Token');
|
||||
users.getByResetToken(req.query.email, req.query.reset_token, function (error, userObject) {
|
||||
if (error) return sendError(req, res, 'Invalid Email or Reset Token');
|
||||
|
||||
renderAccountSetupSite(res, req, userObject, '');
|
||||
});
|
||||
@@ -371,14 +373,15 @@ function accountSetupSite(req, res) {
|
||||
function accountSetup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
|
||||
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
|
||||
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
|
||||
|
||||
debug('acountSetup: with token %s.', req.body.resetToken);
|
||||
debug(`acountSetup: for email ${req.body.email} with token ${req.body.resetToken}`);
|
||||
|
||||
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
users.getByResetToken(req.body.email, req.body.resetToken, function (error, userObject) {
|
||||
if (error) return sendError(req, res, 'Invalid Reset Token');
|
||||
|
||||
var data = _.pick(req.body, 'username', 'displayName');
|
||||
@@ -405,15 +408,17 @@ function accountSetup(req, res, next) {
|
||||
|
||||
// -> GET /api/v1/session/password/reset.html
|
||||
function passwordResetSite(req, res, next) {
|
||||
if (!req.query.email) return next(new HttpError(400, 'Missing email'));
|
||||
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
|
||||
|
||||
users.getByResetToken(req.query.reset_token, function (error, user) {
|
||||
if (error) return next(new HttpError(401, 'Invalid reset_token'));
|
||||
users.getByResetToken(req.query.email, req.query.reset_token, function (error, user) {
|
||||
if (error) return next(new HttpError(401, 'Invalid email or reset token'));
|
||||
|
||||
renderTemplate(res, 'password_reset', {
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token,
|
||||
email: req.query.email,
|
||||
title: 'Password Reset'
|
||||
});
|
||||
});
|
||||
@@ -423,13 +428,14 @@ function passwordResetSite(req, res, next) {
|
||||
function passwordReset(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
|
||||
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
|
||||
|
||||
debug('passwordReset: with token %s.', req.body.resetToken);
|
||||
debug(`passwordReset: for ${req.body.email} with token ${req.body.resetToken}`);
|
||||
|
||||
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
if (error) return next(new HttpError(401, 'Invalid resetToken'));
|
||||
users.getByResetToken(req.body.email, req.body.resetToken, function (error, userObject) {
|
||||
if (error) return next(new HttpError(401, 'Invalid email or resetToken'));
|
||||
|
||||
if (!userObject.username) return next(new HttpError(401, 'No username set'));
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function setAppAutoupdatePattern(req, res, next) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function setBoxAutoupdatePattern(req, res, next) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ function setCloudronName(req, res, next) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ function setTimeZone(req, res, next) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ function setCloudronAvatar(req, res, next) {
|
||||
settings.setCloudronAvatar(avatar, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ function setBackupConfig(req, res, next) {
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ function dnsSetup(req, res, next) {
|
||||
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
|
||||
@@ -13,7 +13,7 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
oauth2 = require('../oauth2.js'),
|
||||
expect = require('expect.js'),
|
||||
uuid = require('uuid'),
|
||||
hat = require('hat'),
|
||||
hat = require('../../hat.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js');
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
database = require('../../database.js'),
|
||||
domains = require('../../domains.js'),
|
||||
expect = require('expect.js'),
|
||||
hat = require('hat'),
|
||||
hat = require('../../hat.js'),
|
||||
nock = require('nock'),
|
||||
oauth2 = require('../oauth2.js'),
|
||||
querystring = require('querystring'),
|
||||
@@ -219,7 +219,7 @@ describe('OAuth2', function () {
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.portBindings, APP_2),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.domain, APP_3.portBindings, APP_3),
|
||||
function (callback) {
|
||||
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, null /* source */, function (error, userObject) {
|
||||
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, null /* source */, function (error, userObject) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// update the global objects to reflect the new user id
|
||||
@@ -1341,9 +1341,19 @@ describe('Password', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('setup succeeds', function (done) {
|
||||
it('setup fails without email', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
|
||||
.query({ reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setup succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
|
||||
.query({ email: USER_0.email, reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
@@ -1361,7 +1371,16 @@ describe('Password', function () {
|
||||
|
||||
it('reset fails due to invalid reset_token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ reset_token: hat(256) })
|
||||
.query({ email: USER_0.email, reset_token: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reset fails due to invalid email', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ email: USER_0.email + 'x', reset_token: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
@@ -1370,7 +1389,7 @@ describe('Password', function () {
|
||||
|
||||
it('reset succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
|
||||
.query({ reset_token: USER_0.resetToken })
|
||||
.query({ email: USER_0.email, reset_token: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
@@ -1427,7 +1446,7 @@ describe('Password', function () {
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '', resetToken: hat(256) })
|
||||
.send({ password: '', email: USER_0.email, resetToken: hat(256) })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
@@ -1436,7 +1455,7 @@ describe('Password', function () {
|
||||
|
||||
it('fails due to empty resetToken', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: '', resetToken: '' })
|
||||
.send({ password: '', email: USER_0.email, resetToken: '' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
@@ -1445,7 +1464,7 @@ describe('Password', function () {
|
||||
|
||||
it('fails due to weak password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'foobar', resetToken: USER_0.resetToken })
|
||||
.send({ password: 'foobar', email: USER_0.email, resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(406);
|
||||
done();
|
||||
@@ -1460,7 +1479,7 @@ describe('Password', function () {
|
||||
.get('/').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'ASF23$%somepassword', resetToken: USER_0.resetToken })
|
||||
.send({ password: '12345678', email: USER_0.email, resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
+2
-3
@@ -13,7 +13,6 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
constants = require('../constants.js'),
|
||||
generatePassword = require('../password.js').generate,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
users = require('../users.js'),
|
||||
@@ -33,13 +32,13 @@ function create(req, res, next) {
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
|
||||
|
||||
var password = req.body.password || generatePassword();
|
||||
var password = req.body.password || null;
|
||||
var email = req.body.email;
|
||||
var sendInvite = req.body.invite;
|
||||
var username = 'username' in req.body ? req.body.username : null;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
users.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
|
||||
users.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, auditSource(req), function (error, user) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
database = require('./database.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
express = require('express'),
|
||||
hat = require('hat'),
|
||||
hat = require('./hat.js'),
|
||||
http = require('http'),
|
||||
middleware = require('./middleware'),
|
||||
passport = require('passport'),
|
||||
@@ -120,8 +120,8 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
|
||||
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
|
||||
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
|
||||
router.get ('/api/v1/cloudron/logs', cloudronScope, routes.cloudron.getLogs);
|
||||
router.get ('/api/v1/cloudron/logstream', cloudronScope, routes.cloudron.getLogStream);
|
||||
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
|
||||
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.getAuthorizedKey);
|
||||
|
||||
+4
-5
@@ -11,8 +11,7 @@ exports = module.exports = {
|
||||
SetupError: SetupError
|
||||
};
|
||||
|
||||
var accesscontrol = require('./accesscontrol.js'),
|
||||
assert = require('assert'),
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
@@ -33,7 +32,6 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
semver = require('semver'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
@@ -176,6 +174,7 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
|
||||
|
||||
function done(error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
@@ -192,9 +191,9 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
if (result) return callback(new SettingsError(SettingsError.ALREADY_EXISTS, 'domain already exists'));
|
||||
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
|
||||
|
||||
async.series([
|
||||
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
|
||||
|
||||
@@ -162,6 +162,8 @@ function testConfig(apiConfig, callback) {
|
||||
|
||||
if ('noHardlinks' in apiConfig && typeof apiConfig.noHardlinks !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'noHardlinks must be boolean'));
|
||||
|
||||
if ('externalDisk' in apiConfig && typeof apiConfig.externalDisk !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'externalDisk must be boolean'));
|
||||
|
||||
fs.stat(apiConfig.backupFolder, function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Directory does not exist or cannot be accessed: ' + error.message));
|
||||
if (!result.isDirectory()) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Backup location is not a directory'));
|
||||
|
||||
+34
-17
@@ -17,7 +17,11 @@ var appdb = require('./appdb.js'),
|
||||
async = require('async'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('box:taskmanager'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -122,26 +126,39 @@ function startAppTask(appId, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// when parent process dies, apptask processes are killed because KillMode=control-group in systemd unit file
|
||||
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
|
||||
// ensure log folder
|
||||
mkdirp.sync(path.join(paths.LOG_DIR, appId));
|
||||
var logFilePath = path.join(paths.LOG_DIR, appId, 'apptask.log');
|
||||
|
||||
var pid = gActiveTasks[appId].pid;
|
||||
debug('Started task of %s pid: %s', appId, pid);
|
||||
|
||||
gActiveTasks[appId].once('exit', function (code, signal) {
|
||||
debug('Task for %s pid %s completed with status %s', appId, pid, code);
|
||||
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
|
||||
debug('Apptask crashed with code %s and signal %s', code, signal);
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
|
||||
} else if (code === 50) {
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
// will autoclose
|
||||
fs.open(logFilePath, 'a', function (error, fd) {
|
||||
if (error) {
|
||||
debug('Unable to open log file, queueing task for %s', appId, error);
|
||||
gPendingTasks.push(appId);
|
||||
return callback();
|
||||
}
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
|
||||
callback();
|
||||
// when parent process dies, apptask processes are killed because KillMode=control-group in systemd unit file
|
||||
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ], { stdio: [ 'pipe', fd, fd, 'ipc' ]});
|
||||
|
||||
var pid = gActiveTasks[appId].pid;
|
||||
debug('Started task of %s pid: %s. See logs at %s', appId, pid, logFilePath);
|
||||
|
||||
gActiveTasks[appId].once('exit', function (code, signal) {
|
||||
debug('Task for %s pid %s completed with status %s', appId, pid, code);
|
||||
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
|
||||
debug('Apptask crashed with code %s and signal %s', code, signal);
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
|
||||
} else if (code === 50) {
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
}
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function stopAppTask(appId, callback) {
|
||||
|
||||
+15
-11
@@ -16,7 +16,7 @@ var appdb = require('../appdb.js'),
|
||||
expect = require('expect.js'),
|
||||
groupdb = require('../groupdb.js'),
|
||||
groups = require('../groups.js'),
|
||||
hat = require('hat'),
|
||||
hat = require('../hat.js'),
|
||||
settings = require('../settings.js'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
userdb = require('../userdb.js');
|
||||
@@ -102,7 +102,9 @@ describe('Apps', function () {
|
||||
},
|
||||
portBindings: { PORT: 5678 },
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
robotsTxt: null,
|
||||
sso: false
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -114,7 +116,7 @@ describe('Apps', function () {
|
||||
version: '0.1', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1',
|
||||
tcpPorts: {}
|
||||
},
|
||||
portBindings: null,
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] },
|
||||
memoryLimit: 0
|
||||
};
|
||||
@@ -128,9 +130,11 @@ describe('Apps', function () {
|
||||
version: '0.1', dockerImage: 'docker/app2', healthCheckPath: '/', httpPort: 80, title: 'app2',
|
||||
tcpPorts: {}
|
||||
},
|
||||
portBindings: null,
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1.id ] },
|
||||
memoryLimit: 0
|
||||
memoryLimit: 0,
|
||||
robotsTxt: null,
|
||||
sso: false
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -409,12 +413,12 @@ describe('Apps', function () {
|
||||
apps.restoreInstalledApps(function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
apps.getAll(function (error, apps) {
|
||||
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(apps[0].oldConfig).to.be(null);
|
||||
expect(apps[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(apps[2].oldConfig).to.be(null);
|
||||
apps.getAll(function (error, result) {
|
||||
expect(result[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(result[0].oldConfig).to.eql(apps.getAppConfig(APP_0));
|
||||
expect(result[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(result[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
|
||||
expect(result[2].oldConfig).to.eql(apps.getAppConfig(APP_2));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -125,13 +125,18 @@ describe('Appstore', function () {
|
||||
});
|
||||
|
||||
it('can purchase an app', function (done) {
|
||||
var scope = nock('http://localhost:6060')
|
||||
var scope1 = nock('http://localhost:6060')
|
||||
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(201, {});
|
||||
|
||||
var scope2 = nock('http://localhost:6060')
|
||||
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(200, { subscription: { id: 'basic' }});
|
||||
|
||||
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ var appdb = require('../appdb.js'),
|
||||
eventlogdb = require('../eventlogdb.js'),
|
||||
expect = require('expect.js'),
|
||||
groupdb = require('../groupdb.js'),
|
||||
hat = require('hat'),
|
||||
hat = require('../hat.js'),
|
||||
mailboxdb = require('../mailboxdb.js'),
|
||||
maildb = require('../maildb.js'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
@@ -315,8 +315,8 @@ describe('database', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by resetToken fails for empty resetToken', function (done) {
|
||||
userdb.getByResetToken('', function (error, user) {
|
||||
it('getByResetToken fails for empty resetToken', function (done) {
|
||||
userdb.getByResetToken(USER_0.email, '', function (error, user) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(DatabaseError.INTERNAL_ERROR);
|
||||
expect(user).to.not.be.ok();
|
||||
@@ -324,8 +324,17 @@ describe('database', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('getByResetToken fails for bad email', function (done) {
|
||||
userdb.getByResetToken(USER_0.email + 'x', USER_0.resetToken, function (error, user) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(user).to.not.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get by resetToken', function (done) {
|
||||
userdb.getByResetToken(USER_0.resetToken, function (error, user) {
|
||||
userdb.getByResetToken(USER_0.email, USER_0.resetToken, function (error, user) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(user).to.eql(USER_0);
|
||||
done();
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
|
||||
checkMails(1, `${USER_0.email}`, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
|
||||
checkMails(1, `${USER_0.email}`, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('digest', function () {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
|
||||
checkMails(1, `${USER_0.email}`, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ var async = require('async'),
|
||||
expect = require('expect.js'),
|
||||
groups = require('../groups.js'),
|
||||
GroupsError = groups.GroupsError,
|
||||
hat = require('hat'),
|
||||
hat = require('../hat.js'),
|
||||
mailboxdb = require('../mailboxdb.js'),
|
||||
userdb = require('../userdb.js');
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ function setup(done) {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
|
||||
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
USER_1.id = result.id;
|
||||
@@ -124,7 +124,7 @@ function setup(done) {
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
users.create(USER_2.username, USER_2.password, USER_2.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
|
||||
users.create(USER_2.username, USER_2.password, USER_2.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
USER_2.id = result.id;
|
||||
|
||||
+26
-2
@@ -2,6 +2,7 @@
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
/* global beforeEach:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -11,7 +12,10 @@ var async = require('async'),
|
||||
domains = require('../domains.js'),
|
||||
expect = require('expect.js'),
|
||||
mail = require('../mail.js'),
|
||||
maildb = require('../maildb.js');
|
||||
MailError = mail.MailError,
|
||||
maildb = require('../maildb.js'),
|
||||
nock = require('nock'),
|
||||
settings = require('../settings.js');
|
||||
|
||||
const DOMAIN_0 = {
|
||||
domain: 'example.com',
|
||||
@@ -22,6 +26,10 @@ const DOMAIN_0 = {
|
||||
tlsConfig: { provider: 'fallback' }
|
||||
};
|
||||
|
||||
const APPSTORE_USER_ID = 'appstoreuserid';
|
||||
const APPSTORE_TOKEN = 'appstoretoken';
|
||||
const CLOUDRON_ID = 'cloudronid';
|
||||
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.set('fqdn', 'example.com');
|
||||
@@ -30,13 +38,27 @@ function setup(done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.initialize,
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain)
|
||||
mail.addDomain.bind(null, DOMAIN_0.domain),
|
||||
function (callback) {
|
||||
var scope = nock('http://localhost:6060')
|
||||
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
|
||||
.reply(201, { cloudron: { id: CLOUDRON_ID }});
|
||||
|
||||
settings.setAppstoreConfig({ userId: APPSTORE_USER_ID, token: APPSTORE_TOKEN }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
async.series([
|
||||
settings.uninitialize,
|
||||
database._clear,
|
||||
database.uninitialize
|
||||
], done);
|
||||
@@ -46,6 +68,8 @@ describe('Mail', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
beforeEach(nock.cleanAll);
|
||||
|
||||
describe('values', function () {
|
||||
it('can get default', function (done) {
|
||||
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
|
||||
|
||||
+11
-64
@@ -119,37 +119,7 @@ describe('User', function () {
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to short password', function (done) {
|
||||
users.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing upper case password', function (done) {
|
||||
users.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing numerics in password', function (done) {
|
||||
users.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing special chars in password', function (done) {
|
||||
users.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -159,7 +129,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to reserved username', function (done) {
|
||||
users.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -169,7 +139,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to invalid username', function (done) {
|
||||
users.create('moo+daemon', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create('moo+daemon', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -179,7 +149,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to short username', function (done) {
|
||||
users.create('', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create('', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -189,7 +159,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to long username', function (done) {
|
||||
users.create(new Array(257).fill('Z').join(''), PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create(new Array(257).fill('Z').join(''), PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -199,7 +169,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to reserved app pattern', function (done) {
|
||||
users.create('maybe.app', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create('maybe.app', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -221,31 +191,8 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails because of invalid BAD_FIELD', function (done) {
|
||||
expect(function () {
|
||||
users.create(EMAIL, {}, function () {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
users.create(12345, PASSWORD, EMAIL, function () {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
users.create(USERNAME, PASSWORD, EMAIL, {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
users.create(USERNAME, PASSWORD, EMAIL, {}, function () {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
users.create(USERNAME, PASSWORD, EMAIL, {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
users.create(USERNAME, PASSWORD, EMAIL, false, null, 'foobar');
|
||||
}).to.throwException();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails because user exists', function (done) {
|
||||
users.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.ALREADY_EXISTS);
|
||||
@@ -255,7 +202,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because password is empty', function (done) {
|
||||
users.create(USERNAME, '', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
|
||||
users.create(USERNAME, '', EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UsersError.BAD_FIELD);
|
||||
@@ -269,7 +216,7 @@ describe('User', function () {
|
||||
maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) {
|
||||
expect(error).not.to.be.ok();
|
||||
|
||||
users.create(USERNAME_1, PASSWORD_1, EMAIL_1, DISPLAY_NAME_1, AUDIT_SOURCE, { sendInvite: true }, function (error, result) {
|
||||
users.create(USERNAME_1, PASSWORD_1, EMAIL_1, DISPLAY_NAME_1, { sendInvite: true }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).not.to.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.username).to.equal(USERNAME_1.toLowerCase());
|
||||
@@ -720,7 +667,7 @@ describe('User', function () {
|
||||
it('make second user admin succeeds', function (done) {
|
||||
|
||||
var invitor = { username: USERNAME, email: EMAIL };
|
||||
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
|
||||
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -773,7 +720,7 @@ describe('User', function () {
|
||||
};
|
||||
|
||||
var invitor = { username: USERNAME, email: EMAIL };
|
||||
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
|
||||
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, AUDIT_SOURCE, function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result).to.be.ok();
|
||||
|
||||
|
||||
+1
-2
@@ -20,8 +20,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
hat = require('hat');
|
||||
|
||||
hat = require('./hat.js');
|
||||
|
||||
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires' ].join(',');
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ function checkAppUpdates(callback) {
|
||||
}
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
if (appstore.isFreePlan(result)) {
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
mailer.appUpdateAvailable(app, false /* subscription */, updateInfo);
|
||||
return iteratorDone();
|
||||
@@ -162,7 +162,7 @@ function checkBoxUpdates(callback) {
|
||||
}
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
if (appstore.isFreePlan(result)) {
|
||||
mailer.boxUpdateAvailable(false /* hasSubscription */, updateInfo.version, updateInfo.changelog);
|
||||
return done();
|
||||
}
|
||||
|
||||
+3
-2
@@ -82,13 +82,14 @@ function getOwner(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(resetToken, callback) {
|
||||
function getByResetToken(email, resetToken, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof resetToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (resetToken.length === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'Empty resetToken not allowed'));
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE resetToken=?', [ resetToken ], function (error, result) {
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE email=? AND resetToken=?', [ email, resetToken ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
|
||||
+40
-29
@@ -5,14 +5,14 @@ exports = module.exports = {
|
||||
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
list: listUsers,
|
||||
create: createUser,
|
||||
list: list,
|
||||
create: create,
|
||||
count: count,
|
||||
verify: verify,
|
||||
verifyWithUsername: verifyWithUsername,
|
||||
verifyWithEmail: verifyWithEmail,
|
||||
remove: removeUser,
|
||||
get: getUser,
|
||||
get: get,
|
||||
getByResetToken: getByResetToken,
|
||||
getAllAdmins: getAllAdmins,
|
||||
resetPasswordByIdentifier: resetPasswordByIdentifier,
|
||||
@@ -37,7 +37,7 @@ var assert = require('assert'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
groups = require('./groups.js'),
|
||||
GroupsError = groups.GroupsError,
|
||||
hat = require('hat'),
|
||||
hat = require('./hat.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
qrcode = require('qrcode'),
|
||||
safe = require('safetydance'),
|
||||
@@ -45,7 +45,6 @@ var assert = require('assert'),
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('uuid'),
|
||||
validatePassword = require('./password.js').validate,
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -122,25 +121,29 @@ function validateDisplayName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (password.length < 8) return new UsersError(UsersError.BAD_FIELD, 'Password must be atleast 8 characters');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function removePrivateFields(user) {
|
||||
return _.pick(user, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'admin');
|
||||
}
|
||||
|
||||
function createUser(username, password, email, displayName, auditSource, options, callback) {
|
||||
function create(username, password, email, displayName, options, auditSource, callback) {
|
||||
assert(username === null || typeof username === 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert(password === null || typeof password === 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
var invitor = options && options.invitor ? options.invitor : null,
|
||||
sendInvite = options && options.sendInvite ? true : false,
|
||||
owner = options && options.owner ? true : false;
|
||||
var invitor = options.invitor || null,
|
||||
sendInvite = !!options.sendInvite,
|
||||
owner = !!options.owner;
|
||||
|
||||
var error;
|
||||
|
||||
@@ -150,8 +153,12 @@ function createUser(username, password, email, displayName, auditSource, options
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
error = validatePassword(password);
|
||||
if (error) return callback(new UsersError(UsersError.BAD_FIELD, error.message));
|
||||
if (password !== null) {
|
||||
error = validatePassword(password);
|
||||
if (error) return callback(new UsersError(UsersError.BAD_FIELD, error.message));
|
||||
} else {
|
||||
password = hat(8 * 8);
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
error = validateEmail(email);
|
||||
@@ -216,7 +223,7 @@ function verify(userId, password, callback) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getUser(userId, function (error, user) {
|
||||
get(userId, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// for just invited users the username may be still null
|
||||
@@ -268,7 +275,7 @@ function removeUser(userId, auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getUser(userId, function (error, user) {
|
||||
get(userId, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (config.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new UsersError(UsersError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
@@ -286,7 +293,7 @@ function removeUser(userId, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listUsers(callback) {
|
||||
function list(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.getAllWithGroupIds(function (error, results) {
|
||||
@@ -310,7 +317,7 @@ function count(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getUser(userId, callback) {
|
||||
function get(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -330,18 +337,22 @@ function getUser(userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByResetToken(resetToken, callback) {
|
||||
function getByResetToken(email, resetToken, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof resetToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateToken(resetToken);
|
||||
var error = validateEmail(email);
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.getByResetToken(resetToken, function (error, result) {
|
||||
error = validateToken(resetToken);
|
||||
if (error) return callback(error);
|
||||
|
||||
userdb.getByResetToken(email, resetToken, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
getUser(result.id, callback);
|
||||
get(result.id, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,7 +396,7 @@ function updateUser(userId, data, auditSource, callback) {
|
||||
|
||||
callback();
|
||||
|
||||
getUser(userId, function (error, result) {
|
||||
get(userId, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId, user: removePrivateFields(result) });
|
||||
@@ -412,7 +423,7 @@ function setGroups(userId, groupIds, callback) {
|
||||
var wasAdmin = oldGroupIds.some(function (g) { return g === constants.ADMIN_GROUP_ID; });
|
||||
|
||||
if ((isAdmin && !wasAdmin) || (!isAdmin && wasAdmin)) {
|
||||
getUser(userId, function (error, result) {
|
||||
get(userId, function (error, result) {
|
||||
if (error) return debug('Failed to send admin change mail.', error);
|
||||
|
||||
mailer.adminChanged(result, isAdmin);
|
||||
@@ -511,7 +522,7 @@ function createOwner(username, password, email, displayName, auditSource, callba
|
||||
// we proceed if it already exists so we can re-create the owner if need be
|
||||
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) {
|
||||
create(username, password, email, displayName, { owner: true }, auditSource, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
groups.addMember(constants.ADMIN_GROUP_ID, user.id, function (error) {
|
||||
@@ -565,7 +576,7 @@ function setTwoFactorAuthenticationSecret(userId, callback) {
|
||||
|
||||
if (result.twoFactorAuthenticationEnabled) return callback(new UsersError(UsersError.ALREADY_EXISTS));
|
||||
|
||||
var secret = speakeasy.generateSecret({ name: `Cloudron (${config.adminFqdn()})` });
|
||||
var secret = speakeasy.generateSecret({ name: `Cloudron ${config.adminFqdn()} (${result.username})` });
|
||||
|
||||
userdb.update(userId, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, function (error) {
|
||||
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
|
||||
|
||||
Reference in New Issue
Block a user