the backtraces just flood the logs apphealthtask: remove console.error remove spurious console.dir cleanup scheduler error logging
333 lines
13 KiB
JavaScript
333 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
login: login,
|
|
logout: logout,
|
|
passwordResetRequest: passwordResetRequest,
|
|
passwordReset: passwordReset,
|
|
setupAccount: setupAccount,
|
|
reboot: reboot,
|
|
isRebootRequired: isRebootRequired,
|
|
getConfig: getConfig,
|
|
getDisks: getDisks,
|
|
getMemory: getMemory,
|
|
getUpdateInfo: getUpdateInfo,
|
|
update: update,
|
|
checkForUpdates: checkForUpdates,
|
|
getLogs: getLogs,
|
|
getLogStream: getLogStream,
|
|
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
|
prepareDashboardDomain: prepareDashboardDomain,
|
|
renewCerts: renewCerts,
|
|
getServerIp: getServerIp,
|
|
syncExternalLdap: syncExternalLdap
|
|
};
|
|
|
|
let assert = require('assert'),
|
|
async = require('async'),
|
|
auditSource = require('../auditsource.js'),
|
|
BoxError = require('../boxerror.js'),
|
|
cloudron = require('../cloudron.js'),
|
|
constants = require('../constants.js'),
|
|
eventlog = require('../eventlog.js'),
|
|
externalLdap = require('../externalldap.js'),
|
|
HttpError = require('connect-lastmile').HttpError,
|
|
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
|
sysinfo = require('../sysinfo.js'),
|
|
system = require('../system.js'),
|
|
tokendb = require('../tokendb.js'),
|
|
tokens = require('../tokens.js'),
|
|
updater = require('../updater.js'),
|
|
users = require('../users.js'),
|
|
updateChecker = require('../updatechecker.js');
|
|
|
|
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 auditSource = { authType: 'basic', ip: ip };
|
|
|
|
const error = tokens.validateTokenType(type);
|
|
if (error) return next(new HttpError(400, error.message));
|
|
|
|
tokens.add(type, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
|
if (error) return next(new HttpError(500, error));
|
|
|
|
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
|
|
|
next(new HttpSuccess(200, result));
|
|
});
|
|
}
|
|
|
|
function logout(req, res) {
|
|
var token;
|
|
|
|
// this determines the priority
|
|
if (req.body && req.body.access_token) token = req.body.access_token;
|
|
if (req.query && req.query.access_token) token = req.query.access_token;
|
|
if (req.headers && req.headers.authorization) {
|
|
var parts = req.headers.authorization.split(' ');
|
|
if (parts.length == 2) {
|
|
var scheme = parts[0];
|
|
var credentials = parts[1];
|
|
|
|
if (/^Bearer$/i.test(scheme)) token = credentials;
|
|
}
|
|
}
|
|
|
|
if (!token) return res.redirect('/login.html');
|
|
|
|
tokendb.delByAccessToken(token, function () { res.redirect('/login.html'); });
|
|
}
|
|
|
|
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'));
|
|
|
|
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
|
|
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, {}));
|
|
});
|
|
}
|
|
|
|
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'));
|
|
|
|
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
|
if (error) return next(new HttpError(401, 'Invalid resetToken'));
|
|
|
|
if (Date.now() - userObject.resetTokenCreationTime > 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
|
|
users.setPassword(userObject, req.body.password, function (error) {
|
|
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { accessToken: result.accessToken }));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupAccount(req, res, next) {
|
|
assert.strictEqual(typeof req.body, 'object');
|
|
|
|
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
|
|
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'));
|
|
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
|
|
if (!req.body.displayName || typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
|
|
|
|
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
|
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
|
|
|
|
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
|
|
|
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
|
|
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
|
|
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
|
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
|
if (error) return next(new HttpError(500, error));
|
|
|
|
userObject.username = req.body.username;
|
|
userObject.displayName = req.body.displayName;
|
|
|
|
// setPassword clears the resetToken
|
|
users.setPassword(userObject, req.body.password, function (error) {
|
|
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
|
if (error) return next(new HttpError(500, error));
|
|
|
|
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
|
if (error) return next(new HttpError(500, error));
|
|
|
|
next(new HttpSuccess(201, { accessToken: result.accessToken }));
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function reboot(req, res, next) {
|
|
// Finish the request, to let the appstore know we triggered the reboot
|
|
next(new HttpSuccess(202, {}));
|
|
|
|
cloudron.reboot(function () {});
|
|
}
|
|
|
|
function isRebootRequired(req, res, next) {
|
|
cloudron.isRebootRequired(function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, { rebootRequired: result }));
|
|
});
|
|
}
|
|
|
|
function getConfig(req, res, next) {
|
|
cloudron.getConfig(function (error, cloudronConfig) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, cloudronConfig));
|
|
});
|
|
}
|
|
|
|
function getDisks(req, res, next) {
|
|
system.getDisks(function (error, result) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, result));
|
|
});
|
|
}
|
|
|
|
function getMemory(req, res, next) {
|
|
system.getMemory(function (error, result) {
|
|
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) {
|
|
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() }));
|
|
}
|
|
|
|
function checkForUpdates(req, res, next) {
|
|
// it can take a while sometimes to get all the app updates one by one
|
|
req.clearTimeout();
|
|
|
|
async.series([
|
|
(done) => updateChecker.checkAppUpdates({ automatic: false }, done),
|
|
(done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
|
|
], function () {
|
|
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
|
|
});
|
|
}
|
|
|
|
function getLogs(req, res, next) {
|
|
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'
|
|
};
|
|
|
|
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="log.txt"',
|
|
'Cache-Control': 'no-cache',
|
|
'X-Accel-Buffering': 'no' // disable nginx buffering
|
|
});
|
|
logStream.pipe(res);
|
|
});
|
|
}
|
|
|
|
function getLogStream(req, res, next) {
|
|
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 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'
|
|
};
|
|
|
|
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
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 setDashboardAndMailDomain(req, res, next) {
|
|
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
|
|
|
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
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'));
|
|
|
|
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId }));
|
|
});
|
|
}
|
|
|
|
function renewCerts(req, res, next) {
|
|
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource.fromRequest(req), function (error, taskId) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(202, { taskId }));
|
|
});
|
|
}
|
|
|
|
function syncExternalLdap(req, res, next) {
|
|
externalLdap.startSyncer(function (error, taskId) {
|
|
if (error) return next(new HttpError(500, error.message));
|
|
|
|
next(new HttpSuccess(202, { taskId: taskId }));
|
|
});
|
|
}
|
|
|
|
function getServerIp(req, res, next) {
|
|
sysinfo.getServerIp(function (error, ip) {
|
|
if (error) return next(BoxError.toHttpError(error));
|
|
|
|
next(new HttpSuccess(200, { ip }));
|
|
});
|
|
}
|