Files
cloudron-box/src/routes/cloudron.js

334 lines
13 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2020-08-15 22:54:32 -07:00
login,
logout,
passwordResetRequest,
passwordReset,
setupAccount,
reboot,
isRebootRequired,
getConfig,
getDisks,
getMemory,
getUpdateInfo,
update,
checkForUpdates,
getLogs,
getLogStream,
updateDashboardDomain,
2020-08-15 22:54:32 -07:00
prepareDashboardDomain,
renewCerts,
getServerIp,
2020-11-18 00:10:06 +01:00
getLanguages,
syncExternalLdap,
syncDnsRecords
};
2021-06-04 09:28:40 -07:00
const assert = require('assert'),
2019-03-25 15:07:06 -07:00
auditSource = require('../auditsource.js'),
2019-10-22 14:06:19 -07:00
BoxError = require('../boxerror.js'),
cloudron = require('../cloudron.js'),
2020-02-04 18:05:12 +01:00
constants = require('../constants.js'),
eventlog = require('../eventlog.js'),
2019-10-25 15:58:11 -07:00
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
2021-06-04 09:28:40 -07:00
safe = require('safetydance'),
speakeasy = require('speakeasy'),
2019-11-07 10:41:15 -08:00
sysinfo = require('../sysinfo.js'),
2019-11-21 12:58:06 -08:00
system = require('../system.js'),
2020-02-06 16:57:33 +01:00
tokens = require('../tokens.js'),
2020-11-19 23:38:59 +01:00
translation = require('../translation.js'),
2018-07-31 11:35:23 -07:00
updater = require('../updater.js'),
2020-02-04 15:27:22 +01:00
users = require('../users.js'),
2019-10-23 09:39:26 -07:00
updateChecker = require('../updatechecker.js');
2021-06-04 09:28:40 -07:00
async function login(req, res, next) {
2020-02-06 14:50:12 +01:00
assert.strictEqual(typeof req.user, 'object');
2020-02-04 14:35:25 +01:00
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'] || '';
2021-07-15 09:50:11 -07:00
const auditSource = { authType: 'basic', ip };
2020-02-04 14:35:25 +01:00
2021-06-04 09:28:40 -07:00
let error = tokens.validateTokenType(type);
if (error) return next(new HttpError(400, error.message));
2021-06-04 09:28:40 -07:00
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));
2020-02-04 14:35:25 +01:00
2021-06-04 09:28:40 -07:00
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) });
2021-07-15 09:50:11 -07:00
await safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource));
2021-06-04 09:28:40 -07:00
next(new HttpSuccess(200, token));
2020-02-04 14:35:25 +01:00
}
2021-06-04 09:28:40 -07:00
async function logout(req, res) {
2021-01-06 21:57:23 -08:00
assert.strictEqual(typeof req.access_token, 'string');
2020-02-04 14:35:25 +01:00
2021-01-06 21:57:23 -08:00
eventlog.add(eventlog.ACTION_USER_LOGOUT, auditSource.fromRequest(req), { userId: req.user.id, user: users.removePrivateFields(req.user) });
2020-02-04 14:35:25 +01:00
2021-06-04 09:28:40 -07:00
await safe(tokens.delByAccessToken(req.access_token));
res.redirect('/login.html');
2020-02-04 14:35:25 +01:00
}
2021-07-15 09:50:11 -07:00
async function passwordResetRequest(req, res, next) {
2020-02-04 15:27:22 +01:00
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
2021-07-15 09:50:11 -07:00
const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, auditSource.fromRequest(req)));
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
2020-02-04 15:27:22 +01:00
2021-07-15 09:50:11 -07:00
next(new HttpSuccess(202, {}));
2020-02-04 15:27:22 +01:00
}
2021-07-15 09:50:11 -07:00
async function passwordReset(req, res, next) {
2020-02-04 16:47:57 +01:00
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'));
2021-07-15 09:50:11 -07:00
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'));
2020-02-04 16:47:57 +01:00
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'));
}
2021-07-15 09:50:11 -07:00
// 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'));
2020-02-04 16:47:57 +01:00
2021-07-15 09:50:11 -07:00
// 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));
2020-02-04 16:47:57 +01:00
2021-07-15 09:50:11 -07:00
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));
2020-02-04 16:47:57 +01:00
2021-07-15 09:50:11 -07:00
next(new HttpSuccess(202, { accessToken: result.accessToken }));
2020-02-04 16:47:57 +01:00
}
2021-07-15 09:50:11 -07:00
async function setupAccount(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken 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'));
2021-07-15 09:50:11 -07:00
const [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'));
2021-07-15 09:50:11 -07:00
// 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'));
2021-07-15 09:50:11 -07:00
const [setupAccountError, accessToken] = await safe(users.setupAccount(userObject, req.body, auditSource.fromRequest(req)));
if (setupAccountError) return next(BoxError.toHttpError(setupAccountError));
2021-07-15 09:50:11 -07:00
next(new HttpSuccess(201, { accessToken }));
}
async function reboot(req, res, next) {
2018-11-25 17:02:29 +01:00
// Finish the request, to let the appstore know we triggered the reboot
next(new HttpSuccess(202, {}));
await safe(cloudron.reboot());
}
async function isRebootRequired(req, res, next) {
const [error, rebootRequired] = await safe(cloudron.isRebootRequired());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { rebootRequired }));
}
async function getConfig(req, res, next) {
const [error, cloudronConfig] = await safe(cloudron.getConfig());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, cloudronConfig));
}
function getDisks(req, res, next) {
2019-11-21 12:58:06 -08:00
system.getDisks(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
2019-10-22 11:11:41 -07:00
next(new HttpSuccess(200, result));
});
}
2019-11-21 12:55:17 -08:00
function getMemory(req, res, next) {
2019-11-21 12:58:06 -08:00
system.getMemory(function (error, result) {
2019-11-21 12:55:17 -08:00
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
function update(req, res, next) {
if ('skipBackup' in req.body && typeof req.body.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
// this only initiates the update, progress can be checked via the progress route
updater.updateToLatest(req.body, auditSource.fromRequest(req), function (error, taskId) {
2019-10-23 09:39:26 -07:00
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(422, error.message));
if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
});
}
function getUpdateInfo(req, res, next) {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
}
2021-08-18 15:54:53 -07:00
async function checkForUpdates(req, res, next) {
// it can take a while sometimes to get all the app updates one by one
req.clearTimeout();
2021-08-18 15:54:53 -07:00
await updateChecker.checkForUpdates({ automatic: false });
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
}
function getLogs(req, res, next) {
2018-06-11 20:09:38 +02:00
assert.strictEqual(typeof req.params.unit, 'string');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format || 'json'
};
2018-06-11 20:09:38 +02:00
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': `attachment; filename="${req.params.unit}.log"`,
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
2017-08-07 16:49:37 +02:00
function getLogStream(req, res, next) {
2018-06-11 20:09:38 +02:00
assert.strictEqual(typeof req.params.unit, 'string');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
2017-08-07 16:49:37 +02:00
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true,
format: req.query.format || 'json'
2017-08-07 16:49:37 +02:00
};
2018-06-11 20:09:38 +02:00
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
2017-08-07 16:49:37 +02:00
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
function updateDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.updateDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
2018-12-07 16:39:22 -08:00
next(new HttpSuccess(204, {}));
});
}
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
2019-03-25 15:07:06 -07:00
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
}
2021-07-12 23:35:30 -07:00
async function renewCerts(req, res, next) {
if ('domain' in req.body && typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
2021-07-12 23:35:30 -07:00
const [error, taskId] = await safe(cloudron.renewCerts({ domain: req.body.domain || null }, auditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
2018-12-10 20:20:53 -08:00
2021-07-12 23:35:30 -07:00
next(new HttpSuccess(202, { taskId }));
2018-12-10 20:20:53 -08:00
}
2019-08-29 17:19:51 +02:00
function syncExternalLdap(req, res, next) {
2019-10-25 15:58:11 -07:00
externalLdap.startSyncer(function (error, taskId) {
2019-08-29 17:19:51 +02:00
if (error) return next(new HttpError(500, error.message));
next(new HttpSuccess(202, { taskId }));
2019-08-29 17:19:51 +02:00
});
}
2019-11-07 10:41:15 -08:00
function getServerIp(req, res, next) {
sysinfo.getServerIp(function (error, ip) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { ip }));
});
}
2020-11-18 00:10:06 +01:00
function getLanguages(req, res, next) {
2020-11-19 23:38:59 +01:00
translation.getLanguages(function (error, languages) {
2020-11-18 00:10:06 +01:00
if (error) return next(new BoxError.toHttpError(error));
next(new HttpSuccess(200, { languages }));
});
}
2021-07-12 23:35:30 -07:00
async function syncDnsRecords(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if ('domain' in req.body && typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string'));
2021-07-12 23:35:30 -07:00
const [error, taskId] = await safe(cloudron.syncDnsRecords(req.body));
if (error && error.reason === BoxError.ACCESS_DENIED) return next(new HttpSuccess(200, { error: { reason: error.reason, message: error.message }}));
if (error) return next(BoxError.toHttpError(error));
2021-07-12 23:35:30 -07:00
next(new HttpSuccess(201, { taskId }));
}