Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7b5b49d96 | |||
| 93ef1919c2 | |||
| 254d6ac92e | |||
| 3a12265f42 | |||
| 71eeb47f0f | |||
| 5ea5023d97 | |||
| 1148e21cd4 | |||
| e9a2b2a7cf | |||
| 7a34f40611 | |||
| c630de1003 | |||
| 74da8f5af8 | |||
| b758be5ae2 | |||
| c585be4eec | |||
| 3ebc569438 | |||
| 5a2cf3cbfe | |||
| 715c5f9f61 | |||
| 6843fda601 | |||
| a78f3b1db3 | |||
| 1419108a86 | |||
| 7a8b457ce9 | |||
| 10967ff8ce | |||
| 1fdfd3681c | |||
| 187d4f9ca2 | |||
| 6b67e64bf1 | |||
| 7ae6061d72 | |||
| e96b9c3e3f | |||
| c9ca05a703 | |||
| 23e5bed247 | |||
| bae0d728b3 | |||
| 5cd1c7d714 | |||
| d430e902bf | |||
| 4fb89de34f | |||
| 7cd3bb31e1 | |||
| 2857158543 | |||
| 82a347ea4b | |||
| b5c7f978a2 | |||
| 625da29fce | |||
| b82b183df6 | |||
| ce36fadf2b | |||
| 2429599733 | |||
| 261a0a1728 | |||
| d8def61f67 | |||
| 2732af24c1 | |||
| 3d48da0e8d | |||
| d3b8bd1314 | |||
| f600ebcf19 | |||
| 160467e199 | |||
| 384c410e7c | |||
| 84c4187fa9 | |||
| 4f7fd9177c | |||
| b5b0ab7475 | |||
| a0d7406b3c | |||
| 7165be0513 | |||
| 9c995277f7 | |||
| aa693e529b | |||
| 63013c7297 | |||
| c8db6419d8 | |||
| 93c1ddd982 | |||
| df102ec374 | |||
| 9688e4c124 | |||
| 00d277b1c3 | |||
| 0fb44bfbc1 | |||
| c167bd8996 | |||
| a3737c3797 | |||
| 8fcb0b46a5 | |||
| f5189e0a56 | |||
| 86f14b0149 | |||
| 30913006e3 | |||
| 81bd4f2ea5 | |||
| 351ddcb218 | |||
| dd18f9741a | |||
| cdce6e605d | |||
| d4480ec407 | |||
| 85c92ab0b4 | |||
| 230c24d6c6 | |||
| 07c935dfec | |||
| eab3bda8e1 | |||
| f731c1ed0b | |||
| edec3601f4 | |||
| 9e87fd0440 | |||
| 8cb304e1c9 | |||
| a24335d68b | |||
| 78d1ed7aa5 | |||
| deb30e440a | |||
| 86ef9074b1 | |||
| 1a13128ae1 | |||
| b41642552d | |||
| f5570c2e63 | |||
| b0d11ddcab | |||
| 804464c304 | |||
| ecf7f442ba | |||
| 9ddd3aeb07 | |||
| 864e3ff217 | |||
| 9bf1fe3b7d | |||
| b32a48c212 | |||
| 22a3dd7653 | |||
| 132b463e0a | |||
| 7aefe5226a | |||
| 656c1bfd3a | |||
| e237b609f5 | |||
| 057b9e954e | |||
| f79c00d9be | |||
| 5f96d862ab | |||
| 79199bf023 | |||
| beec4dddca | |||
| 7c243cb219 | |||
| 754e33af2a | |||
| 63cab7d751 | |||
| 503714a10b | |||
| ada5be6ae0 | |||
| 2112494b43 | |||
| c0b45ad71e | |||
| 5669d387af | |||
| 957f20a9a8 | |||
| 71bfc1cbda | |||
| 489ea3a980 | |||
| 8c6f655628 | |||
| 75d22d7988 | |||
| a7bf043a9e | |||
| 402385faca | |||
| cdd82fa456 | |||
| 2f7d99f3f6 | |||
| e4799991ec | |||
| 66167e74dc | |||
| 5643d49bef | |||
| 81ec26e45c | |||
| 72c5ebcc06 | |||
| ecf7575dd3 | |||
| 98a7f44dc1 | |||
| 5fce9c8d1f | |||
| 0ea89fccb8 | |||
| 2c2922d725 | |||
| fbeefeca7d | |||
| 163ceef527 | |||
| db5cc1f694 | |||
| a3b9a7365c | |||
| 213b2a2802 | |||
| 229d09bb9e | |||
| f127680c8c | |||
| f767f7f1b9 | |||
| acb1afa955 | |||
| d132109925 | |||
| 820e417026 | |||
| 94bd0c606b | |||
| 9a8328e6db | |||
| 5c75d64a07 | |||
| a8001995c8 | |||
| 9ba4d52fb7 | |||
| 0e613a1cab | |||
| cf3d503a74 | |||
| 1ab46a96f9 | |||
| 1a3164ef32 | |||
| bd62efcff5 | |||
| 7fc37b7c70 | |||
| 8ddccae15a | |||
| 675d7c8730 | |||
| ba35d4a313 | |||
| c1280ddcc2 | |||
| 36ded4c06a | |||
| 9fb276019e | |||
| 19982b1815 | |||
| 459d5b8f60 | |||
| 8ba5dc2352 | |||
| 8c73a7c7c2 | |||
| e78dd41e88 | |||
| 59ecb056d0 | |||
| 11b17fec3a | |||
| 5ea81d0fd3 | |||
| 19cbd1f394 | |||
| 1b7265f866 | |||
| 1cdb64e78d | |||
| eec8708249 | |||
| ab003bf81f | |||
| 2d60901b6e | |||
| 3fc9bde4f4 | |||
| 4fc0df31fe | |||
| 3ac326e766 | |||
| 4770f9ddf6 | |||
| 7e60fd554a | |||
| c1cd7ac129 | |||
| aab62263a7 | |||
| 79889a0aac | |||
| f413bfb3a0 | |||
| 2b0791f4a3 | |||
| d95339534f | |||
| 82cf667f3b | |||
| e20b3f75e4 | |||
| 6cca7b3e0e | |||
| 0b814af206 | |||
| bfdabf9272 | |||
| 60988ff7f3 | |||
| 3649fd0c31 | |||
| 00c5aa041f | |||
| 4569b67007 | |||
| 1fb26bc441 | |||
| e6d23a9701 | |||
| 0785266741 | |||
| e752949752 | |||
| 199eb2b3e1 | |||
| 49cbea93fb | |||
| 451c410547 | |||
| f6541720c4 | |||
| 5e5435e869 | |||
| 0d4f113d7d | |||
| 14fab0992f | |||
| d7eb004bc1 | |||
| c34f3ee653 | |||
| 96d595de39 | |||
| b1f4508313 | |||
| 52ce59faaf | |||
| 85085ae0b2 | |||
| c14cf9c260 | |||
| a47c6f0774 | |||
| 888955bd9b | |||
| 6abf5e2c44 | |||
| b1935c3550 | |||
| e39d7750c5 | |||
| 1d83a48a1a | |||
| 802ee6c456 | |||
| 278085ba22 | |||
| b945a8a04c | |||
| 7ef92071c5 | |||
| c16ab95193 | |||
| c5e2d9a9cc | |||
| 07df76b25e | |||
| 5b264565db | |||
| a3561bd040 | |||
| 6e4f47e807 | |||
| 471965dc66 | |||
| 3b109ea2e7 | |||
| 6011526d5e | |||
| 1395d2971b | |||
| e9d6badae7 | |||
| 65ddc7f24c | |||
| fa871c7ada | |||
| 8652d6c136 | |||
| 16d976a145 | |||
| fa1f5cc454 | |||
| 84c3b367d5 | |||
| 793aa6512d | |||
| 98ab99ab34 | |||
| 24a826bdd1 | |||
| 05245f5fc7 | |||
| b718c8d044 | |||
| 2888a85081 | |||
| 307262244a | |||
| 9a875634f8 | |||
| 4af33486ae | |||
| befa898f18 | |||
| 18525e1236 | |||
| 28ffd01cf4 | |||
| 09c7aa4440 | |||
| ea4862d351 | |||
| 3e4d62329e | |||
| d12366576b | |||
| 7b1d906494 | |||
| 0972c88b8b | |||
| 9464a26a7e |
@@ -400,3 +400,27 @@
|
||||
- Improved box update management using prereleases
|
||||
- Less aggressive disk space checks
|
||||
|
||||
[0.8.0]
|
||||
- MySQL addon : multiple database support
|
||||
|
||||
[0.8.1]
|
||||
- Set Host HTTP header when querying healthCheckPath
|
||||
- Show application Changelog in app update emails
|
||||
|
||||
[0.9.0]
|
||||
- Fix bug in multdb mysql addon backup
|
||||
- Add initial user group support
|
||||
- Improved app memory limit handling
|
||||
|
||||
[0.9.1]
|
||||
- Introduce per app group access control
|
||||
|
||||
[0.9.2]
|
||||
- Fix bug where reconfiguring apps would trigger memory limit warning
|
||||
- Allow more apps to be installed in bigger sized cloudrons
|
||||
- Allow user to override memory limit warning and install anyway
|
||||
|
||||
[0.9.3]
|
||||
- Admin flag is handled outside of groups
|
||||
- User interface fixes for groups
|
||||
- Allow to set access restrictions on app installation
|
||||
|
||||
@@ -268,7 +268,7 @@ echo "==== Install box-setup systemd script ===="
|
||||
cat > /etc/systemd/system/box-setup.service <<EOF
|
||||
[Unit]
|
||||
Description=Box Setup
|
||||
Before=docker.service umount.target collectd.service
|
||||
Before=docker.service collectd.service
|
||||
After=do-resize.service
|
||||
|
||||
[Service]
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ var ejs = require('gulp-ejs'),
|
||||
serve = require('gulp-serve'),
|
||||
sass = require('gulp-sass'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
minifyCSS = require('gulp-minify-css'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
argv = require('yargs').argv;
|
||||
|
||||
@@ -119,7 +119,7 @@ gulp.task('css', function () {
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(minifyCSS())
|
||||
.pipe(cssnano())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist'))
|
||||
.pipe(gulp.dest('setup/splash/website'));
|
||||
|
||||
@@ -20,10 +20,15 @@ readonly provider="${5}"
|
||||
readonly revision="${6}"
|
||||
|
||||
# environment specific urls
|
||||
readonly api_server_origin="https://api.dev.cloudron.io"
|
||||
readonly web_server_origin="https://dev.cloudron.io"
|
||||
readonly release_bucket_url="https://s3.amazonaws.com/dev-cloudron-releases"
|
||||
readonly versions_url="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
|
||||
<% if (env === 'prod') { %>
|
||||
readonly api_server_origin="https://api.cloudron.io"
|
||||
readonly web_server_origin="https://cloudron.io"
|
||||
<% } else { %>
|
||||
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
|
||||
readonly web_server_origin="https://<%= env %>.cloudron.io"
|
||||
<% } %>
|
||||
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
|
||||
readonly versions_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases/versions.json"
|
||||
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
|
||||
|
||||
# runtime consts
|
||||
@@ -132,7 +137,7 @@ cat > /root/provision.json <<EOF
|
||||
"secretAccessKey": "${aws_access_key_secret}"
|
||||
},
|
||||
"tlsConfig": {
|
||||
"provider": "letsencrypt-dev"
|
||||
"provider": "letsencrypt-<%= env %>"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ readonly DATA_DIR=/home/yellowtent/data
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly json="${script_dir}/../../node_modules/.bin/json"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 180"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 300"
|
||||
|
||||
readonly is_update=$([[ -d "${BOX_SRC_DIR}" ]] && echo "yes" || echo "no")
|
||||
|
||||
|
||||
@@ -19,12 +19,11 @@ fi
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size="${physical_memory}"
|
||||
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
|
||||
readonly app_count=$((${physical_memory} / 200)) # estimated app count
|
||||
readonly disk_size_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ print $3 }')
|
||||
readonly disk_size=$((disk_size_gb * 1024))
|
||||
readonly backup_swap_size=1024
|
||||
# readonly system_size=5120 # 5 gigs for system libs, installer, box code and tmp
|
||||
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
|
||||
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
|
||||
|
||||
@@ -59,7 +58,9 @@ echo "Resizing data volume"
|
||||
home_data_size=$((disk_size - system_size - swap_size - backup_swap_size - ext4_reserved))
|
||||
echo "Resizing up btrfs user data to size ${home_data_size}M"
|
||||
umount "${USER_DATA_DIR}"
|
||||
fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
|
||||
# Do not preallocate (non-sparse). Doing so overallocates for data too much in advance and causes problems when using many apps with smaller data
|
||||
# fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
|
||||
truncate -s "${home_data_size}m" "${USER_DATA_FILE}" # this will shrink it if the file had existed. this is useful when running this script on a live system
|
||||
mount "${USER_DATA_FILE}"
|
||||
btrfs filesystem resize max "${USER_DATA_DIR}"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN memoryLimit', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE groups(" +
|
||||
"id VARCHAR(128) NOT NULL UNIQUE," +
|
||||
"name VARCHAR(128) NOT NULL UNIQUE," +
|
||||
"PRIMARY KEY(id))";
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE groups', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
|
||||
"groupId VARCHAR(128) NOT NULL," +
|
||||
"userId VARCHAR(128) NOT NULL," +
|
||||
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
|
||||
"FOREIGN KEY(userId) REFERENCES users(id));";
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE groupMembers', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var async = require('async');
|
||||
|
||||
var ADMIN_GROUP_ID = 'admin'; // see groups.js
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
|
||||
function migrateAdminFlag(done) {
|
||||
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
console.dir(results);
|
||||
|
||||
async.eachSeries(results, function (r, next) {
|
||||
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -21,6 +21,17 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
displayName VARCHAR(512) DEFAULT '',
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
username VARCHAR(254) NOT NULL UNIQUE,
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
groupId VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(groupId) REFERENCES groups(id),
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
||||
identifier VARCHAR(128) NOT NULL,
|
||||
@@ -53,6 +64,7 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
accessRestrictionJson TEXT,
|
||||
oauthProxy BOOLEAN DEFAULT 0,
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
|
||||
lastBackupId VARCHAR(128),
|
||||
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
|
||||
|
||||
Generated
+456
-450
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -18,7 +18,7 @@
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"bytes": "^2.1.0",
|
||||
"cloudron-manifestformat": "^2.2.0",
|
||||
"cloudron-manifestformat": "^2.3.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"connect-timeout": "^1.5.0",
|
||||
@@ -77,8 +77,8 @@
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-autoprefixer": "^2.3.0",
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-ejs": "^1.0.0",
|
||||
"gulp-minify-css": "^1.1.3",
|
||||
"gulp-sass": "^2.0.1",
|
||||
"gulp-serve": "^1.0.0",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=21
|
||||
INFRA_VERSION=23
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
BASE_IMAGE=cloudron/base:0.8.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.8.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.10.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.8.0
|
||||
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
|
||||
|
||||
+4
-4
@@ -420,7 +420,7 @@ function setupMySql(app, options, callback) {
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', 'add', app.id ];
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
|
||||
|
||||
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
|
||||
if (error) return callback(error);
|
||||
@@ -453,7 +453,7 @@ function teardownMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
|
||||
@@ -481,7 +481,7 @@ function backupMySql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', 'backup', app.id ]);
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
|
||||
@@ -504,7 +504,7 @@ function restoreMySql(app, options, callback) {
|
||||
input.on('error', callback);
|
||||
|
||||
// cannot get this to work through docker.exec
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', 'restore', app.id ]);
|
||||
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ]);
|
||||
cp.on('error', callback);
|
||||
cp.on('exit', function (code, signal) {
|
||||
debugApp(app, 'restoreMySql: done %s %s', code, signal);
|
||||
|
||||
+6
-7
@@ -59,7 +59,7 @@ var assert = require('assert'),
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.oauthProxy' ].join(',');
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -92,8 +92,6 @@ function postProcess(result) {
|
||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||
}
|
||||
|
||||
result.oauthProxy = !!result.oauthProxy;
|
||||
|
||||
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
||||
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
|
||||
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||
@@ -179,7 +177,7 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
@@ -187,7 +185,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
portBindings = portBindings || { };
|
||||
@@ -197,8 +195,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, oauthProxy ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
@@ -283,6 +281,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert.strictEqual(typeof constraints, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
|
||||
var queries = [ ];
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js').connection,
|
||||
@@ -24,8 +25,10 @@ var gDockerEventStream = null;
|
||||
function debugApp(app) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
var prefix = app ? app.location : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
|
||||
var id = app ? app.id : '';
|
||||
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
|
||||
}
|
||||
|
||||
function setHealth(app, health, callback) {
|
||||
@@ -89,6 +92,7 @@ function checkAppHealth(app, callback) {
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
@@ -114,7 +118,7 @@ function processApps(callback) {
|
||||
|
||||
var alive = apps
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return a.location; }).join(', ');
|
||||
.map(function (a) { return a.location || 'naked_domain'; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
|
||||
+153
-32
@@ -6,10 +6,13 @@ exports = module.exports = {
|
||||
AppsError: AppsError,
|
||||
|
||||
hasAccessTo: hasAccessTo,
|
||||
requiresOAuthProxy: requiresOAuthProxy,
|
||||
|
||||
get: get,
|
||||
getBySubdomain: getBySubdomain,
|
||||
getByIpAddress: getByIpAddress,
|
||||
getAll: getAll,
|
||||
getAllByUser: getAllByUser,
|
||||
purchase: purchase,
|
||||
install: install,
|
||||
configure: configure,
|
||||
@@ -56,6 +59,7 @@ var addons = require('./addons.js'),
|
||||
debug = require('debug')('box:apps'),
|
||||
docker = require('./docker.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
@@ -192,9 +196,38 @@ function validateAccessRestriction(accessRestriction) {
|
||||
|
||||
if (accessRestriction === null) return null;
|
||||
|
||||
if (!accessRestriction.users || !Array.isArray(accessRestriction.users)) return new Error('users array property required');
|
||||
if (accessRestriction.users.length === 0) return new Error('users array cannot be empty');
|
||||
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
|
||||
var noUsers = true, noGroups = true;
|
||||
|
||||
if (accessRestriction.users) {
|
||||
if (!Array.isArray(accessRestriction.users)) return new Error('users array property required');
|
||||
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
|
||||
noUsers = accessRestriction.users.length === 0;
|
||||
}
|
||||
|
||||
if (accessRestriction.groups) {
|
||||
if (!Array.isArray(accessRestriction.groups)) return new Error('groups array property required');
|
||||
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new Error('All groups have to be strings');
|
||||
noGroups = accessRestriction.groups.length === 0;
|
||||
}
|
||||
|
||||
if (noUsers && noGroups) return new Error('users and groups array cannot both be empty');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateMemoryLimit(manifest, memoryLimit) {
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
|
||||
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
var max = (4096 * 1024 * 1024);
|
||||
|
||||
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
|
||||
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
|
||||
if (memoryLimit === 0) return null;
|
||||
|
||||
if (memoryLimit < min) return new Error('memoryLimit too small');
|
||||
if (memoryLimit > max) return new Error('memoryLimit too large');
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -226,12 +259,39 @@ function getIconUrlSync(app) {
|
||||
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
|
||||
}
|
||||
|
||||
function hasAccessTo(app, user) {
|
||||
function hasAccessTo(app, user, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.accessRestriction === null) return true;
|
||||
return app.accessRestriction.users.some(function (e) { return e === user.id; });
|
||||
if (app.accessRestriction === null) return callback(null, true);
|
||||
|
||||
// check user access
|
||||
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
|
||||
|
||||
// check group access
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
|
||||
groups.isMember(groupId, user.id, function (error, member) {
|
||||
iteratorDone(!error && member); // async.some does not take error argument in callback
|
||||
});
|
||||
}, function (result) {
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function requiresOAuthProxy(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
var tmp = app.accessRestriction;
|
||||
|
||||
// if no accessRestriction set, or the app uses one of the auth modules, we do not need the oauth proxy
|
||||
if (tmp === null) return false;
|
||||
if (app.manifest.addons['ldap'] || app.manifest.addons['oauth'] || app.manifest.addons['simpleauth']) return false;
|
||||
|
||||
// check if any restrictions are set
|
||||
return !!((tmp.users && tmp.users.length) || (tmp.groups && tmp.groups.length));
|
||||
}
|
||||
|
||||
function get(appId, callback) {
|
||||
@@ -264,6 +324,25 @@ function getBySubdomain(subdomain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = config.appFqdn(app.location);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -279,6 +358,21 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAllByUser(user, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.filter(result, function (app, callback) {
|
||||
hasAccessTo(app, user, function (error, hasAccess) {
|
||||
callback(hasAccess);
|
||||
});
|
||||
}, callback.bind(null, null)); // never error
|
||||
});
|
||||
}
|
||||
|
||||
function purchase(appStoreId, callback) {
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -301,17 +395,17 @@ function purchase(appStoreId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, cert, key, callback) {
|
||||
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert(!icon || typeof icon === 'string');
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = manifestFormat.parse(manifest);
|
||||
@@ -329,6 +423,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
error = validateMemoryLimit(manifest, memoryLimit);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
// memoryLimit might come in as 0 if not specified
|
||||
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
|
||||
// singleUser mode requires accessRestriction to contain exactly one user
|
||||
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
|
||||
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
|
||||
@@ -349,7 +449,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
purchase(appStoreId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, oauthProxy, function (error) {
|
||||
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -366,14 +466,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, location, portBindings, accessRestriction, oauthProxy, cert, key, callback) {
|
||||
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateHostname(location, config.fqdn());
|
||||
@@ -392,6 +492,12 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
|
||||
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
error = validateMemoryLimit(app.manifest, memoryLimit);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
|
||||
|
||||
// memoryLimit might come in as 0 if not specified
|
||||
memoryLimit = memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
|
||||
// save cert to data/box/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
@@ -401,14 +507,14 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
|
||||
var values = {
|
||||
location: location.toLowerCase(),
|
||||
accessRestriction: accessRestriction,
|
||||
oauthProxy: oauthProxy,
|
||||
portBindings: portBindings,
|
||||
memoryLimit: memoryLimit,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
oauthProxy: app.oauthProxy
|
||||
memoryLimit: app.memoryLimit
|
||||
}
|
||||
};
|
||||
|
||||
@@ -457,14 +563,19 @@ function update(appId, force, manifest, portBindings, icon, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// Ensure we update the memory limit in case the new app requires more memory as a minimum
|
||||
var memoryLimit = manifest.memoryLimit ? (app.memoryLimit < manifest.memoryLimit ? manifest.memoryLimit : app.memoryLimit) : app.memoryLimit;
|
||||
|
||||
var values = {
|
||||
manifest: manifest,
|
||||
portBindings: portBindings,
|
||||
memoryLimit: memoryLimit,
|
||||
|
||||
oldConfig: {
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy
|
||||
memoryLimit: app.memoryLimit
|
||||
}
|
||||
};
|
||||
|
||||
@@ -550,12 +661,13 @@ function restore(appId, callback) {
|
||||
values = {
|
||||
manifest: restoreConfig.manifest,
|
||||
portBindings: restoreConfig.portBindings,
|
||||
memoryLimit: restoreConfig.memoryLimit,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
manifest: app.manifest
|
||||
}
|
||||
};
|
||||
@@ -578,13 +690,13 @@ function uninstall(appId, callback) {
|
||||
|
||||
debug('Will uninstall app with id:%s', appId);
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
taskmanager.stopAppTask(appId, function () {
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId); // since uninstall is allowed from any state, kill current task
|
||||
|
||||
callback(null);
|
||||
taskmanager.startAppTask(appId, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,6 +758,10 @@ function exec(appId, options, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
|
||||
}
|
||||
|
||||
var container = docker.connection.getContainer(app.containerId);
|
||||
|
||||
var execOptions = {
|
||||
@@ -763,21 +879,26 @@ function createNewBackup(app, addonsToBackup, callback) {
|
||||
assert(!addonsToBackup || typeof addonsToBackup, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
backups.getBackupUrl(app, function (error, result) {
|
||||
backups.getBackupUrl(app, function (error, backupArchive) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'backupApp: backup url:%s backup id:%s', result.url, result.id);
|
||||
|
||||
async.series([
|
||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
||||
addons.backupAddons.bind(null, app, addonsToBackup),
|
||||
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
|
||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
||||
], function (error) {
|
||||
backups.getAppBackupConfigUrl(app, function (error, backupConfig) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result.id);
|
||||
debugApp(app, 'backupApp: backup url:%s backup config url:%s', backupArchive.url, backupConfig.url);
|
||||
|
||||
async.series([
|
||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
||||
addons.backupAddons.bind(null, app, addonsToBackup),
|
||||
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, backupArchive.url, backupConfig.url, backupArchive.backupKey, backupArchive.sessionToken ]),
|
||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
||||
], function (error) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, backupArchive.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -803,7 +924,7 @@ function backupApp(app, addonsToBackup, callback) {
|
||||
location: app.location,
|
||||
portBindings: app.portBindings,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy
|
||||
memoryLimit: app.memoryLimit
|
||||
};
|
||||
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
|
||||
|
||||
|
||||
+20
-10
@@ -101,11 +101,12 @@ function configureNginx(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.appFqdn(app.location);
|
||||
var oauthProxy = apps.requiresOAuthProxy(app);
|
||||
|
||||
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
nginx.configureApp(app, certFilePath, keyFilePath, callback);
|
||||
nginx.configureApp(app, oauthProxy, certFilePath, keyFilePath, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,7 +163,7 @@ function allocateOAuthProxyCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.oauthProxy) return callback(null);
|
||||
if (!apps.requiresOAuthProxy(app)) return callback(null);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
@@ -598,15 +599,21 @@ function update(app, callback) {
|
||||
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
|
||||
|
||||
// app does not want these addons anymore
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
|
||||
verifyManifest.bind(null, app),
|
||||
|
||||
// download new image before app is stopped. this is so we can reduce downtime
|
||||
// and also not remove the 'common' layers when the old image is deleted
|
||||
updateApp.bind(null, app, { installationProgress: '15, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
|
||||
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
updateApp.bind(null, app, { installationProgress: '25, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
@@ -622,17 +629,14 @@ function update(app, callback) {
|
||||
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '20, Backup app' }),
|
||||
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
|
||||
apps.backupApp.bind(null, app, app.oldConfig.manifest.addons)
|
||||
], next);
|
||||
},
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Downloading icon' }),
|
||||
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
@@ -697,7 +701,13 @@ function uninstall(app, callback) {
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
|
||||
appdb.del.bind(null, app.id)
|
||||
], callback);
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error uninstalling app: %s', error);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function runApp(app, callback) {
|
||||
@@ -760,7 +770,7 @@ function startTask(appId, callback) {
|
||||
case appdb.ISTATE_PENDING_INSTALL: return install(app, callback);
|
||||
case appdb.ISTATE_PENDING_FORCE_UPDATE: return update(app, callback);
|
||||
case appdb.ISTATE_ERROR:
|
||||
debugApp(app, 'Apptask launched with error states.');
|
||||
debugApp(app, 'Internal error. apptask launched with error status.');
|
||||
return callback(null);
|
||||
default:
|
||||
debugApp(app, 'apptask launched with invalid command');
|
||||
|
||||
+9
-1
@@ -16,6 +16,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:auth'),
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
crypto = require('crypto'),
|
||||
groups = require('./groups'),
|
||||
passport = require('passport'),
|
||||
tokendb = require('./tokendb'),
|
||||
user = require('./user'),
|
||||
@@ -123,7 +124,14 @@ function initialize(callback) {
|
||||
// amend the tokenType of the token owner
|
||||
user.tokenType = tokenType;
|
||||
|
||||
callback(null, user, info);
|
||||
// amend the admin flag
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, user.id, function (error, isAdmin) {
|
||||
if (error) return callback(error);
|
||||
|
||||
user.admin = isAdmin;
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
+38
-4
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
getBackupUrl: getBackupUrl,
|
||||
getAppBackupConfigUrl: getAppBackupConfigUrl,
|
||||
getRestoreUrl: getRestoreUrl,
|
||||
|
||||
copyLastBackup: copyLastBackup
|
||||
@@ -98,6 +99,31 @@ function getBackupUrl(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAppBackupConfigUrl(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var filename = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var obj = {
|
||||
id: filename,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken
|
||||
};
|
||||
|
||||
debug('getAppBackupConfigUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
|
||||
function getRestoreUrl(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
@@ -124,19 +150,27 @@ function getRestoreUrl(backupId, callback) {
|
||||
}
|
||||
|
||||
function copyLastBackup(app, callback) {
|
||||
assert(app && typeof app === 'object');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
|
||||
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, toFilename);
|
||||
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
|
||||
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
|
||||
|
||||
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, toFilenameArchive);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ exports = module.exports = {
|
||||
updateToLatest: updateToLatest,
|
||||
update: update,
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
backup: backup,
|
||||
retire: retire,
|
||||
ensureBackup: ensureBackup,
|
||||
@@ -460,49 +459,6 @@ function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
}
|
||||
|
||||
function migrate(size, region, callback) {
|
||||
assert.strictEqual(typeof size, 'string');
|
||||
assert.strictEqual(typeof region, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
function unlock(error) {
|
||||
if (error) {
|
||||
debug('Failed to migrate', error);
|
||||
locker.unlock(locker.OP_MIGRATE);
|
||||
} else {
|
||||
debug('Migration initiated successfully');
|
||||
// do not unlock; cloudron is migrating
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// initiate the migration in the background
|
||||
backupBoxAndApps(function (error, restoreKey) {
|
||||
if (error) return unlock(error);
|
||||
|
||||
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send({ size: size, region: region, restoreKey: restoreKey })
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error);
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
return unlock(null);
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function update(boxUpdateInfo, callback) {
|
||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
+9
-1
@@ -37,7 +37,7 @@ exports = module.exports = {
|
||||
isDev: isDev,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: initConfig
|
||||
_reset: _reset
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -70,6 +70,14 @@ function saveSync() {
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
|
||||
function _reset (callback) {
|
||||
safe.fs.unlinkSync(cloudronConfigFileName);
|
||||
|
||||
initConfig();
|
||||
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.fqdn = 'localhost';
|
||||
|
||||
+3
-1
@@ -7,6 +7,8 @@ exports = module.exports = {
|
||||
ADMIN_NAME: 'Settings',
|
||||
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
ADMIN_APPID: 'admin' // admin appid (settingsdb)
|
||||
ADMIN_APPID: 'admin', // admin appid (settingsdb)
|
||||
|
||||
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024) // see also client.js
|
||||
};
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ function clear(callback) {
|
||||
require('./authcodedb.js')._clear,
|
||||
require('./clientdb.js')._clear,
|
||||
require('./tokendb.js')._clear,
|
||||
require('./groupdb.js')._clear,
|
||||
require('./userdb.js')._clear,
|
||||
require('./settingsdb.js')._clear
|
||||
], callback);
|
||||
|
||||
@@ -30,3 +30,4 @@ DatabaseError.INTERNAL_ERROR = 'Internal error';
|
||||
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
|
||||
DatabaseError.NOT_FOUND = 'Record not found';
|
||||
DatabaseError.BAD_FIELD = 'Invalid field';
|
||||
DatabaseError.IN_USE = 'In Use';
|
||||
|
||||
+14
-12
@@ -39,7 +39,8 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.listHostedZones({}, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
var zone = result.HostedZones.filter(function (zone) {
|
||||
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
|
||||
@@ -84,11 +85,9 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'PriorRequestNotComplete') {
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
} else if (error) {
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'PriorRequestNotComplete') return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
@@ -131,7 +130,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.listResourceRecordSets(params, function (error, result) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
|
||||
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
|
||||
|
||||
@@ -175,21 +175,22 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('del: resource record set not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('del: hosted zone not found.', error);
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('del: resource is still busy', error);
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('del: invalid change batch. No such record to be deleted.');
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
} else if (error) {
|
||||
debug('del: error', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -206,6 +207,7 @@ function getChangeStatus(dnsConfig, changeId, callback) {
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.getChange({ Id: changeId }, function (error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.ChangeInfo.Status);
|
||||
|
||||
+45
-2
@@ -4,6 +4,7 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:src/docker.js'),
|
||||
Docker = require('dockerode'),
|
||||
safe = require('safetydance'),
|
||||
@@ -23,7 +24,8 @@ exports = module.exports = {
|
||||
deleteContainerByName: deleteContainer,
|
||||
deleteImage: deleteImage,
|
||||
deleteContainers: deleteContainers,
|
||||
createSubcontainer: createSubcontainer
|
||||
createSubcontainer: createSubcontainer,
|
||||
getContainerIdByIp: getContainerIdByIp
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
@@ -156,7 +158,15 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || (developmentMode ? 0 : 1024 * 1024 * 200); // 200mb by default
|
||||
// first check db record, then manifest
|
||||
var memoryLimit = app.memoryLimit || manifest.memoryLimit;
|
||||
|
||||
// ensure we never go below minimum
|
||||
memoryLimit = memoryLimit < constants.DEFAULT_MEMORY_LIMIT ? constants.DEFAULT_MEMORY_LIMIT : memoryLimit; // 256mb by default
|
||||
|
||||
// developerMode does not restrict memory usage
|
||||
memoryLimit = developmentMode ? 0 : memoryLimit;
|
||||
|
||||
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
|
||||
// this means cloudron exec does not work
|
||||
var isolatedNetworkNs = true;
|
||||
@@ -346,3 +356,36 @@ function deleteImage(manifest, callback) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function getContainerIdByIp(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('get container by ip %s', ip);
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
docker.listNetworks({}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var bridge;
|
||||
result.forEach(function (n) {
|
||||
if (n.Name === 'bridge') bridge = n;
|
||||
});
|
||||
|
||||
if (!bridge) return callback(new Error('Unable to find the bridge network'));
|
||||
|
||||
var containerId;
|
||||
for (var id in bridge.Containers) {
|
||||
if (bridge.Containers[id].IPv4Address.indexOf(ip) === 0) {
|
||||
containerId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!containerId) return callback(new Error('No container with that ip'));
|
||||
|
||||
debug('found container %s with ip %s', containerId, ip);
|
||||
|
||||
callback(null, containerId);
|
||||
});
|
||||
}
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getWithMembers: getWithMembers,
|
||||
getAll: getAll,
|
||||
add: add,
|
||||
del: del,
|
||||
count: count,
|
||||
|
||||
getMembers: getMembers,
|
||||
addMember: addMember,
|
||||
removeMember: removeMember,
|
||||
isMember: isMember,
|
||||
|
||||
getGroups: getGroups,
|
||||
setGroups: setGroups,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
|
||||
|
||||
function get(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ?', [ groupId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getWithMembers(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' WHERE groups.id = ? ' +
|
||||
' GROUP BY groups.id', [ groupId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
var result = results[0];
|
||||
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, name, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ id, name ];
|
||||
database.query('INSERT INTO groups (id, name) VALUES (?, ?)',
|
||||
data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// also cleanup the groupMembers table
|
||||
var queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function count(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
database.query('DELETE FROM groupMembers', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('DELETE FROM groups WHERE id != ?', [ 'admin' ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getMembers(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
|
||||
|
||||
callback(error, result.map(function (r) { return r.userId; }));
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
|
||||
|
||||
callback(error, result.map(function (r) { return r.groupId; }));
|
||||
});
|
||||
}
|
||||
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({ query: 'DELETE from groupMembers WHERE userId = ?', args: [ userId ] });
|
||||
groupIds.forEach(function (gid) {
|
||||
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, userId ] });
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function addMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function removeMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function isMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result.length !== 0);
|
||||
});
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
/* jshint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
GroupError: GroupError,
|
||||
|
||||
create: create,
|
||||
remove: remove,
|
||||
get: get,
|
||||
getWithMembers: getWithMembers,
|
||||
getAll: getAll,
|
||||
|
||||
getMembers: getMembers,
|
||||
addMember: addMember,
|
||||
removeMember: removeMember,
|
||||
isMember: isMember,
|
||||
|
||||
getGroups: getGroups,
|
||||
setGroups: setGroups,
|
||||
|
||||
ADMIN_GROUP_ID: 'admin' // see db migration code and groupdb._clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
util = require('util');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
function GroupError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(GroupError, Error);
|
||||
GroupError.INTERNAL_ERROR = 'Internal Error';
|
||||
GroupError.ALREADY_EXISTS = 'Already Exists';
|
||||
GroupError.NOT_FOUND = 'Not Found';
|
||||
GroupError.BAD_NAME = 'Bad name';
|
||||
GroupError.NOT_EMPTY = 'Not Empty';
|
||||
GroupError.NOT_ALLOWED = 'Not Allowed';
|
||||
|
||||
function validateGroupname(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
var RESERVED = [ 'admins', 'users' ]; // ldap code uses 'users' pseudo group
|
||||
|
||||
if (name.length <= 2) return new GroupError(GroupError.BAD_NAME, 'name must be atleast 2 chars');
|
||||
if (name.length >= 200) return new GroupError(GroupError.BAD_NAME, 'name too long');
|
||||
|
||||
if (!/^[A-Za-z0-9_-]*$/.test(name)) return new GroupError(GroupError.BAD_NAME, 'name can only have A-Za-z0-9_-');
|
||||
|
||||
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_NAME, 'name is reserved');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function create(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateGroupname(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
groupdb.add(name /* id */, name, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupError(GroupError.ALREADY_EXISTS));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { id: name, name: name });
|
||||
});
|
||||
}
|
||||
|
||||
function remove(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// never allow admin group to be deleted
|
||||
if (id === exports.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
|
||||
|
||||
groupdb.del(id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.get(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getWithMembers(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getWithMembers(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getAll(function (error, result) {
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getMembers(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getMembers(groupId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getGroups(userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.setGroups(userId, groupIds, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function addMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.addMember(groupId, userId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function removeMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.removeMember(groupId, userId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function isMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.isMember(groupId, userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
+30
-3
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
apps = require('./apps.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
user = require('./user.js'),
|
||||
@@ -28,6 +29,16 @@ var gLogger = {
|
||||
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
|
||||
|
||||
function getAppByRequest(req, callback) {
|
||||
var sourceIp = req.connection.ldap.id.split(':')[0];
|
||||
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
|
||||
apps.getByIpAddress(sourceIp, function (error, app) {
|
||||
// we currently allow access in case we can't find the source app
|
||||
callback(null, app || null);
|
||||
});
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -36,7 +47,7 @@ function start(callback) {
|
||||
gServer.search('ou=users,dc=cloudron', function (req, res, next) {
|
||||
debug('ldap user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
|
||||
|
||||
user.list(function (error, result){
|
||||
user.list(function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// send user objects
|
||||
@@ -119,12 +130,28 @@ function start(callback) {
|
||||
var commonName = req.dn.rdns[0][Object.keys(req.dn.rdns[0])[0]];
|
||||
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
user.verify(commonName, req.credentials || '', function (error, result) {
|
||||
// TODO this should be done after we verified the app has access to avoid leakage of user existence
|
||||
user.verify(commonName, req.credentials || '', function (error, userObject) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error));
|
||||
|
||||
res.end();
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return next(error);
|
||||
|
||||
if (!app) return res.end();
|
||||
|
||||
debug('no app found for this container, allow access');
|
||||
|
||||
apps.hasAccessTo(app, userObject, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
Dear Admin,
|
||||
|
||||
A new version of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
Changes:
|
||||
<%= updateInfo.manifest.changelog %>
|
||||
|
||||
Thank you,
|
||||
your Cloudron
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear Admin,
|
||||
|
||||
A new version of Cloudron <%= fqdn %> is available!
|
||||
Version <%= newBoxVersion %> of Cloudron <%= fqdn %> is now available!
|
||||
|
||||
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= user.username %>,
|
||||
|
||||
Welcome to my Cloudron <%= fqdn %>!
|
||||
Welcome to our Cloudron <%= fqdn %>!
|
||||
|
||||
The Cloudron is our own Smart Server. You can read more about it
|
||||
at https://www.cloudron.io.
|
||||
|
||||
+15
-8
@@ -22,7 +22,8 @@ exports = module.exports = {
|
||||
|
||||
FEEDBACK_TYPE_FEEDBACK: 'feedback',
|
||||
FEEDBACK_TYPE_TICKET: 'ticket',
|
||||
FEEDBACK_TYPE_APP: 'app',
|
||||
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
|
||||
FEEDBACK_TYPE_APP_ERROR: 'app_error',
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
@@ -176,8 +177,8 @@ function sendMails(queue) {
|
||||
function enqueue(mailOptions) {
|
||||
assert.strictEqual(typeof mailOptions, 'object');
|
||||
|
||||
if (!mailOptions.from) console.error('from is missing');
|
||||
if (!mailOptions.to) console.error('to is missing');
|
||||
if (!mailOptions.from) console.error('sender address is missing');
|
||||
if (!mailOptions.to) console.error('recipient address is missing');
|
||||
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
@@ -280,12 +281,13 @@ function userRemoved(username) {
|
||||
mailUserEventToAdmins({ username: username }, 'was removed');
|
||||
}
|
||||
|
||||
function adminChanged(user) {
|
||||
function adminChanged(user, admin) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
|
||||
debug('Sending mail for adminChanged');
|
||||
|
||||
mailUserEventToAdmins(user, user.admin ? 'is now an admin' : 'is no more an admin');
|
||||
mailUserEventToAdmins(user, admin ? 'is now an admin' : 'is no more an admin');
|
||||
}
|
||||
|
||||
function passwordReset(user) {
|
||||
@@ -353,7 +355,7 @@ function appUpdateAvailable(app, updateInfo) {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, format: 'text' })
|
||||
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
@@ -395,7 +397,10 @@ function sendFeedback(user, type, subject, description) {
|
||||
assert.strictEqual(typeof subject, 'string');
|
||||
assert.strictEqual(typeof description, 'string');
|
||||
|
||||
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
|
||||
assert(type === exports.FEEDBACK_TYPE_TICKET ||
|
||||
type === exports.FEEDBACK_TYPE_FEEDBACK ||
|
||||
type === exports.FEEDBACK_TYPE_APP_MISSING ||
|
||||
type === exports.FEEDBACK_TYPE_APP_ERROR);
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
@@ -411,6 +416,8 @@ function _getMailQueue() {
|
||||
return gMailQueue;
|
||||
}
|
||||
|
||||
function _clearMailQueue() {
|
||||
function _clearMailQueue(callback) {
|
||||
gMailQueue = [];
|
||||
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
+3
-2
@@ -43,14 +43,15 @@ function configureAdmin(certFilePath, keyFilePath, callback) {
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function configureApp(app, certFilePath, keyFilePath, callback) {
|
||||
function configureApp(app, oauthProxy, certFilePath, keyFilePath, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof oauthProxy, 'boolean');
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof keyFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var sourceDir = path.resolve(__dirname, '..');
|
||||
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
|
||||
var endpoint = oauthProxy ? 'oauthproxy' : 'app';
|
||||
var vhost = config.appFqdn(app.location);
|
||||
|
||||
var data = {
|
||||
|
||||
@@ -13,7 +13,7 @@ app.controller('Controller', [function () {}]);
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<h1>Hello <%= user.username %> create a password</h1>
|
||||
<h1>Hello <%= user.username %>, set a password</h1>
|
||||
</center>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ function authenticate(req, res, next) {
|
||||
|
||||
clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Unkonwn OAuth client.', error);
|
||||
console.error('Unknown OAuth client.', error);
|
||||
return res.send(500, 'Unknown OAuth client.');
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -44,12 +44,12 @@ function removeInternalAppFields(app) {
|
||||
health: app.health,
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
oauthProxy: app.oauthProxy,
|
||||
lastBackupId: app.lastBackupId,
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
iconUrl: app.iconUrl,
|
||||
fqdn: app.fqdn
|
||||
fqdn: app.fqdn,
|
||||
memoryLimit: app.memoryLimit
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,7 +76,10 @@ function getAppBySubdomain(req, res, next) {
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
var func = req.user.admin ? apps.getAll : apps.getAllByUser.bind(null, req.user);
|
||||
func(function (error, allApps) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
allApps = allApps.map(removeInternalAppFields);
|
||||
@@ -116,19 +119,19 @@ function installApp(req, res, next) {
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
|
||||
// allow tests to provide an appId for testing
|
||||
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
|
||||
|
||||
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
|
||||
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j memoryLimit:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.memoryLimit, data.manifest);
|
||||
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, data.cert || null, data.key || null, function (error) {
|
||||
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
@@ -160,15 +163,15 @@ function configureApp(req, res, next) {
|
||||
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
|
||||
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
|
||||
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
|
||||
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j oauthProxy:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.oauthProxy);
|
||||
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j memoryLimit:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.memoryLimit);
|
||||
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.cert || null, data.key || null, function (error) {
|
||||
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
|
||||
+4
-16
@@ -10,7 +10,6 @@ exports = module.exports = {
|
||||
getProgress: getProgress,
|
||||
getConfig: getConfig,
|
||||
update: update,
|
||||
migrate: migrate,
|
||||
feedback: feedback
|
||||
};
|
||||
|
||||
@@ -129,24 +128,13 @@ function update(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function migrate(req, res, next) {
|
||||
if (typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if (typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
debug('Migration requested', req.body.size, req.body.region);
|
||||
|
||||
cloudron.migrate(req.body.size, req.body.region, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function feedback(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app"'));
|
||||
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_TICKET &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app_missing" or "app_error"'));
|
||||
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
|
||||
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
list: list,
|
||||
create: create,
|
||||
remove: remove
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
groups = require('../groups.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
groups = require('../groups.js'),
|
||||
GroupError = groups.GroupError;
|
||||
|
||||
function create(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
|
||||
|
||||
groups.create(req.body.name, function (error, group) {
|
||||
if (error && error.reason === GroupError.BAD_NAME) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === GroupError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
var groupInfo = {
|
||||
id: group.id,
|
||||
name: group.name
|
||||
};
|
||||
|
||||
next(new HttpSuccess(201, groupInfo));
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.groupId, 'string');
|
||||
|
||||
groups.getWithMembers(req.params.groupId, function (error, result) {
|
||||
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'No such group'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
});
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
groups.getAll(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { groups: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function remove(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.groupId, 'string');
|
||||
|
||||
groups.remove(req.params.groupId, function (error) {
|
||||
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'Group not found'));
|
||||
if (error && error.reason === GroupError.NOT_ALLOWED) return next(new HttpError(409, 'Group deletion not allowed'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
+4
-4
@@ -2,14 +2,14 @@
|
||||
|
||||
exports = module.exports = {
|
||||
apps: require('./apps.js'),
|
||||
backups: require('./backups.js'),
|
||||
clients: require('./clients.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
developer: require('./developer.js'),
|
||||
graphs: require('./graphs.js'),
|
||||
groups: require('./groups.js'),
|
||||
internal: require('./internal.js'),
|
||||
oauth2: require('./oauth2.js'),
|
||||
settings: require('./settings.js'),
|
||||
clients: require('./clients.js'),
|
||||
backups: require('./backups.js'),
|
||||
internal: require('./internal.js'),
|
||||
user: require('./user.js')
|
||||
};
|
||||
|
||||
|
||||
@@ -375,14 +375,17 @@ var authorization = [
|
||||
|
||||
if (type === clientdb.TYPE_ADMIN) return next();
|
||||
if (type === clientdb.TYPE_EXTERNAL) return next();
|
||||
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unkonwn OAuth client.');
|
||||
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
|
||||
|
||||
appdb.get(req.oauth2.client.appId, function (error, appObject) {
|
||||
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
|
||||
|
||||
if (!apps.hasAccessTo(appObject, req.oauth2.user)) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
|
||||
apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
|
||||
if (error) return sendError(req, res, 'Internal error');
|
||||
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
|
||||
|
||||
next();
|
||||
next();
|
||||
});
|
||||
});
|
||||
},
|
||||
gServer.decision({ loadTransaction: false })
|
||||
|
||||
+1304
-1310
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
userdb._clear,
|
||||
database._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
@@ -51,7 +51,7 @@ function setup(done) {
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
|
||||
@@ -24,8 +24,8 @@ var token = null; // authentication token
|
||||
var server;
|
||||
function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
config.set('version', '0.5.0');
|
||||
config.set('fqdn', 'localhost');
|
||||
server.start(done);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
config._reset();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
@@ -258,181 +260,6 @@ describe('Cloudron', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('migrate', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function setupBackupConfig(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
|
||||
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing size', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong size type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 4, region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with missing region', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong region type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 4, password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when in wrong state', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope1 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.size && body.region && body.restoreKey;
|
||||
}).reply(409, {});
|
||||
|
||||
injectShellMock();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.size && body.region && body.restoreKey;
|
||||
}).reply(202, {});
|
||||
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
injectShellMock();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedback', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var appdb = require('../../appdb.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
groups = require('../../groups.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
nock = require('nock'),
|
||||
userdb = require('../../userdb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1337', EMAIL_1 ='happy@me.com';
|
||||
var token, token_1 = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
database._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
token_1 = tokendb.generateToken();
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback);
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Groups API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('list', function () {
|
||||
it('cannot get groups without token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/groups')
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get groups as normal user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/groups')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get groups', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/groups')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.groups).to.be.an(Array);
|
||||
expect(res.body.groups.length).to.be(1);
|
||||
expect(res.body.groups[0].name).to.eql('admin');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', function () {
|
||||
it('fails due to mising token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/groups')
|
||||
.send({ name: 'externals'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/groups')
|
||||
.query({ access_token: token })
|
||||
.send({ name: 'externals'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for already exists', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/groups')
|
||||
.query({ access_token: token })
|
||||
.send({ name: 'externals'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', function () {
|
||||
it('cannot get non-existing group', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/groups/nope')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get existing group with normal user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/groups/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get existing group', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/groups/admin')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.name).to.be('admin');
|
||||
expect(result.body.userIds.length).to.be(1);
|
||||
expect(result.body.userIds[0]).to.be(USERNAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', function () {
|
||||
it('cannot remove without token', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/groups/externals')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can remove empty group', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/groups/externals')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot remove non-empty group', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/groups/admin')
|
||||
.send({ password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set groups', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
groups.create.bind(null, 'group0'),
|
||||
groups.create.bind(null, 'group1')
|
||||
], done);
|
||||
});
|
||||
|
||||
it('cannot add user to invalid group', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups')
|
||||
.query({ access_token: token })
|
||||
.send({ groupIds: [ 'admin', 'something' ]})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can add user to valid group', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups')
|
||||
.query({ access_token: token })
|
||||
.send({ groupIds: [ 'admin', 'group0', 'group1' ]})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can remove last user from admin', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups')
|
||||
.query({ access_token: token })
|
||||
.send({ groupIds: [ 'group0', 'group1' ]})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403); // not allowed
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -141,7 +141,6 @@ describe('OAuth2', function () {
|
||||
username: 'someusername',
|
||||
password: '@#45Strongpassword',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
@@ -156,7 +155,7 @@ describe('OAuth2', function () {
|
||||
location: 'test',
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
oauthProxy: true
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -166,7 +165,7 @@ describe('OAuth2', function () {
|
||||
location: 'test1',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar' ] },
|
||||
oauthProxy: true
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
@@ -176,7 +175,17 @@ describe('OAuth2', function () {
|
||||
location: 'test2',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ USER_0.id ] },
|
||||
oauthProxy: true
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_3 = {
|
||||
id: 'app3',
|
||||
appStoreId: '',
|
||||
manifest: { version: '0.1.0', addons: { } },
|
||||
location: 'test3',
|
||||
portBindings: {},
|
||||
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
// unknown app
|
||||
@@ -269,6 +278,16 @@ describe('OAuth2', function () {
|
||||
scope: 'profile'
|
||||
};
|
||||
|
||||
// app with accessRestriction allowing group
|
||||
var CLIENT_9 = {
|
||||
id: 'cid-client9',
|
||||
appId: APP_3.id,
|
||||
type: clientdb.TYPE_OAUTH,
|
||||
clientSecret: 'secret9',
|
||||
redirectURI: 'http://redirect9',
|
||||
scope: 'profile'
|
||||
};
|
||||
|
||||
// make csrf always succeed for testing
|
||||
oauth2.csrf = function (req, res, next) {
|
||||
req.csrfToken = function () { return hat(256); };
|
||||
@@ -288,11 +307,13 @@ describe('OAuth2', function () {
|
||||
clientdb.add.bind(null, CLIENT_6.id, CLIENT_6.appId, CLIENT_6.type, CLIENT_6.clientSecret, CLIENT_6.redirectURI, CLIENT_6.scope),
|
||||
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
|
||||
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy),
|
||||
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit),
|
||||
function (callback) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, true, '', false, function (error, userObject) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, userObject) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// update the global objects to reflect the new user id
|
||||
@@ -778,7 +799,7 @@ describe('OAuth2', function () {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(body.indexOf('Unkonwn OAuth client.')).to.not.equal(-1);
|
||||
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -802,6 +823,21 @@ describe('OAuth2', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for grant type code with accessRestriction (group)', function (done) { // USER_0 is not an admin
|
||||
startAuthorizationFlow(CLIENT_9, 'code', function (jar) {
|
||||
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_9.redirectURI + '&client_id=' + CLIENT_9.id + '&response_type=code';
|
||||
|
||||
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(body.indexOf('No access to this app.')).to.not.equal(-1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for grant type token due to accessRestriction', function (done) {
|
||||
startAuthorizationFlow(CLIENT_6, 'token', function (jar) {
|
||||
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_6.redirectURI + '&client_id=' + CLIENT_6.id + '&response_type=token';
|
||||
@@ -825,7 +861,7 @@ describe('OAuth2', function () {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
|
||||
expect(body.indexOf('Unkonwn OAuth client.')).to.not.equal(-1);
|
||||
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ function setup(done) {
|
||||
|
||||
function addApp(callback) {
|
||||
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
|
||||
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test0',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar', 'someone'] },
|
||||
oauthProxy: true
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -40,7 +40,7 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test1',
|
||||
portBindings: {},
|
||||
accessRestriction: { users: [ 'foobar', USERNAME, 'someone' ] },
|
||||
oauthProxy: true
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
@@ -50,7 +50,17 @@ describe('SimpleAuth API', function () {
|
||||
location: 'test2',
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
oauthProxy: true
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_3 = {
|
||||
id: 'app3',
|
||||
appStoreId: '',
|
||||
manifest: { version: '0.1.0', addons: { } },
|
||||
location: 'test3',
|
||||
portBindings: {},
|
||||
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var CLIENT_0 = {
|
||||
@@ -98,6 +108,15 @@ describe('SimpleAuth API', function () {
|
||||
scope: 'user,profile'
|
||||
};
|
||||
|
||||
var CLIENT_5 = {
|
||||
id: 'someclientid5',
|
||||
appId: APP_3.id,
|
||||
type: clientdb.TYPE_SIMPLE_AUTH,
|
||||
clientSecret: 'someclientsecret5',
|
||||
redirectURI: '',
|
||||
scope: 'user,profile'
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
@@ -128,9 +147,11 @@ describe('SimpleAuth API', function () {
|
||||
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
|
||||
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
|
||||
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy)
|
||||
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
|
||||
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit)
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -333,6 +354,37 @@ describe('SimpleAuth API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for app with group accessRestriction', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT_5.id,
|
||||
username: USERNAME,
|
||||
password: PASSWORD
|
||||
};
|
||||
|
||||
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
|
||||
.send(body)
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.accessToken).to.be.a('string');
|
||||
expect(result.body.user).to.be.an('object');
|
||||
expect(result.body.user.id).to.be.a('string');
|
||||
expect(result.body.user.username).to.be.a('string');
|
||||
expect(result.body.user.email).to.be.a('string');
|
||||
expect(result.body.user.admin).to.be.a('boolean');
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/profile')
|
||||
.query({ access_token: result.body.accessToken })
|
||||
.end(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.body).to.be.an('object');
|
||||
expect(result.body.username).to.eql(USERNAME);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for wrong client credentials', function (done) {
|
||||
var body = {
|
||||
clientId: CLIENT_4.id,
|
||||
|
||||
@@ -79,7 +79,7 @@ start_mongodb
|
||||
start_mail
|
||||
|
||||
echo -n "Waiting for addons to start"
|
||||
for i in {1..20}; do
|
||||
for i in {1..10}; do
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
+108
-79
@@ -10,6 +10,7 @@ var config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
expect = require('expect.js'),
|
||||
groups = require('../../groups.js'),
|
||||
mailer = require('../../mailer.js'),
|
||||
superagent = require('superagent'),
|
||||
nock = require('nock'),
|
||||
@@ -18,9 +19,9 @@ var config = require('../../config.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'Foobar?1337', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com', DISPLAY_NAME_0_NEW = 'New Name';
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com', DISPLAY_NAME_0_NEW = 'New Name';
|
||||
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'tao@zen.mac';
|
||||
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar';
|
||||
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar', EMAIL_2_NEW = 'happy@me.com';
|
||||
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@foo.bar';
|
||||
|
||||
var server;
|
||||
@@ -30,7 +31,11 @@ function setup(done) {
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
userdb._clear(done);
|
||||
userdb._clear(function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
groups.create('somegroupid', done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +105,7 @@ describe('User API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL })
|
||||
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
@@ -128,7 +133,7 @@ describe('User API', function () {
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.email).to.equal(EMAIL_0);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
|
||||
// stash for further use
|
||||
@@ -162,7 +167,7 @@ describe('User API', function () {
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.email).to.equal(EMAIL_0);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
done();
|
||||
});
|
||||
@@ -201,7 +206,7 @@ describe('User API', function () {
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL);
|
||||
expect(res.body.email).to.equal(EMAIL_0);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.displayName).to.be.a('string');
|
||||
expect(res.body.password).to.not.be.ok();
|
||||
@@ -272,53 +277,48 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('set second user as admin succeeds', function (done) {
|
||||
// TODO is USERNAME_1 in body and url redundant?
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/set_groups')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, admin: true })
|
||||
.send({ groupIds: [ groups.ADMIN_GROUP_ID ] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.admin).to.equal(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('remove first user from admins succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_0, admin: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove second user by first, now normal, user fails', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
it('remove itself from admins fails', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/set_groups')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.send({ groupIds: [ 'somegroupid' ] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('remove second user from admins and thus last admin fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_1, admin: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('reset first user as admin succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ username: USERNAME_0, admin: true })
|
||||
it('remove second user from admins succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/set_groups')
|
||||
.query({ access_token: token })
|
||||
.send({ groupIds: [ 'somegroupid' ] })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.admin).to.equal(false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -376,10 +376,19 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('second user userInfo', function (done) {
|
||||
it('second user userInfo fails for first user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('second user userInfo succeeds for second user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token_2 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.username).to.equal(USERNAME_2);
|
||||
expect(result.body.email).to.equal(EMAIL_2);
|
||||
@@ -392,16 +401,25 @@ describe('User API', function () {
|
||||
it('create user with same username should fail', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL, invite: false })
|
||||
.send({ username: USERNAME_2, email: EMAIL_0, invite: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('list users', function (done) {
|
||||
it('list users fails for normal user', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token_2 })
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('list users succeeds for admin', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, res) {
|
||||
expect(error).to.be(null);
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -432,7 +450,7 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user without giving a password', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
@@ -441,7 +459,7 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user with empty password', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.send({ password: '' })
|
||||
.end(function (err, res) {
|
||||
@@ -451,7 +469,7 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('admin cannot remove normal user with giving wrong password', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD + PASSWORD })
|
||||
.end(function (err, res) {
|
||||
@@ -461,7 +479,7 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('admin removes normal user', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
|
||||
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
@@ -483,29 +501,9 @@ describe('User API', function () {
|
||||
// Change email
|
||||
it('change email fails due to missing token', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to missing password', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email fails due to wrong password', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD+PASSWORD, email: EMAIL_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -513,37 +511,68 @@ describe('User API', function () {
|
||||
it('change email fails due to invalid email', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: 'foo@bar' })
|
||||
.send({ email: 'foo@bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email for other user fails', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token_2 })
|
||||
.send({ email: 'foobar@bar.baz' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.send({})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('change email succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, email: EMAIL_0_NEW })
|
||||
it('change email for own user succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token_2 })
|
||||
.send({ email: EMAIL_2_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token_2 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_2);
|
||||
expect(res.body.email).to.equal(EMAIL_2_NEW);
|
||||
expect(res.body.admin).to.equal(false);
|
||||
expect(res.body.displayName).to.equal('');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('change email as admin for other user succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token })
|
||||
.send({ email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL_0_NEW);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.username).to.equal(USERNAME_2);
|
||||
expect(res.body.email).to.equal(EMAIL_2);
|
||||
expect(res.body.admin).to.equal(false);
|
||||
expect(res.body.displayName).to.equal('');
|
||||
|
||||
done();
|
||||
@@ -554,7 +583,7 @@ describe('User API', function () {
|
||||
it('change displayName succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, displayName: DISPLAY_NAME_0_NEW })
|
||||
.send({ displayName: DISPLAY_NAME_0_NEW })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(204);
|
||||
|
||||
@@ -563,7 +592,7 @@ describe('User API', function () {
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.username).to.equal(USERNAME_0);
|
||||
expect(res.body.email).to.equal(EMAIL_0_NEW);
|
||||
expect(res.body.email).to.equal(EMAIL_0);
|
||||
expect(res.body.admin).to.be.ok();
|
||||
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
|
||||
|
||||
|
||||
+57
-34
@@ -9,15 +9,16 @@ exports = module.exports = {
|
||||
list: listUser,
|
||||
create: createUser,
|
||||
changePassword: changePassword,
|
||||
changeAdmin: changeAdmin,
|
||||
remove: removeUser,
|
||||
verifyPassword: verifyPassword,
|
||||
requireAdmin: requireAdmin,
|
||||
sendInvite: sendInvite
|
||||
sendInvite: sendInvite,
|
||||
setGroups: setGroups
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
generatePassword = require('../password.js').generate,
|
||||
groups = require('../groups.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
user = require('../user.js'),
|
||||
@@ -34,11 +35,18 @@ function profile(req, res, next) {
|
||||
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
|
||||
result.username = req.user.username;
|
||||
result.email = req.user.email;
|
||||
result.admin = req.user.admin;
|
||||
result.displayName = req.user.displayName;
|
||||
}
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
result.admin = isAdmin;
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
});
|
||||
} else {
|
||||
next(new HttpSuccess(200, result));
|
||||
}
|
||||
}
|
||||
|
||||
function createUser(req, res, next) {
|
||||
@@ -55,7 +63,7 @@ function createUser(req, res, next) {
|
||||
var sendInvite = req.body.invite;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
user.create(username, password, email, displayName, false /* admin */, req.user /* creator */, sendInvite, function (error, user) {
|
||||
user.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
|
||||
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
|
||||
@@ -84,6 +92,7 @@ function update(req, res, next) {
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
|
||||
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
|
||||
|
||||
user.get(req.params.userId, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
@@ -99,20 +108,6 @@ function update(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function changeAdmin(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'API call requires a username.'));
|
||||
if (typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'API call requires an admin setting.'));
|
||||
|
||||
user.changeAdmin(req.body.username, req.body.admin, function (error) {
|
||||
if (error && error.reason === UserError.NOT_ALLOWED) return next(new HttpError(403, 'Last admin'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
@@ -141,18 +136,25 @@ function listUser(req, res, next) {
|
||||
|
||||
function info(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
|
||||
|
||||
user.get(req.params.userId, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {
|
||||
id: result.id,
|
||||
username: result.username,
|
||||
email: result.email,
|
||||
admin: result.admin,
|
||||
displayName: result.displayName
|
||||
}));
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.params.userId, function (error, isAdmin) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {
|
||||
id: result.id,
|
||||
username: result.username,
|
||||
email: result.email,
|
||||
admin: isAdmin,
|
||||
displayName: result.displayName
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,15 +184,19 @@ function verifyPassword(req, res, next) {
|
||||
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
|
||||
|
||||
// Only allow admins or users, operating on themselves
|
||||
if (req.params.userId && !(req.user.id === req.params.userId || req.user.admin)) return next(new HttpError(403, 'Not allowed'));
|
||||
|
||||
user.verify(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next();
|
||||
// Only allow admins or users, operating on themselves
|
||||
if (req.params.userId && !(req.user.id === req.params.userId || isAdmin)) return next(new HttpError(403, 'Not allowed'));
|
||||
|
||||
user.verify(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,3 +221,20 @@ function sendInvite(req, res, next) {
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setGroups(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
if (!Array.isArray(req.body.groupIds)) return next(new HttpError(400, 'API call requires a groups array.'));
|
||||
|
||||
// this route is only allowed for admins, so req.user has to be an admin
|
||||
if (req.user.id === req.params.userId && req.body.groupIds.indexOf(groups.ADMIN_GROUP_ID) === -1) return next(new HttpError(403, 'Admin removing itself from admins is not allowed'));
|
||||
|
||||
user.setGroups(req.params.userId, req.body.groupIds, function (error) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'One or more groups not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
|
||||
if [ $# -lt 4 ]; then
|
||||
echo "Usage: backupapp.sh <appid> <url> <url> <key> [aws session token]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -21,8 +21,9 @@ readonly DATA_DIR="${HOME}/data"
|
||||
|
||||
app_id="$1"
|
||||
backup_url="$2"
|
||||
backup_key="$3"
|
||||
session_token="$4"
|
||||
backup_config_url="$3"
|
||||
backup_key="$4"
|
||||
session_token="$5"
|
||||
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
|
||||
readonly app_data_dir="${DATA_DIR}/${app_id}"
|
||||
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
|
||||
@@ -48,12 +49,34 @@ for try in `seq 1 5`; do
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
done
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed uploading backup tarball"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for try in `seq 1 5`; do
|
||||
echo "Uploading config.json to ${backup_config_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
|
||||
headers=("-H" "Content-Type:")
|
||||
|
||||
# federated tokens in CaaS case need session token
|
||||
if [ ! -z "$session_token" ]; then
|
||||
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
|
||||
fi
|
||||
|
||||
if cat "${app_data_snapshot}/config.json" \
|
||||
| curl --fail -X PUT ${headers[@]} --data @- "${backup_config_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
done
|
||||
|
||||
btrfs subvolume delete "${app_data_snapshot}"
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed"
|
||||
echo "Backup failed uploading config.json"
|
||||
exit 1
|
||||
else
|
||||
echo "Backup successful"
|
||||
fi
|
||||
|
||||
|
||||
+15
-7
@@ -40,7 +40,7 @@ function initializeExpressSync() {
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
|
||||
app.set('views', path.join(__dirname, 'oauth2views'));
|
||||
app.set('view options', { layout: true, debug: true });
|
||||
app.set('view options', { layout: true, debug: false });
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
@@ -93,7 +93,6 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
|
||||
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
|
||||
router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
|
||||
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
|
||||
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
|
||||
|
||||
// feedback
|
||||
@@ -101,15 +100,24 @@ function initializeExpressSync() {
|
||||
|
||||
router.get ('/api/v1/profile', profileScope, routes.user.profile);
|
||||
|
||||
router.get ('/api/v1/users', usersScope, routes.user.list);
|
||||
// user routes only for admins
|
||||
router.get ('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.list);
|
||||
router.post('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.create);
|
||||
router.get ('/api/v1/users/:userId', usersScope, routes.user.info);
|
||||
router.put ('/api/v1/users/:userId', usersScope, routes.user.verifyPassword, routes.user.update);
|
||||
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
|
||||
router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password
|
||||
router.post('/api/v1/users/:userId/admin', usersScope, routes.user.requireAdmin, routes.user.changeAdmin);
|
||||
router.put ('/api/v1/users/:userId/set_groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
|
||||
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
|
||||
|
||||
// user routes for admins and users operating on their own account
|
||||
router.get ('/api/v1/users/:userId', usersScope, routes.user.info);
|
||||
router.put ('/api/v1/users/:userId', usersScope, routes.user.update);
|
||||
router.post('/api/v1/users/:userId/password', usersScope, routes.user.changePassword); // changePassword verifies password
|
||||
|
||||
// Group management
|
||||
router.get ('/api/v1/groups', usersScope, routes.user.requireAdmin, routes.groups.list);
|
||||
router.post('/api/v1/groups', usersScope, routes.user.requireAdmin, routes.groups.create);
|
||||
router.get ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.groups.get);
|
||||
router.del ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.groups.remove);
|
||||
|
||||
// form based login routes used by oauth2 frame
|
||||
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
|
||||
router.post('/api/v1/session/login', csrf, routes.oauth2.login);
|
||||
|
||||
+11
-8
@@ -45,17 +45,20 @@ function loginLogic(clientId, username, password, callback) {
|
||||
apps.get(clientObject.appId, function (error, appObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!apps.hasAccessTo(appObject, userObject)) return callback(new AppsError(AppsError.ACCESS_DENIED));
|
||||
|
||||
var accessToken = tokendb.generateToken();
|
||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
|
||||
apps.hasAccessTo(appObject, userObject, function (error, access) {
|
||||
if (error) return callback(error);
|
||||
if (!access) return callback(new AppsError(AppsError.ACCESS_DENIED));
|
||||
|
||||
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
|
||||
var accessToken = tokendb.generateToken();
|
||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||
|
||||
callback(null, { accessToken: accessToken, user: userObject });
|
||||
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
|
||||
|
||||
callback(null, { accessToken: accessToken, user: userObject });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ function getSignedUploadUrl(backupConfig, filename, callback) {
|
||||
|
||||
var url = s3.getSignedUrl('putObject', params);
|
||||
|
||||
callback(null, { url : url, sessionToken: credentials.sessionToken });
|
||||
callback(null, { url: url, sessionToken: credentials.sessionToken });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ SubdomainError.EXTERNAL_ERROR = 'External error';
|
||||
SubdomainError.STILL_BUSY = 'Still busy';
|
||||
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
|
||||
SubdomainError.INTERNAL_ERROR = 'Missing credentials';
|
||||
SubdomainError.ACCESS_DENIED = 'Access denied';
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
function api(provider) {
|
||||
|
||||
+38
-8
@@ -4,7 +4,12 @@ exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
restartAppTask: restartAppTask
|
||||
stopAppTask: stopAppTask,
|
||||
startAppTask: startAppTask,
|
||||
restartAppTask: restartAppTask,
|
||||
|
||||
stopPendingTasks: stopPendingTasks,
|
||||
waitForPendingTasks: waitForPendingTasks
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
@@ -14,6 +19,7 @@ var appdb = require('./appdb.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
debug = require('debug')('box:taskmanager'),
|
||||
locker = require('./locker.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gActiveTasks = { };
|
||||
@@ -47,6 +53,24 @@ function uninitialize(callback) {
|
||||
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
|
||||
}
|
||||
|
||||
function stopPendingTasks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gPendingTasks = [];
|
||||
|
||||
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
|
||||
}
|
||||
|
||||
function waitForPendingTasks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function checkTasks() {
|
||||
if (Object.keys(gActiveTasks).length === 0 && gPendingTasks.length === 0) return callback();
|
||||
setTimeout(checkTasks, 1000);
|
||||
}
|
||||
|
||||
checkTasks();
|
||||
}
|
||||
|
||||
// resume app installs and uninstalls
|
||||
function resumeTasks(callback) {
|
||||
@@ -59,7 +83,7 @@ function resumeTasks(callback) {
|
||||
if (app.installationState === appdb.ISTATE_INSTALLED && app.runState === appdb.RSTATE_RUNNING) return;
|
||||
|
||||
debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState);
|
||||
startAppTask(app.id);
|
||||
startAppTask(app.id, NOOP_CALLBACK);
|
||||
});
|
||||
|
||||
callback(null);
|
||||
@@ -71,17 +95,21 @@ function startNextTask() {
|
||||
|
||||
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
|
||||
|
||||
startAppTask(gPendingTasks.shift());
|
||||
startAppTask(gPendingTasks.shift(), NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
function startAppTask(appId) {
|
||||
function startAppTask(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(!(appId in gActiveTasks));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (appId in gActiveTasks) {
|
||||
return callback(new Error(util.format('Task for %s is already active', appId)));
|
||||
}
|
||||
|
||||
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
|
||||
debug('Reached concurrency limit, queueing task for %s', appId);
|
||||
gPendingTasks.push(appId);
|
||||
return;
|
||||
return callback();
|
||||
}
|
||||
|
||||
var lockError = locker.recursiveLock(locker.OP_APPTASK);
|
||||
@@ -89,7 +117,7 @@ function startAppTask(appId) {
|
||||
if (lockError) {
|
||||
debug('Locked for another operation, queueing task for %s', appId);
|
||||
gPendingTasks.push(appId);
|
||||
return;
|
||||
return callback();
|
||||
}
|
||||
|
||||
// when parent process dies, apptask processes are killed because KillMode=control-group in systemd unit file
|
||||
@@ -107,6 +135,8 @@ function startAppTask(appId) {
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function stopAppTask(appId, callback) {
|
||||
@@ -116,7 +146,7 @@ function stopAppTask(appId, callback) {
|
||||
if (gActiveTasks[appId]) {
|
||||
debug('stopAppTask : Killing existing task of %s with pid %s', appId, gActiveTasks[appId].pid);
|
||||
gActiveTasks[appId].once('exit', function () { callback(); });
|
||||
gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler
|
||||
gActiveTasks[appId].kill('SIGKILL'); // this will end up calling the 'exit' handler
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+158
-19
@@ -13,15 +13,54 @@ var appdb = require('../appdb.js'),
|
||||
config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js');
|
||||
expect = require('expect.js'),
|
||||
groups = require('../groups.js'),
|
||||
hat = require('hat'),
|
||||
userdb = require('../userdb.js');
|
||||
|
||||
describe('Apps', function () {
|
||||
var ADMIN_0 = {
|
||||
id: 'admin123',
|
||||
username: 'admin123',
|
||||
password: 'secret',
|
||||
email: 'admin@me.com',
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var USER_0 = {
|
||||
id: 'uuid213',
|
||||
username: 'uuid213',
|
||||
password: 'secret',
|
||||
email: 'safe@me.com',
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
id: 'uuid2134',
|
||||
username: 'uuid2134',
|
||||
password: 'secret',
|
||||
email: 'safe1@me.com',
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var GROUP_0 = 'somegroup';
|
||||
var GROUP_1 = 'someothergroup';
|
||||
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'appStoreId-0',
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL,
|
||||
installationProgress: null,
|
||||
runState: null,
|
||||
location: 'some-location-0',
|
||||
manifest: {
|
||||
version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0',
|
||||
@@ -32,19 +71,51 @@ describe('Apps', function () {
|
||||
}
|
||||
}
|
||||
},
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { PORT: 5678 },
|
||||
healthy: null,
|
||||
accessRestriction: null,
|
||||
oauthProxy: false
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
appStoreId: 'appStoreId-1',
|
||||
location: 'some-location-1',
|
||||
manifest: {
|
||||
version: '0.1', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1',
|
||||
tcpPorts: {}
|
||||
},
|
||||
portBindings: null,
|
||||
accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0 ] },
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var APP_2 = {
|
||||
id: 'appid-2',
|
||||
appStoreId: 'appStoreId-2',
|
||||
location: 'some-location-2',
|
||||
manifest: {
|
||||
version: '0.1', dockerImage: 'docker/app2', healthCheckPath: '/', httpPort: 80, title: 'app2',
|
||||
tcpPorts: {}
|
||||
},
|
||||
portBindings: null,
|
||||
accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1 ] },
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy)
|
||||
userdb.add.bind(null, ADMIN_0.id, ADMIN_0),
|
||||
userdb.add.bind(null, USER_0.id, USER_0),
|
||||
userdb.add.bind(null, USER_1.id, USER_1),
|
||||
groups.create.bind(null, GROUP_0),
|
||||
groups.create.bind(null, GROUP_1),
|
||||
groups.addMember.bind(null, groups.ADMIN_GROUP_ID, ADMIN_0.id),
|
||||
groups.addMember.bind(null, GROUP_0, USER_1.id),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
|
||||
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit)
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -125,6 +196,7 @@ describe('Apps', function () {
|
||||
expect(app).to.be.ok();
|
||||
expect(app.iconUrl).to.be(null);
|
||||
expect(app.fqdn).to.eql(APP_0.location + '-' + config.fqdn());
|
||||
expect(app.memoryLimit).to.eql(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -182,24 +254,91 @@ describe('Apps', function () {
|
||||
});
|
||||
|
||||
describe('hasAccessTo', function () {
|
||||
it('returns true for unrestricted access', function () {
|
||||
expect(apps.hasAccessTo({ accessRestriction: null }, { id: 'someuser' })).to.equal(true);
|
||||
it('returns true for unrestricted access', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: null }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true for allowed user', function () {
|
||||
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'someuser' ] } }, { id: 'someuser' })).to.equal(true);
|
||||
it('returns true for allowed user', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: { users: [ 'someuser' ] } }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true for allowed user with multiple allowed', function () {
|
||||
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, { id: 'someuser' })).to.equal(true);
|
||||
it('returns true for allowed user with multiple allowed', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for not allowed user', function () {
|
||||
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo' ] } }, { id: 'someuser' })).to.equal(false);
|
||||
it('returns false for not allowed user', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: { users: [ 'foo' ] } }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for not allowed user with multiple allowed', function () {
|
||||
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'anotheruser' ] } }, { id: 'someuser' })).to.equal(false);
|
||||
it('returns false for not allowed user with multiple allowed', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'anotheruser' ] } }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for no group or user', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ ] } }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for invalid group or user', function (done) {
|
||||
apps.hasAccessTo({ accessRestriction: { users: [ ], groups: [ 'nop' ] } }, { id: 'someuser' }, function (error, access) {
|
||||
expect(error).to.be(null);
|
||||
expect(access).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllByUser', function () {
|
||||
it('succeeds for USER_0', function (done) {
|
||||
apps.getAllByUser(USER_0, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result.length).to.equal(2);
|
||||
expect(result[0].id).to.equal(APP_0.id);
|
||||
expect(result[1].id).to.equal(APP_2.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for USER_1', function (done) {
|
||||
apps.getAllByUser(USER_1, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result.length).to.equal(2);
|
||||
expect(result[0].id).to.equal(APP_0.id);
|
||||
expect(result[1].id).to.equal(APP_1.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with admin not being special', function (done) {
|
||||
apps.getAllByUser(ADMIN_0, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result.length).to.equal(1);
|
||||
expect(result[0].id).to.equal(APP_0.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,8 +60,8 @@ var APP = {
|
||||
httpPort: 4567,
|
||||
portBindings: null,
|
||||
accessRestriction: null,
|
||||
oauthProxy: false,
|
||||
dnsRecordId: 'someDnsRecordId'
|
||||
dnsRecordId: 'someDnsRecordId',
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
var awsHostedZones = {
|
||||
@@ -84,7 +84,7 @@ describe('apptask', function () {
|
||||
config.set('version', '0.5.0');
|
||||
async.series([
|
||||
database.initialize,
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy),
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.memoryLimit),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' })
|
||||
], done);
|
||||
|
||||
+25
-31
@@ -16,7 +16,8 @@ var appdb = require('../appdb.js'),
|
||||
async = require('async'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
userdb = require('../userdb.js');
|
||||
userdb = require('../userdb.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
describe('database', function () {
|
||||
before(function (done) {
|
||||
@@ -36,7 +37,6 @@ describe('database', function () {
|
||||
username: 'uuid213',
|
||||
password: 'secret',
|
||||
email: 'safe@me.com',
|
||||
admin: false,
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
@@ -44,12 +44,11 @@ describe('database', function () {
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var ADMIN_0 = {
|
||||
var USER_1 = {
|
||||
id: 'uuid456',
|
||||
username: 'uuid456',
|
||||
password: 'secret',
|
||||
email: 'safe2@me.com',
|
||||
admin: true,
|
||||
salt: 'tata',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
@@ -64,8 +63,8 @@ describe('database', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can add admin user', function (done) {
|
||||
userdb.add(ADMIN_0.id, ADMIN_0, function (error) {
|
||||
it('can add another user', function (done) {
|
||||
userdb.add(USER_1.id, USER_1, function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
done();
|
||||
});
|
||||
@@ -120,12 +119,16 @@ describe('database', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can get all', function (done) {
|
||||
userdb.getAll(function (error, all) {
|
||||
it('can get all with group ids', function (done) {
|
||||
userdb.getAllWithGroupIds(function (error, all) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(all.length).to.equal(2);
|
||||
expect(all[0]).to.eql(USER_0);
|
||||
expect(all[1]).to.eql(ADMIN_0);
|
||||
var user0Copy = _.extend({}, USER_0);
|
||||
user0Copy.groupIds = [ ];
|
||||
expect(all[0]).to.eql(user0Copy);
|
||||
var user1Copy = _.extend({}, USER_1);
|
||||
user1Copy.groupIds = [ ];
|
||||
expect(all[1]).to.eql(user1Copy);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -133,8 +136,7 @@ describe('database', function () {
|
||||
it('can get all admins', function (done) {
|
||||
userdb.getAllAdmins(function (error, all) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(all.length).to.equal(1);
|
||||
expect(all[0]).to.eql(ADMIN_0);
|
||||
expect(all.length).to.equal(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -147,15 +149,7 @@ describe('database', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('counts the admin users', function (done) {
|
||||
userdb.adminCount(function (error, count) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(count).to.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can update the user', function (done) {
|
||||
it('can update the user', function (done) {
|
||||
userdb.update(USER_0.id, { email: 'some@thing.com', displayName: 'Heiter' }, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
userdb.get(USER_0.id, function (error, user) {
|
||||
@@ -482,10 +476,10 @@ describe('database', function () {
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
oauthProxy: false,
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296
|
||||
};
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
@@ -501,10 +495,10 @@ describe('database', function () {
|
||||
portBindings: { },
|
||||
health: null,
|
||||
accessRestriction: { users: [ 'foobar' ] },
|
||||
oauthProxy: true,
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null
|
||||
oldConfig: null,
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
@@ -521,7 +515,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -545,7 +539,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add of same app fails', function (done) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
|
||||
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.memoryLimit, function (error) {
|
||||
expect(error).to.be.a(DatabaseError);
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
@@ -575,16 +569,16 @@ describe('database', function () {
|
||||
APP_0.location = 'some-other-location';
|
||||
APP_0.manifest.version = '0.2';
|
||||
APP_0.accessRestriction = '';
|
||||
APP_0.oauthProxy = true;
|
||||
APP_0.httpPort = 1337;
|
||||
APP_0.memoryLimit = 1337;
|
||||
|
||||
var data = {
|
||||
installationState: APP_0.installationState,
|
||||
location: APP_0.location,
|
||||
manifest: APP_0.manifest,
|
||||
accessRestriction: APP_0.accessRestriction,
|
||||
oauthProxy: APP_0.oauthProxy,
|
||||
httpPort: APP_0.httpPort
|
||||
httpPort: APP_0.httpPort,
|
||||
memoryLimit: APP_0.memoryLimit
|
||||
};
|
||||
|
||||
appdb.update(APP_0.id, data, function (error) {
|
||||
@@ -617,7 +611,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add second app succeeds', function (done) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_0.oauthProxy, function (error) {
|
||||
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_1.memoryLimit, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
groups = require('../groups.js'),
|
||||
GroupError = groups.GroupError,
|
||||
hat = require('hat'),
|
||||
userdb = require('../userdb.js');
|
||||
|
||||
var GROUP0_NAME = 'administrators',
|
||||
GROUP0_ID = GROUP0_NAME;
|
||||
|
||||
var GROUP1_NAME = 'externs',
|
||||
GROUP1_ID = GROUP1_NAME;
|
||||
|
||||
var USER_0 = {
|
||||
id: 'uuid213',
|
||||
username: 'uuid213',
|
||||
password: 'secret',
|
||||
email: 'safe@me.com',
|
||||
admin: false,
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
database._clear(done);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
describe('Groups', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('cannot create group - too small', function (done) {
|
||||
groups.create('a', function (error) {
|
||||
expect(error.reason).to.be(GroupError.BAD_NAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot create group - too big', function (done) {
|
||||
groups.create(new Array(256).join('a'), function (error) {
|
||||
expect(error.reason).to.be(GroupError.BAD_NAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot create group - bad name', function (done) {
|
||||
groups.create('bad:name', function (error) {
|
||||
expect(error.reason).to.be(GroupError.BAD_NAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot create group - reserved', function (done) {
|
||||
groups.create('users', function (error) {
|
||||
expect(error.reason).to.be(GroupError.BAD_NAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can create valid group', function (done) {
|
||||
groups.create(GROUP0_NAME, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot add existing group', function (done) {
|
||||
groups.create(GROUP0_NAME, function (error) {
|
||||
expect(error.reason).to.be(GroupError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get invalid group', function (done) {
|
||||
groups.get('sometrandom', function (error) {
|
||||
expect(error.reason).to.be(GroupError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get valid group', function (done) {
|
||||
groups.get(GROUP0_ID, function (error, group) {
|
||||
expect(error).to.be(null);
|
||||
expect(group.name).to.equal(GROUP0_NAME);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete invalid group', function (done) {
|
||||
groups.remove('random', function (error) {
|
||||
expect(error.reason).to.be(GroupError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete valid group', function (done) {
|
||||
groups.remove(GROUP0_ID, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Group membership', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
groups.create.bind(null, GROUP0_NAME),
|
||||
userdb.add.bind(null, USER_0.id, USER_0)
|
||||
], done);
|
||||
});
|
||||
after(cleanup);
|
||||
|
||||
it('cannot add non-existent user', function (done) {
|
||||
groups.addMember(GROUP0_ID, 'randomuser', function (error) {
|
||||
expect(error.reason).to.be(GroupError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot add non-existent group', function (done) {
|
||||
groups.addMember('randomgroup', USER_0.id, function (error) {
|
||||
expect(error.reason).to.be(GroupError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('isMember returns false', function (done) {
|
||||
groups.isMember(GROUP0_ID, USER_0.id, function (error, member) {
|
||||
expect(error).to.be(null);
|
||||
expect(member).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can add member', function (done) {
|
||||
groups.addMember(GROUP0_ID, USER_0.id, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('isMember returns true', function (done) {
|
||||
groups.isMember(GROUP0_ID, USER_0.id, function (error, member) {
|
||||
expect(error).to.be(null);
|
||||
expect(member).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get members', function (done) {
|
||||
groups.getMembers(GROUP0_ID, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.length).to.be(1);
|
||||
expect(result[0]).to.be(USER_0.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get members of non-existent group', function (done) {
|
||||
groups.getMembers('randomgroup', function (error, result) {
|
||||
expect(result.length).to.be(0); // currently, we cannot differentiate invalid groups and empty groups
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot remove non-existent user', function (done) {
|
||||
groups.removeMember(GROUP0_ID, 'randomuser', function (error) {
|
||||
expect(error.reason).to.be(GroupError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot remove non-existent group', function (done) {
|
||||
groups.removeMember('randomgroup', USER_0.id, function (error) {
|
||||
expect(error.reason).to.be(GroupError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can remove member', function (done) {
|
||||
groups.removeMember(GROUP0_ID, USER_0.id, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('has no members', function (done) {
|
||||
groups.getMembers(GROUP0_ID, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result.length).to.be(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can remove group with no members', function (done) {
|
||||
groups.remove(GROUP0_ID, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can remove group with member', function (done) {
|
||||
groups.create(GROUP0_NAME, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
groups.addMember(GROUP0_ID, USER_0.id, function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
groups.remove(GROUP0_ID, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set user groups', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
groups.create.bind(null, GROUP0_NAME),
|
||||
groups.create.bind(null, GROUP1_NAME),
|
||||
userdb.add.bind(null, USER_0.id, USER_0)
|
||||
], done);
|
||||
});
|
||||
after(cleanup);
|
||||
|
||||
it('can set user to single group', function (done) {
|
||||
groups.setGroups(USER_0.id, [ GROUP0_ID ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
groups.getGroups(USER_0.id, function (error, groupIds) {
|
||||
expect(error).to.be(null);
|
||||
expect(groupIds.length).to.be(1);
|
||||
expect(groupIds[0]).to.be(GROUP0_ID);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can set user to multiple groups', function (done) {
|
||||
groups.setGroups(USER_0.id, [ GROUP0_ID, GROUP1_ID ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
groups.getGroups(USER_0.id, function (error, groupIds) {
|
||||
expect(error).to.be(null);
|
||||
expect(groupIds.length).to.be(2);
|
||||
expect(groupIds[0]).to.be(GROUP0_ID);
|
||||
expect(groupIds[1]).to.be(GROUP1_ID);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Admin group', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
userdb.add.bind(null, USER_0.id, USER_0)
|
||||
], done);
|
||||
});
|
||||
after(cleanup);
|
||||
|
||||
it('cannot delete admin group ever', function (done) {
|
||||
groups.remove(groups.ADMIN_GROUP_ID, function (error) {
|
||||
expect(error.reason).to.equal(GroupError.NOT_ALLOWED);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+124
-19
@@ -6,37 +6,116 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
var appdb = require('../appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
user = require('../user.js'),
|
||||
database = require('../database.js'),
|
||||
config = require('../config.js'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
expect = require('expect.js'),
|
||||
http = require('http'),
|
||||
ldapServer = require('../ldap.js'),
|
||||
ldap = require('ldapjs');
|
||||
ldap = require('ldapjs'),
|
||||
user = require('../user.js');
|
||||
|
||||
// owner
|
||||
var USER_0 = {
|
||||
username: 'foobar0',
|
||||
password: 'Foobar?1234',
|
||||
email: 'foo0@bar.com',
|
||||
displayName: 'Bob bobson'
|
||||
username: 'username0',
|
||||
password: 'Username0pass?1234',
|
||||
email: 'user0@email.com',
|
||||
displayName: 'User 0'
|
||||
};
|
||||
|
||||
// normal user
|
||||
var USER_1 = {
|
||||
username: 'foobar1',
|
||||
password: 'Foobar?12345',
|
||||
email: 'foo1@bar.com',
|
||||
displayName: 'Jesus'
|
||||
username: 'username1',
|
||||
password: 'Username1pass?12345',
|
||||
email: 'user1@email.com',
|
||||
displayName: 'User 1'
|
||||
};
|
||||
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'appStoreId-0',
|
||||
dnsRecordId: null,
|
||||
installationState: appdb.ISTATE_INSTALLED,
|
||||
installationProgress: null,
|
||||
runState: appdb.RSTATE_RUNNING,
|
||||
location: 'some-location-0',
|
||||
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
|
||||
httpPort: null,
|
||||
containerId: 'someContainerId',
|
||||
portBindings: { port: 5678 },
|
||||
health: null,
|
||||
accessRestriction: null,
|
||||
lastBackupId: null,
|
||||
lastBackupConfig: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296
|
||||
};
|
||||
|
||||
var dockerProxy;
|
||||
|
||||
function startDockerProxy(interceptor, callback) {
|
||||
assert.strictEqual(typeof interceptor, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return http.createServer(interceptor).listen(5687, callback);
|
||||
}
|
||||
|
||||
function setup(done) {
|
||||
async.series([
|
||||
database.initialize.bind(null),
|
||||
database._clear.bind(null),
|
||||
ldapServer.start.bind(null),
|
||||
user.create.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, true, null, false),
|
||||
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, false, USER_0, false)
|
||||
], done);
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
|
||||
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName),
|
||||
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 })
|
||||
], function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
dockerProxy = startDockerProxy(function interceptor(req, res) {
|
||||
var answer = {};
|
||||
var status = 500;
|
||||
|
||||
if (req.method === 'GET' && req.url === '/networks') {
|
||||
answer = [{
|
||||
Name: "irrelevant"
|
||||
}, {
|
||||
Name: "bridge",
|
||||
Id: "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566",
|
||||
Scope: "local",
|
||||
Driver: "bridge",
|
||||
IPAM: {
|
||||
Driver: "default",
|
||||
Config: [{
|
||||
Subnet: "172.17.0.0/16"
|
||||
}]
|
||||
},
|
||||
"Containers": {
|
||||
someOtherContainerId: {
|
||||
"EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda",
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"IPv4Address": "127.0.0.2/16",
|
||||
"IPv6Address": ""
|
||||
},
|
||||
someContainerId: {
|
||||
"EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda",
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"IPv4Address": "127.0.0.1/16",
|
||||
"IPv6Address": ""
|
||||
}
|
||||
}
|
||||
}];
|
||||
status = 200;
|
||||
}
|
||||
|
||||
res.writeHead(status);
|
||||
res.write(JSON.stringify(answer));
|
||||
res.end();
|
||||
}, done);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
@@ -66,7 +145,7 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
it('succeeds without accessRestriction', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) {
|
||||
@@ -74,6 +153,32 @@ describe('Ldap', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with accessRestriction denied', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
appdb.update(APP_0.id, { accessRestriction: { users: [ USER_1.username ], groups: [] }}, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with accessRestriction allowed', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
appdb.update(APP_0.id, { accessRestriction: { users: [ USER_1.username, USER_0.username ], groups: [] }}, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search users', function () {
|
||||
@@ -81,7 +186,7 @@ describe('Ldap', function () {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '(&(l=Seattle)(email=*@foo.com))'
|
||||
filter: '(&(l=Seattle)(email=*@email.com))'
|
||||
};
|
||||
|
||||
client.search('o=example', opts, function (error, result) {
|
||||
@@ -127,7 +232,7 @@ describe('Ldap', function () {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '&(objectcategory=person)(username=foobar*)'
|
||||
filter: '&(objectcategory=person)(username=username*)'
|
||||
};
|
||||
|
||||
client.search('ou=users,dc=cloudron', opts, function (error, result) {
|
||||
|
||||
@@ -196,7 +196,7 @@ describe('updatechecker - checkAppUpdates', function () {
|
||||
portBindings: { PORT: 5678 },
|
||||
healthy: null,
|
||||
accessRestriction: null,
|
||||
oauthProxy: false
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -205,7 +205,7 @@ describe('updatechecker - checkAppUpdates', function () {
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy)
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit)
|
||||
], done);
|
||||
});
|
||||
|
||||
|
||||
+51
-55
@@ -6,8 +6,11 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('../database.js'),
|
||||
var async = require('async'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
groupdb = require('../groupdb.js'),
|
||||
groups = require('../groups.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
user = require('../user.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
@@ -19,38 +22,37 @@ var EMAIL = 'nobody@no.body';
|
||||
var EMAIL_NEW = 'nobodynew@no.body';
|
||||
var PASSWORD = 'sTrOnG#$34134';
|
||||
var NEW_PASSWORD = 'oTHER@#$235';
|
||||
var IS_ADMIN = true;
|
||||
var DISPLAY_NAME = 'Nobody cares';
|
||||
var DISPLAY_NAME_NEW = 'Somone cares';
|
||||
var userObject = null;
|
||||
|
||||
function cleanupUsers(done) {
|
||||
userdb._clear(function () {
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
});
|
||||
async.series([
|
||||
groupdb._clear,
|
||||
userdb._clear,
|
||||
mailer._clearMailQueue
|
||||
], done);
|
||||
}
|
||||
|
||||
function createUser(done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
function createOwner(done) {
|
||||
groups.create('admin', function () { // ignore error since it might already exist
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
userObject = result;
|
||||
userObject = result;
|
||||
|
||||
done();
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
done();
|
||||
});
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
mailer._clearMailQueue
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
@@ -77,7 +79,7 @@ describe('User', function () {
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to short password', function (done) {
|
||||
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -87,7 +89,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing upper case password', function (done) {
|
||||
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
user.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -97,7 +99,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing numerics in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -107,7 +109,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails due to missing special chars in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
user.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -117,14 +119,14 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('succeeds and attempts to send invite', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, true, function (error, result) {
|
||||
user.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).not.to.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
expect(result.username).to.equal(USERNAME);
|
||||
expect(result.email).to.equal(EMAIL);
|
||||
|
||||
// first user is owner, do not send mail to admins
|
||||
checkMails(1, done);
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +154,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because user exists', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.ALREADY_EXISTS);
|
||||
@@ -162,7 +164,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because password is empty', function (done) {
|
||||
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -184,7 +186,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
createUser(function (error) {
|
||||
createOwner(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
user.getOwner(function (error, owner) {
|
||||
@@ -197,7 +199,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('verify', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to non existing username', function (done) {
|
||||
@@ -241,7 +243,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('verifyWithEmail', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to non existing user', function (done) {
|
||||
@@ -285,7 +287,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('retrieving', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to non existing user', function (done) {
|
||||
@@ -311,7 +313,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('update', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to unknown userid', function (done) {
|
||||
@@ -374,17 +376,10 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin change', function () {
|
||||
before(createUser);
|
||||
xdescribe('admin change triggers mail', function () {
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails to remove admin flag of only admin', function (done) {
|
||||
user.changeAdmin(USERNAME, false, function (error) {
|
||||
expect(error).to.be.an('object');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('make second user admin succeeds', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
@@ -392,30 +387,30 @@ describe('User', function () {
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, false, { username: USERNAME, email: EMAIL } /* invitor */, false, function (error, result) {
|
||||
var invitor = { username: USERNAME, email: EMAIL };
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user.changeAdmin(user1.username, true, function (error) {
|
||||
groups.setGroups(user1.username, [ groups.ADMIN_GROUP_ID ], function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// one mail for user creation, one mail for admin change
|
||||
checkMails(2, done);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds to remove admin flag of first user', function (done) {
|
||||
user.changeAdmin(USERNAME, false, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
groups.setGroups(USERNAME, [], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get admins', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('succeeds for one admins', function (done) {
|
||||
@@ -434,11 +429,12 @@ describe('User', function () {
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, false, { username: USERNAME, email: EMAIL } /* invitor */, false, function (error, result) {
|
||||
var invitor = { username: USERNAME, email: EMAIL };
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user.changeAdmin(user1.username, true, function (error) {
|
||||
groups.setGroups(user1.username, [ groups.ADMIN_GROUP_ID ], function (error) {
|
||||
expect(error).to.eql(null);
|
||||
|
||||
user.getAllAdmins(function (error, admins) {
|
||||
@@ -448,7 +444,7 @@ describe('User', function () {
|
||||
expect(admins[1].username).to.equal(user1.username);
|
||||
|
||||
// one mail for user creation one mail for admin change
|
||||
checkMails(2, done);
|
||||
checkMails(1, done); // FIXME should be 2 for admin change
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -456,7 +452,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('password change', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to wrong arumgent count', function () {
|
||||
@@ -519,7 +515,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('resetPasswordByIdentifier', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails due to unkown email', function (done) {
|
||||
@@ -554,7 +550,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('send invite', function () {
|
||||
before(createUser);
|
||||
before(createOwner);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails for unknown user', function (done) {
|
||||
|
||||
+57
-48
@@ -12,7 +12,6 @@ exports = module.exports = {
|
||||
remove: removeUser,
|
||||
get: getUser,
|
||||
getByResetToken: getByResetToken,
|
||||
changeAdmin: changeAdmin,
|
||||
getAllAdmins: getAllAdmins,
|
||||
resetPasswordByIdentifier: resetPasswordByIdentifier,
|
||||
setPassword: setPassword,
|
||||
@@ -20,19 +19,22 @@ exports = module.exports = {
|
||||
update: updateUser,
|
||||
createOwner: createOwner,
|
||||
getOwner: getOwner,
|
||||
sendInvite: sendInvite
|
||||
sendInvite: sendInvite,
|
||||
setGroups: setGroups
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
groups = require('./groups.js'),
|
||||
GroupError = groups.GroupError,
|
||||
hat = require('hat'),
|
||||
userdb = require('./userdb.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
validatePassword = require('./password.js').validate,
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util'),
|
||||
validatePassword = require('./password.js').validate,
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -70,17 +72,6 @@ UserError.BAD_USERNAME = 'Bad username';
|
||||
UserError.BAD_EMAIL = 'Bad email';
|
||||
UserError.BAD_PASSWORD = 'Bad password';
|
||||
UserError.BAD_TOKEN = 'Bad token';
|
||||
UserError.NOT_ALLOWED = 'Not Allowed';
|
||||
|
||||
function listUsers(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.getAll(function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result.map(function (obj) { return _.pick(obj, 'id', 'username', 'email', 'admin', 'displayName'); }));
|
||||
});
|
||||
}
|
||||
|
||||
function validateUsername(username) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
@@ -113,15 +104,20 @@ function validateDisplayName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, displayName, admin, invitor, sendInvite, callback) {
|
||||
function createUser(username, password, email, displayName, options, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
assert(invitor || admin);
|
||||
assert.strictEqual(typeof sendInvite, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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 error = validateUsername(username);
|
||||
if (error) return callback(error);
|
||||
@@ -147,7 +143,6 @@ function createUser(username, password, email, displayName, admin, invitor, send
|
||||
username: username,
|
||||
email: email,
|
||||
password: new Buffer(derivedKey, 'binary').toString('hex'),
|
||||
admin: admin,
|
||||
salt: salt.toString('hex'),
|
||||
createdAt: now,
|
||||
modifiedAt: now,
|
||||
@@ -161,8 +156,7 @@ function createUser(username, password, email, displayName, admin, invitor, send
|
||||
|
||||
callback(null, user);
|
||||
|
||||
// WARNING do not send email for admins (this can only be the case for the owner, the first user creation during activation)
|
||||
if (!admin) mailer.userAdded(user, sendInvite);
|
||||
if (!owner) mailer.userAdded(user, sendInvite);
|
||||
if (sendInvite) mailer.sendInvite(user, invitor);
|
||||
});
|
||||
});
|
||||
@@ -225,6 +219,21 @@ function removeUser(userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listUsers(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.getAllWithGroupIds(function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
var allUsers = result.map(function (obj) {
|
||||
var u = _.pick(obj, 'id', 'username', 'email', 'displayName', 'groupIds');
|
||||
u.admin = u.groupIds.indexOf(groups.ADMIN_GROUP_ID) !== -1;
|
||||
return u;
|
||||
});
|
||||
return callback(null, allUsers);
|
||||
});
|
||||
}
|
||||
|
||||
function getUser(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -233,7 +242,13 @@ function getUser(userId, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
groups.getGroups(userId, function (error, groupIds) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
result.groupIds = groupIds;
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,30 +287,16 @@ function updateUser(userId, username, email, displayName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function changeAdmin(username, admin, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getUser(username, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
groups.setGroups(userId, groupIds, function (error) {
|
||||
if (error && error.reason === GroupError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND, 'One or more groups not found'));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
userdb.getAllAdmins(function (error, result) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
// protect from a system where there is no admin left
|
||||
if (result.length <= 1 && !admin) return callback(new UserError(UserError.NOT_ALLOWED, 'Only admin'));
|
||||
|
||||
user.admin = admin;
|
||||
|
||||
userdb.update(username, user, function (error) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
|
||||
mailer.adminChanged(user);
|
||||
});
|
||||
});
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -396,7 +397,15 @@ function createOwner(username, password, email, displayName, callback) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
|
||||
createUser(username, password, email, displayName, true /* admin */, null /* invitor */, false /* sendInvite */, callback);
|
||||
createUser(username, password, email, displayName, { owner: true }, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
groups.addMember(groups.ADMIN_GROUP_ID, user.id, function (error) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, user);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+26
-23
@@ -7,13 +7,12 @@ exports = module.exports = {
|
||||
getByAccessToken: getByAccessToken,
|
||||
getByResetToken: getByResetToken,
|
||||
getOwner: getOwner,
|
||||
getAll: getAll,
|
||||
getAllWithGroupIds: getAllWithGroupIds,
|
||||
getAllAdmins: getAllAdmins,
|
||||
add: add,
|
||||
del: del,
|
||||
update: update,
|
||||
count: count,
|
||||
adminCount: adminCount,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
@@ -21,9 +20,10 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:userdb'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
DatabaseError = require('./databaseerror'),
|
||||
groups = require('./groups.js');
|
||||
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'admin', 'resetToken', 'displayName' ].join(',');
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'resetToken', 'displayName' ].join(',');
|
||||
|
||||
function get(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
@@ -61,7 +61,8 @@ function getOwner(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// the first created user it the admin
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE admin=1 ORDER BY createdAt LIMIT 1', function (error, result) {
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId ORDER BY createdAt LIMIT 1',
|
||||
[ groups.ADMIN_GROUP_ID ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -83,12 +84,18 @@ function getByResetToken(resetToken, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
function getAllWithGroupIds(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users', function (error, results) {
|
||||
database.query('SELECT ' + USERS_FIELDS + ',GROUP_CONCAT(groupMembers.groupId) AS groupIds ' +
|
||||
' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' +
|
||||
' GROUP BY users.id', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(function (result) {
|
||||
result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ];
|
||||
});
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
@@ -96,7 +103,8 @@ function getAll(callback) {
|
||||
function getAllAdmins(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE admin=1', function (error, results) {
|
||||
database.query('SELECT ' + USERS_FIELDS + ' FROM users, groupMembers WHERE groupMembers.groupId = ? AND users.id = groupMembers.userId',
|
||||
[ groups.ADMIN_GROUP_ID ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
@@ -108,7 +116,6 @@ function add(userId, user, callback) {
|
||||
assert.strictEqual(typeof user.username, 'string');
|
||||
assert.strictEqual(typeof user.password, 'string');
|
||||
assert.strictEqual(typeof user.email, 'string');
|
||||
assert.strictEqual(typeof user.admin, 'boolean');
|
||||
assert.strictEqual(typeof user.salt, 'string');
|
||||
assert.strictEqual(typeof user.createdAt, 'string');
|
||||
assert.strictEqual(typeof user.modifiedAt, 'string');
|
||||
@@ -116,8 +123,8 @@ function add(userId, user, callback) {
|
||||
assert.strictEqual(typeof user.displayName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ userId, user.username, user.password, user.email, user.admin, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName ];
|
||||
database.query('INSERT INTO users (id, username, password, email, admin, salt, createdAt, modifiedAt, resetToken, displayName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
var data = [ userId, user.username, user.password, user.email, user.salt, user.createdAt, user.modifiedAt, user.resetToken, user.displayName ];
|
||||
database.query('INSERT INTO users (id, username, password, email, salt, createdAt, modifiedAt, resetToken, displayName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -130,9 +137,15 @@ function del(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM users WHERE id = ?', [ userId ], function (error, result) {
|
||||
// also cleanup the groupMembers table
|
||||
var queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ userId ] });
|
||||
queries.push({ query: 'DELETE FROM users WHERE id = ?', args: [ userId ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND, error));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
@@ -190,13 +203,3 @@ function count(callback) {
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
function adminCount(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT COUNT(*) AS total FROM users WHERE admin=1', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/*! =======================================================
|
||||
VERSION 6.0.12
|
||||
========================================================= */
|
||||
/*! =========================================================
|
||||
* bootstrap-slider.js
|
||||
*
|
||||
* Maintainers:
|
||||
* Kyle Kemp
|
||||
* - Twitter: @seiyria
|
||||
* - Github: seiyria
|
||||
* Rohit Kalkur
|
||||
* - Twitter: @Rovolutionary
|
||||
* - Github: rovolution
|
||||
*
|
||||
* =========================================================
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* ========================================================= */
|
||||
.slider {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
.slider.slider-horizontal {
|
||||
width: 210px;
|
||||
height: 20px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-track {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
margin-top: -5px;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-selection,
|
||||
.slider.slider-horizontal .slider-track-low,
|
||||
.slider.slider-horizontal .slider-track-high {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick,
|
||||
.slider.slider-horizontal .slider-handle {
|
||||
margin-left: -10px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick.triangle,
|
||||
.slider.slider-horizontal .slider-handle.triangle {
|
||||
border-width: 0 10px 10px 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom-color: #0480be;
|
||||
margin-top: 0;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-label-container {
|
||||
white-space: nowrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
|
||||
padding-top: 4px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
.slider.slider-vertical {
|
||||
height: 210px;
|
||||
width: 20px;
|
||||
}
|
||||
.slider.slider-vertical .slider-track {
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
margin-left: -5px;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-selection {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-track-low,
|
||||
.slider.slider-vertical .slider-track-high {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick,
|
||||
.slider.slider-vertical .slider-handle {
|
||||
margin-left: -5px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick.triangle,
|
||||
.slider.slider-vertical .slider-handle.triangle {
|
||||
border-width: 10px 0 10px 10px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border-left-color: #0480be;
|
||||
margin-left: 0;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick-label-container {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slider.slider-vertical .slider-tick-label-container .slider-tick-label {
|
||||
padding-left: 4px;
|
||||
}
|
||||
.slider.slider-disabled .slider-handle {
|
||||
background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
|
||||
background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
|
||||
background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
|
||||
}
|
||||
.slider.slider-disabled .slider-track {
|
||||
background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.slider input {
|
||||
display: none;
|
||||
}
|
||||
.slider .tooltip.top {
|
||||
margin-top: -36px;
|
||||
}
|
||||
.slider .tooltip-inner {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slider .hide {
|
||||
display: none;
|
||||
}
|
||||
.slider-track {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-selection {
|
||||
position: absolute;
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-selection.tick-slider-selection {
|
||||
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
|
||||
}
|
||||
.slider-track-low,
|
||||
.slider-track-high {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.slider-handle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #337ab7;
|
||||
background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
|
||||
background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
|
||||
background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
|
||||
filter: none;
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
.slider-handle.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider-handle.triangle {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-handle.custom {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-handle.custom::before {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
content: '\2605';
|
||||
color: #726204;
|
||||
}
|
||||
.slider-tick {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
|
||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
filter: none;
|
||||
opacity: 0.8;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
.slider-tick.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider-tick.triangle {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-tick.custom {
|
||||
background: transparent none;
|
||||
}
|
||||
.slider-tick.custom::before {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
content: '\2605';
|
||||
color: #726204;
|
||||
}
|
||||
.slider-tick.in-selection {
|
||||
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
|
||||
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
|
||||
opacity: 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+221
@@ -0,0 +1,221 @@
|
||||
angular.module('ui.bootstrap-slider', [])
|
||||
.directive('slider', ['$parse', '$timeout', '$rootScope', function ($parse, $timeout, $rootScope) {
|
||||
return {
|
||||
restrict: 'AE',
|
||||
replace: true,
|
||||
template: '<div><input class="slider-input" type="text" style="width:100%" /></div>',
|
||||
require: 'ngModel',
|
||||
scope: {
|
||||
max: "=",
|
||||
min: "=",
|
||||
step: "=",
|
||||
value: "=",
|
||||
ngModel: '=',
|
||||
ngDisabled: '=',
|
||||
range: '=',
|
||||
sliderid: '=',
|
||||
ticks: '=',
|
||||
ticksLabels: '=',
|
||||
ticksSnapBounds: '=',
|
||||
ticksPositions: '=',
|
||||
scale: '=',
|
||||
focus: '=',
|
||||
formatter: '&',
|
||||
onStartSlide: '&',
|
||||
onStopSlide: '&',
|
||||
onSlide: '&'
|
||||
},
|
||||
link: function ($scope, element, attrs, ngModelCtrl, $compile) {
|
||||
var ngModelDeregisterFn, ngDisabledDeregisterFn;
|
||||
|
||||
var slider = initSlider();
|
||||
|
||||
function initSlider() {
|
||||
var options = {};
|
||||
|
||||
function setOption(key, value, defaultValue) {
|
||||
options[key] = value || defaultValue;
|
||||
}
|
||||
|
||||
function setFloatOption(key, value, defaultValue) {
|
||||
options[key] = value || value === 0 ? parseFloat(value) : defaultValue;
|
||||
}
|
||||
|
||||
function setBooleanOption(key, value, defaultValue) {
|
||||
options[key] = value ? value + '' === 'true' : defaultValue;
|
||||
}
|
||||
|
||||
function getArrayOrValue(value) {
|
||||
return (angular.isString(value) && value.indexOf("[") === 0) ? angular.fromJson(value) : value;
|
||||
}
|
||||
|
||||
setOption('id', $scope.sliderid);
|
||||
setOption('orientation', attrs.orientation, 'horizontal');
|
||||
setOption('selection', attrs.selection, 'before');
|
||||
setOption('handle', attrs.handle, 'round');
|
||||
setOption('tooltip', attrs.sliderTooltip || attrs.tooltip, 'show');
|
||||
setOption('tooltip_position', attrs.sliderTooltipPosition, 'top');
|
||||
setOption('tooltipseparator', attrs.tooltipseparator, ':');
|
||||
setOption('ticks', $scope.ticks);
|
||||
setOption('ticks_labels', $scope.ticksLabels);
|
||||
setOption('ticks_snap_bounds', $scope.ticksSnapBounds);
|
||||
setOption('ticks_positions', $scope.ticksPositions);
|
||||
setOption('scale', $scope.scale, 'linear');
|
||||
setOption('focus', $scope.focus);
|
||||
|
||||
setFloatOption('min', $scope.min, 0);
|
||||
setFloatOption('max', $scope.max, 10);
|
||||
setFloatOption('step', $scope.step, 1);
|
||||
var strNbr = options.step + '';
|
||||
var dotPos = strNbr.search(/[^.,]*$/);
|
||||
var decimals = strNbr.substring(dotPos);
|
||||
setFloatOption('precision', attrs.precision, decimals.length);
|
||||
|
||||
setBooleanOption('tooltip_split', attrs.tooltipsplit, false);
|
||||
setBooleanOption('enabled', attrs.enabled, true);
|
||||
setBooleanOption('naturalarrowkeys', attrs.naturalarrowkeys, false);
|
||||
setBooleanOption('reversed', attrs.reversed, false);
|
||||
|
||||
setBooleanOption('range', $scope.range, false);
|
||||
if (options.range) {
|
||||
if (angular.isArray($scope.value)) {
|
||||
options.value = $scope.value;
|
||||
}
|
||||
else if (angular.isString($scope.value)) {
|
||||
options.value = getArrayOrValue($scope.value);
|
||||
if (!angular.isArray(options.value)) {
|
||||
var value = parseFloat($scope.value);
|
||||
if (isNaN(value)) value = 5;
|
||||
|
||||
if (value < $scope.min) {
|
||||
value = $scope.min;
|
||||
options.value = [value, options.max];
|
||||
}
|
||||
else if (value > $scope.max) {
|
||||
value = $scope.max;
|
||||
options.value = [options.min, value];
|
||||
}
|
||||
else {
|
||||
options.value = [options.min, options.max];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
options.value = [options.min, options.max]; // This is needed, because of value defined at $.fn.slider.defaults - default value 5 prevents creating range slider
|
||||
}
|
||||
$scope.ngModel = options.value; // needed, otherwise turns value into [null, ##]
|
||||
}
|
||||
else {
|
||||
setFloatOption('value', $scope.value, 5);
|
||||
}
|
||||
|
||||
if (attrs.formatter) {
|
||||
options.formatter = function(value) {
|
||||
return $scope.formatter({value: value});
|
||||
}
|
||||
}
|
||||
|
||||
// check if slider jQuery plugin exists
|
||||
if ('$' in window && $.fn.slider) {
|
||||
// adding methods to jQuery slider plugin prototype
|
||||
$.fn.slider.constructor.prototype.disable = function () {
|
||||
this.picker.off();
|
||||
};
|
||||
$.fn.slider.constructor.prototype.enable = function () {
|
||||
this.picker.on();
|
||||
};
|
||||
}
|
||||
|
||||
// destroy previous slider to reset all options
|
||||
if (element[0].__slider)
|
||||
element[0].__slider.destroy();
|
||||
|
||||
var slider = new Slider(element[0].getElementsByClassName('slider-input')[0], options);
|
||||
element[0].__slider = slider;
|
||||
|
||||
// everything that needs slider element
|
||||
var updateEvent = getArrayOrValue(attrs.updateevent);
|
||||
if (angular.isString(updateEvent)) {
|
||||
// if only single event name in string
|
||||
updateEvent = [updateEvent];
|
||||
}
|
||||
else {
|
||||
// default to slide event
|
||||
updateEvent = ['slide'];
|
||||
}
|
||||
angular.forEach(updateEvent, function (sliderEvent) {
|
||||
slider.on(sliderEvent, function (ev) {
|
||||
ngModelCtrl.$setViewValue(ev);
|
||||
});
|
||||
});
|
||||
slider.on('change', function (ev) {
|
||||
ngModelCtrl.$setViewValue(ev.newValue);
|
||||
});
|
||||
|
||||
|
||||
// Event listeners
|
||||
var sliderEvents = {
|
||||
slideStart: 'onStartSlide',
|
||||
slide: 'onSlide',
|
||||
slideStop: 'onStopSlide'
|
||||
};
|
||||
angular.forEach(sliderEvents, function (sliderEventAttr, sliderEvent) {
|
||||
var fn = $parse(attrs[sliderEventAttr]);
|
||||
slider.on(sliderEvent, function (ev) {
|
||||
if ($scope[sliderEventAttr]) {
|
||||
$scope.$apply(function () {
|
||||
fn($scope.$parent, { $event: ev, value: ev });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// deregister ngDisabled watcher to prevent memory leaks
|
||||
if (angular.isFunction(ngDisabledDeregisterFn)) {
|
||||
ngDisabledDeregisterFn();
|
||||
ngDisabledDeregisterFn = null;
|
||||
}
|
||||
|
||||
ngDisabledDeregisterFn = $scope.$watch('ngDisabled', function (value) {
|
||||
if (value) {
|
||||
slider.disable();
|
||||
}
|
||||
else {
|
||||
slider.enable();
|
||||
}
|
||||
});
|
||||
|
||||
// deregister ngModel watcher to prevent memory leaks
|
||||
if (angular.isFunction(ngModelDeregisterFn)) ngModelDeregisterFn();
|
||||
ngModelDeregisterFn = $scope.$watch('ngModel', function (value) {
|
||||
if($scope.range){
|
||||
slider.setValue(value);
|
||||
}else{
|
||||
slider.setValue(parseFloat(value));
|
||||
}
|
||||
slider.relayout();
|
||||
}, true);
|
||||
|
||||
return slider;
|
||||
}
|
||||
|
||||
|
||||
var watchers = ['min', 'max', 'step', 'range', 'scale', 'ticksLabels'];
|
||||
angular.forEach(watchers, function (prop) {
|
||||
$scope.$watch(prop, function () {
|
||||
slider = initSlider();
|
||||
});
|
||||
});
|
||||
|
||||
var globalEvents = ['relayout', 'refresh'];
|
||||
angular.forEach(globalEvents, function(event) {
|
||||
if(angular.isFunction(slider[event])) {
|
||||
$scope.$on('slider:' + event, function () {
|
||||
slider[event]();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
;
|
||||
@@ -25,7 +25,7 @@
|
||||
This app is currently not running. <a id="appLink" href="">Please retry later</a>.
|
||||
|
||||
<footer>
|
||||
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright © Cloudron 2014-15</span>
|
||||
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright © Cloudron 2014-16</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright © Cloudron 2014-15</span>
|
||||
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright © <a href="https://cloudron.io" target="_blank">Cloudron</a> 2014-16</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
<script src="3rdparty/js/showdown-1.1.0.min.js"></script>
|
||||
<script src="3rdparty/js/showdown-target-blank.min.js"></script>
|
||||
|
||||
<!-- Bootstrap slider -->
|
||||
<link rel="stylesheet" type="text/css" href="/3rdparty/bootstrap-slider/bootstrap-slider.min.css"/>
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/bootstrap-slider.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/slider.js"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script src="js/index.js"></script>
|
||||
|
||||
@@ -165,7 +170,7 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">Copyright © Cloudron 2014-15</span>
|
||||
<span class="text-muted">Copyright © <a href="https://cloudron.io" target="_blank">Cloudron</a> 2014-16</span>
|
||||
<span class="text-muted"> {{config.version}}</span>
|
||||
</footer>
|
||||
|
||||
|
||||
+54
-22
@@ -6,8 +6,8 @@
|
||||
angular.module('Application').service('Client', ['$http', 'md5', 'Notification', function ($http, md5, Notification) {
|
||||
var client = null;
|
||||
|
||||
// Keep this in sync with docs and docker.js
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 200;
|
||||
// Keep this in sync with docs and constants.js, docker.js
|
||||
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;
|
||||
|
||||
function ClientError(statusCode, messageOrObject) {
|
||||
Error.call(this);
|
||||
@@ -162,6 +162,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._userInfo.id = userInfo.id;
|
||||
this._userInfo.username = userInfo.username;
|
||||
this._userInfo.email = userInfo.email;
|
||||
this._userInfo.displayName = userInfo.displayName;
|
||||
this._userInfo.admin = !!userInfo.admin;
|
||||
this._userInfo.gravatar = 'https://www.gravatar.com/avatar/' + md5.createHash(userInfo.email.toLowerCase()) + '.jpg?s=24&d=mm';
|
||||
this._userInfo.gravatarHuge = 'https://www.gravatar.com/avatar/' + md5.createHash(userInfo.email.toLowerCase()) + '.jpg?s=128&d=mm';
|
||||
@@ -248,7 +249,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
location: config.location,
|
||||
portBindings: config.portBindings,
|
||||
accessRestriction: config.accessRestriction,
|
||||
oauthProxy: config.oauthProxy,
|
||||
cert: config.cert,
|
||||
key: config.key
|
||||
};
|
||||
@@ -292,9 +292,9 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
location: config.location,
|
||||
portBindings: config.portBindings,
|
||||
accessRestriction: config.accessRestriction,
|
||||
oauthProxy: config.oauthProxy,
|
||||
cert: config.cert,
|
||||
key: config.key
|
||||
key: config.key,
|
||||
memoryLimit: config.memoryLimit
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
|
||||
@@ -392,6 +392,49 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getGroups = function (callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/groups').success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data.groups);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setGroups = function (userId, groupIds, callback) {
|
||||
$http.put(client.apiOrigin + '/api/v1/users/' + userId + '/set_groups', { groupIds: groupIds }).success(function (data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getGroup = function (groupId, callback) {
|
||||
$http.get(client.apiOrigin + '/api/v1/groups/' + groupId).success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.createGroup = function (name, callback) {
|
||||
var data = {
|
||||
name: name
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/groups', data).success(function(data, status) {
|
||||
if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.removeGroup = function (groupId, password, callback) {
|
||||
var data = {
|
||||
password: password
|
||||
};
|
||||
|
||||
$http({ method: 'DELETE', url: client.apiOrigin + '/api/v1/groups/' + groupId, data: data, headers: { 'Content-Type': 'application/json' }}).success(function(data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getNonApprovedApps = function (callback) {
|
||||
if (!this._config.developerMode) return callback(null, []);
|
||||
|
||||
@@ -565,9 +608,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.updateUser = function (user, password, callback) {
|
||||
Client.prototype.updateUser = function (user, callback) {
|
||||
var data = {
|
||||
password: password,
|
||||
email: user.email,
|
||||
displayName: user.displayName
|
||||
};
|
||||
@@ -601,18 +643,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.changeEmail = function (email, password, callback) {
|
||||
var data = {
|
||||
password: password,
|
||||
email: email
|
||||
};
|
||||
|
||||
$http.put(client.apiOrigin + '/api/v1/users/' + this._userInfo.username, data).success(function(data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.refreshUserInfo = function (callback) {
|
||||
var that = this;
|
||||
|
||||
@@ -732,11 +762,13 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
};
|
||||
|
||||
Client.prototype.enoughResourcesAvailable = function (app) {
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT;
|
||||
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM+Swap
|
||||
var used = this.getInstalledApps().reduce(function (prev, cur) { return prev + (cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT); }, 0);
|
||||
var available = (this.getConfig().memory || 0) - used;
|
||||
var roundedMemory = Math.round(this.getConfig().memory / (1024 * 1024 * 1024)) * 1024 * 1024 * 1024; // round to nearest GB
|
||||
var totalMemory = roundedMemory * 1.2; // box-setup.sh creates equal amount of swap. 1.2 factor is arbitrary
|
||||
var available = (totalMemory || 0) - used;
|
||||
|
||||
return (available - needed) > 0;
|
||||
return (available - needed) >= 0;
|
||||
};
|
||||
|
||||
client = new Client();
|
||||
|
||||
@@ -9,7 +9,7 @@ if (search.accessToken) localStorage.token = search.accessToken;
|
||||
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'slick', 'ui-notification']);
|
||||
var app = angular.module('Application', ['ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'slick', 'ui-notification', 'ui.bootstrap-slider']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
@@ -144,6 +144,15 @@ app.filter('inProgressApps', function () {
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('ignoreAdminGroup', function () {
|
||||
return function (groups) {
|
||||
return groups.filter(function (group) {
|
||||
if (group.id) return group.id !== 'admin';
|
||||
return group !== 'admin';
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('applicationLink', function() {
|
||||
return function(app) {
|
||||
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY) {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</p>
|
||||
|
||||
<footer>
|
||||
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright © Cloudron 2014-15</span>
|
||||
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright © <a href="https://cloudron.io" target="_blank">Cloudron</a> 2014-16</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+37
-1
@@ -77,6 +77,20 @@ $table-border-color: transparent !default;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.btn-admin {
|
||||
color: white !important;
|
||||
background-color: $brand-danger !important;
|
||||
border-color: $brand-danger !important;
|
||||
}
|
||||
|
||||
.elide-table-cell {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Main classes
|
||||
// ----------------------------
|
||||
@@ -103,6 +117,11 @@ html {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content-large {
|
||||
max-width: 970px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.navbar-brand-icon {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
@@ -354,7 +373,7 @@ html {
|
||||
}
|
||||
|
||||
.card-large {
|
||||
max-width: 800px;
|
||||
max-width: 970px;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
@@ -578,6 +597,10 @@ footer a {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.hand {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// Upgrade
|
||||
@@ -899,3 +922,16 @@ $graphs-success-alt: lighten(#27CE65, 20%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Users view
|
||||
// ----------------------------
|
||||
|
||||
.group-badge {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -6,39 +6,38 @@
|
||||
<h4 class="modal-title">Change Your Password</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="passwordchange_form" class="form-signin" role="form" novalidate ng-submit="doChangePassword(passwordchange_form)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordchange_form.password.$dirty && passwordchange.error.password) || (passwordchange_form.password.$dirty && passwordchange_form.password.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangePassword">Current Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.password.$dirty && passwordchange.error.password) || (passwordchange_form.password.$dirty && passwordchange_form.password.$invalid)">
|
||||
<small ng-show="!passwordchange_form.password.$dirty && passwordchange.error.password">Wrong password</small>
|
||||
<small ng-show="passwordchange_form.password.$dirty && passwordchange_form.password.$error.required">A password is required</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.password" id="inputPasswordChangePassword" name="password" required autofocus>
|
||||
<form name="passwordChangeForm" role="form" novalidate ng-submit="passwordchange.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangePassword">Current Password</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.password.$dirty && passwordchange.error.password) || (passwordChangeForm.password.$dirty && passwordChangeForm.password.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.password.$dirty && passwordchange.error.password">Wrong password</small>
|
||||
<small ng-show="passwordChangeForm.password.$dirty && passwordChangeForm.password.$error.required">A password is required</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordchange_form.newPassword.$dirty && passwordchange.error.newPassword) || (passwordchange_form.newPassword.$dirty && passwordchange_form.newPassword.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPassword">New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.newPassword.$dirty && passwordchange.error.newPassword) || (passwordchange_form.newPassword.$dirty && passwordchange_form.newPassword.$invalid)">
|
||||
<small ng-show="!passwordchange_form.newPassword.$dirty && passwordchange.error.newPassword">{{ passwordchange.error.newPassword }}<br/><br/></small>
|
||||
<small ng-show=" passwordchange_form.newPassword.$dirty && passwordchange_form.newPassword.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.password" id="inputPasswordChangePassword" name="password" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPassword">New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid)">
|
||||
<small ng-show="!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword">{{ passwordchange.error.newPassword }}<br/><br/></small>
|
||||
<small ng-show=" passwordChangeForm.newPassword.$dirty && passwordChangeForm.newPassword.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordchange_form.newPassword.$dirty && passwordchange.error.newPassword) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$error.required) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPasswordRepeat">Repeat New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.newPassword.$dirty && passwordchange.error.newPassword) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$error.required) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat && passwordchange.newPasswordRepeat">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPasswordRepeat" id="inputPasswordChangeNewPasswordRepeat" name="newPasswordRepeat" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputPasswordChangeNewPassword" name="newPassword" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordChangeNewPasswordRepeat">Repeat New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordChangeForm.newPassword.$dirty && passwordchange.error.newPassword) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required) || (passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordChangeForm.newPasswordRepeat.$error.required">A password is required</small>
|
||||
<small ng-show="passwordChangeForm.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat && passwordchange.newPasswordRepeat">Passwords don't match</small>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="passwordchange_form.$invalid"/>
|
||||
</fieldset>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPasswordRepeat" id="inputPasswordChangeNewPasswordRepeat" name="newPasswordRepeat" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="passwordChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doChangePassword(passwordchange_form)" ng-disabled="passwordchange_form.$invalid || passwordchange.busy"><i class="fa fa-spinner fa-pulse" ng-show="passwordchange.busy"></i> Change</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="passwordchange.submit()" ng-disabled="passwordChangeForm.$invalid || passwordchange.busy"><i class="fa fa-spinner fa-pulse" ng-show="passwordchange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,31 +51,49 @@
|
||||
<h4 class="modal-title">Change Your Email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="emailchange_form" class="form-signin" role="form" novalidate ng-submit="doChangeEmail(emailchange_form)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailchange_form.email.$dirty && emailchange_form.email.$invalid) }">
|
||||
<label class="control-label" for="inputEmailChangeEmail">New Email Address</label>
|
||||
<div class="control-label" ng-show="(!emailchange_form.email.$dirty && emailchange.error.email) || (emailchange_form.email.$dirty && emailchange_form.email.$invalid)">
|
||||
<small ng-show="emailchange_form.email.$error.required">A valid email address is required</small>
|
||||
<small ng-show="(emailchange_form.email.$dirty && emailchange_form.email.$invalid) && !emailchange_form.email.$error.required">The Email address is not valid</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="emailchange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
<form name="emailChangeForm" role="form" novalidate ng-submit="emailchange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) }">
|
||||
<label class="control-label" for="inputEmailChangeEmail">New Email Address</label>
|
||||
<div class="control-label" ng-show="(!emailChangeForm.email.$dirty && emailchange.error.email) || (emailChangeForm.email.$dirty && emailChangeForm.email.$invalid)">
|
||||
<small ng-show="emailChangeForm.email.$error.required">A valid email address is required</small>
|
||||
<small ng-show="(emailChangeForm.email.$dirty && emailChangeForm.email.$invalid) && !emailChangeForm.email.$error.required">The Email address is not valid</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailchange_form.password.$dirty && emailchange_form.password.$invalid) || (!emailchange_form.password.$dirty && emailchange.error.password) }">
|
||||
<label class="control-label" for="inputEmailChangePassword">Password</label>
|
||||
<div class="control-label" ng-show="(emailchange_form.password.$dirty && emailchange_form.password.$invalid) || (!emailchange_form.password.$dirty && emailchange.error.password)">
|
||||
<small ng-show=" emailchange_form.password.$dirty && emailchange_form.password.$invalid">Password required</small>
|
||||
<small ng-show="!emailchange_form.password.$dirty && emailchange.error.password">Wrong password</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="emailchange.password" id="inputEmailChangePassword" name="password" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailchange_form.$invalid"/>
|
||||
</fieldset>
|
||||
<input type="email" class="form-control" ng-model="emailchange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doChangeEmail(emailchange_form)" ng-disabled="emailchange_form.$invalid || emailchange.busy"><i class="fa fa-spinner fa-pulse" ng-show="emailchange.busy"></i> Change</button>
|
||||
<button type="button" class="btn btn-success" ng-click="emailchange.submit()" ng-disabled="emailChangeForm.$invalid || emailchange.busy"><i class="fa fa-spinner fa-pulse" ng-show="emailchange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change displayName -->
|
||||
<div class="modal fade" id="displayNameChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change Your Display Name</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="displayNameChangeForm" role="form" novalidate ng-submit="displayNameChange.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) }">
|
||||
<label class="control-label">Display Name</label>
|
||||
<div class="control-label" ng-show="(!displayNameChangeForm.displayName.$dirty && displayNameChange.error.displayName) || (displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid)">
|
||||
<small ng-show="displayNameChangeForm.displayName.$error.required">A valid display name is required</small>
|
||||
<small ng-show="(displayNameChangeForm.displayName.$dirty && displayNameChangeForm.displayName.$invalid) && !displayNameChangeForm.displayName.$error.required">This display name is not valid</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="displayNameChange.displayName" name="displayName" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="displayNameChangeForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="displayNameChange.submit()" ng-disabled="displayNameChangeForm.$invalid || displayNameChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="displayNameChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,16 +117,20 @@
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Username</td>
|
||||
<td class="text-right" style="vertical-align: top;">{{ user.username }} </td>
|
||||
<td class="text-right" style="vertical-align: top;">{{ user.username }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Display Name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.displayName }} <a href="" ng-click="displayNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.email }} <a href="" ng-click="showChangeEmail(emailchange_form)"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.email }} <a href="" ng-click="emailchange.show()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-xs btn-danger" ng-click="showChangePassword(passwordchange_form)">Change Password</button>
|
||||
<button class="btn btn-outline btn-xs btn-danger" ng-click="passwordchange.show()">Change Password</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
+118
-83
@@ -12,113 +12,148 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
|
||||
error: {},
|
||||
password: '',
|
||||
newPassword: '',
|
||||
newPasswordRepeat: ''
|
||||
newPasswordRepeat: '',
|
||||
|
||||
reset: function () {
|
||||
$scope.passwordchange.error.password = null;
|
||||
$scope.passwordchange.error.newPassword = null;
|
||||
$scope.passwordchange.error.newPasswordRepeat = null;
|
||||
$scope.passwordchange.password = '';
|
||||
$scope.passwordchange.newPassword = '';
|
||||
$scope.passwordchange.newPasswordRepeat = '';
|
||||
|
||||
$scope.passwordChangeForm.$setUntouched();
|
||||
$scope.passwordChangeForm.$setPristine();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.passwordchange.reset();
|
||||
$('#passwordChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.passwordchange.error.password = null;
|
||||
$scope.passwordchange.error.newPassword = null;
|
||||
$scope.passwordchange.error.newPasswordRepeat = null;
|
||||
$scope.passwordchange.busy = true;
|
||||
|
||||
Client.changePassword($scope.passwordchange.password, $scope.passwordchange.newPassword, function (error) {
|
||||
$scope.passwordchange.busy = false;
|
||||
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.passwordchange.error.password = true;
|
||||
$scope.passwordchange.password = '';
|
||||
$('#inputPasswordChangePassword').focus();
|
||||
$scope.passwordChangeForm.password.$setPristine();
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.passwordchange.error.newPassword = error.message;
|
||||
$scope.passwordchange.newPassword = '';
|
||||
$scope.passwordchange.newPasswordRepeat = '';
|
||||
$scope.passwordChangeForm.newPassword.$setPristine();
|
||||
$scope.passwordChangeForm.newPasswordRepeat.$setPristine();
|
||||
$('#inputPasswordChangeNewPassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change password.', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.passwordchange.reset();
|
||||
$('#passwordChangeModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.emailchange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
email: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
function passwordChangeReset (form) {
|
||||
$scope.passwordchange.error.password = null;
|
||||
$scope.passwordchange.error.newPassword = null;
|
||||
$scope.passwordchange.error.newPasswordRepeat = null;
|
||||
$scope.passwordchange.password = '';
|
||||
$scope.passwordchange.newPassword = '';
|
||||
$scope.passwordchange.newPasswordRepeat = '';
|
||||
reset: function () {
|
||||
$scope.emailchange.busy = false;
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.email = '';
|
||||
|
||||
if (form) {
|
||||
form.$setPristine();
|
||||
form.$setUntouched();
|
||||
}
|
||||
}
|
||||
$scope.emailChangeForm.$setUntouched();
|
||||
$scope.emailChangeForm.$setPristine();
|
||||
},
|
||||
|
||||
function emailChangeReset (form) {
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.error.password = null;
|
||||
$scope.emailchange.email = '';
|
||||
$scope.emailchange.password = '';
|
||||
show: function () {
|
||||
$scope.emailchange.reset();
|
||||
$('#emailChangeModal').modal('show');
|
||||
},
|
||||
|
||||
if (form) {
|
||||
form.$setPristine();
|
||||
form.$setUntouched();
|
||||
}
|
||||
}
|
||||
submit: function () {
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.busy = true;
|
||||
|
||||
$scope.doChangePassword = function (form) {
|
||||
$scope.passwordchange.error.password = null;
|
||||
$scope.passwordchange.error.newPassword = null;
|
||||
$scope.passwordchange.error.newPasswordRepeat = null;
|
||||
$scope.passwordchange.busy = true;
|
||||
var user = {
|
||||
id: $scope.user.id,
|
||||
email: $scope.emailchange.email
|
||||
};
|
||||
|
||||
Client.changePassword($scope.passwordchange.password, $scope.passwordchange.newPassword, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.passwordchange.error.password = true;
|
||||
$scope.passwordchange.password = '';
|
||||
$('#inputPasswordChangePassword').focus();
|
||||
$scope.passwordchange_form.password.$setPristine();
|
||||
} else if (error.statusCode === 400) {
|
||||
$scope.passwordchange.error.newPassword = error.message;
|
||||
$scope.passwordchange.newPassword = '';
|
||||
$scope.passwordchange.newPasswordRepeat = '';
|
||||
$scope.passwordchange_form.newPassword.$setPristine();
|
||||
$scope.passwordchange_form.newPasswordRepeat.$setPristine();
|
||||
$('#inputPasswordChangeNewPassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change password.', error);
|
||||
}
|
||||
} else {
|
||||
passwordChangeReset(form);
|
||||
Client.updateUser(user, function (error) {
|
||||
$scope.emailchange.busy = false;
|
||||
|
||||
$('#passwordChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.passwordchange.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doChangeEmail = function (form) {
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.error.password = null;
|
||||
$scope.emailchange.busy = true;
|
||||
|
||||
Client.changeEmail($scope.emailchange.email, $scope.emailchange.password, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.emailchange.error.password = true;
|
||||
$scope.emailchange.password = '';
|
||||
$scope.emailchange_form.password.$setPristine();
|
||||
$('#inputEmailChangePassword').focus();
|
||||
} else {
|
||||
if (error) {
|
||||
console.error('Unable to change email.', error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
emailChangeReset(form);
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
|
||||
$scope.emailchange.reset();
|
||||
$('#emailChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.emailchange.busy = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showChangePassword = function (form) {
|
||||
passwordChangeReset(form);
|
||||
$scope.displayNameChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
displayName: '',
|
||||
|
||||
$('#passwordChangeModal').modal('show');
|
||||
};
|
||||
reset: function () {
|
||||
$scope.displayNameChange.busy = false;
|
||||
$scope.displayNameChange.error.displayName = null;
|
||||
$scope.displayNameChange.displayName = '';
|
||||
|
||||
$scope.showChangeEmail = function (form) {
|
||||
emailChangeReset(form);
|
||||
$scope.displayNameChangeForm.$setUntouched();
|
||||
$scope.displayNameChangeForm.$setPristine();
|
||||
},
|
||||
|
||||
$('#emailChangeModal').modal('show');
|
||||
show: function () {
|
||||
$scope.displayNameChange.reset();
|
||||
$scope.displayNameChange.displayName = $scope.user.displayName;
|
||||
$('#displayNameChangeModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.displayNameChange.error.displayName = null;
|
||||
$scope.displayNameChange.busy = true;
|
||||
|
||||
var user = {
|
||||
id: $scope.user.id,
|
||||
displayName: $scope.displayNameChange.displayName
|
||||
};
|
||||
|
||||
Client.updateUser(user, function (error) {
|
||||
$scope.displayNameChange.busy = false;
|
||||
|
||||
if (error) {
|
||||
console.error('Unable to change displayName.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
|
||||
$scope.displayNameChange.reset();
|
||||
$('#displayNameChangeModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeAccessTokens = function (client) {
|
||||
@@ -149,7 +184,7 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['passwordChangeModal', 'emailChangeModal'].forEach(function (id) {
|
||||
['passwordChangeModal', 'emailChangeModal', 'displayNameChangeModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0">
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.admin">
|
||||
<div class="col-lg-6 col-lg-offset-3" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> Your Cloudron does not have any apps installed yet!</h1>
|
||||
@@ -7,6 +7,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.admin">
|
||||
<div class="col-lg-6 col-lg-offset-3" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1>You don't have access to any apps on this Cloudron yet!</h1>
|
||||
<br/></br>
|
||||
<h3>Once you do, they will show up here.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal configure app -->
|
||||
<div class="modal fade" id="appConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -16,7 +25,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appConfigureForm" ng-submit="doConfigure()" autocomplete="off">
|
||||
<form role="form" name="appConfigureForm" ng-submit="doConfigure()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
|
||||
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
|
||||
@@ -43,13 +52,35 @@
|
||||
Access is granted to <b>{{appConfigure.app.accessRestriction.users[0]}}</b>.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Not sure if oauthproxy makes any sense with singleuser apps, it certainly looks strange in the UI, so we hide it for now -->
|
||||
<div class="form-group" ng-hide="appConfigure.app.manifest.singleUser">
|
||||
<label class="control-label" for="oauthProxy">Website Visibility</label>
|
||||
<select class="form-control" id="oauthProxy" ng-model="appConfigure.oauthProxy">
|
||||
<option value="">Visible to all</option>
|
||||
<option value="1">Visible only to Cloudron users</option>
|
||||
</select>
|
||||
<label class="control-label">Access control</label>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="">
|
||||
Every Cloudron user
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="restricted">
|
||||
Restrict to groups
|
||||
</label>
|
||||
</div>
|
||||
<div class="has-error" ng-show="appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid()">Select at least one group</div>
|
||||
<div>
|
||||
<div>
|
||||
<span ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<button class="btn btn-default" type="button" ng-disabled="appConfigure.accessRestrictionOption === ''" ng-click="appConfigureToggleGroup(group);" ng-class="{ 'btn-primary': (appConfigure.accessRestriction.groups && appConfigure.accessRestriction.groups.indexOf(group.id) !== -1) }">{{ group.name }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="true">
|
||||
<label class="control-label" for="memoryUsage">Maximum Memory Usage: <b>{{ appConfigure.memoryUsage / 1024 / 1024 }} MB</b></label>
|
||||
<br/>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="memoryUsage" ng-model="appConfigure.memoryUsage" step="33554432" tooltip="hide" ticks="memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
@@ -86,13 +117,13 @@
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="appConfigure.password" id="appConfigurePasswordInput" name="password" required>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || busy"/>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid())"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,7 +140,7 @@
|
||||
<p ng-show="appRestore.app.lastBackupId !== null">Restoring the app will lose all content generated since last backup of this app!</p>
|
||||
<p ng-show="appRestore.app.lastBackupId === null">This app was never backed up. Restoring the app will lose all content!</p>
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appRestoreForm" ng-submit="doRestore()" autocomplete="off">
|
||||
<form role="form" name="appRestoreForm" ng-submit="doRestore()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password) }">
|
||||
<label class="control-label" for="appRestorePasswordInput">Provide your password to confirm this action</label>
|
||||
<div class="control-label" ng-show="(appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password)">
|
||||
@@ -139,7 +170,7 @@
|
||||
<h4 class="modal-title">{{ appError.app.location }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>There was an error installing this app</p>
|
||||
<p><b>There was an error:</b></p>
|
||||
<p>{{appError.app.installationProgress}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -159,7 +190,7 @@
|
||||
<div class="modal-body">
|
||||
<p>Deleting the app will also remove all content generated within this app!</p>
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appUninstallForm" ng-submit="doUninstall()" autocomplete="off">
|
||||
<form role="form" name="appUninstallForm" ng-submit="doUninstall()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password) }">
|
||||
<label class="control-label" for="appUninstallPasswordInput">Provide your password to confirm this action</label>
|
||||
<div class="control-label" ng-show="(appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password)">
|
||||
@@ -189,8 +220,11 @@
|
||||
<h4 class="modal-title">Update {{ appUpdate.app.location }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
|
||||
<pre>{{ appUpdate.manifest.changelog }}</pre>
|
||||
<br/>
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appUpdateForm" ng-submit="doUpdate(appUpdateForm)" autocomplete="off">
|
||||
<form role="form" name="appUpdateForm" ng-submit="doUpdate(appUpdateForm)" autocomplete="off">
|
||||
<div ng-repeat="(env, info) in appUpdate.portBindingsInfo" ng-class="{ 'newPort': info.isNew }">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid }">
|
||||
@@ -246,12 +280,12 @@
|
||||
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="col-lg-12">
|
||||
<h1>Installed Applications</h1>
|
||||
<h1>Your Applications</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps">
|
||||
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps | orderBy:'location'">
|
||||
<div style="background-color: white;" class="highlight grid-item-content">
|
||||
<a ng-href="{{app | applicationLink}}" ng-click="(app | installError) === true && showError(app)" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
|
||||
+40
-10
@@ -1,6 +1,3 @@
|
||||
/* global ISTATES:false */
|
||||
/* global HSTATES:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$location', 'Client', 'AppStore', function ($scope, $location, Client, AppStore) {
|
||||
@@ -10,6 +7,15 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.groups = [];
|
||||
|
||||
$scope.memoryTicks = [
|
||||
256 * 1024 * 1024,
|
||||
512 * 1024 * 1024,
|
||||
1024 * 1024 * 1024,
|
||||
2048 * 1024 * 1024,
|
||||
4096 * 1024 * 1024
|
||||
];
|
||||
|
||||
$scope.appConfigure = {
|
||||
busy: false,
|
||||
@@ -20,11 +26,18 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
portBindingsInfo: {},
|
||||
oauthProxy: '',
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
keyFileName: '',
|
||||
memoryLimit: $scope.memoryTicks[0],
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appConfigure.accessRestriction;
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appUninstall = {
|
||||
@@ -62,11 +75,13 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.password = '';
|
||||
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
$scope.appConfigure.oauthProxy = '';
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.certificateFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.memoryLimit = $scope.memoryTicks[0];
|
||||
$scope.appConfigure.accessRestrictionOption = '';
|
||||
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
|
||||
|
||||
$scope.appConfigureForm.$setPristine();
|
||||
$scope.appConfigureForm.$setUntouched();
|
||||
@@ -126,14 +141,24 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
});
|
||||
};
|
||||
|
||||
$scope.appConfigureToggleGroup = function (group) {
|
||||
var groups = $scope.appConfigure.accessRestriction.groups;
|
||||
var pos = groups.indexOf(group.id);
|
||||
|
||||
if (pos === -1) groups.push(group.id);
|
||||
else groups.splice(pos, 1);
|
||||
};
|
||||
|
||||
$scope.showConfigure = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.location;
|
||||
$scope.appConfigure.oauthProxy = app.oauthProxy ? '1' : '';
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'restricted' : '';
|
||||
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
|
||||
$scope.appConfigure.memoryUsage = app.memoryUsage || 256;
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
@@ -166,10 +191,10 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
var data = {
|
||||
location: $scope.appConfigure.location || '',
|
||||
portBindings: finalPortBindings,
|
||||
oauthProxy: !!$scope.appConfigure.oauthProxy,
|
||||
accessRestriction: $scope.appConfigure.app.accessRestriction,
|
||||
accessRestriction: !$scope.appConfigure.accessRestrictionOption ? null : $scope.appConfigure.accessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
key: $scope.appConfigure.keyFile
|
||||
// memoryLimit: $scope.appConfigure.memoryLimit
|
||||
};
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, data, function (error) {
|
||||
@@ -386,6 +411,11 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) return console.error('Unable to get group listing.', error);
|
||||
$scope.groups = result;
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['appConfigureModal', 'appUninstallModal', 'appUpdateModal', 'appRestoreModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
|
||||
<h3 class="appstore-install-title" title="Version {{ appInstall.app.manifest.version }}">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ appInstall.app.manifest.author }}</span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">{{ appInstall.app.manifest.version }}</span>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">Website</a></span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
<form class="form-signin" role="form" name="appInstallForm" ng-submit="doInstall()" autocomplete="off">
|
||||
<form role="form" name="appInstallForm" ng-submit="appInstall.submit()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appInstall.error.other" ng-bind-html="appInstall.error.other"></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appInstallForm.location.$dirty && appInstallForm.location.$invalid) || (!appInstallForm.location.$dirty && appInstall.error.location) }">
|
||||
<label class="control-label" for="appInstallLocationInput">Location {{ appInstall.error.location }} </label>
|
||||
@@ -36,10 +36,32 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="appInstall.app.manifest.singleUser">
|
||||
<label class="control-label" for="accessRestriction">User</label>
|
||||
<p>This is a single user application.</p>
|
||||
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-options="user as user.username for user in users track by user.id" ng-required="appInstall.app.manifest.singleUser">
|
||||
</select>
|
||||
<label class="control-label">User</label>
|
||||
<select class="form-control" ng-model="appInstall.accessRestrictionSingleUser" ng-options="user as user.username for user in users track by user.id" ng-required="appInstall.app.manifest.singleUser"></select>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="appInstall.app.manifest.singleUser">
|
||||
<label class="control-label">Access control</label>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="">
|
||||
Every Cloudron user
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="restricted" ng-disabled="groups.length <= 1">
|
||||
Restrict to groups
|
||||
</label>
|
||||
</div>
|
||||
<div ng-show="groups.length <= 1">No groups available. Create groups to restrict access to them first.</div>
|
||||
<div class="has-error" ng-show="appInstall.accessRestrictionOption !== '' && !appInstall.isAccessRestrictionValid()">Select at least one group</div>
|
||||
<div>
|
||||
<div>
|
||||
<span ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<button class="btn btn-default" type="button" ng-disabled="appInstall.accessRestrictionOption === ''" ng-click="appInstall.toggleGroup(group);" ng-class="{ 'btn-primary': (appInstall.accessRestriction.groups && appInstall.accessRestriction.groups.indexOf(group.id) !== -1) }">{{ group.name }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
@@ -80,14 +102,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
|
||||
<h4 class="text-danger">Not enough resources left to install this application.</h4>
|
||||
<p>The Cloudron's resources can be extended with a model upgrade or available resources may be freed up by uninstalling unused applications.</p>
|
||||
<h4 class="text-danger">This Cloudron is running low on resources.</h4>
|
||||
<p>Installing this app might decrease the performance of other apps. The Cloudron's resources can be extended with a model upgrade or available resources may be freed up by uninstalling unused applications.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="showInstallForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="doInstall()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button>
|
||||
<button type="button" class="btn btn-danger" ng-show="!appInstall.installFormVisible && user.admin && appInstall.resourceConstraintVisible" ng-click="appInstall.showForm(true)">Install anyway</button>
|
||||
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="appInstall.showForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="appInstall.submit()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +125,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form name="feedbackForm" ng-submit="submitFeedback()">
|
||||
<form name="feedbackForm" ng-submit="feedback.submit()">
|
||||
<div ng-show="feedback.error" class="text-danger text-bold">{{feedback.error}}</div>
|
||||
<textarea class="form-control" id="feedbackDescriptionTextarea" cols="3" ng-model="feedback.description" ng-minlength="1" required placeholder="Name, Category, Links ..." autofocus></textarea>
|
||||
<input class="ng-hide" type="submit" ng-disabled="feedbackForm.$invalid || feedback.busy"/>
|
||||
@@ -111,7 +134,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="submitFeedback()" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-fw fa-paper-plane"></i> Submit</button>
|
||||
<button type="button" class="btn btn-success" ng-click="feedback.submit()" ng-disabled="feedbackForm.$invalid || feedback.busy"><i class="fa fa-fw fa-paper-plane"></i> Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +194,7 @@
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="" ng-click="showFeedbackModal()">Missing an app? Let us know.</a>
|
||||
<a href="" ng-click="feedback.show()">Missing an app? Let us know.</a>
|
||||
</div>
|
||||
<div class="col-md-10" ng-show="ready && apps.length">
|
||||
<div class="row-no-margin">
|
||||
@@ -193,7 +216,7 @@
|
||||
</div>
|
||||
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="ready && !apps.length">
|
||||
<h3 class="text-muted">No applications in this category.</h3>
|
||||
<a href="" ng-click="showFeedbackModal()"><h3>Let us know if you miss something.</h3></a>
|
||||
<a href="" ng-click="feedback.show()"><h3>Let us know if you miss something.</h3></a>
|
||||
</div>
|
||||
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="!ready">
|
||||
<h2><i class="fa fa-spinner fa-pulse"></i> Loading</h2>
|
||||
|
||||
+208
-169
@@ -6,6 +6,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.users = [];
|
||||
$scope.groups = [];
|
||||
$scope.category = '';
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
@@ -17,13 +18,154 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
app: {},
|
||||
location: '',
|
||||
portBindings: {},
|
||||
accessRestriction: null,
|
||||
oauthProxy: false,
|
||||
mediaLinks: [],
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: ''
|
||||
keyFileName: '',
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
accessRestrictionSingleUser: null,
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appInstall.accessRestriction;
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
},
|
||||
|
||||
toggleGroup: function (group) {
|
||||
var groups = $scope.appInstall.accessRestriction.groups;
|
||||
var pos = groups.indexOf(group.id);
|
||||
|
||||
if (pos === -1) groups.push(group.id);
|
||||
else groups.splice(pos, 1);
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.installFormVisible = false;
|
||||
$scope.appInstall.resourceConstraintVisible = false;
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.accessRestrictionOption = '';
|
||||
$scope.appInstall.accessRestriction = { users: [], groups: [] };
|
||||
$scope.appInstall.accessRestrictionSingleUser = null;
|
||||
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
},
|
||||
|
||||
showForm: function (force) {
|
||||
if (Client.enoughResourcesAvailable($scope.appInstall.app) || force) {
|
||||
$scope.appInstall.installFormVisible = true;
|
||||
$scope.appInstall.resourceConstraintVisible = false;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.installFormVisible = false;
|
||||
$scope.appInstall.resourceConstraintVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('show');
|
||||
}
|
||||
},
|
||||
|
||||
show: function (app) {
|
||||
$scope.appInstall.reset();
|
||||
|
||||
// make a copy to work with in case the app object gets updated while polling
|
||||
angular.copy(app, $scope.appInstall.app);
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.location = app.location;
|
||||
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
$scope.appInstall.accessRestrictionOption = app.accessRestriction ? 'restricted' : '';
|
||||
$scope.appInstall.accessRestriction = app.accessRestriction || { users: [], groups: [] };
|
||||
$scope.appInstall.accessRestrictionSingleUser = null;
|
||||
|
||||
// set default ports
|
||||
for (var env in $scope.appInstall.app.manifest.tcpPorts) {
|
||||
$scope.appInstall.portBindings[env] = $scope.appInstall.app.manifest.tcpPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portBindingsEnabled[env] = true;
|
||||
}
|
||||
|
||||
$('#appInstallModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.appInstall.busy = true;
|
||||
$scope.appInstall.error.other = null;
|
||||
$scope.appInstall.error.location = null;
|
||||
$scope.appInstall.error.port = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appInstall.portBindings) {
|
||||
if ($scope.appInstall.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appInstall.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
// translate to accessRestriction object
|
||||
var accessRestriction = $scope.appInstall.app.manifest.singleUser ? {
|
||||
users: [ $scope.appInstall.accessRestrictionSingleUser.id ]
|
||||
} : (!$scope.appInstall.accessRestrictionOption ? null : $scope.appInstall.accessRestriction);
|
||||
|
||||
var data = {
|
||||
location: $scope.appInstall.location || '',
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: accessRestriction,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
key: $scope.appInstall.keyFile,
|
||||
};
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appInstall.error.location = 'This name is already taken.';
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appInstall.error.other = 'Unable to purchase this app<br/>Please make sure your payment is setup <a href="' + $scope.config.webServerOrigin + '/console.html#/userprofile" target="_blank">here</a>';
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appInstall.error.cert = error.message;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
$scope.appInstall.reset();
|
||||
$location.path('/apps');
|
||||
});
|
||||
|
||||
$('#appInstallModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appNotFound = {
|
||||
@@ -33,39 +175,40 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
|
||||
$scope.feedback = {
|
||||
error: null,
|
||||
success: false,
|
||||
subject: 'App feedback',
|
||||
description: '',
|
||||
type: 'app'
|
||||
};
|
||||
|
||||
function resetFeedback() {
|
||||
$scope.feedback.description = '';
|
||||
|
||||
$scope.feedbackForm.$setUntouched();
|
||||
$scope.feedbackForm.$setPristine();
|
||||
}
|
||||
|
||||
$scope.submitFeedback = function () {
|
||||
$scope.feedback.busy = true;
|
||||
$scope.feedback.success = false;
|
||||
$scope.feedback.error = null;
|
||||
|
||||
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
|
||||
if (error) {
|
||||
$scope.feedback.error = error;
|
||||
} else {
|
||||
$scope.feedback.success = true;
|
||||
$('#feedbackModal').modal('hide');
|
||||
resetFeedback();
|
||||
}
|
||||
type: 'app_missing',
|
||||
|
||||
reset: function () {
|
||||
$scope.feedback.busy = false;
|
||||
});
|
||||
};
|
||||
$scope.feedback.error = null;
|
||||
$scope.feedback.description = '';
|
||||
|
||||
$scope.showFeedbackModal = function () {
|
||||
$('#feedbackModal').modal('show');
|
||||
$scope.feedbackForm.$setUntouched();
|
||||
$scope.feedbackForm.$setPristine();
|
||||
},
|
||||
|
||||
show: function () {
|
||||
$scope.feedback.reset();
|
||||
$('#feedbackModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.feedback.busy = true;
|
||||
$scope.feedback.error = null;
|
||||
|
||||
Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) {
|
||||
$scope.feedback.busy = false;
|
||||
|
||||
if (error) {
|
||||
$scope.feedback.error = error;
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
$('#feedbackModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getAppList(callback) {
|
||||
@@ -136,42 +279,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reset = function () {
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.accessRestriction = null;
|
||||
$scope.appInstall.oauthProxy = false;
|
||||
$scope.appInstall.installFormVisible = false;
|
||||
$scope.appInstall.resourceConstraintVisible = false;
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
};
|
||||
|
||||
$scope.showInstallForm = function () {
|
||||
if (Client.enoughResourcesAvailable($scope.appInstall.app)) {
|
||||
$scope.appInstall.installFormVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else {
|
||||
$scope.appInstall.resourceConstraintVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseResourceConstraint').collapse('show');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.appInstall.certificateFile = null;
|
||||
@@ -200,28 +307,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showInstall = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// make a copy to work with in case the app object gets updated while polling
|
||||
angular.copy(app, $scope.appInstall.app);
|
||||
$('#appInstallModal').modal('show');
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.location = app.location;
|
||||
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
$scope.appInstall.accessRestriction = app.accessRestriction ? app.accessRestriction.users[0] : $scope.user;
|
||||
$scope.appInstall.oauthProxy = false;
|
||||
|
||||
// set default ports
|
||||
for (var env in $scope.appInstall.app.manifest.tcpPorts) {
|
||||
$scope.appInstall.portBindings[env] = $scope.appInstall.app.manifest.tcpPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portBindingsEnabled[env] = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showAppNotFound = function (appId, version) {
|
||||
$scope.appNotFound.appId = appId;
|
||||
$scope.appNotFound.version = version;
|
||||
@@ -229,70 +314,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$('#appNotFoundModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doInstall = function () {
|
||||
$scope.appInstall.busy = true;
|
||||
$scope.appInstall.error.other = null;
|
||||
$scope.appInstall.error.location = null;
|
||||
$scope.appInstall.error.port = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appInstall.portBindings) {
|
||||
if ($scope.appInstall.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appInstall.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
// translate to accessRestriction object
|
||||
var accessRestriction = $scope.appInstall.app.manifest.singleUser ? {
|
||||
users: [ $scope.appInstall.accessRestriction.id ]
|
||||
} : null;
|
||||
|
||||
var data = {
|
||||
location: $scope.appInstall.location || '',
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: accessRestriction,
|
||||
oauthProxy: $scope.appInstall.oauthProxy,
|
||||
cert: $scope.appInstall.certificateFile,
|
||||
key: $scope.appInstall.keyFile,
|
||||
};
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appInstall.error.location = 'This name is already taken.';
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appInstall.error.other = 'Unable to purchase this app<br/>Please make sure your payment is setup <a href="' + $scope.config.webServerOrigin + '/console.html#/userprofile" target="_blank">here</a>';
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appInstall.error.cert = error.message;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
$scope.appInstall.certificateFile = null;
|
||||
$scope.appInstall.keyFileName = '';
|
||||
$scope.appInstall.keyFile = null;
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
$scope.reset();
|
||||
$location.path('/apps');
|
||||
});
|
||||
|
||||
$('#appInstallModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.gotoApp = function (app) {
|
||||
$location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
|
||||
};
|
||||
@@ -310,7 +331,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.showInstall(result);
|
||||
$scope.appInstall.show(result);
|
||||
});
|
||||
} else {
|
||||
var found = $scope.apps.filter(function (app) {
|
||||
@@ -318,40 +339,58 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
});
|
||||
|
||||
if (found.length) {
|
||||
$scope.showInstall(found[0]);
|
||||
$scope.appInstall.show(found[0]);
|
||||
} else {
|
||||
$scope.showAppNotFound(appId, null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$scope.reset();
|
||||
$scope.appInstall.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchUsers() {
|
||||
Client.getUsers(function (error, users) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchUsers, 5000);
|
||||
}
|
||||
|
||||
$scope.users = users;
|
||||
});
|
||||
}
|
||||
|
||||
function fetchGroups() {
|
||||
Client.getGroups(function (error, groups) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(fetchUsers, 5000);
|
||||
}
|
||||
|
||||
$scope.groups = groups;
|
||||
});
|
||||
}
|
||||
|
||||
(function refresh() {
|
||||
$scope.ready = false;
|
||||
|
||||
Client.getUsers(function (error, users) {
|
||||
getAppList(function (error, apps) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(refresh, 1000);
|
||||
}
|
||||
|
||||
$scope.users = users;
|
||||
$scope.apps = apps;
|
||||
|
||||
getAppList(function (error, apps) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(refresh, 1000);
|
||||
}
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
hashChangeListener();
|
||||
|
||||
$scope.apps = apps;
|
||||
if ($scope.user.admin) {
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
}
|
||||
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
hashChangeListener();
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
$scope.ready = true;
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-hide="config.developerMode">Enable Developer Mode</h4>
|
||||
<h4 class="modal-title" ng-show="config.developerMode">Disable Developer Mode</h4>
|
||||
<h4 class="modal-title" ng-hide="config.developerMode">Enable CLI Mode</h4>
|
||||
<h4 class="modal-title" ng-show="config.developerMode">Disable CLI Mode</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="developerModeChangeForm" class="form-signin" role="form" novalidate ng-submit="doChangeDeveloperMode(developerModeChangeForm)" autocomplete="off">
|
||||
<form name="developerModeChangeForm" role="form" novalidate ng-submit="doChangeDeveloperMode(developerModeChangeForm)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!developerModeChangeForm.password.$dirty && developerModeChange.error.password) || (developerModeChangeForm.password.$dirty && developerModeChangeForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputDeveloperModeChangePassword">Give your password to verify that you are performing that action</label>
|
||||
@@ -143,17 +143,17 @@
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>Developer Mode</h3>
|
||||
<h3>CLI</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
The developer mode will enable additional functionality for developers. This mode allows for example the Cloudron commandline tool to control parts of the Cloudron, like installing and debugging applications.
|
||||
Enabling this will allow the <a href="https://cloudron.io/references/cli.html" target="_blank">CLI tool</a> to control this Cloudron. The CLI tool can be used to install, configure, inspect and backup applications.
|
||||
<br/>
|
||||
<br/>
|
||||
If you are interested in application development, please visit the <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank">application developer documentation</a>.
|
||||
If you are a developer, please see the <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank">docs</a>.
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
<select class="form-control" name="type" style="width: 50%;" ng-model="feedback.type" required>
|
||||
<option value="feedback">Enhancement / Idea</option>
|
||||
<option value="ticket">Bug Report</option>
|
||||
<option value="app">Missing App</option>
|
||||
<option value="app_missing">Missing App</option>
|
||||
<option value="app_error">App Error/Failing</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (feedbackForm.subject.$dirty && feedbackForm.subject.$invalid) }">
|
||||
|
||||
+207
-101
@@ -1,51 +1,49 @@
|
||||
<!-- Modal add user -->
|
||||
<div class="modal fade" id="userAddModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal fade" id="userAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="userAddModalLabel">Add User</h4>
|
||||
<h4 class="modal-title">Add User</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="useradd_form" class="form-signin" role="form" novalidate ng-submit="doAdd()" autocomplete="off">
|
||||
<fieldset>
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.username.$dirty && useradd_form.username.$invalid) || (!useradd_form.username.$dirty && useradd.error.username) }">
|
||||
<label class="control-label" for="inputUserAddUsername">Username</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.username.$dirty && useradd.error.username) || (useradd_form.username.$dirty && useradd_form.username.$invalid) || (!useradd_form.username.$dirty && useradd.error.username)">
|
||||
<small ng-show="useradd_form.username.$error.required">A username is required</small>
|
||||
<small ng-show="useradd_form.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="useradd_form.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="!useradd_form.username.$dirty && useradd.error.username">{{ useradd.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useradd.username" id="inputUserAddUsername" name="username" ng-maxlength="512" ng-minlength="3" required autofocus>
|
||||
<form name="useradd_form" role="form" ng-submit="doAdd()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.username.$dirty && useradd_form.username.$invalid) || (!useradd_form.username.$dirty && useradd.error.username) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.username.$dirty && useradd.error.username) || (useradd_form.username.$dirty && useradd_form.username.$invalid) || (!useradd_form.username.$dirty && useradd.error.username)">
|
||||
<small ng-show="useradd_form.username.$error.required">A username is required</small>
|
||||
<small ng-show="useradd_form.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="useradd_form.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="!useradd_form.username.$dirty && useradd.error.username">{{ useradd.error.username }}</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.email.$dirty && useradd_form.email.$invalid) || (!useradd_form.email.$dirty && useradd.error.email) }">
|
||||
<label class="control-label" for="inputUserAddEmail">Email</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.email.$dirty && useradd.error.email) || (useradd_form.email.$dirty && useradd_form.email.$invalid) || (!useradd_form.email.$dirty && useradd.error.email)">
|
||||
<small ng-show="useradd_form.email.$error.required">An email is required</small>
|
||||
<small ng-show="useradd_form.email.$error.email">This is not a valid email</small>
|
||||
<small ng-show="!useradd_form.email.$dirty && useradd.error.email">{{ useradd.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="useradd.email" id="inputUserAddEmail" name="email" required>
|
||||
<input type="text" class="form-control" ng-model="useradd.username" name="username" id="inputUserAddUsername" ng-maxlength="512" ng-minlength="3" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.email.$dirty && useradd_form.email.$invalid) || (!useradd_form.email.$dirty && useradd.error.email) }">
|
||||
<label class="control-label">Email</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.email.$dirty && useradd.error.email) || (useradd_form.email.$dirty && useradd_form.email.$invalid) || (!useradd_form.email.$dirty && useradd.error.email)">
|
||||
<small ng-show="useradd_form.email.$error.required">An email is required</small>
|
||||
<small ng-show="useradd_form.email.$error.email">This is not a valid email</small>
|
||||
<small ng-show="!useradd_form.email.$dirty && useradd.error.email">{{ useradd.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="useradd.email" name="email" id="inputUserAddEmail" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.displayName.$dirty && useradd_form.displayName.$invalid) || (!useradd_form.displayName.$dirty && useradd.error.displayName) }">
|
||||
<label class="control-label" for="inputUserAddDisplayName">Full Name</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.displayName.$dirty && useradd.error.displayName) || (useradd_form.displayname.$dirty && useradd_form.displayName.$invalid) || (!useradd_form.displayName.$dirty && useradd.error.displayName)">
|
||||
<small ng-show="useradd_form.displayName.$error.required">A Name is required</small>
|
||||
<small ng-show="useradd_form.displayName.$error.displayName">This is not a valid Name</small>
|
||||
<small ng-show="!useradd_form.displayName.$dirty && useradd.error.displayName">{{ useradd.error.displayName }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useradd.displayName" id="inputUserAddDisplayName" name="displayName">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.displayName.$dirty && useradd_form.displayName.$invalid) || (!useradd_form.displayName.$dirty && useradd.error.displayName) }">
|
||||
<label class="control-label">Full Name</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.displayName.$dirty && useradd.error.displayName) || (useradd_form.displayname.$dirty && useradd_form.displayName.$invalid) || (!useradd_form.displayName.$dirty && useradd.error.displayName)">
|
||||
<small ng-show="useradd_form.displayName.$error.required">A Name is required</small>
|
||||
<small ng-show="useradd_form.displayName.$error.displayName">This is not a valid Name</small>
|
||||
<small ng-show="!useradd_form.displayName.$dirty && useradd.error.displayName">{{ useradd.error.displayName }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useradd.displayName" name="displayName" id="inputUserAddDisplayName">
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="useradd.sendInvite" id="inputUserAddSendInvite"> Send invite
|
||||
</label>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="useradd_form.$invalid || useradd.alreadyTaken === username"/>
|
||||
</fieldset>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="useradd.sendInvite" id="inputUserAddSendInvite"> Send invite
|
||||
</label>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="useradd_form.$invalid || useradd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -57,34 +55,32 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal remove user -->
|
||||
<div class="modal fade" id="userRemoveModal" tabindex="-1" role="dialog" aria-labelledby="userRemoveModalLabel" aria-hidden="true" style="text-align: left;">
|
||||
<div class="modal fade" id="userRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="userRemoveModalLabel">Delete user {{ userremove.userInfo.username }}</h4>
|
||||
<h4 class="modal-title">Delete user {{ userremove.userInfo.username }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="userremove_form" class="form-user-delete" role="form" ng-submit="doUserRemove()" name="userDeleteConfirm" autocomplete="off">
|
||||
<fieldset>
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (userremove_form.username.$dirty && userremove_form.username.$invalid) || (!userremove_form.username.$dirty && userremove.error.username) }">
|
||||
<label class="control-label" for="inputUserRemoveUsername">Just to be sure you really want to delete this user, please type the user's name</label>
|
||||
<div class="control-label" ng-show="(!userremove_form.username.$dirty && userremove.error.username) || (userremove_form.username.$dirty && userremove_form.username.$invalid)">
|
||||
<small ng-show="userremove_form.username.$error.required">A username is required</small>
|
||||
<small ng-show="userremove_form.error.username">The username does not match</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="userremove.username" id="inputUserRemoveUsername" name="userDeleteConfirm" placeholder="Username" required autofocus>
|
||||
<form name="userremove_form" role="form" ng-submit="doUserRemove()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (userremove_form.username.$dirty && userremove_form.username.$invalid) || (!userremove_form.username.$dirty && userremove.error.username) }">
|
||||
<label class="control-label">Just to be sure you really want to delete this user, please type the user's name</label>
|
||||
<div class="control-label" ng-show="(!userremove_form.username.$dirty && userremove.error.username) || (userremove_form.username.$dirty && userremove_form.username.$invalid)">
|
||||
<small ng-show="userremove_form.username.$error.required">A username is required</small>
|
||||
<small ng-show="userremove_form.error.username">The username does not match</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (userremove_form.password.$dirty && userremove_form.password.$invalid) || (!userremove_form.password.$dirty && userremove.error.password)}">
|
||||
<label class="control-label" for="inputUserRemovePassword">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!userremove_form.password.$dirty && userremove.error.password) || (userremove_form.password.$dirty && userremove_form.password.$invalid)">
|
||||
<small ng-show="userremove_form.password.$error.required && !userremove.error.password">A password is required</small>
|
||||
<small ng-show="!useradd_form.email.$dirty && userremove.error.password">{{ userremove.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="userremove.password" id="inputUserRemovePassword" name="password" placeholder="Password" required>
|
||||
<input type="text" class="form-control" ng-model="userremove.username" id="inputUserRemoveUsername" name="username" placeholder="Username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (userremove_form.password.$dirty && userremove_form.password.$invalid) || (!userremove_form.password.$dirty && userremove.error.password)}">
|
||||
<label class="control-label">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!userremove_form.password.$dirty && userremove.error.password) || (userremove_form.password.$dirty && userremove_form.password.$invalid)">
|
||||
<small ng-show="userremove_form.password.$error.required && !userremove.error.password">A password is required</small>
|
||||
<small ng-show="!useradd_form.email.$dirty && userremove.error.password">{{ userremove.error.password }}</small>
|
||||
</div>
|
||||
<input class="hide" type="submit"/>
|
||||
</fieldset>
|
||||
<input type="password" class="form-control" ng-model="userremove.password" id="inputUserRemovePassword" name="password" placeholder="Password" required>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="userremove_form.$invalid || userremove.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -96,53 +92,124 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal edit user -->
|
||||
<div class="modal fade" id="userEditModal" tabindex="-1" role="dialog" aria-labelledby="userEditModalLabel" aria-hidden="true" style="text-align: left;">
|
||||
<div class="modal fade" id="userEditModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="userEditModalLabel">Edit user {{ useredit.userInfo.username }}</h4>
|
||||
<h4 class="modal-title">Edit user {{ useredit.userInfo.username }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="useredit_form" class="form-user-delete" role="form" ng-submit="doUserEdit()" autocomplete="off">
|
||||
<fieldset>
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && useredit.error.displayName) }">
|
||||
<label class="control-label" for="inputUsereditDisplayName">Full Name</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.displayName.$dirty && useredit.error.displayName) || (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid)">
|
||||
<small ng-show="useredit_form.error.displayName">Invalid name</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useredit.displayName" id="inputUsereditDisplayName" name="displayName" placeholder="Full Name" autofocus>
|
||||
<form name="useredit_form" role="form" ng-submit="doUserEdit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid) || (!useredit_form.displayName.$dirty && useredit.error.displayName) }">
|
||||
<label class="control-label">Full Name</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.displayName.$dirty && useredit.error.displayName) || (useredit_form.displayName.$dirty && useredit_form.displayName.$invalid)">
|
||||
<small ng-show="useredit_form.error.displayName">Invalid name</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && useredit.error.email) }">
|
||||
<label class="control-label" for="inputUserEditEmail">Email</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.email.$dirty && useredit.error.email) || (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && useredit.error.email)">
|
||||
<small ng-show="useredit_form.email.$error.required">An email is required</small>
|
||||
<small ng-show="useredit_form.email.$error.email">This is not a valid email</small>
|
||||
<small ng-show="!useredit_form.email.$dirty && useredit.error.email">{{ useredit.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="useredit.email" id="inputUserEditEmail" name="email" required>
|
||||
<input type="text" class="form-control" ng-model="useredit.displayName" name="displayName" placeholder="Full Name" autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && useredit.error.email) }">
|
||||
<label class="control-label">Email</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.email.$dirty && useredit.error.email) || (useredit_form.email.$dirty && useredit_form.email.$invalid) || (!useredit_form.email.$dirty && useredit.error.email)">
|
||||
<small ng-show="useredit_form.email.$error.required">An email is required</small>
|
||||
<small ng-show="useredit_form.email.$error.email">This is not a valid email</small>
|
||||
<small ng-show="!useredit_form.email.$dirty && useredit.error.email">{{ useredit.error.email }}</small>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useredit_form.password.$dirty && useredit_form.password.$invalid) || (!useredit_form.password.$dirty && useredit.error.password)}">
|
||||
<label class="control-label" for="inputusereditPassword">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!useredit_form.password.$dirty && useredit.error.password) || (useredit_form.password.$dirty && useredit_form.password.$invalid)">
|
||||
<small ng-show="useredit_form.password.$error.required && !useredit.error.password">A password is required</small>
|
||||
<small ng-show="!useredit_form.password.$dirty && useredit.error.password">{{ useredit.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="useredit.password" id="inputUserRemovePassword" name="password" placeholder="Password" required>
|
||||
<input type="email" class="form-control" ng-model="useredit.email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Groups</label>
|
||||
<div>
|
||||
<span ng-repeat="group in groups | ignoreAdminGroup" ng-show="group.id !== 'admin'">
|
||||
<button class="btn btn-default" type="button" ng-click="userEditToggleGroup(group);" ng-class="{ 'btn-primary': (useredit.groupIds.indexOf(group.id) !== -1) }">{{ group.name }}</button>
|
||||
</span>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="useredit_form.$invalid"/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="form-group" ng-hide="isMe(useredit.userInfo)">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="useredit.superuser"> Allow this user to manage apps, groups and other users
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="useredit_form.$invalid || useredit.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doUserEdit()" ng-disabled="useredit_form.$invalid || useredit.busy"><i class="fa fa-spinner fa-pulse" ng-show="useredit.busy"></i> Edit</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doUserEdit()" ng-disabled="useredit_form.$invalid || useredit.busy"><i class="fa fa-spinner fa-pulse" ng-show="useredit.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Modal add group -->
|
||||
<div class="modal fade" id="groupAddModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add Group</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="groupAddForm" role="form" novalidate ng-submit="groupAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (groupAddForm.name.$dirty && groupAddForm.name.$invalid) || (!groupAddForm.name.$dirty && groupAdd.error.name) }">
|
||||
<label class="control-label" for="groupAddName">Name</label>
|
||||
<div class="control-label" ng-show="(!groupAddForm.name.$dirty && groupAdd.error.name) || (groupAddForm.name.$dirty && groupAddForm.name.$invalid) || (!groupAddForm.name.$dirty && groupAdd.error.name)">
|
||||
<small ng-show="groupAddForm.name.$error.required">A name is required</small>
|
||||
<small ng-show="groupAddForm.name.$error.minlength">The name is too short</small>
|
||||
<small ng-show="groupAddForm.name.$error.maxlength">The name is too long</small>
|
||||
<small ng-show="!groupAddForm.name.$dirty && groupAdd.error.name">{{ groupAdd.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="groupAdd.name" id="groupAddName" name="name" ng-maxlength="200" ng-minlength="2" required autofocus>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="groupAddForm.$invalid || groupAdd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="groupAdd.submit()" ng-disabled="groupAddForm.$invalid || groupAdd.busy"><i class="fa fa-spinner fa-pulse" ng-show="groupAdd.busy"></i> Add Group</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove group -->
|
||||
<div class="modal fade" id="groupRemoveModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Delete group {{ groupRemove.group.name }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="groupRemove.memberCount" class="text-danger">
|
||||
<b>This group still has {{ groupRemove.memberCount }} member(s). Are you sure this group is not used?</b>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<form name="groupRemoveForm" role="form" novalidate ng-submit="groupRemove.submit()" autocomplete="off">
|
||||
<input type="password" style="display: none;">
|
||||
<div class="form-group" ng-class="{ 'has-error': (groupRemoveForm.password.$dirty && groupRemoveForm.password.$invalid) || (!groupRemoveForm.password.$dirty && groupRemove.error.password)}">
|
||||
<label class="control-label" for="groupRemovePasswordInput">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!groupRemoveForm.password.$dirty && groupRemove.error.password) || (groupRemoveForm.password.$dirty && groupRemoveForm.password.$invalid)">
|
||||
<small ng-show="groupRemoveForm.password.$error.required && !groupRemove.error.password">A password is required</small>
|
||||
<small ng-show="!groupRemoveForm.password.$dirty && groupRemove.error.password">{{ groupRemove.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="groupRemove.password" id="groupRemovePasswordInput" name="password" placeholder="Password" required autofocus>
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="groupRemoveForm.$invalid || groupRemove.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="groupRemove.submit()" ng-disabled="groupRemoveForm.$invalid || groupRemove.busy"><i class="fa fa-spinner fa-pulse" ng-show="groupRemove.busy"></i> Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-large">
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -164,29 +231,68 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1px;"></th>
|
||||
<th style="">User</th>
|
||||
<th style="width: 1px" class="text-right">Group</th>
|
||||
<th style="width: 300px" class="text-right">Actions</th>
|
||||
<th style="" class="text-left hidden-xs hidden-sm">Groups</th>
|
||||
<th style="width: 100px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td class="text-overflow: ellipsis; white-space: nowrap;">
|
||||
<td>
|
||||
<i class="fa fa-briefcase" ng-show="user.admin" data-toggle="tooltip" title="This user can manage apps, groups and other users" ng-init="initTooltip()"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="showUserEdit(user)">
|
||||
{{ user.username }}
|
||||
<span class="text-muted">{{ user.email }}</span>
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: bottom">
|
||||
<span ng-show="isAdmin(user)" class="label label-default">Admin</span>
|
||||
<td class="text-left hand elide-table-cell hidden-xs hidden-sm" ng-click="showUserEdit(user)">
|
||||
<span class="group-badge" ng-repeat="groupId in user.groupIds | ignoreAdminGroup">{{ groupId }}</span>
|
||||
</td>
|
||||
<td class="text-right no-wrap" style="vertical-align: bottom">
|
||||
<button ng-show="!isMe(user)" class="btn btn-xs btn-default" ng-click="sendInvite(user)" title="Send Invite"><i class="fa fa-paper-plane-o"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="showUserEdit(user)" title="Edit User Profile"><i class="fa fa-pencil"></i></button>
|
||||
<button ng-show="!isMe(user)" class="btn btn-xs btn-danger" ng-click="showUserRemove(user)" title="Remove User"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<div class="text-left">
|
||||
<h1>Groups <button class="btn btn-primary btn-outline pull-right" ng-click="groupAdd.show()"><i class="fa fa-plus"></i> New Group</button></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-spinner fa-pulse"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="">Name</th>
|
||||
<th style="width: 300px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in groups | ignoreAdminGroup">
|
||||
<td class="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ group.name }}
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: bottom">
|
||||
<span ng-show="isMe(user)" class="label label-success">This is you!</span>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" ng-click="toggleAdmin(user)" class="btn btn-xs btn-default" title="{{ user.admin ? 'Remove Admin Privileges' : 'Make User an Admin' }}">
|
||||
<span ng-show="!user.admin"><i class="fa fa-plus"></i> Admin</span>
|
||||
<span ng-show="user.admin"><i class="fa fa-minus"></i> Admin</span>
|
||||
</button>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" class="btn btn-xs btn-default" ng-click="sendInvite(user)" title="Send Invite"><i class="fa fa-paper-plane-o"></i> Invite</button>
|
||||
<button ng-show="isMe(user) || userInfo.admin" class="btn btn-xs btn-default" ng-click="showUserEdit(user)" title="Edit User Profile"><i class="fa fa-pencil"></i> Edit</button>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" class="btn btn-xs btn-danger" ng-click="showUserRemove(user)" title="Remove User"><i class="fa fa-trash-o"></i> Delete</button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="groupRemove.show(group)" title="Remove Group"><i class="fa fa-trash-o"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
+154
-25
@@ -5,6 +5,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.users = [];
|
||||
$scope.groups = [];
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
|
||||
$scope.userremove = {
|
||||
@@ -31,7 +32,108 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
userInfo: {},
|
||||
email: '',
|
||||
displayName: '',
|
||||
password: ''
|
||||
superuser: false
|
||||
};
|
||||
|
||||
$scope.showBubble = function ($event) {
|
||||
$($event.target).tooltip('show');
|
||||
|
||||
setTimeout(function () {
|
||||
$($event.target).tooltip('hide');
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
$scope.groupAdd = {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: '',
|
||||
|
||||
show: function () {
|
||||
$scope.groupAdd.busy = false;
|
||||
|
||||
$scope.groupAdd.error = {};
|
||||
$scope.groupAdd.name = '';
|
||||
|
||||
$scope.groupAddForm.$setUntouched();
|
||||
$scope.groupAddForm.$setPristine();
|
||||
|
||||
$('#groupAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.groupAdd.busy = true;
|
||||
$scope.groupAdd.error = {};
|
||||
|
||||
Client.createGroup($scope.groupAdd.name, function (error) {
|
||||
$scope.groupAdd.busy = false;
|
||||
|
||||
if (error && error.statusCode === 409) {
|
||||
$scope.groupAdd.error.name = 'Name already taken';
|
||||
$scope.groupAddForm.name.$setPristine();
|
||||
$('#groupAddName').focus();
|
||||
return;
|
||||
}
|
||||
if (error && error.statusCode === 400) {
|
||||
$scope.groupAdd.error.name = error.message;
|
||||
$scope.groupAddForm.name.$setPristine();
|
||||
$('#groupAddName').focus();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Unable to create group.', error.statusCode, error.message);
|
||||
|
||||
refresh();
|
||||
$('#groupAddModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.groupRemove = {
|
||||
busy: false,
|
||||
error: {},
|
||||
group: null,
|
||||
password: '',
|
||||
memberCount: 0,
|
||||
|
||||
show: function (group) {
|
||||
$scope.groupRemove.busy = false;
|
||||
|
||||
$scope.groupRemove.error = {};
|
||||
$scope.groupRemove.password = '';
|
||||
|
||||
$scope.groupRemove.group = angular.copy(group);
|
||||
|
||||
$scope.groupRemoveForm.$setUntouched();
|
||||
$scope.groupRemoveForm.$setPristine();
|
||||
|
||||
Client.getGroup(group.id, function (error, result) {
|
||||
if (error) return console.error('Unable to fetch group information.', error.statusCode, error.message);
|
||||
|
||||
$scope.groupRemove.memberCount = result.userIds.length;
|
||||
|
||||
$('#groupRemoveModal').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.groupRemove.busy = true;
|
||||
$scope.groupRemove.error = {};
|
||||
|
||||
Client.removeGroup($scope.groupRemove.group.id, $scope.groupRemove.password, function (error) {
|
||||
$scope.groupRemove.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.groupRemove.error.password = 'Wrong password';
|
||||
$scope.groupRemove.password = '';
|
||||
$scope.groupRemoveForm.password.$setPristine();
|
||||
$('#groupRemovePasswordInput').focus();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Unable to remove group.', error.statusCode, error.message);
|
||||
|
||||
refresh();
|
||||
$('#groupRemoveModal').modal('hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isMe = function (user) {
|
||||
@@ -134,6 +236,8 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.useredit.displayName = userInfo.displayName;
|
||||
$scope.useredit.email = userInfo.email;
|
||||
$scope.useredit.userInfo = userInfo;
|
||||
$scope.useredit.groupIds = angular.copy(userInfo.groupIds);
|
||||
$scope.useredit.superuser = userInfo.groupIds.indexOf('admin') !== -1;
|
||||
|
||||
$scope.useredit_form.$setPristine();
|
||||
$scope.useredit_form.$setUntouched();
|
||||
@@ -141,10 +245,18 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$('#userEditModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.userEditToggleGroup = function (group) {
|
||||
var pos = $scope.useredit.groupIds.indexOf(group.id);
|
||||
if (pos === -1) {
|
||||
$scope.useredit.groupIds.push(group.id);
|
||||
} else {
|
||||
$scope.useredit.groupIds.splice(pos, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.doUserEdit = function () {
|
||||
$scope.useredit.error.displayName = null;
|
||||
$scope.useredit.error.email = null;
|
||||
$scope.useredit.error.password = null;
|
||||
$scope.useredit.busy = true;
|
||||
|
||||
var data = {
|
||||
@@ -153,29 +265,36 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
displayName: $scope.useredit.displayName
|
||||
};
|
||||
|
||||
Client.updateUser(data, $scope.useredit.password, function (error) {
|
||||
$scope.useredit.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.useredit.error.password = 'Wrong password';
|
||||
$scope.useredit.password = '';
|
||||
$scope.useredit_form.password.$setPristine();
|
||||
$('#inputUserEditPassword').focus();
|
||||
return;
|
||||
Client.updateUser(data, function (error) {
|
||||
if (error) {
|
||||
$scope.useredit.busy = false;
|
||||
return console.error('Unable to update user:', error);
|
||||
}
|
||||
if (error) return console.error('Unable to update user:', error);
|
||||
|
||||
$scope.useredit.userInfo = {};
|
||||
$scope.useredit.email = '';
|
||||
$scope.useredit.displayName = '';
|
||||
$scope.useredit.password = '';
|
||||
if ($scope.useredit.superuser) {
|
||||
if ($scope.useredit.groupIds.indexOf('admin') === -1) $scope.useredit.groupIds.push('admin');
|
||||
} else {
|
||||
$scope.useredit.groupIds = $scope.useredit.groupIds.filter(function (groupId) { return groupId !== 'admin'; });
|
||||
}
|
||||
|
||||
$scope.useredit_form.$setPristine();
|
||||
$scope.useredit_form.$setUntouched();
|
||||
Client.setGroups(data.id, $scope.useredit.groupIds, function (error) {
|
||||
$scope.useredit.busy = false;
|
||||
|
||||
refresh();
|
||||
if (error) return console.error('Unable to update groups for user:', error);
|
||||
|
||||
$('#userEditModal').modal('hide');
|
||||
$scope.useredit.userInfo = {};
|
||||
$scope.useredit.email = '';
|
||||
$scope.useredit.displayName = '';
|
||||
$scope.useredit.superuser = false;
|
||||
$scope.useredit.groupIds = [];
|
||||
|
||||
$scope.useredit_form.$setPristine();
|
||||
$scope.useredit_form.$setUntouched();
|
||||
|
||||
refresh();
|
||||
|
||||
$('#userEditModal').modal('hide');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -231,18 +350,28 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
Client.listUsers(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
Client.getGroups(function (error, result) {
|
||||
if (error) return console.error('Unable to get group listing.', error);
|
||||
|
||||
$scope.users = result.users;
|
||||
$scope.ready = true;
|
||||
$scope.groups = result;
|
||||
|
||||
Client.listUsers(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
$scope.users = result.users;
|
||||
$scope.ready = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
$scope.initTooltip = function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['userAddModal', 'userRemoveModal', 'userEditModal'].forEach(function (id) {
|
||||
['userAddModal', 'userRemoveModal', 'userEditModal', 'groupAddModal', 'groupRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user