Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f5ce651cc | |||
| 6b8d5f92de | |||
| 55e556c725 | |||
| 19bb0a6ec2 | |||
| 290132f432 | |||
| 4a8be8e62d | |||
| 23b61aef0c | |||
| 24cc433a3d | |||
| e014b7de81 | |||
| 0895a2bdea | |||
| 03ca4887ba | |||
| 9eeb17c397 | |||
| 6a5da2745a | |||
| e1111ba2bb | |||
| d186084835 | |||
| 06c2ba9fa9 | |||
| b82e5fd8c6 | |||
| 6e1f96a832 | |||
| f68135c7aa | |||
| f48cbb457b | |||
| 8d192dc992 | |||
| b70324aa24 | |||
| 390afaf614 | |||
| 5112322e7d | |||
| 2cb498d500 | |||
| 2bd6e02cdc | |||
| 85423cbc20 | |||
| 1c0d027bd3 | |||
| 5a8a023039 | |||
| 196b059cfb | |||
| 2d930b9c3d | |||
| a5ba3faa49 | |||
| 02ba91f1bb | |||
| bfa917e057 | |||
| 909dd0725a | |||
| 74860f2d16 | |||
| 132ebb4e74 | |||
| 698158cd93 | |||
| 5bfc684f1b | |||
| c944c9b65b | |||
| d61698b894 | |||
| a4d32009ad | |||
| 3007875e35 | |||
| b4aad138fc | |||
| 8df7eb2acb | |||
| 18cab6f861 | |||
| b2071c65d8 | |||
| 402dba096e | |||
| abf0c81de4 | |||
| 613985a17c | |||
| bfc9801699 | |||
| ee705eb979 | |||
| 67b94c7fde | |||
| 77e5d3f4bb | |||
| 30618b8644 | |||
| 57a2613286 | |||
| e15bd89ba2 | |||
| d2ed816f44 | |||
| e51234928b | |||
| 3aa668aea3 | |||
| 870edab78a | |||
| ebc9d9185d | |||
| 093150d4e3 | |||
| de80a6692d | |||
| c28f564a47 | |||
| eb6a09c2bd | |||
| 19f404e092 | |||
| 55799ebb2d | |||
| fdf4d8fdcf | |||
| 6dc11edafe | |||
| c82ca1c69d | |||
| 7ef3d55cbf | |||
| 44e4f53827 | |||
| 643e490cbb | |||
| e61498c3b6 | |||
| bb6b61d810 | |||
| cff173c2e6 | |||
| 226501d103 | |||
| c5b8b0e3db | |||
| 46878e4363 | |||
| f77682365e | |||
| d9850fa660 | |||
| 9258585746 | |||
| e635aaaa58 | |||
| d0d6725df5 | |||
| 61f4fea9c3 | |||
| 66d59c1d6c | |||
| f9725965e2 | |||
| 4629739a14 | |||
| e9b3a1e99c | |||
| 8ac27b9dc7 | |||
| 2edd434474 | |||
| bebf480321 | |||
| 10c09d9def | |||
| 6ce6b96e5c | |||
| 16a9cae80e | |||
| e865e2ae6d | |||
| 06363a43f9 |
@@ -372,3 +372,13 @@
|
||||
- Fix `cloudron exec` container to have same namespaces as app
|
||||
- Add developmentMode to manifest
|
||||
|
||||
[0.6.3]
|
||||
- Make sending invite for new users optional
|
||||
|
||||
[0.6.4]
|
||||
- Add support for display names
|
||||
- Send invite links to admins for user setup
|
||||
- Enforce stronger passwords
|
||||
|
||||
[0.6.5]
|
||||
- Finalize stronger password requirement
|
||||
|
||||
+1
-10
@@ -39,7 +39,7 @@ gulp.task('3rdparty', function () {
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-update', 'js-error'], function () {});
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-update'], function () {});
|
||||
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
@@ -80,14 +80,6 @@ gulp.task('js-setup', function () {
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-error', function () {
|
||||
gulp.src(['webadmin/src/js/error.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(uglify())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-update', function () {
|
||||
gulp.src(['webadmin/src/js/update.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
@@ -149,7 +141,6 @@ gulp.task('watch', ['default'], function () {
|
||||
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
|
||||
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
|
||||
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
||||
gulp.watch(['webadmin/src/js/error.js'], ['js-error']);
|
||||
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
||||
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
||||
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN displayName VARCHAR(512) DEFAULT ""', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN displayName', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
createdAt VARCHAR(512) NOT NULL,
|
||||
modifiedAt VARCHAR(512) NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
displayName VARCHAR(512) DEFAULT '',
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
|
||||
@@ -58,6 +58,11 @@ server {
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/apps/.*/exec$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 30m;
|
||||
}
|
||||
|
||||
# graphite paths
|
||||
location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
|
||||
+1
-1
@@ -368,7 +368,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || exports.ISTATE_PENDING_CONFIGURE || installationState == exports.ISTATE_PENDING_BACKUP) {
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_CONFIGURE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else {
|
||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||
|
||||
+36
-9
@@ -22,6 +22,7 @@ exports = module.exports = {
|
||||
|
||||
backup: backup,
|
||||
backupApp: backupApp,
|
||||
listBackups: listBackups,
|
||||
|
||||
getLogs: getLogs,
|
||||
|
||||
@@ -56,7 +57,6 @@ var addons = require('./addons.js'),
|
||||
docker = require('./docker.js'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -73,8 +73,6 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
|
||||
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
|
||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
@@ -654,7 +652,7 @@ function exec(appId, options, callback) {
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Tty: options.tty,
|
||||
Cmd: cmd
|
||||
};
|
||||
|
||||
@@ -662,7 +660,7 @@ function exec(appId, options, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var startOptions = {
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
Tty: options.tty,
|
||||
stdin: true // this is a dockerode option that enabled openStdin in the modem
|
||||
};
|
||||
exec.start(startOptions, function(error, stream) {
|
||||
@@ -831,9 +829,9 @@ function backup(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
appdb.exists(appId, function (error, exists) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
@@ -846,13 +844,14 @@ function backup(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(app, addonsToRestore, callback) {
|
||||
function restoreApp(app, addonsToRestore, backupId, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof addonsToRestore, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(app.lastBackupId);
|
||||
|
||||
backups.getRestoreUrl(app.lastBackupId, function (error, result) {
|
||||
backups.getRestoreUrl(backupId, function (error, result) {
|
||||
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));
|
||||
|
||||
@@ -865,3 +864,31 @@ function restoreApp(app, addonsToRestore, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listBackups(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.exists(appId, function (error, exists) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
|
||||
// TODO pagination is not implemented in the backend yet
|
||||
backups.getAllPaged(0, 1000, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var appBackups = [];
|
||||
|
||||
result.forEach(function (backup) {
|
||||
appBackups = appBackups.concat(backup.dependsOn.filter(function (d) {
|
||||
return d.indexOf('appbackup_' + appId) === 0;
|
||||
}));
|
||||
});
|
||||
|
||||
// alphabetic should be sufficient
|
||||
appBackups.sort();
|
||||
|
||||
callback(null, appBackups);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+3
-1
@@ -464,6 +464,8 @@ function restore(app, callback) {
|
||||
return install(app, callback);
|
||||
}
|
||||
|
||||
var backupId = app.lastBackupId;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -499,7 +501,7 @@ function restore(app, callback) {
|
||||
createVolume.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }),
|
||||
apps.restoreApp.bind(null, app, app.manifest.addons),
|
||||
apps.restoreApp.bind(null, app, app.manifest.addons, backupId),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '75, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
+3
-2
@@ -196,10 +196,11 @@ function setTimeZone(ip, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, ip, callback) {
|
||||
function activate(username, password, email, displayName, ip, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -207,7 +208,7 @@ function activate(username, password, email, ip, callback) {
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
user.createOwner(username, password, email, function (error, userObject) {
|
||||
user.createOwner(username, password, email, displayName, function (error, userObject) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
|
||||
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
|
||||
|
||||
@@ -100,6 +100,7 @@ function initConfig() {
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.adminEmail = 'test@cloudron.foo';
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
}
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && developmentMode) ? [ '/bin/sleep', 'infinity' ] : cmd,
|
||||
Cmd: (isAppContainer && developmentMode) ? [ '/bin/bash', '-c', 'echo "Development mode. Use cloudron exec to debug. Sleeping" && sleep infinity' ] : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv),
|
||||
ExposedPorts: isAppContainer ? exposedPorts : { },
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ function start(callback) {
|
||||
cn: entry.id,
|
||||
uid: entry.id,
|
||||
mail: entry.email,
|
||||
displayname: entry.username,
|
||||
displayname: entry.displayName || entry.username,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
memberof: groups
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Admin,
|
||||
|
||||
User with name '<%= username %>' (<%= email %>) was added in the Cloudron at <%= fqdn %>.
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
This user was not invited immediately, he has to get invited manually later, using the "send invite" button in the admin panel.
|
||||
To perform any configuration on behalf of the user, please use this link
|
||||
<%= inviteLink %>
|
||||
It allows to setup a temporary password, which the user will be able to override, once he gets invited.
|
||||
This link will become invalid as soon as the user was invited.
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
User Manager
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -15,8 +15,12 @@ To get started, create your account by visiting the following page:
|
||||
When you visit the above page, you will be prompted to enter a new password.
|
||||
After you have submitted the form, you can login using the new password.
|
||||
|
||||
<% if (invitor && invitor.email) { %>
|
||||
Thank you,
|
||||
<%= invitor.email %>
|
||||
<% } else { %>
|
||||
Thank you
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
|
||||
+41
-4
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
boxUpdateAvailable: boxUpdateAvailable,
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
|
||||
sendInvite: sendInvite,
|
||||
sendCrashNotification: sendCrashNotification,
|
||||
|
||||
appDied: appDied,
|
||||
@@ -20,7 +21,10 @@ exports = module.exports = {
|
||||
FEEDBACK_TYPE_FEEDBACK: 'feedback',
|
||||
FEEDBACK_TYPE_TICKET: 'ticket',
|
||||
FEEDBACK_TYPE_APP: 'app',
|
||||
sendFeedback: sendFeedback
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
_clearMailQueue: _clearMailQueue
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -170,6 +174,9 @@ 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');
|
||||
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
|
||||
@@ -214,11 +221,11 @@ function mailUserEventToAdmins(user, event) {
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(user, invitor) {
|
||||
function sendInvite(user, invitor) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert(typeof invitor === 'object');
|
||||
|
||||
debug('Sending mail for userAdded');
|
||||
debug('Sending invite mail');
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
@@ -237,8 +244,30 @@ function userAdded(user, invitor) {
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
mailUserEventToAdmins(user, 'was added');
|
||||
function userAdded(user, inviteSent) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof inviteSent, 'boolean');
|
||||
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/password/setup.html?reset_token=' + user.resetToken;
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s added in Cloudron %s', user.username, config.fqdn()),
|
||||
text: render('user_added.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, inviteLink: inviteLink, format: 'text' }),
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function userRemoved(username) {
|
||||
@@ -362,3 +391,11 @@ function sendFeedback(user, type, subject, description) {
|
||||
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
function _getMailQueue() {
|
||||
return gMailQueue;
|
||||
}
|
||||
|
||||
function _clearMailQueue() {
|
||||
gMailQueue = [];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<title> Cloudron Login </title>
|
||||
<title> <%= title %> </title>
|
||||
|
||||
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
|
||||
@@ -25,10 +25,16 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (resetForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
|
||||
@@ -25,10 +25,16 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="30" ng-minlength="8" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (setupForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
// From https://www.npmjs.com/package/password-generator
|
||||
|
||||
exports = module.exports = {
|
||||
generate: generate,
|
||||
validate: validate
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
generatePassword = require('password-generator');
|
||||
|
||||
// http://www.w3resource.com/javascript/form/example4-javascript-form-validation-password.html
|
||||
// WARNING!!! if this is changed, the UI parts in the setup and account view have to be adjusted!
|
||||
var gPasswordTestRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/;
|
||||
|
||||
var UPPERCASE_RE = /([A-Z])/g;
|
||||
var LOWERCASE_RE = /([a-z])/g;
|
||||
var NUMBER_RE = /([\d])/g;
|
||||
var SPECIAL_CHAR_RE = /([\?\-])/g;
|
||||
|
||||
function isStrongEnough(password) {
|
||||
var uc = password.match(UPPERCASE_RE);
|
||||
var lc = password.match(LOWERCASE_RE);
|
||||
var n = password.match(NUMBER_RE);
|
||||
var sc = password.match(SPECIAL_CHAR_RE);
|
||||
|
||||
return uc && lc && n && sc;
|
||||
}
|
||||
|
||||
function generate() {
|
||||
var password = '';
|
||||
|
||||
while (!isStrongEnough(password)) password = generatePassword(8, false, /[\w\d\?\-]/);
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
function validate(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (!password.match(gPasswordTestRegExp)) return new Error('Password must be 8-30 character with at least one uppercase, one numeric and one special character');
|
||||
|
||||
return null;
|
||||
}
|
||||
+15
-1
@@ -15,6 +15,7 @@ exports = module.exports = {
|
||||
updateApp: updateApp,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
listBackups: listBackups,
|
||||
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
@@ -357,7 +358,9 @@ function exec(req, res, next) {
|
||||
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
||||
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns }, function (error, duplexStream) {
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -371,3 +374,14 @@ function exec(req, res, next) {
|
||||
res.socket.pipe(duplexStream);
|
||||
});
|
||||
}
|
||||
|
||||
function listBackups(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
apps.listBackups(req.params.id, function (error, result) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,15 +41,17 @@ function activate(req, res, next) {
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
var username = req.body.username;
|
||||
var password = req.body.password;
|
||||
var email = req.body.email;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
debug('activate: username:%s ip:%s', username, ip);
|
||||
|
||||
cloudron.activate(username, password, email, ip, function (error, info) {
|
||||
cloudron.activate(username, password, email, displayName, ip, function (error, info) {
|
||||
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
|
||||
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
|
||||
|
||||
+14
-8
@@ -150,14 +150,16 @@ function sendErrorPageOrRedirect(req, res, message) {
|
||||
if (typeof req.query.returnTo !== 'string') {
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
message: message,
|
||||
title: 'Cloudron Error'
|
||||
});
|
||||
} else {
|
||||
var u = url.parse(req.query.returnTo);
|
||||
if (!u.protocol || !u.host) {
|
||||
return renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message
|
||||
message: 'Invalid request. returnTo query is not a valid URI. ' + message,
|
||||
title: 'Cloudron Error'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,7 +176,8 @@ function sendError(req, res, message) {
|
||||
|
||||
renderTemplate(res, 'error', {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
message: message
|
||||
message: message,
|
||||
title: 'Cloudron Error'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,7 +194,8 @@ function loginForm(req, res) {
|
||||
csrf: req.csrfToken(),
|
||||
applicationName: applicationName,
|
||||
applicationLogo: applicationLogo,
|
||||
error: req.query.error || null
|
||||
error: req.query.error || null,
|
||||
title: applicationName + ' Login'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,7 +241,7 @@ function logout(req, res) {
|
||||
// Form to enter email address to send a password reset request mail
|
||||
// -> GET /api/v1/session/password/resetRequest.html
|
||||
function passwordResetRequestSite(req, res) {
|
||||
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
|
||||
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken(), title: 'Cloudron Password Reset' });
|
||||
}
|
||||
|
||||
// This route is used for above form submission
|
||||
@@ -261,7 +265,7 @@ function passwordResetRequest(req, res, next) {
|
||||
|
||||
// -> GET /api/v1/session/password/sent.html
|
||||
function passwordSentSite(req, res) {
|
||||
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin() });
|
||||
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin(), title: 'Cloudron Password Reset' });
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/setup.html
|
||||
@@ -275,7 +279,8 @@ function passwordSetupSite(req, res, next) {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token
|
||||
resetToken: req.query.reset_token,
|
||||
title: 'Cloudron Password Setup'
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -291,7 +296,8 @@ function passwordResetSite(req, res, next) {
|
||||
adminOrigin: config.adminOrigin(),
|
||||
user: user,
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token
|
||||
resetToken: req.query.reset_token,
|
||||
title: 'Cloudron Password Reset'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-a
|
||||
APP_MANIFEST_1.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
APP_MANIFEST_1.singleUser = true;
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='admin@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
|
||||
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1338', EMAIL_1 ='user@me.com';
|
||||
var token = null; // authentication token
|
||||
var token_1 = null;
|
||||
|
||||
@@ -138,7 +138,7 @@ function setup(done) {
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
@@ -941,14 +941,17 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('did start the app', function (done) {
|
||||
setTimeout(function () {
|
||||
var count = 0;
|
||||
function checkStartState() {
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
expect(!err).to.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
if (res && res.statusCode === 200) return done();
|
||||
if (++count > 50) return done(new Error('Timedout'));
|
||||
setTimeout(checkStartState, 500);
|
||||
});
|
||||
}, 2000); // give some time for docker to settle
|
||||
}
|
||||
|
||||
checkStartState();
|
||||
});
|
||||
|
||||
it('can uninstall app', function (done) {
|
||||
|
||||
@@ -19,7 +19,7 @@ var appdb = require('../../appdb.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
|
||||
@@ -20,7 +20,7 @@ var async = require('async'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
function cleanup(done) {
|
||||
@@ -392,7 +392,7 @@ describe('Clients', function () {
|
||||
var USER_0 = {
|
||||
userId: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
password: 'Strong#$%2345',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
|
||||
@@ -18,7 +18,7 @@ var async = require('async'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
var server;
|
||||
@@ -68,7 +68,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(500);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -81,7 +81,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -107,7 +107,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: '' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -115,12 +115,12 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty name', function (done) {
|
||||
it('fails due to wrong displayName type', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar', name: '' })
|
||||
.send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -133,7 +133,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'invalidemail' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
@@ -147,7 +147,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar', name: 'tester' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
@@ -161,7 +161,7 @@ describe('Cloudron', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(409);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
@@ -17,7 +17,7 @@ var async = require('async'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
|
||||
var server;
|
||||
|
||||
@@ -139,13 +139,14 @@ describe('OAuth2', function () {
|
||||
var USER_0 = {
|
||||
id: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
password: '@#45Strongpassword',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var APP_0 = {
|
||||
@@ -291,7 +292,7 @@ describe('OAuth2', function () {
|
||||
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),
|
||||
function (callback) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, true, '', function (error, userObject) {
|
||||
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, true, '', false, function (error, userObject) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// update the global objects to reflect the new user id
|
||||
@@ -1239,13 +1240,14 @@ describe('Password', function () {
|
||||
var USER_0 = {
|
||||
userId: uuid.v4(),
|
||||
username: 'someusername',
|
||||
password: 'somepassword',
|
||||
password: 'passWord%1234',
|
||||
email: 'some@email.com',
|
||||
admin: true,
|
||||
salt: 'somesalt',
|
||||
createdAt: (new Date()).toUTCString(),
|
||||
modifiedAt: (new Date()).toUTCString(),
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
// make csrf always succeed for testing
|
||||
@@ -1415,7 +1417,7 @@ describe('Password', function () {
|
||||
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
|
||||
.send({ password: 'somepassword', resetToken: USER_0.resetToken })
|
||||
.send({ password: 'ASF23$%somepassword', resetToken: USER_0.resetToken })
|
||||
.end(function (error, result) {
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('SimpleAuth API', function () {
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
|
||||
|
||||
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
|
||||
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
|
||||
var APP_0 = {
|
||||
id: 'app0',
|
||||
|
||||
@@ -10,6 +10,7 @@ var config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../../mailer.js'),
|
||||
superagent = require('superagent'),
|
||||
nock = require('nock'),
|
||||
server = require('../../server.js'),
|
||||
@@ -17,7 +18,7 @@ var config = require('../../config.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'password', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com';
|
||||
var USERNAME_0 = 'admin', PASSWORD = 'Foobar?1337', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com';
|
||||
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'tao@zen.mac';
|
||||
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar';
|
||||
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@foo.bar';
|
||||
@@ -26,6 +27,9 @@ var server;
|
||||
function setup(done) {
|
||||
server.start(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
userdb._clear(done);
|
||||
});
|
||||
}
|
||||
@@ -34,10 +38,21 @@ function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
expect(mailer._getMailQueue().length).to.equal(number);
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
describe('User API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
@@ -213,15 +228,45 @@ describe('User API', function () {
|
||||
});
|
||||
|
||||
it('create second user succeeds', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1 })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// 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() + 10000, '*', done);
|
||||
checkMails(2, function () {
|
||||
// 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() + 10000, '*', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reinvite unknown user fails', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(res.statusCode).to.equal(404);
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('reinvite second user succeeds', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/invite')
|
||||
.query({ access_token: token })
|
||||
.send({})
|
||||
.end(function (err, res) {
|
||||
expect(err).to.not.be.ok();
|
||||
expect(res.statusCode).to.equal(200);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,21 +341,36 @@ describe('User API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
it('create user missing invite fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create second and third user', function (done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3 })
|
||||
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
|
||||
checkMails(3, function () {
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -331,7 +391,7 @@ 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 })
|
||||
.send({ username: USERNAME_2, email: EMAIL, invite: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
@@ -493,7 +553,7 @@ describe('User API', function () {
|
||||
it('change password fails due to wrong password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: 'some wrong password', newPassword: 'newpassword' })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
@@ -513,7 +573,7 @@ describe('User API', function () {
|
||||
it('change password succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, newPassword: 'new_password' })
|
||||
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
done();
|
||||
|
||||
+19
-4
@@ -12,11 +12,12 @@ exports = module.exports = {
|
||||
changeAdmin: changeAdmin,
|
||||
remove: removeUser,
|
||||
verifyPassword: verifyPassword,
|
||||
requireAdmin: requireAdmin
|
||||
requireAdmin: requireAdmin,
|
||||
sendInvite: sendInvite
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
generatePassword = require('password-generator'),
|
||||
generatePassword = require('../password.js').generate,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
user = require('../user.js'),
|
||||
@@ -44,12 +45,16 @@ function createUser(req, res, next) {
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
|
||||
var username = req.body.username;
|
||||
var password = generatePassword(8, true /* memorable */);
|
||||
var password = generatePassword();
|
||||
var email = req.body.email;
|
||||
var sendInvite = req.body.invite;
|
||||
var displayName = req.body.displayName || '';
|
||||
|
||||
user.create(username, password, email, false /* admin */, req.user /* creator */, function (error, user) {
|
||||
user.create(username, password, email, displayName, false /* admin */, req.user /* creator */, 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'));
|
||||
@@ -188,3 +193,13 @@ function requireAdmin(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function sendInvite(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.userId, 'string');
|
||||
|
||||
user.sendInvite(req.params.userId, function (error) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
+6
-1
@@ -34,7 +34,7 @@ function initializeExpressSync() {
|
||||
var QUERY_LIMIT = '10mb', // max size for json and urlencoded queries
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests
|
||||
var REQUEST_TIMEOUT = 10000; // timeout for all requests (see also setTimeout on the httpServer)
|
||||
|
||||
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||||
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||||
@@ -108,6 +108,7 @@ function initializeExpressSync() {
|
||||
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.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
|
||||
|
||||
// form based login routes used by oauth2 frame
|
||||
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
|
||||
@@ -143,6 +144,7 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.updateApp);
|
||||
router.post('/api/v1/apps/:id/restore', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.restoreApp);
|
||||
router.post('/api/v1/apps/:id/backup', appsScope, routes.user.requireAdmin, routes.apps.backupApp);
|
||||
router.get ('/api/v1/apps/:id/backups', appsScope, routes.user.requireAdmin, routes.apps.listBackups);
|
||||
router.post('/api/v1/apps/:id/stop', appsScope, routes.user.requireAdmin, routes.apps.stopApp);
|
||||
router.post('/api/v1/apps/:id/start', appsScope, routes.user.requireAdmin, routes.apps.startApp);
|
||||
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
|
||||
@@ -170,6 +172,9 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.create);
|
||||
|
||||
// disable server timeout. we use the timeout middleware to handle timeouts on a route level
|
||||
httpServer.setTimeout(0);
|
||||
|
||||
// upgrade handler
|
||||
httpServer.on('upgrade', function (req, socket, head) {
|
||||
if (req.headers['upgrade'] !== 'tcp') return req.end('Only TCP upgrades are possible');
|
||||
|
||||
+2
-1
@@ -53,7 +53,8 @@ function getAllPaged(backupConfig, page, perPage, callback) {
|
||||
var results = data.Contents.map(function (backup) {
|
||||
return {
|
||||
creationTime: backup.LastModified,
|
||||
restoreKey: backup.Key.slice(backupConfig.prefix.length + 1)
|
||||
restoreKey: backup.Key.slice(backupConfig.prefix.length + 1),
|
||||
dependsOn: [] // FIXME empty dependsOn is wrong and version property is missing!!
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ describe('database', function () {
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: ''
|
||||
};
|
||||
|
||||
var ADMIN_0 = {
|
||||
@@ -52,7 +53,8 @@ describe('database', function () {
|
||||
salt: 'tata',
|
||||
createdAt: 'sometime back',
|
||||
modifiedAt: 'now',
|
||||
resetToken: ''
|
||||
resetToken: '',
|
||||
displayName: 'Herbert Heidelberg'
|
||||
};
|
||||
|
||||
it('can add user', function (done) {
|
||||
@@ -154,10 +156,11 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('can update the user', function (done) {
|
||||
userdb.update(USER_0.id, { email: 'some@thing.com' }, function (error) {
|
||||
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) {
|
||||
expect(user.email).to.equal('some@thing.com');
|
||||
expect(user.displayName).to.equal('Heiter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,16 @@ var database = require('../database.js'),
|
||||
|
||||
var USER_0 = {
|
||||
username: 'foobar0',
|
||||
password: 'password0',
|
||||
email: 'foo0@bar.com'
|
||||
password: 'Foobar?1234',
|
||||
email: 'foo0@bar.com',
|
||||
displayName: 'Bob bobson'
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
username: 'foobar1',
|
||||
password: 'password1',
|
||||
email: 'foo1@bar.com'
|
||||
password: 'Foobar?12345',
|
||||
email: 'foo1@bar.com',
|
||||
displayName: 'Jesus'
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
@@ -32,8 +34,8 @@ function setup(done) {
|
||||
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, true, null),
|
||||
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, false, USER_0)
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+108
-17
@@ -8,6 +8,7 @@
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
user = require('../user.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
UserError = user.UserError;
|
||||
@@ -16,20 +17,26 @@ var USERNAME = 'nobody';
|
||||
var USERNAME_NEW = 'nobodynew';
|
||||
var EMAIL = 'nobody@no.body';
|
||||
var EMAIL_NEW = 'nobodynew@no.body';
|
||||
var PASSWORD = 'foobar';
|
||||
var NEW_PASSWORD = 'somenewpassword';
|
||||
var PASSWORD = 'sTrOnG#$34134';
|
||||
var NEW_PASSWORD = 'oTHER@#$235';
|
||||
var IS_ADMIN = true;
|
||||
var DISPLAY_NAME = 'Nobody cares';
|
||||
var userObject = null;
|
||||
|
||||
function cleanupUsers(done) {
|
||||
userdb._clear(function () {
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function createUser(done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
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();
|
||||
|
||||
userObject = result;
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
@@ -38,14 +45,28 @@ function setup(done) {
|
||||
// ensure data/config/mount paths
|
||||
database.initialize(function (error) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
mailer._clearMailQueue();
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
mailer._clearMailQueue();
|
||||
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
expect(mailer._getMailQueue().length).to.equal(number);
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
describe('User', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
@@ -54,14 +75,55 @@ describe('User', function () {
|
||||
before(cleanupUsers);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
it('fails due to short password', function (done) {
|
||||
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to missing numerics in password', function (done) {
|
||||
user.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).to.not.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds and attempts to send invite', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, true, 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);
|
||||
|
||||
done();
|
||||
// first user is owner, do not send mail to admins
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,12 +143,15 @@ describe('User', function () {
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, {});
|
||||
}).to.throwException();
|
||||
expect(function () {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, false, null, 'foobar');
|
||||
}).to.throwException();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('fails because user exists', function (done) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
user.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.ALREADY_EXISTS);
|
||||
@@ -96,7 +161,7 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
it('fails because password is empty', function (done) {
|
||||
user.create(USERNAME, '', EMAIL, IS_ADMIN, null /* invitor */, function (error, result) {
|
||||
user.create(USERNAME, '', EMAIL, DISPLAY_NAME, IS_ADMIN, null /* invitor */, false, function (error, result) {
|
||||
expect(error).to.be.ok();
|
||||
expect(result).not.to.be.ok();
|
||||
expect(error.reason).to.equal(UserError.BAD_PASSWORD);
|
||||
@@ -302,17 +367,19 @@ describe('User', function () {
|
||||
it('make second user admin succeeds', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'foobar',
|
||||
password: 'ASDFkljsf#$^%2354',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, false, { username: USERNAME, email: EMAIL } /* invitor */, function (error, result) {
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, false, { username: USERNAME, email: EMAIL } /* invitor */, false, function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
user.changeAdmin(user1.username, true, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
|
||||
// one mail for user creation, one mail for admin change
|
||||
checkMails(2, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -320,7 +387,8 @@ describe('User', function () {
|
||||
it('succeeds to remove admin flag of first user', function (done) {
|
||||
user.changeAdmin(USERNAME, false, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -341,11 +409,11 @@ describe('User', function () {
|
||||
it('succeeds for two admins', function (done) {
|
||||
var user1 = {
|
||||
username: 'seconduser',
|
||||
password: 'foobar',
|
||||
password: 'Adfasdkjf#$%43',
|
||||
email: 'some@thi.ng'
|
||||
};
|
||||
|
||||
user.create(user1.username, user1.password, user1.email, false, { username: USERNAME, email: EMAIL } /* invitor */, function (error, result) {
|
||||
user.create(user1.username, user1.password, user1.email, DISPLAY_NAME, false, { username: USERNAME, email: EMAIL } /* invitor */, false, function (error, result) {
|
||||
expect(error).to.eql(null);
|
||||
expect(result).to.be.ok();
|
||||
|
||||
@@ -357,7 +425,9 @@ describe('User', function () {
|
||||
expect(admins.length).to.equal(2);
|
||||
expect(admins[0].username).to.equal(USERNAME);
|
||||
expect(admins[1].username).to.equal(user1.username);
|
||||
done();
|
||||
|
||||
// one mail for user creation one mail for admin change
|
||||
checkMails(2, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -450,14 +520,35 @@ describe('User', function () {
|
||||
it('succeeds with email', function (done) {
|
||||
user.resetPasswordByIdentifier(EMAIL, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with username', function (done) {
|
||||
user.resetPasswordByIdentifier(USERNAME, function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
done();
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('send invite', function () {
|
||||
before(createUser);
|
||||
after(cleanupUsers);
|
||||
|
||||
it('fails for unknown user', function (done) {
|
||||
user.sendInvite('unknown user', function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.NOT_FOUND);
|
||||
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
user.sendInvite(userObject.id, function (error) {
|
||||
expect(error).to.eql(null);
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+46
-19
@@ -19,7 +19,8 @@ exports = module.exports = {
|
||||
changePassword: changePassword,
|
||||
update: updateUser,
|
||||
createOwner: createOwner,
|
||||
getOwner: getOwner
|
||||
getOwner: getOwner,
|
||||
sendInvite: sendInvite
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -30,6 +31,7 @@ var assert = require('assert'),
|
||||
userdb = require('./userdb.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
validatePassword = require('./password.js').validate,
|
||||
util = require('util'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
@@ -89,14 +91,6 @@ function validateUsername(username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
if (password.length < 5) return new UserError(UserError.BAD_PASSWORD, 'Password must be atleast 5 chars');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateEmail(email) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
|
||||
@@ -113,23 +107,34 @@ function validateToken(token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, admin, invitor, callback) {
|
||||
function validateDisplayName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function createUser(username, password, email, displayName, admin, invitor, sendInvite, 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');
|
||||
|
||||
var error = validateUsername(username);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePassword(password);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
error = validateEmail(email);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateDisplayName(displayName);
|
||||
if (error) return callback(error);
|
||||
|
||||
crypto.randomBytes(CRYPTO_SALT_SIZE, function (error, salt) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -146,7 +151,8 @@ function createUser(username, password, email, admin, invitor, callback) {
|
||||
salt: salt.toString('hex'),
|
||||
createdAt: now,
|
||||
modifiedAt: now,
|
||||
resetToken: hat(256)
|
||||
resetToken: hat(256),
|
||||
displayName: displayName
|
||||
};
|
||||
|
||||
userdb.add(user.id, user, function (error) {
|
||||
@@ -155,9 +161,9 @@ function createUser(username, password, email, admin, invitor, callback) {
|
||||
|
||||
callback(null, user);
|
||||
|
||||
// only send welcome mail if user is not an admin. This is only the case for the first user!
|
||||
// The welcome email contains a link to create a new password
|
||||
if (!user.admin) mailer.userAdded(user, invitor);
|
||||
// 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 (sendInvite) mailer.sendInvite(user, invitor);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -332,7 +338,7 @@ function setPassword(userId, newPassword, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
userdb.get(userId, function (error, user) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
@@ -375,7 +381,7 @@ function changePassword(username, oldPassword, newPassword, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validatePassword(newPassword);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new UserError(UserError.BAD_PASSWORD, error.message));
|
||||
|
||||
verify(username, oldPassword, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
@@ -384,12 +390,12 @@ function changePassword(username, oldPassword, newPassword, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createOwner(username, password, email, callback) {
|
||||
function createOwner(username, password, email, displayName, callback) {
|
||||
userdb.count(function (error, count) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS));
|
||||
|
||||
createUser(username, password, email, true /* admin */, null /* invitor */, callback);
|
||||
createUser(username, password, email, displayName, true /* admin */, null /* invitor */, false /* sendInvite */, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,3 +407,24 @@ function getOwner(callback) {
|
||||
return callback(null, owner);
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvite(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
userdb.get(userId, function (error, userObject) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
userObject.resetToken = hat(256);
|
||||
|
||||
userdb.update(userId, userObject, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
mailer.sendInvite(userObject, null);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:userdb'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'admin', 'resetToken' ].join(',');
|
||||
var USERS_FIELDS = [ 'id', 'username', 'email', 'password', 'salt', 'createdAt', 'modifiedAt', 'admin', 'resetToken', 'displayName' ].join(',');
|
||||
|
||||
function get(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
@@ -113,10 +113,11 @@ function add(userId, user, callback) {
|
||||
assert.strictEqual(typeof user.createdAt, 'string');
|
||||
assert.strictEqual(typeof user.modifiedAt, 'string');
|
||||
assert.strictEqual(typeof user.resetToken, 'string');
|
||||
assert.strictEqual(typeof user.displayName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ userId, user.username, user.password, user.email, user.admin, user.salt, user.createdAt, user.modifiedAt, user.resetToken ];
|
||||
database.query('INSERT INTO users (id, username, password, email, admin, salt, createdAt, modifiedAt, resetToken) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
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));
|
||||
@@ -199,4 +200,3 @@ function adminCount(callback) {
|
||||
return callback(null, result[0].total);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -33,6 +33,7 @@
|
||||
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
|
||||
$scope.webServerOriginLink = '/';
|
||||
$scope.errorMessage = '';
|
||||
$scope.statusOk = false;
|
||||
|
||||
// try to fetch at least config.json to get appstore url
|
||||
$http.get('/config.json').success(function(data, status) {
|
||||
@@ -43,6 +44,15 @@
|
||||
else console.error(status, data);
|
||||
});
|
||||
|
||||
// try to fetch the cloudron status
|
||||
$http.get('/api/v1/cloudron/status').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
|
||||
$scope.statusOk = true;
|
||||
}).error(function (data, status) {
|
||||
console.error(status, data);
|
||||
$scope.statusOk = false;
|
||||
});
|
||||
|
||||
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.errorCode = search.errorCode || 0;
|
||||
@@ -62,7 +72,8 @@
|
||||
|
||||
<div ng-show="errorCode == 0">
|
||||
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
|
||||
Please check your cloudron status <a href="{{ webServerOriginLink }}">here</a>.
|
||||
<span ng-hide="statusOk">Please check your cloudron status <a href="{{ webServerOriginLink }}">here</a>.</span>
|
||||
<span ng-show="statusOk">Please try again reloading the page <a href="/">here</a>.</span>
|
||||
</div>
|
||||
|
||||
<div ng-show="errorCode == 1">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="/3rdparty/slick.css"/>
|
||||
@@ -86,15 +86,13 @@
|
||||
<br/>
|
||||
<fieldset>
|
||||
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
|
||||
<div class="form-group" ng-class="{ 'has-error': (update_form.password.$dirty && update_form.password.$invalid) || (!update_form.password.$dirty && update.error.password) }">
|
||||
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!update_form.password.$dirty && update.error.password) || (update_form.password.$dirty && update_form.password.$invalid)">
|
||||
<small ng-show="update_form.password.$error.required && !update.error.password">A password is required</small>
|
||||
<small ng-show="update_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="update_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="update.error.password">Incorrect password</small>
|
||||
<div class="control-label" ng-show="(update_form.password.$dirty && update_form.password.$invalid) || (!update_form.password.$dirty && update.error.password)">
|
||||
<small ng-show=" update_form.password.$dirty && update_form.password.$invalid">Password required</small>
|
||||
<small ng-show="!update_form.password.$dirty && update.error.password">Wrong password</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="update.password" id="inputUpdatePassword" name="password" placeholder="Password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="update.password" id="inputUpdatePassword" name="password" placeholder="Password" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="update_form.$invalid || update.busy"/>
|
||||
</form>
|
||||
@@ -121,7 +119,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
|
||||
<a class="navbar-brand navbar-brand-icon" href="#/"><img ng-src="{{ client.avatar }}" width="40" height="40"/></a>
|
||||
<a class="navbar-brand" href="#/">Cloudron</a>
|
||||
</div>
|
||||
<!-- /.navbar-header -->
|
||||
|
||||
+36
-15
@@ -86,6 +86,9 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
this._clientId = '<%= oauth.clientId %>';
|
||||
this._clientSecret = '<%= oauth.clientSecret %>';
|
||||
this.apiOrigin = '<%= oauth.apiOrigin %>';
|
||||
this.avatar = '';
|
||||
|
||||
this.resetAvatar();
|
||||
|
||||
this.setToken(localStorage.token);
|
||||
}
|
||||
@@ -112,15 +115,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
Client.notify('title', 'message', true, actionScope);
|
||||
|
||||
*/
|
||||
Client.prototype.notify = function (title, message, delay, actionScope) {
|
||||
var options = { title: title, message: message, delay: delay};
|
||||
Client.prototype.notify = function (title, message, persitent, type, actionScope) {
|
||||
var options = { title: title, message: message};
|
||||
|
||||
if (persitent) options.delay = 'never'; // any non Number means never timeout
|
||||
|
||||
if (actionScope) {
|
||||
if (typeof actionScope.action !== 'string') throw('an actionScope has to have an action url');
|
||||
options.scope = actionScope;
|
||||
}
|
||||
|
||||
Notification.error(options);
|
||||
if (type === 'error') Notification.error(options);
|
||||
else if (type === 'success') Notification.success(options);
|
||||
else if (type === 'info') Notification.info(options);
|
||||
else throw('Invalid notification type "' + type + '"');
|
||||
};
|
||||
|
||||
Client.prototype.setReady = function () {
|
||||
@@ -142,6 +150,13 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
callback(this._config);
|
||||
};
|
||||
|
||||
Client.prototype.resetAvatar = function () {
|
||||
this.avatar = this.apiOrigin + '/api/v1/cloudron/avatar?' + String(Math.random()).slice(2);
|
||||
|
||||
var favicon = $('#favicon');
|
||||
if (favicon) favicon.attr('href', this.avatar);
|
||||
};
|
||||
|
||||
Client.prototype.setUserInfo = function (userInfo) {
|
||||
// In order to keep the angular bindings alive, set each property individually
|
||||
this._userInfo.id = userInfo.id;
|
||||
@@ -402,21 +417,24 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
};
|
||||
|
||||
Client.prototype.getAppLogStream = function (appId) {
|
||||
var source = new EventSource('/api/v1/apps/' + appId + '/logstream');
|
||||
var source = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream');
|
||||
return source;
|
||||
};
|
||||
|
||||
Client.prototype.getAppLogUrl = function (appId) {
|
||||
return '/api/v1/apps/' + appId + '/logs?access_token=' + this._token;
|
||||
};
|
||||
|
||||
Client.prototype.getAppIconUrls = function (app) {
|
||||
return {
|
||||
cloudron: this.apiOrigin + app.iconUrl + '?access_token=' + this._token,
|
||||
store: this._config.apiServerOrigin + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon'
|
||||
cloudron: app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + this._token) : null,
|
||||
store: app.appStoreId ? (this._config.apiServerOrigin + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon') : null
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.sendInvite = function (username, callback) {
|
||||
$http.post(client.apiOrigin + '/api/v1/users/' + username + '/invite', {}).success(function (data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setAdmin = function (username, admin, callback) {
|
||||
var payload = {
|
||||
username: username,
|
||||
@@ -429,11 +447,12 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.createAdmin = function (username, password, email, setupToken, callback) {
|
||||
Client.prototype.createAdmin = function (username, password, email, displayName, setupToken, callback) {
|
||||
var payload = {
|
||||
username: username,
|
||||
password: password,
|
||||
email: email
|
||||
email: email,
|
||||
displayName: displayName
|
||||
};
|
||||
|
||||
var that = this;
|
||||
@@ -532,10 +551,12 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.createUser = function (username, email, callback) {
|
||||
Client.prototype.createUser = function (username, email, displayName, sendInvite, callback) {
|
||||
var data = {
|
||||
username: username,
|
||||
email: email
|
||||
email: email,
|
||||
displayName: displayName,
|
||||
invite: !!sendInvite
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/users', data).success(function(data, status) {
|
||||
@@ -549,7 +570,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
password: password
|
||||
};
|
||||
|
||||
$http({ method: 'DELETE', url: '/api/v1/users/' + userId, data: data, headers: { 'Content-Type': 'application/json' }}).success(function(data, status) {
|
||||
$http({ method: 'DELETE', url: client.apiOrigin + '/api/v1/users/' + userId, data: data, headers: { 'Content-Type': 'application/json' }}).success(function(data, status) {
|
||||
if (status !== 204) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', []);
|
||||
|
||||
app.controller('ErrorController', ['$scope', '$http', function ($scope, $http) {
|
||||
$scope.webServerOriginLink = '/';
|
||||
$scope.errorMessage = '';
|
||||
|
||||
// try to fetch at least config.json to get appstore url
|
||||
$http.get('config.json').success(function(data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
|
||||
$scope.webServerOriginLink = data.webServerOrigin + '/console.html';
|
||||
}).error(function (data, status) {
|
||||
if (status === 404) console.error('No config.json found');
|
||||
else console.error(status, data);
|
||||
});
|
||||
|
||||
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.errorCode = search.errorCode || 0;
|
||||
$scope.errorContext = search.errorContext || '';
|
||||
}]);
|
||||
@@ -5,6 +5,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = {};
|
||||
$scope.client = Client;
|
||||
|
||||
$scope.update = {
|
||||
busy: false,
|
||||
@@ -53,8 +54,9 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
Client.update($scope.update.password, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.update.error.password = 'Incorrect password';
|
||||
$scope.update.error.password = true;
|
||||
$scope.update.password = '';
|
||||
$scope.update_form.password.$setPristine();
|
||||
$('#inputUpdatePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to update.', error);
|
||||
@@ -82,6 +84,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
|
||||
Client.refreshUserInfo(function (error, result) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
@@ -114,7 +117,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
if (result.provider === 'route53' && (!result.accessKeyId || !result.secretAccessKey)) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/certs';
|
||||
Client.notify('Missing AWS credentials', 'Please provide AWS credentials, click here to add them.', true, actionScope);
|
||||
Client.notify('Missing AWS credentials', 'Please provide AWS credentials, click here to add them.', true, 'error', actionScope);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ app.service('Wizard', [ function () {
|
||||
this.username = '';
|
||||
this.email = '';
|
||||
this.password = '';
|
||||
this.displayName = '';
|
||||
this.setupToken = null;
|
||||
this.provider = null;
|
||||
this.availableAvatars = [{
|
||||
@@ -221,7 +222,7 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
|
||||
app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', function ($scope, $location, Wizard, Client) {
|
||||
$scope.wizard = Wizard;
|
||||
|
||||
Client.createAdmin(Wizard.username, Wizard.password, Wizard.email, Wizard.setupToken, function (error) {
|
||||
Client.createAdmin(Wizard.username, Wizard.password, Wizard.email, Wizard.displayName, Wizard.setupToken, function (error) {
|
||||
if (error) {
|
||||
console.error('Internal error', error);
|
||||
window.location.href = '/error.html';
|
||||
@@ -286,6 +287,7 @@ app.controller('SetupController', ['$scope', '$location', 'Client', 'Wizard', fu
|
||||
}
|
||||
|
||||
Wizard.email = search.email;
|
||||
Wizard.displayName = search.displayName;
|
||||
Wizard.requireEmail = !search.email;
|
||||
Wizard.provider = status.provider;
|
||||
|
||||
|
||||
@@ -8,34 +8,29 @@
|
||||
<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_form.password.$invalid)}">
|
||||
<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.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="passwordchange_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="passwordchange_form.password.$dirty && passwordchange.error.password">Invalid pasword</small>
|
||||
<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" ng-maxlength="512" ng-minlength="5" 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': (passwordchange_form.newPassword.$dirty && passwordchange_form.newPassword.$invalid)}">
|
||||
<label class="control-label" for="inputnewpassword">New Password</label>
|
||||
<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.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.newPassword.$error.minlength">The password is too short</small>
|
||||
<small ng-show="passwordchange_form.newPassword.$error.maxlength">The password is too long</small>
|
||||
<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="inputnewpassword" name="newPassword" ng-maxlength="512" ng-minlength="5" 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': (passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$invalid) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputnewpasswordrepeat">Repeat New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.newPasswordRepeat.$dirty && passwordchange.error.newPasswordRepeat) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$invalid) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$error.minlength">The password is too short</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat">Passwords don't match</small>
|
||||
<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="inputnewpasswordrepeat" name="newPasswordRepeat" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<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="passwordchange_form.$invalid"/>
|
||||
</fieldset>
|
||||
@@ -65,17 +60,15 @@
|
||||
<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" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<input type="email" class="form-control" ng-model="emailchange.email" id="inputEmailChangeEmail" name="email" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailchange_form.password.$dirty && emailchange_form.password.$invalid)}">
|
||||
<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.error.password) || (emailchange_form.password.$dirty && emailchange_form.password.$invalid)">
|
||||
<small ng-show="emailchange_form.password.$error.required">A password is required</small>
|
||||
<small ng-show="emailchange_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="emailchange_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="emailchange_form.password.$dirty && emailchange.error.password">Invalid pasword</small>
|
||||
<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" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<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>
|
||||
|
||||
@@ -60,6 +60,14 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
|
||||
$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);
|
||||
}
|
||||
@@ -83,6 +91,7 @@ angular.module('Application').controller('AccountController', ['$scope', '$locat
|
||||
if (error.statusCode === 403) {
|
||||
$scope.emailchange.error.password = true;
|
||||
$scope.emailchange.password = '';
|
||||
$scope.emailchange_form.password.$setPristine();
|
||||
$('#inputEmailChangePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change email.', error);
|
||||
|
||||
@@ -38,9 +38,13 @@
|
||||
</div>
|
||||
<div class="form-group" ng-show="appConfigure.app.manifest.singleUser">
|
||||
<label class="control-label">User</label>
|
||||
<p>This is a single user application. Access is granted to <b>{{appConfigure.app.accessRestriction.users[0]}}</b>.</p>
|
||||
<p>
|
||||
This is a single user application.<br/><br/>
|
||||
Access is granted to <b>{{appConfigure.app.accessRestriction.users[0]}}</b>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<!-- 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>
|
||||
@@ -48,8 +52,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
|
||||
@@ -78,7 +80,11 @@
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
|
||||
<label class="control-label" for="appConfigurePasswordInput">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appConfigure.password" id="appConfigurePasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required>
|
||||
<div class="control-label" ng-show="(appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password)">
|
||||
<small ng-show=" appConfigureForm.password.$dirty && appConfigureForm.password.$invalid">Password required</small>
|
||||
<small ng-show="!appConfigureForm.password.$dirty && appConfigure.error.password">Wrong password</small>
|
||||
</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"/>
|
||||
</form>
|
||||
@@ -104,9 +110,13 @@
|
||||
<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">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appRestoreForm.password.$dirty && appRestore.error.password) || (appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) }">
|
||||
<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>
|
||||
<input type="password" class="form-control" ng-model="appRestore.password" id="appRestorePasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<div class="control-label" ng-show="(appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password)">
|
||||
<small ng-show=" appRestoreForm.password.$dirty && appRestoreForm.password.$invalid">Password required</small>
|
||||
<small ng-show="!appRestoreForm.password.$dirty && appRestore.error.password">Wrong password</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="appRestore.password" id="appRestorePasswordInput" name="password" required autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="appRestoreForm.$invalid || busy"/>
|
||||
@@ -150,9 +160,13 @@
|
||||
<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">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appUninstallForm.password.$dirty && appUninstall.error.password) || (appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) }">
|
||||
<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>
|
||||
<input type="password" class="form-control" ng-model="appUninstall.password" id="appUninstallPasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<div class="control-label" ng-show="(appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password)">
|
||||
<small ng-show=" appUninstallForm.password.$dirty && appUninstallForm.password.$invalid">Password required</small>
|
||||
<small ng-show="!appUninstallForm.password.$dirty && appUninstall.error.password">Wrong password</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="appUninstall.password" id="appUninstallPasswordInput" name="password" required autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUninstallForm.$invalid || busy"/>
|
||||
@@ -196,7 +210,7 @@
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appUpdateForm.password.$dirty && appUpdate.error.password) || (appUpdateForm.password.$dirty && appUpdateForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputUpdatePassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="30" ng-minlength="8" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
|
||||
</form>
|
||||
@@ -243,7 +257,7 @@
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{app.iconUrl}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -272,12 +286,7 @@
|
||||
<i class="fa fa-wrench scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-center">
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<a href="" ng-click="showUpdate(app)" class="ng-hide animateMe" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<i class="fa fa-arrow-up text-success scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-center"></div>
|
||||
<div class="col-xs-4 text-right">
|
||||
<a href="" ng-click="showUninstall(app)">
|
||||
<i class="fa fa-remove scale"></i>
|
||||
@@ -300,7 +309,7 @@
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && (app | installSuccess)">
|
||||
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -181,8 +181,9 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigureForm.location.$setPristine();
|
||||
$('#appConfigureLocationInput').focus();
|
||||
} else if (error.statusCode === 403) {
|
||||
$scope.appConfigure.error.password = 'Wrong password provided.';
|
||||
$scope.appConfigure.error.password = true;
|
||||
$scope.appConfigure.password = '';
|
||||
$scope.appConfigureForm.password.$setPristine();
|
||||
$('#appConfigurePasswordInput').focus();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appConfigure.error.cert = error.message;
|
||||
@@ -232,6 +233,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appRestore.password = '';
|
||||
$scope.appRestore.error.password = true;
|
||||
$scope.appRestoreForm.password.$setPristine();
|
||||
$('#appRestorePasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
@@ -260,6 +262,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appUninstall.password = '';
|
||||
$scope.appUninstall.error.password = true;
|
||||
$scope.appUninstallForm.password.$setPristine();
|
||||
$('#appUninstallPasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
<div class="modal-body">
|
||||
<form name="developerModeChangeForm" class="form-signin" role="form" novalidate ng-submit="doChangeDeveloperMode(developerModeChangeForm)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (developerModeChangeForm.password.$dirty && developerModeChangeForm.password.$invalid)}">
|
||||
<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>
|
||||
<div class="control-label" ng-show="(!developerModeChangeForm.password.$dirty && developerModeChange.error.password) || (developerModeChangeForm.password.$dirty && developerModeChangeForm.password.$invalid)">
|
||||
<small ng-show="developerModeChangeForm.password.$error.required">A password is required</small>
|
||||
<small ng-show="developerModeChangeForm.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="developerModeChangeForm.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="developerModeChangeForm.password.$dirty && developerModeChange.error.password">Invalid pasword</small>
|
||||
<small ng-show=" developerModeChangeForm.password.$dirty && developerModeChangeForm.password.$invalid">A password is required</small>
|
||||
<small ng-show="!developerModeChangeForm.password.$dirty && developerModeChange.error.password">Wrong password</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="developerModeChange.password" id="inputDeveloperModeChangePassword" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="developerModeChange.password" id="inputDeveloperModeChangePassword" name="password" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="developerModeChangeForm.$invalid"/>
|
||||
</fieldset>
|
||||
@@ -40,7 +38,7 @@
|
||||
<h4 class="modal-title">Change your Cloudron Avatar</h4>
|
||||
</div>
|
||||
<div class="modal-body settings-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{avatarChange.avatar.data || avatarChange.avatar.url || avatar.data || avatar.url}}"/>
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{avatarChange.avatar.data || avatarChange.avatar.url || client.avatar}}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
|
||||
|
||||
<br/>
|
||||
@@ -94,7 +92,7 @@
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
|
||||
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{ client.avatar }}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,3 +167,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('SettingsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$rootScope', 'Client', function ($scope, $location, $rootScope, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = {};
|
||||
|
||||
$scope.lastBackup = null;
|
||||
$scope.backups = [];
|
||||
$scope.avatar = {
|
||||
data: null,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.developerModeChange = {
|
||||
busy: false,
|
||||
@@ -129,6 +126,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
if (error.statusCode === 403) {
|
||||
$scope.developerModeChange.error.password = true;
|
||||
$scope.developerModeChange.password = '';
|
||||
$scope.developerModeChangeForm.password.$setPristine();
|
||||
$('#inputDeveloperModeChangePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change developer mode.', error);
|
||||
@@ -188,11 +186,11 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
if (error) {
|
||||
console.error('Unable to change developer mode.', error);
|
||||
} else {
|
||||
// Do soft reload, since the browser will not update the avatar URLs in the UI
|
||||
window.location.reload();
|
||||
Client.resetAvatar();
|
||||
}
|
||||
|
||||
$scope.avatarChange.busy = false;
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
avatarChangeReset();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -262,8 +260,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchBackups();
|
||||
|
||||
$scope.avatar.url = ($scope.config.isCustomDomain ? '//my.' : '//my-') + $scope.config.fqdn + '/api/v1/cloudron/avatar';
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
<input type="email" class="form-control" ng-model="wizard.email" id="inputEmail" name="email" placeholder="Email" ng-enter="focusNext('inputPassword', setup_form.email.$invalid)" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
|
||||
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next(setup_form.username.$invalid || setup_form.password.$invalid || setup_form.email.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
|
||||
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next(setup_form.username.$invalid || setup_form.password.$invalid || setup_form.email.$invalid)" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required autocomplete="off">
|
||||
<div class="control-label" ng-show="setup_form.password.$dirty && setup_form.password.$invalid">
|
||||
<small ng-show="setup_form.password.$dirty && setup_form.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<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)">
|
||||
@@ -27,6 +28,22 @@
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="useradd.email" id="inputUserAddEmail" name="email" 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="email" required>
|
||||
</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>
|
||||
</form>
|
||||
@@ -47,8 +64,9 @@
|
||||
<h4 class="modal-title" id="userRemoveModalLabel">Delete user {{ userremove.userInfo.username }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form name="userremove_form" class="form-user-delete" role="form" ng-submit="doUserRemove()" name="userDeleteConfirm" autocomplete="off">
|
||||
<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)">
|
||||
@@ -65,11 +83,11 @@
|
||||
<small ng-show="userremove_form.password.$error.maxlength">The password is too long</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" ng-maxlength="512" ng-minlength="5" required>
|
||||
<input type="password" class="form-control" ng-model="userremove.password" id="inputUserRemovePassword" name="password" placeholder="Password" ng-maxlength="30" ng-minlength="8" required>
|
||||
</div>
|
||||
<input class="hide" type="submit"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
@@ -103,7 +121,7 @@
|
||||
<tr>
|
||||
<th style="">User</th>
|
||||
<th style="width: 1px" class="text-right">Group</th>
|
||||
<th style="width: 200px" class="text-right">Actions</th>
|
||||
<th style="width: 300px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -121,6 +139,7 @@
|
||||
<span ng-show="!user.admin"><i class="fa fa-plus"></i> Add Admin</span>
|
||||
<span ng-show="user.admin"><i class="fa fa-minus"></i> Remove Admin</span>
|
||||
</button>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" class="btn btn-xs btn-default" ng-click="sendInvite(user)"><i class="fa fa-paper-plane-o"></i> Send Invite</button>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" class="btn btn-xs btn-danger" ng-click="showUserRemove(user)"><i class="fa fa-trash-o"></i> Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -130,4 +149,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,9 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
alreadyTaken: false,
|
||||
error: {},
|
||||
username: '',
|
||||
email: ''
|
||||
email: '',
|
||||
displayName: '',
|
||||
sendInvite: true
|
||||
};
|
||||
|
||||
$scope.isMe = function (user) {
|
||||
@@ -39,20 +41,30 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
});
|
||||
};
|
||||
|
||||
$scope.sendInvite = function (user) {
|
||||
Client.sendInvite(user.username, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
Client.notify('', 'Invitation was successfully sent to ' + user.email + '.', false, 'success');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doAdd = function () {
|
||||
$scope.useradd.busy = true;
|
||||
|
||||
$scope.useradd.alreadyTaken = false;
|
||||
$scope.useradd.error.username = null;
|
||||
$scope.useradd.error.email = null;
|
||||
$scope.useradd.error.displayName = null;
|
||||
|
||||
Client.createUser($scope.useradd.username, $scope.useradd.email, function (error) {
|
||||
Client.createUser($scope.useradd.username, $scope.useradd.email, $scope.useradd.displayName, $scope.useradd.sendInvite, function (error) {
|
||||
$scope.useradd.busy = false;
|
||||
|
||||
if (error && error.statusCode === 409) {
|
||||
$scope.useradd.error.username = 'Username or Email already taken';
|
||||
$scope.useradd_form.username.$setPristine();
|
||||
$scope.useradd_form.email.$setPristine();
|
||||
$scope.useradd_form.displayName.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
return;
|
||||
}
|
||||
@@ -67,6 +79,12 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.useradd.error.usernameAttempted = $scope.useradd.username;
|
||||
$scope.useradd_form.username.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
} else if (error.message.indexOf('displayName') !== -1) {
|
||||
$scope.useradd.error.displayName = 'Invalid Name';
|
||||
$scope.useradd.error.displayNameAttempted = $scope.useradd.displayName;
|
||||
$scope.useradd_form.displayName.$setPristine();
|
||||
$('#inputUserAddDisplayName').focus();
|
||||
|
||||
} else {
|
||||
console.error('Unable to create user.', error.statusCode, error.message);
|
||||
}
|
||||
@@ -77,6 +95,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.useradd.error = {};
|
||||
$scope.useradd.username = '';
|
||||
$scope.useradd.email = '';
|
||||
$scope.useradd.displayName = '';
|
||||
|
||||
$scope.useradd_form.$setUntouched();
|
||||
$scope.useradd_form.$setPristine();
|
||||
@@ -90,7 +109,13 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
$scope.showUserRemove = function (userInfo) {
|
||||
$scope.userremove.error.username = null;
|
||||
$scope.userremove.error.password = null;
|
||||
$scope.userremove.username = '';
|
||||
$scope.userremove.password = '';
|
||||
$scope.userremove.userInfo = userInfo;
|
||||
|
||||
$scope.userremove_form.$setPristine();
|
||||
$scope.userremove_form.$setUntouched();
|
||||
|
||||
$('#userRemoveModal').modal('show');
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user