Compare commits

..

98 Commits

Author SHA1 Message Date
Johannes Zellner 0f5ce651cc Show errors if passwords do not match for reset and setup 2016-01-21 16:33:51 +01:00
Johannes Zellner 6b8d5f92de Set meaningful page title for oauth rendered pages 2016-01-21 16:19:38 +01:00
Johannes Zellner 55e556c725 Also provide client side password validation for password setup and reset forms 2016-01-21 16:08:51 +01:00
Johannes Zellner 19bb0a6ec2 Move form feedback below in setup screen 2016-01-21 16:03:46 +01:00
Johannes Zellner 290132f432 Add warning in password.js to update the UI parts 2016-01-21 16:00:12 +01:00
Johannes Zellner 4a8be8e62d Add changes for 0.6.5 2016-01-21 15:57:48 +01:00
Johannes Zellner 23b61aef0c Use client side pattern validation for setup password 2016-01-21 15:52:24 +01:00
Johannes Zellner 24cc433a3d Also take care of the developermode toggle form 2016-01-21 15:17:49 +01:00
Johannes Zellner e014b7de81 It should be called 'Wrong password' 2016-01-21 15:11:42 +01:00
Johannes Zellner 0895a2bdea Same procedure for the app uninstall dialog 2016-01-21 15:09:22 +01:00
Johannes Zellner 03ca4887ba Streamline the app restore password validation 2016-01-21 15:06:22 +01:00
Johannes Zellner 9eeb17c397 Fixup app configure password validation 2016-01-21 15:03:51 +01:00
Johannes Zellner 6a5da2745a Simplify password validation in email edit 2016-01-21 14:59:39 +01:00
Johannes Zellner e1111ba2bb Simplify password validation for cloudron update 2016-01-21 14:57:21 +01:00
Johannes Zellner d186084835 Use password regexp instead of min-max to do client side validation also 2016-01-21 14:47:21 +01:00
Johannes Zellner 06c2ba9fa9 Fixup email edit form with password changes 2016-01-21 14:34:36 +01:00
Johannes Zellner b82e5fd8c6 Remove console.log() 2016-01-21 14:29:04 +01:00
Johannes Zellner 6e1f96a832 Set min and max length for all password fields 2016-01-21 14:26:24 +01:00
Johannes Zellner f68135c7aa Fixup password requirement feedback in account settings 2016-01-21 14:03:24 +01:00
Johannes Zellner f48cbb457b Call reset avatar to trigger favicon change 2016-01-20 17:15:13 +01:00
Johannes Zellner 8d192dc992 Add Client.resetAvatar() 2016-01-20 17:14:50 +01:00
Johannes Zellner b70324aa24 Give favicon an id 2016-01-20 17:14:36 +01:00
Johannes Zellner 390afaf614 Remove dead code 2016-01-20 16:56:14 +01:00
Johannes Zellner 5112322e7d Ensure the avatar is always updated in all places
Fixes #549
2016-01-20 16:55:44 +01:00
Johannes Zellner 2cb498d500 Improve singleUser configure dialog
Fixes #565
2016-01-20 16:27:43 +01:00
Johannes Zellner 2bd6e02cdc Do not show oauth proxy settings for singleUser apps 2016-01-20 16:23:31 +01:00
Johannes Zellner 85423cbc20 Actually send displayName instead of name in cloudron activation tests 2016-01-20 16:14:44 +01:00
Johannes Zellner 1c0d027bd3 Fix error message if displayName has wrong type 2016-01-20 16:14:21 +01:00
Johannes Zellner 5a8a023039 Fixup all the route tests with new password requirement 2016-01-20 16:06:51 +01:00
Johannes Zellner 196b059cfb Immediately set icon fallback to avoid flickering 2016-01-20 16:01:39 +01:00
Johannes Zellner 2d930b9c3d Explicitly set icon urls to null if we dont have them 2016-01-20 16:01:21 +01:00
Johannes Zellner a5ba3faa49 Correctly report password errors 2016-01-20 15:41:29 +01:00
Johannes Zellner 02ba91f1bb Move password generation into separate file and ensure we generate strong passwords 2016-01-20 15:33:11 +01:00
Johannes Zellner bfa917e057 Add password strength unit tests 2016-01-20 14:50:06 +01:00
Johannes Zellner 909dd0725a Fix copy and paste error 2016-01-20 14:49:45 +01:00
Johannes Zellner 74860f2d16 Fix tests for password strength change 2016-01-20 14:39:08 +01:00
Johannes Zellner 132ebb4e74 Require strong passwords
Fixes #568
2016-01-20 14:38:41 +01:00
Johannes Zellner 698158cd93 Change some of the mail added email text 2016-01-20 13:14:19 +01:00
Johannes Zellner 5bfc684f1b Fix crash due to missing event 2016-01-20 13:10:16 +01:00
Johannes Zellner c944c9b65b Add changelog for 0.6.4 2016-01-20 12:43:40 +01:00
Johannes Zellner d61698b894 Send user_added email instead of generic user event to admins
Fixes #569
2016-01-20 12:40:56 +01:00
Johannes Zellner a4d32009ad Make it clear why this if condition is there 2016-01-20 12:39:28 +01:00
Johannes Zellner 3007875e35 Add user_added.ejs email template
This allows us to also send an invitation link to admins
in case the user was not invited already.
2016-01-20 12:38:07 +01:00
Johannes Zellner b4aad138fc Fixup the update badge for mobile 2016-01-20 12:00:19 +01:00
Johannes Zellner 8df7eb2acb Check if the versions for app updates match
Fixes #566
2016-01-20 11:56:46 +01:00
girish@cloudron.io 18cab6f861 initialize displayName from activation link 2016-01-20 00:16:48 -08:00
girish@cloudron.io b2071c65d8 Fix typo 2016-01-20 00:05:06 -08:00
girish@cloudron.io 402dba096e webadmin: display name for users 2016-01-19 23:58:52 -08:00
girish@cloudron.io abf0c81de4 provide displayName in createAdmin route 2016-01-19 23:58:08 -08:00
girish@cloudron.io 613985a17c Set default displayName as empty 2016-01-19 23:47:29 -08:00
girish@cloudron.io bfc9801699 provide displayName in ldap response when available 2016-01-19 23:47:24 -08:00
girish@cloudron.io ee705eb979 Add displayName to create user and activate routes 2016-01-19 23:34:49 -08:00
girish@cloudron.io 67b94c7fde give message for development mode 2016-01-19 10:20:24 -08:00
Johannes Zellner 77e5d3f4bb Retry checking for app start state in test 2016-01-19 16:45:58 +01:00
Johannes Zellner 30618b8644 add missing argument 2016-01-19 14:03:01 +01:00
Johannes Zellner 57a2613286 Remove leftover js-error target reference in gulpfile 2016-01-19 13:36:32 +01:00
Johannes Zellner e15bd89ba2 Add route to list application backups 2016-01-19 13:35:28 +01:00
Johannes Zellner d2ed816f44 Add apps.listBackups() 2016-01-19 13:35:18 +01:00
Johannes Zellner e51234928b Add FIXME for selfhost backup listing 2016-01-19 13:32:11 +01:00
Johannes Zellner 3aa668aea3 Fixup tests 2016-01-19 12:42:19 +01:00
Johannes Zellner 870edab78a Set empty displayName for users 2016-01-19 12:40:50 +01:00
Johannes Zellner ebc9d9185d Use displayName in userdb 2016-01-19 12:39:54 +01:00
Johannes Zellner 093150d4e3 Add displayName to users table 2016-01-19 12:37:22 +01:00
Johannes Zellner de80a6692d Remove unused error.js the code is inline 2016-01-19 11:22:06 +01:00
Johannes Zellner c28f564a47 Remove unused gulp target for error.js 2016-01-19 11:21:43 +01:00
Johannes Zellner eb6a09c2bd Try to fetch the cloudron status to offer a way to reload 2016-01-19 11:20:32 +01:00
Johannes Zellner 19f404e092 Changes for 0.6.3 2016-01-19 11:00:29 +01:00
girish@cloudron.io 55799ebb2d cli tool demuxes stream now 2016-01-18 21:36:05 -08:00
girish@cloudron.io fdf4d8fdcf maybe stream is duplex 2016-01-18 13:39:18 -08:00
girish@cloudron.io 6dc11edafe make exec route more debugging friedly
allow upto 30 minutes of idle connection
2016-01-18 12:49:06 -08:00
girish@cloudron.io c82ca1c69d disable http server timeout 2016-01-18 12:28:53 -08:00
girish@cloudron.io 7ef3d55cbf add tty option to exec 2016-01-18 11:39:09 -08:00
Johannes Zellner 44e4f53827 Change user creation api to require the invite flag 2016-01-18 16:53:51 +01:00
Johannes Zellner 643e490cbb Allow to specify if a new user gets invited immediately 2016-01-18 16:44:11 +01:00
Johannes Zellner e61498c3b6 Ensure the avatar is also based on the apiOrigin 2016-01-18 16:35:25 +01:00
Johannes Zellner bb6b61d810 Remove unused Client.prototype.getAppLogUrl() 2016-01-18 16:29:33 +01:00
Johannes Zellner cff173c2e6 Ensure all api calls are absolute 2016-01-18 16:29:13 +01:00
Johannes Zellner 226501d103 Clear user remove form 2016-01-18 16:25:42 +01:00
Johannes Zellner c5b8b0e3db Split up userAdd and sendInvite mailer calls 2016-01-18 16:11:00 +01:00
Johannes Zellner 46878e4363 Add button to trigger the invite email 2016-01-18 15:45:54 +01:00
Johannes Zellner f77682365e Add client.sendInvite() 2016-01-18 15:45:44 +01:00
Johannes Zellner d9850fa660 Add send invite route 2016-01-18 15:37:03 +01:00
Johannes Zellner 9258585746 add user.sendInvite() with tests 2016-01-18 15:16:18 +01:00
Johannes Zellner e635aaaa58 Fix oauth2 tests 2016-01-18 14:31:25 +01:00
Johannes Zellner d0d6725df5 Remove obsolete comments 2016-01-18 14:19:38 +01:00
Johannes Zellner 61f4fea9c3 Check for emails sent in users tests 2016-01-18 14:19:20 +01:00
Johannes Zellner 66d59c1d6c Do not require the invitor in the invite mail 2016-01-18 14:18:57 +01:00
Johannes Zellner f9725965e2 Add test hooks to check if mails are queued 2016-01-18 14:00:35 +01:00
Johannes Zellner 4629739a14 Fix ldap tests 2016-01-18 13:56:59 +01:00
Johannes Zellner e9b3a1e99c Fixup user tests 2016-01-18 13:50:54 +01:00
Johannes Zellner 8ac27b9dc7 Adjust api to set a flag if invitiation should be sent on user creation 2016-01-18 13:48:10 +01:00
Johannes Zellner 2edd434474 Support more notification types 2016-01-18 13:39:27 +01:00
Johannes Zellner bebf480321 Use appdb.exists() instead of a apps.get() 2016-01-17 16:05:47 +01:00
Johannes Zellner 10c09d9def Fix wrong conditional in appdb 2016-01-17 16:01:17 +01:00
Johannes Zellner 6ce6b96e5c Fix linter issues 2016-01-17 15:59:11 +01:00
Johannes Zellner 16a9cae80e Allow to specify the restore id 2016-01-17 15:50:20 +01:00
Johannes Zellner e865e2ae6d Employ hack to ensure chrome and firefox do not autocomplete
http://stackoverflow.com/questions/12374442/chrome-browser-ignoring-autocomplete-off
2016-01-16 17:38:18 +01:00
Johannes Zellner 06363a43f9 Offset the status bar 2016-01-16 16:48:47 +01:00
54 changed files with 742 additions and 265 deletions
+10
View File
@@ -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
View File
@@ -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);
});
};
+1
View File
@@ -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(
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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));
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+23
View File
@@ -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 { %>
<% } %>
+4
View File
@@ -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
View File
@@ -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 = [];
}
+1 -1
View File
@@ -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">
+8 -2
View File
@@ -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"/>
+8 -2
View File
@@ -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"/>
+47
View File
@@ -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
View File
@@ -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 }));
});
}
+3 -1
View File
@@ -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
View File
@@ -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'
});
});
}
+11 -8
View File
@@ -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) {
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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',
+9 -9
View File
@@ -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();
+1 -1
View File
@@ -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;
+8 -6
View File
@@ -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);
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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',
+71 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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!!
};
});
+6 -3
View File
@@ -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();
});
});
+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">
+7 -9
View File
@@ -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
View File
@@ -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));
-23
View File
@@ -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 -2
View File
@@ -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);
}
});
}
+3 -1
View File
@@ -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;
+21 -28
View File
@@ -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>
+9
View File
@@ -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);
+27 -18
View File
@@ -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>
+4 -1
View File
@@ -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 -8
View File
@@ -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/>
+6 -10
View File
@@ -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
+4 -1
View File
@@ -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>
+26 -7
View File
@@ -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>
+27 -2
View File
@@ -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');
};