diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 9f48532913..c4855ef3e8 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -12,6 +12,10 @@ "usernameTaken": "Username already taken.", "passwordConfirmationMatch": "Password confirmation doesn't match password.", "invalidLoginCredentials": "Incorrect username / email and / or password.", + "passwordReset": "If we have your email on file, your password reset link has been sent to your email.", + "passwordResetEmailSubject": "Password Reset for Habitica", + "passwordResetEmailText": "Password for <%= username %> has been reset to <%= newPassword %> . Important! Both username and password are case-sensitive -- you must enter both exactly as shown here. We recommend copying and pasting both instead of typing them. Log in at <%= baseUrl %>. After you have logged in, head to <%= baseUrl %>/#/options/settings/settings and change your password.", + "passwordResetEmailHtml": "Password for <%= username %> has been reset to <%= newPassword %>.

Important! Both username and password are case-sensitive -- you must enter both exactly as shown here. We recommend copying and pasting both instead of typing them.

Log in at <%= baseUrl %>. After you have logged in, head to <%= baseUrl %>/#/options/settings/settings and change your password.", "invalidCredentials": "User not found with given auth credentials.", "accountSuspended": "Account has been suspended, please contact leslie@habitica.com with your UUID \"<%= userId %>\" for assistance.", "onlyFbSupported": "Only Facebook supported currently.", diff --git a/test/api/v3/integration/user/auth/POST-user_reset_password.test.js b/test/api/v3/integration/user/auth/POST-user_reset_password.test.js new file mode 100644 index 0000000000..6116f67a35 --- /dev/null +++ b/test/api/v3/integration/user/auth/POST-user_reset_password.test.js @@ -0,0 +1,39 @@ +import { + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; + +describe('POST /user/reset-password', async () => { + let endpoint = '/user/reset-password'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('resets password', async () => { + let previousPassword = user.auth.local.hashed_password; + let response = await user.post(endpoint, { + email: user.auth.local.email, + }); + expect(response).to.eql({ message: t('passwordReset') }); + await user.sync(); + expect(user.auth.local.hashed_password).to.not.eql(previousPassword); + }); + + it('same message on error as on success', async () => { + let response = await user.post(endpoint, { + email: 'nonExistent@email.com', + }); + expect(response).to.eql({ message: t('passwordReset') }); + }); + + it('errors if email is not provided', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); +}); + diff --git a/website/src/controllers/api-v3/auth.js b/website/src/controllers/api-v3/auth.js index 875d8672ca..e278d95f01 100644 --- a/website/src/controllers/api-v3/auth.js +++ b/website/src/controllers/api-v3/auth.js @@ -20,6 +20,7 @@ import { model as EmailUnsubscription } from '../../models/emailUnsubscription'; import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email'; import { decrypt } from '../../libs/api-v3/encryption'; import FirebaseTokenGenerator from 'firebase-token-generator'; +import { send as sendEmail } from '../../libs/api-v3/email'; let api = {}; @@ -368,6 +369,56 @@ api.updatePassword = { }, }; +/** + * @api {post} /user/reset-password + * @apiVersion 3.0.0 + * @apiName resetPassword + * @apiGroup User + * @apiParam {string} email email + * @apiSuccess {Object} The success message + **/ +api.resetPassword = { + method: 'POST', + middlewares: [], + url: '/user/reset-password', + async handler (req, res) { + req.checkBody({ + email: { + notEmpty: {errorMessage: res.t('missingEmail')}, + }, + }); + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let email = req.body.email.toLowerCase(); + let salt = passwordUtils.makeSalt(); + let newPassword = passwordUtils.makeSalt(); // use a salt as the new password too (they'll change it later) + let hashedPassword = passwordUtils.encrypt(newPassword, salt); + + let user = await User.findOne({ 'auth.local.email': email }, { 'auth.local': 1 }); + + if (user) { + user.auth.local.salt = salt; + user.auth.local.hashed_password = hashedPassword; // eslint-disable-line camelcase + sendEmail({ + from: 'Habitica ', + to: email, + subject: res.t('passwordResetEmailSubject'), + text: res.t('passwordResetEmailText', { username: user.auth.local.username, + newPassword, + baseUrl: nconf.get('BASE_URL'), + }), + html: res.t('passwordResetEmailHtml', { username: user.auth.local.username, + newPassword, + baseUrl: nconf.get('BASE_URL'), + }), + }); + await user.save(); + } + res.respond(200, { message: res.t('passwordReset') }); + }, +}; + /** * @api {put} /user/auth/update-email * @apiVersion 3.0.0 diff --git a/website/src/controllers/api-v3/email.js b/website/src/controllers/api-v3/email.js index f1a5324872..ed2204baed 100644 --- a/website/src/controllers/api-v3/email.js +++ b/website/src/controllers/api-v3/email.js @@ -28,7 +28,6 @@ api.unsubscribe = { notEmpty: {errorMessage: res.t('missingUnsubscriptionCode')}, }, }); - let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors;