'use strict'; exports = module.exports = { login, logout, passwordResetRequest, passwordReset, setupAccount, getBranding }; const assert = require('assert'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), branding = require('../branding.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, 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.socket.remoteAddress || null; const userAgent = req.headers['user-agent'] || ''; const tokenTypeError = tokens.validateTokenType(type); if (tokenTypeError) return next(new HttpError(400, tokenTypeError.message)); const [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)); const auditSource = AuditSource.fromRequest(req); 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), type, appId: tokens.ID_CLI }); await 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')); const [getError, userObject] = await safe(users.getByResetToken(req.body.resetToken)); if (getError) 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 const [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)); const [addError, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); if (addError) return next(BoxError.toHttpError(addError)); 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 })); } async function getBranding(req, res, next) { // still used in passwordreset and setupaccount views. we should server render them const result = { cloudronName: await branding.getCloudronName(), footer: await branding.renderFooter(), language: await cloudron.getLanguage(), }; next(new HttpSuccess(200, result)); }