Compare commits

..

47 Commits

Author SHA1 Message Date
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
41 changed files with 369 additions and 152 deletions
+5
View File
@@ -374,3 +374,8 @@
[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
+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(
+29
View File
@@ -22,6 +22,7 @@ exports = module.exports = {
backup: backup,
backupApp: backupApp,
listBackups: listBackups,
getLogs: getLogs,
@@ -863,3 +864,31 @@ function restoreApp(app, addonsToRestore, backupId, 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 -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 -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 { %>
<% } %>
+19 -3
View File
@@ -246,12 +246,28 @@ function sendInvite(user, invitor) {
enqueue(mailOptions);
}
function userAdded(user) {
function userAdded(user, inviteSent) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof inviteSent, 'boolean');
debug('Sending mail for userAdded');
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
mailUserEventToAdmins(user, 'was added');
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) {
+46
View File
@@ -0,0 +1,46 @@
/* 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
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;
}
+12
View File
@@ -15,6 +15,7 @@ exports = module.exports = {
updateApp: updateApp,
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
stopApp: stopApp,
startApp: startApp,
@@ -373,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'));
+10 -7
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;
@@ -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, '', false, 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',
+3 -3
View File
@@ -18,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';
@@ -553,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();
@@ -573,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();
+5 -3
View File
@@ -17,7 +17,7 @@ exports = module.exports = {
};
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'),
@@ -46,13 +46,15 @@ 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 */, sendInvite, 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'));
+1
View File
@@ -144,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);
+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, false),
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, false, USER_0, false)
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);
}
+51 -10
View File
@@ -17,9 +17,10 @@ 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) {
@@ -30,7 +31,7 @@ function cleanupUsers(done) {
}
function createUser(done) {
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, false, 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();
@@ -74,8 +75,48 @@ describe('User', function () {
before(cleanupUsers);
after(cleanupUsers);
it('fails due to short password', function (done) {
user.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, IS_ADMIN, null, true, function (error, result) {
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, IS_ADMIN, null /* invitor */, true, function (error, result) {
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);
@@ -110,7 +151,7 @@ describe('User', function () {
});
it('fails because user exists', function (done) {
user.create(USERNAME, PASSWORD, EMAIL, IS_ADMIN, null /* invitor */, false, 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);
@@ -120,7 +161,7 @@ describe('User', function () {
});
it('fails because password is empty', function (done) {
user.create(USERNAME, '', EMAIL, IS_ADMIN, null /* invitor */, false, 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);
@@ -326,11 +367,11 @@ 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 */, false, 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();
@@ -368,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 */, false, 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();
+21 -17
View File
@@ -31,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');
@@ -90,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');
@@ -114,10 +107,17 @@ function validateToken(token) {
return null;
}
function createUser(username, password, email, admin, invitor, sendInvite, 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');
@@ -127,11 +127,14 @@ function createUser(username, password, email, admin, invitor, sendInvite, callb
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));
@@ -148,7 +151,8 @@ function createUser(username, password, email, admin, invitor, sendInvite, callb
salt: salt.toString('hex'),
createdAt: now,
modifiedAt: now,
resetToken: hat(256)
resetToken: hat(256),
displayName: displayName
};
userdb.add(user.id, user, function (error) {
@@ -157,8 +161,8 @@ function createUser(username, password, email, admin, invitor, sendInvite, callb
callback(null, user);
// do not send email for admins (this can only be the case for the owner)
if (!admin) mailer.userAdded(user);
// WARNING do not send email for admins (this can only be the case for the owner, the first user creation during activation)
if (!admin) mailer.userAdded(user, sendInvite);
if (sendInvite) mailer.sendInvite(user, invitor);
});
});
@@ -334,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));
@@ -377,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);
@@ -386,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 */, false /* sendInvite */, callback);
createUser(username, password, email, displayName, true /* admin */, null /* invitor */, false /* sendInvite */, callback);
});
}
+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">
+2 -2
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"/>
@@ -121,7 +121,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand navbar-brand-icon" href="#/"><img ng-src="{{ cloudronAvatar }}" 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 -->
+19 -5
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);
}
@@ -147,6 +150,15 @@ 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');
console.log('rseet 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;
@@ -413,8 +425,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
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
};
};
@@ -437,11 +449,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;
@@ -540,10 +553,11 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.createUser = function (username, email, sendInvite, callback) {
Client.prototype.createUser = function (username, email, displayName, sendInvite, callback) {
var data = {
username: username,
email: email,
displayName: displayName,
invite: !!sendInvite
};
-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 || '';
}]);
+1 -1
View File
@@ -4,8 +4,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
$scope.initialized = false;
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.cloudronAvatar = Client.apiOrigin + '/api/v1/cloudron/avatar';
$scope.config = {};
$scope.client = Client;
$scope.update = {
busy: false,
+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;
+9 -12
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>
@@ -243,7 +245,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 +274,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 +297,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>
+2 -2
View File
@@ -40,7 +40,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 +94,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>
+5 -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,
@@ -188,11 +185,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 +259,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
+12 -1
View File
@@ -28,6 +28,17 @@
</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
@@ -138,4 +149,4 @@
</div>
</div>
</div>
</div>
</div>
+11 -1
View File
@@ -21,6 +21,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
error: {},
username: '',
email: '',
displayName: '',
sendInvite: true
};
@@ -54,14 +55,16 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
$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, $scope.useradd.sendInvite, 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;
}
@@ -76,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);
}
@@ -86,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();