diff --git a/dashboard/scripts/addUsers.js b/dashboard/scripts/addUsers.js index 748d992cf..0a58da74e 100755 --- a/dashboard/scripts/addUsers.js +++ b/dashboard/scripts/addUsers.js @@ -24,7 +24,7 @@ function getAccessToken(callback) { let username = readlineSync.question('Username: ', {}); let password = readlineSync.question('Password: ', { noEchoBack: true }); - superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) { + superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) { if (error || result.statusCode !== 200) { console.log('Login failed'); return getAccessToken(callback); @@ -63,4 +63,4 @@ getAccessToken(function (accessToken) { console.log('Done'); }); -}); \ No newline at end of file +}); diff --git a/dashboard/scripts/delUsers.js b/dashboard/scripts/delUsers.js index a7ddcf2a8..328f26620 100755 --- a/dashboard/scripts/delUsers.js +++ b/dashboard/scripts/delUsers.js @@ -20,7 +20,7 @@ function getAccessToken(callback) { let username = readlineSync.question('Username: ', {}); let password = readlineSync.question('Password: ', { noEchoBack: true }); - superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) { + superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) { if (error || result.statusCode !== 200) { console.log('Login failed'); return getAccessToken(callback); @@ -66,4 +66,4 @@ getAccessToken(function (accessToken) { }); }); -}); \ No newline at end of file +}); diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index 79da2472c..e3935e689 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -2342,7 +2342,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.sendSelfPasswordReset = function (identifier, callback) { - post('/api/v1/cloudron/password_reset_request', { identifier }, null, function (error, data, status) { + post('/api/v1/auth/password_reset_request', { identifier }, null, function (error, data, status) { if (error) return callback(error); if (status !== 202) return callback(new ClientError(status, data)); diff --git a/dashboard/src/js/passwordreset.js b/dashboard/src/js/passwordreset.js index b436b70a8..6debd1cdb 100644 --- a/dashboard/src/js/passwordreset.js +++ b/dashboard/src/js/passwordreset.js @@ -86,7 +86,7 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func $scope.mode = 'passwordResetDone'; } - $http.post(API_ORIGIN + '/api/v1/cloudron/password_reset_request', data).success(done).error(done); + $http.post(API_ORIGIN + '/api/v1/auth/password_reset_request', data).success(done).error(done); }; $scope.onNewPassword = function () { @@ -107,7 +107,7 @@ app.controller('PasswordResetController', ['$scope', '$translate', '$http', func else $scope.error = 'Unknown error'; } - $http.post(API_ORIGIN + '/api/v1/cloudron/password_reset', data).success(function (data, status) { + $http.post(API_ORIGIN + '/api/v1/auth/password_reset', data).success(function (data, status) { if (status !== 202) return error(data, status); // set token to autologin diff --git a/dashboard/src/js/setupaccount.js b/dashboard/src/js/setupaccount.js index eff80f598..093aba868 100644 --- a/dashboard/src/js/setupaccount.js +++ b/dashboard/src/js/setupaccount.js @@ -119,7 +119,7 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct } } - $http.post(API_ORIGIN + '/api/v1/cloudron/setup_account', data).success(function (data, status) { + $http.post(API_ORIGIN + '/api/v1/auth/setup_account', data).success(function (data, status) { if (status !== 201) return error(data, status); // set token to autologin diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index 123dfe56e..976d1432f 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -224,7 +224,7 @@ server { client_max_body_size 2m; } - location ~ ^/api/v1/cloudron/login$ { + location ~ ^/api/v1/auth/login$ { proxy_pass http://127.0.0.1:3000; client_max_body_size 1m; limit_req zone=admin_login burst=5; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 000000000..756228e64 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,117 @@ +'use strict'; + +exports = module.exports = { + login, + logout, + passwordResetRequest, + passwordReset, + setupAccount, +}; + +const assert = require('assert'), + AuditSource = require('../auditsource.js'), + BoxError = require('../boxerror.js'), + constants = require('../constants.js'), + debug = require('debug')('box:routes/cloudron'), + eventlog = require('../eventlog.js'), + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'), + speakeasy = require('speakeasy'), + tokens = require('../tokens.js'), + users = require('../users.js'); + +async function login(req, res, next) { + assert.strictEqual(typeof req.user, 'object'); + + if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string')); + + const type = req.body.type || tokens.ID_WEBADMIN; + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; + const userAgent = req.headers['user-agent'] || ''; + const auditSource = { authType: 'basic', ip }; + + let error = tokens.validateTokenType(type); + if (error) return next(new HttpError(400, error.message)); + + let token; + [error, token] = await safe(tokens.add({ clientId: type, identifier: req.user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + if (error) return next(new HttpError(500, error)); + + await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + + if (!req.user.ghost) safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource), { debug }); + + next(new HttpSuccess(200, token)); +} + +async function logout(req, res) { + assert.strictEqual(typeof req.token, 'object'); + + await eventlog.add(eventlog.ACTION_USER_LOGOUT, AuditSource.fromRequest(req), { userId: req.user.id, user: users.removePrivateFields(req.user) }); + + await safe(tokens.delByAccessToken(req.token.accessToken)); + res.redirect('/'); +} + +async function passwordResetRequest(req, res, next) { + if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string')); + + const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req))); + if (error && !(error.reason === BoxError.NOT_FOUND || error.reason === BoxError.CONFLICT)) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, {})); +} + +async function passwordReset(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken')); + if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password')); + + let [error, userObject] = await safe(users.getByResetToken(req.body.resetToken)); + if (error) return next(new HttpError(401, 'Invalid resetToken')); + if (!userObject) return next(new HttpError(401, 'Invalid resetToken')); + + if (userObject.twoFactorAuthenticationEnabled) { + if (typeof req.body.totpToken !== 'string') return next(new HttpError(401, 'A totpToken must be provided')); + + const verified = speakeasy.totp.verify({ secret: userObject.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); + if (!verified) return next(new HttpError(401, 'Invalid totpToken')); + } + + // if you fix the duration here, the emails and UI have to be fixed as well + if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); + if (!userObject.username) return next(new HttpError(409, 'No username set')); + + // setPassword clears the resetToken + [error] = await safe(users.setPassword(userObject, req.body.password, AuditSource.fromRequest(req))); + if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); + if (error) return next(BoxError.toHttpError(error)); + + let result; + [error, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { accessToken: result.accessToken })); +} + +async function setupAccount(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (!req.body.inviteToken || typeof req.body.inviteToken !== 'string') return next(new HttpError(400, 'inviteToken must be a non-empty string')); + if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string')); + + // only sent if profile is not locked + if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string')); + if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string')); + + const [error, userObject] = await safe(users.getByInviteToken(req.body.inviteToken)); + if (error) return next(new HttpError(401, 'Invalid inviteToken')); + if (!userObject) return next(new HttpError(401, 'Invalid inviteToken')); + + const [setupAccountError, accessToken] = await safe(users.setupAccount(userObject, req.body, AuditSource.fromRequest(req))); + if (setupAccountError) return next(BoxError.toHttpError(setupAccountError)); + + next(new HttpSuccess(201, { accessToken })); +} diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 06735187b..19423c351 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -1,11 +1,6 @@ 'use strict'; exports = module.exports = { - login, - logout, - passwordResetRequest, - passwordReset, - setupAccount, getConfig, updateDashboardDomain, @@ -25,112 +20,11 @@ const assert = require('assert'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), cloudron = require('../cloudron.js'), - constants = require('../constants.js'), - debug = require('debug')('box:routes/cloudron'), - eventlog = require('../eventlog.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, platform = require('../platform.js'), safe = require('safetydance'), - speakeasy = require('speakeasy'), - tokens = require('../tokens.js'), - translation = require('../translation.js'), - users = require('../users.js'); - -async function login(req, res, next) { - assert.strictEqual(typeof req.user, 'object'); - - if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string')); - - const type = req.body.type || tokens.ID_WEBADMIN; - const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; - const userAgent = req.headers['user-agent'] || ''; - const auditSource = { authType: 'basic', ip }; - - let error = tokens.validateTokenType(type); - if (error) return next(new HttpError(400, error.message)); - - let token; - [error, token] = await safe(tokens.add({ clientId: type, identifier: req.user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); - if (error) return next(new HttpError(500, error)); - - await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); - - if (!req.user.ghost) safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource), { debug }); - - next(new HttpSuccess(200, token)); -} - -async function logout(req, res) { - assert.strictEqual(typeof req.token, 'object'); - - await eventlog.add(eventlog.ACTION_USER_LOGOUT, AuditSource.fromRequest(req), { userId: req.user.id, user: users.removePrivateFields(req.user) }); - - await safe(tokens.delByAccessToken(req.token.accessToken)); - res.redirect('/'); -} - -async function passwordResetRequest(req, res, next) { - if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string')); - - const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req))); - if (error && !(error.reason === BoxError.NOT_FOUND || error.reason === BoxError.CONFLICT)) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(202, {})); -} - -async function passwordReset(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken')); - if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password')); - - let [error, userObject] = await safe(users.getByResetToken(req.body.resetToken)); - if (error) return next(new HttpError(401, 'Invalid resetToken')); - if (!userObject) return next(new HttpError(401, 'Invalid resetToken')); - - if (userObject.twoFactorAuthenticationEnabled) { - if (typeof req.body.totpToken !== 'string') return next(new HttpError(401, 'A totpToken must be provided')); - - const verified = speakeasy.totp.verify({ secret: userObject.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); - if (!verified) return next(new HttpError(401, 'Invalid totpToken')); - } - - // if you fix the duration here, the emails and UI have to be fixed as well - if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); - if (!userObject.username) return next(new HttpError(409, 'No username set')); - - // setPassword clears the resetToken - [error] = await safe(users.setPassword(userObject, req.body.password, AuditSource.fromRequest(req))); - if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error) return next(BoxError.toHttpError(error)); - - let result; - [error, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(202, { accessToken: result.accessToken })); -} - -async function setupAccount(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (!req.body.inviteToken || typeof req.body.inviteToken !== 'string') return next(new HttpError(400, 'inviteToken must be a non-empty string')); - if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string')); - - // only sent if profile is not locked - if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string')); - if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string')); - - const [error, userObject] = await safe(users.getByInviteToken(req.body.inviteToken)); - if (error) return next(new HttpError(401, 'Invalid inviteToken')); - if (!userObject) return next(new HttpError(401, 'Invalid inviteToken')); - - const [setupAccountError, accessToken] = await safe(users.setupAccount(userObject, req.body, AuditSource.fromRequest(req))); - if (setupAccountError) return next(BoxError.toHttpError(setupAccountError)); - - next(new HttpSuccess(201, { accessToken })); -} + translation = require('../translation.js'); async function getConfig(req, res, next) { const [error, cloudronConfig] = await safe(cloudron.getConfig()); diff --git a/src/routes/index.js b/src/routes/index.js index 619c66667..da3bc89d5 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -6,6 +6,7 @@ exports = module.exports = { apps: require('./apps.js'), applinks: require('./applinks.js'), appstore: require('./appstore.js'), + auth: require('./auth.js'), backups: require('./backups.js'), branding: require('./branding.js'), cloudron: require('./cloudron.js'), diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index b7dfea7e1..01dca1410 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -70,7 +70,7 @@ describe('Cloudron API', function () { .ok(() => true); expect(response2.statusCode).to.equal(200); - const response3 = await superagent.post(`${serverUrl}/api/v1/cloudron/setup_account`) + const response3 = await superagent.post(`${serverUrl}/api/v1/auth/setup_account`) .send({ inviteToken: require('url').parse(response2.body.inviteLink, true).query.inviteToken, password: USER.password, @@ -89,7 +89,7 @@ describe('Cloudron API', function () { expect(response4.body.username).to.equal(USER.username); expect(response4.body.displayName).to.equal(USER.displayName); - const response5 = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response5 = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: USER.username, password: USER.password }); expect(response5.statusCode).to.equal(200); }); @@ -113,7 +113,7 @@ describe('Cloudron API', function () { .ok(() => true); expect(response2.statusCode).to.equal(200); - const response3 = await superagent.post(`${serverUrl}/api/v1/cloudron/setup_account`) + const response3 = await superagent.post(`${serverUrl}/api/v1/auth/setup_account`) .send({ inviteToken: require('url').parse(response2.body.inviteLink, true).query.inviteToken, password: USER.password, @@ -123,7 +123,7 @@ describe('Cloudron API', function () { .ok(() => true); expect(response3.statusCode).to.equal(409); - const response4 = await superagent.post(`${serverUrl}/api/v1/cloudron/setup_account`) + const response4 = await superagent.post(`${serverUrl}/api/v1/auth/setup_account`) .send({ inviteToken: require('url').parse(response2.body.inviteLink, true).query.inviteToken, password: USER.password, @@ -141,7 +141,7 @@ describe('Cloudron API', function () { expect(response5.body.username).to.equal(USER.username); expect(response5.body.displayName).to.equal(USER.displayName); - const response6 = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response6 = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: USER.username, password: USER.password }); expect(response6.statusCode).to.equal(200); }); @@ -170,7 +170,7 @@ describe('Cloudron API', function () { .ok(() => true); expect(response2.statusCode).to.equal(200); - const response3 = await superagent.post(`${serverUrl}/api/v1/cloudron/setup_account`) + const response3 = await superagent.post(`${serverUrl}/api/v1/auth/setup_account`) .send({ inviteToken: require('url').parse(response2.body.inviteLink, true).query.inviteToken, password: USER.password, @@ -189,7 +189,7 @@ describe('Cloudron API', function () { expect(response4.body.username).to.equal('presetup3'); // what the admin provided expect(response4.body.displayName).to.equal('pre setup3'); // what the admin provided - const response5 = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response5 = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'presetup3', password: USER.password }); expect(response5.statusCode).to.equal(200); }); @@ -197,13 +197,13 @@ describe('Cloudron API', function () { describe('login', function () { it('cannot login without body', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .ok(() => true); expect(response.statusCode).to.equal(400); }); it('cannot login without username', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ password: owner.password }) .ok(() => true); @@ -211,7 +211,7 @@ describe('Cloudron API', function () { }); it('cannot login without password', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.username }) .ok(() => true); @@ -219,7 +219,7 @@ describe('Cloudron API', function () { }); it('cannot login with empty username', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: '', password: owner.password }) .ok(() => true); @@ -227,7 +227,7 @@ describe('Cloudron API', function () { }); it('cannot login with empty password', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.username, password: '' }) .ok(() => true); @@ -235,7 +235,7 @@ describe('Cloudron API', function () { }); it('cannot login with unknown username', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'somethingrandom', password: owner.password }) .ok(() => true); @@ -243,7 +243,7 @@ describe('Cloudron API', function () { }); it('cannot login with unknown email', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'randomgemail', password: owner.password }) .ok(() => true); @@ -251,7 +251,7 @@ describe('Cloudron API', function () { }); it('cannot login with wrong password', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.username, password: owner.password.toUpperCase() }) .ok(() => true); @@ -259,7 +259,7 @@ describe('Cloudron API', function () { }); it('can login with username', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.username, password: owner.password }); expect(response.statusCode).to.equal(200); @@ -268,7 +268,7 @@ describe('Cloudron API', function () { }); it('can login with uppercase username', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.username.toUpperCase(), password: owner.password }); expect(response.statusCode).to.equal(200); @@ -277,7 +277,7 @@ describe('Cloudron API', function () { }); it('can login with email', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.email, password: owner.password }); expect(response.statusCode).to.equal(200); @@ -286,7 +286,7 @@ describe('Cloudron API', function () { }); it('can login with uppercase email', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: owner.email.toUpperCase(), password: owner.password }); expect(response.statusCode).to.equal(200); diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index 368245529..0bc509215 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -242,7 +242,7 @@ describe('Profile API', function () { }); it('fails due to missing token', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: user.username, password: user.password }) .ok(() => true); @@ -250,7 +250,7 @@ describe('Profile API', function () { }); it('fails due to wrong token', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: user.username, password: user.password, totpToken: '12345' }) .ok(() => true); @@ -263,7 +263,7 @@ describe('Profile API', function () { encoding: 'base32' }); - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: user.username, password: user.password, totpToken: totpToken }); expect(response.statusCode).to.equal(200); @@ -278,7 +278,7 @@ describe('Profile API', function () { }); it('did disable 2fa', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: user.username, password: user.password }); expect(response.statusCode).to.equal(200); diff --git a/src/server.js b/src/server.js index 430b0323f..d0924959e 100644 --- a/src/server.js +++ b/src/server.js @@ -98,14 +98,15 @@ async function initializeExpressSync() { router.post('/api/v1/cloudron/activate', json, routes.provision.setupTokenAuth, routes.provision.activate); router.get ('/api/v1/cloudron/status', routes.provision.getStatus); router.get ('/api/v1/cloudron/block_devices', routes.provision.getBlockDevices); + router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // this is a public alias for /api/v1/branding/cloudron_avatar - // login/logout routes - router.post('/api/v1/cloudron/login', json, password, routes.cloudron.login); - router.get ('/api/v1/cloudron/logout', token, routes.cloudron.logout); // this will invalidate the token if any and redirect to / always - router.post('/api/v1/cloudron/password_reset_request', json, routes.cloudron.passwordResetRequest); - router.post('/api/v1/cloudron/password_reset', json, routes.cloudron.passwordReset); - router.post('/api/v1/cloudron/setup_account', json, routes.cloudron.setupAccount); + // auth routes + router.post('/api/v1/auth/login', json, password, routes.auth.login); + router.get ('/api/v1/auth/logout', token, routes.auth.logout); // this will invalidate the token if any and redirect to / always + router.post('/api/v1/auth/password_reset_request', json, routes.auth.passwordResetRequest); + router.post('/api/v1/auth/password_reset', json, routes.auth.passwordReset); + router.post('/api/v1/auth/setup_account', json, routes.auth.setupAccount); // config route (for dashboard). can return some private configuration unlike status router.get ('/api/v1/config', token, authorizeUser, routes.cloudron.getConfig); diff --git a/src/test/externalldap-test.js b/src/test/externalldap-test.js index e1a5267e6..8b818fdd0 100644 --- a/src/test/externalldap-test.js +++ b/src/test/externalldap-test.js @@ -483,7 +483,7 @@ describe('External LDAP', function () { email: 'auto0@login.com' }); - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'autologinuser0', password: LDAP_SHARED_PASSWORD }) .ok(() => true); @@ -502,7 +502,7 @@ describe('External LDAP', function () { }); it('fails for unknown user', async function () { - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'doesnotexist', password: LDAP_SHARED_PASSWORD }) .ok(() => true); @@ -521,7 +521,7 @@ describe('External LDAP', function () { email: 'auto1@login.com' }); - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'autologinuser1', password: 'wrongpassword' }) .ok(() => true); @@ -540,7 +540,7 @@ describe('External LDAP', function () { password: LDAP_SHARED_PASSWORD }); - const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) + const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'autologinuser2', password: LDAP_SHARED_PASSWORD }) .ok(() => true);