diff --git a/src/mailer.js b/src/mailer.js index 9efa25889..e9eabf342 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -152,8 +152,10 @@ async function sendNewLoginLocation(user, loginLocation) { await sendMail(mailOptions); } -async function passwordReset(user, resetLink) { +async function passwordReset(user, email, resetLink) { assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof email, 'string'); + assert.strictEqual(typeof resetLink, 'string'); const mailConfig = await getMailConfig(); const translationAssets = await translation.getTranslations(); @@ -167,7 +169,7 @@ async function passwordReset(user, resetLink) { const mailOptions = { from: mailConfig.notificationFrom, - to: user.fallbackEmail, + to: email, subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }), text: render('password_reset-text.ejs', templateData, translationAssets), html: render('password_reset-html.ejs', templateData, translationAssets) diff --git a/src/routes/users.js b/src/routes/users.js index a08e9c751..49926d379 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -13,6 +13,9 @@ exports = module.exports = { setGhost, makeOwner, + getPasswordResetLink, + sendPasswordResetEmail, + disableTwoFactorAuthentication, load @@ -216,3 +219,24 @@ async function makeOwner(req, res, next) { next(new HttpSuccess(204)); } + +// This will always return a reset link, if none is set or expired a new one will be created +async function getPasswordResetLink(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); + + let [error, passwordResetLink] = await safe(users.getPasswordResetLink(req.resource, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, { passwordResetLink })); +} + +async function sendPasswordResetEmail(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); + + if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string')); + + let [error] = await safe(users.sendPasswordResetEmail(req.resource, req.body.email, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, {})); +} diff --git a/src/server.js b/src/server.js index c37085b8e..24593443b 100644 --- a/src/server.js +++ b/src/server.js @@ -180,6 +180,8 @@ function initializeExpressSync() { router.put ('/api/v1/users/:userId/groups', json, token, authorizeUserManager, routes.users.load, routes.users.setGroups); router.post('/api/v1/users/:userId/make_owner', json, token, authorizeOwner, routes.users.load, routes.users.makeOwner); router.post('/api/v1/users/:userId/send_invite', json, token, authorizeUserManager, routes.users.load, routes.users.sendInvite); + router.get ('/api/v1/users/:userId/password_reset_link', json, token, authorizeUserManager, routes.users.load, routes.users.getPasswordResetLink); + router.post('/api/v1/users/:userId/send_password_reset_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendPasswordResetEmail); router.post('/api/v1/users/:userId/twofactorauthentication_disable', json, token, authorizeUserManager, routes.users.load, routes.users.disableTwoFactorAuthentication); // Group management diff --git a/src/users.js b/src/users.js index 0fe7a57ef..d4c04231f 100644 --- a/src/users.js +++ b/src/users.js @@ -37,6 +37,9 @@ exports = module.exports = { sendPasswordResetByIdentifier, + getPasswordResetLink, + sendPasswordResetEmail, + notifyLoginLocation, setupAccount, @@ -623,11 +626,39 @@ async function sendPasswordResetByIdentifier(identifier, auditSource) { await update(user, { resetToken,resetTokenCreationTime }, auditSource); const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${user.resetToken}`; - await mailer.passwordReset(user, resetLink); + await mailer.passwordReset(user, user.fallbackEmail || user.email, resetLink); return resetLink; } +async function getPasswordResetLink(user, auditSource) { + assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + + let resetToken = user.resetToken; + let resetTokenCreationTime = user.resetTokenCreationTime || 0; + + if (!resetToken || (Date.now() - resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000)) { + resetToken = hat(256); + resetTokenCreationTime = new Date(); + + await update(user, { resetToken, resetTokenCreationTime }, auditSource); + } + + const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${resetToken}`; + + return resetLink; +} + +async function sendPasswordResetEmail(user, email, auditSource) { + assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof email, 'string'); + assert.strictEqual(typeof auditSource, 'object'); + + const resetLink = await getPasswordResetLink(user, auditSource); + await mailer.passwordReset(user, email, resetLink); +} + async function notifyLoginLocation(user, ip, userAgent, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof ip, 'string');