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;