diff --git a/test/api/v4/user/POST-user_reset.test.js b/test/api/v4/user/POST-user_reset.test.js index 2aba79baff..8a489e2e38 100644 --- a/test/api/v4/user/POST-user_reset.test.js +++ b/test/api/v4/user/POST-user_reset.test.js @@ -6,6 +6,8 @@ import { translate as t, } from '../../../helpers/api-integration/v4'; +const RESET_CONFIRMATION = 'RESET'; + describe('POST /user/reset', () => { let user; @@ -172,4 +174,68 @@ describe('POST /user/reset', () => { expect(heroRes.secret).to.exist; expect(heroRes.secret.text).to.be.eq('Super-Hero'); }); + + context('user with Google auth', async () => { + beforeEach(async () => { + user = await generateUser({ + auth: { + google: { + id: 'google-id', + }, + }, + }); + }); + + it('resets a Google user', async () => { + const task = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + await user.post('/user/reset', { + password: RESET_CONFIRMATION, + }); + await user.sync(); + + await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageTaskNotFound'), + }); + + expect(user.tasksOrder.habits).to.be.empty; + }); + }); + + context('user with Apple auth', async () => { + beforeEach(async () => { + user = await generateUser({ + auth: { + apple: { + id: 'apple-id', + }, + }, + }); + }); + + it('resets an Apple user', async () => { + const task = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + await user.post('/user/reset', { + password: RESET_CONFIRMATION, + }); + await user.sync(); + + await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageTaskNotFound'), + }); + + expect(user.tasksOrder.habits).to.be.empty; + }); + }); }); diff --git a/website/client/package-lock.json b/website/client/package-lock.json index cab468beaa..3ce658b16c 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -18676,7 +18676,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" }, "strip-eof": { "version": "1.0.0", diff --git a/website/client/src/pages/settings/settingRows/resetAccount.vue b/website/client/src/pages/settings/settingRows/resetAccount.vue index 2db5351e71..9da36aed56 100644 --- a/website/client/src/pages/settings/settingRows/resetAccount.vue +++ b/website/client/src/pages/settings/settingRows/resetAccount.vue @@ -35,7 +35,7 @@ v-html="$t('resetText1')" > -
+
+ +
+
+
+ +
+
+
+ {{ $t("confirm") }} +
+
+ +
+
+
", "wrongPassword": "Password is incorrect. If you forgot your password, click \"Forgot Password.\"", "incorrectDeletePhrase": "Please type <%= magicWord %> in all capital letters to delete your account.", + "incorrectResetPhrase": "Please type <%= magicWord %> in all capital letters to reset your account.", "notAnEmail": "Invalid email address.", "emailTaken": "Email address is already used in an account.", "newEmailRequired": "Missing new email address.", diff --git a/website/common/locales/en/settings.json b/website/common/locales/en/settings.json index a9586a943c..56006ba652 100644 --- a/website/common/locales/en/settings.json +++ b/website/common/locales/en/settings.json @@ -80,6 +80,8 @@ "resetDetail3": "All your tasks (except those from challenges) will be deleted permanently and you will lose all of their historical data.", "resetDetail4": "You will lose all your equipment except Subscriber Mystery Items and free commemorative items. You will be able to buy the deleted items back, including all limited edition equipment (you will need to be in the correct class to re-buy class-specific gear).", "resetText2": "Another option is using an Orb of Rebirth, which will reset everything else while preserving your Tasks and Equipment.", + "resetTextLocal": "If you're absolutely certain, type your password into the text box below.", + "resetTextSocial": "If you're absolutely certain, type \"<%= magicWord %>\" into the text box below.", "deleteLocalAccountText": "Are you sure? This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type your password into the text box below.", "deleteSocialAccountText": "Are you sure? This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type \"<%= magicWord %>\" into the text box below.", "API": "API", diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index 2ba2fa20cb..33f4c0ec44 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -285,7 +285,7 @@ api.deleteUser = { (user.auth.facebook.id || user.auth.google.id || user.auth.apple.id) && password !== DELETE_CONFIRMATION ) { - throw new NotAuthorized(res.t('incorrectDeletePhrase', { magicWord: 'DELETE' })); + throw new NotAuthorized(res.t('incorrectDeletePhrase', { magicWord: DELETE_CONFIRMATION })); } const { feedback } = req.body; diff --git a/website/server/controllers/api-v4/user.js b/website/server/controllers/api-v4/user.js index 52b5e6d895..e324ce13b6 100644 --- a/website/server/controllers/api-v4/user.js +++ b/website/server/controllers/api-v4/user.js @@ -7,6 +7,7 @@ import { BadRequest, NotAuthorized } from '../../libs/errors'; import * as passwordUtils from '../../libs/password'; const api = {}; +const RESET_CONFIRMATION = 'RESET'; /* * NOTE most user routes are still in the v3 controller @@ -224,9 +225,14 @@ api.userReset = { throw new BadRequest(res.t('missingPassword')); } - const isValidPassword = await passwordUtils.compare(user, password); - if (!isValidPassword) { - throw new NotAuthorized(res.t('wrongPassword')); + if (user.auth.local.hashed_password && user.auth.local.email) { + const isValidPassword = await passwordUtils.compare(user, password); + if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword')); + } else if ( + (user.auth.facebook.id || user.auth.google.id || user.auth.apple.id) + && password !== RESET_CONFIRMATION + ) { + throw new NotAuthorized(res.t('incorrectResetPhrase', { magicWord: RESET_CONFIRMATION })); } await userLib.reset(req, res, { isV3: false });