Fix resetting account for social accounts (#15087)

* Fix resetting account for social accounts

* added integration tests

* chore(packages): reinstall modules

* only enable reset button if user typed RESET

* fix enabling reset button

---------

Co-authored-by: negue <eugen.bolz@gmail.com>
Co-authored-by: Sabe Jones <sabe@habitica.com>
This commit is contained in:
Phillip Thelen
2024-01-18 22:51:36 +01:00
committed by GitHub
parent 67069b1adc
commit 1ade4c6b3e
7 changed files with 119 additions and 7 deletions

View File

@@ -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;
});
});
});

View File

@@ -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",

View File

@@ -35,7 +35,7 @@
v-html="$t('resetText1')"
>
</div>
<div class="split-lists my-3 ">
<div class="split-lists my-3">
<ul>
<li
v-once
@@ -60,18 +60,49 @@
<div
v-once
v-html="$t('resetText2')"
class="mb-3"
>
</div>
<div
v-if="hasPassword"
v-html="$t('resetTextLocal')"
>
</div>
<div
v-else
v-html="$t('resetTextSocial', {magicWord: 'RESET'})"
>
</div>
<div class="input-area">
<current-password-input
v-if="hasPassword"
:show-forget-password="true"
:is-valid="mixinData.currentPasswordIssues.length === 0"
:invalid-issues="mixinData.currentPasswordIssues"
@passwordValue="passwordValue = $event"
/>
<div
v-else
class="input-area"
>
<div class="form">
<div class="settings-label">
{{ $t("confirm") }}
</div>
<div class="form-group">
<input
v-model="passwordValue"
class="form-control"
type="text"
>
</div>
</div>
</div>
<save-cancel-buttons
:disable-save="passwordValue === ''"
:disable-save="!enableReset"
primary-button-color="btn-danger"
primary-button-label="resetAccount"
@saveClicked="reset()"
@@ -118,6 +149,12 @@ export default {
...mapState({
user: 'user.data',
}),
hasPassword () {
return this.user.auth.local.has_password;
},
enableReset () {
return this.hasPassword ? Boolean(this.passwordValue) : this.passwordValue === 'RESET';
},
},
methods: {
async reset () {

View File

@@ -116,6 +116,7 @@
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
"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.",

View File

@@ -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 <b>Orb of Rebirth</b>, 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 <b>\"<%= magicWord %>\"</b> into the text box below.",
"deleteLocalAccountText": "<b>Are you sure?</b> 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": "<b>Are you sure?</b> 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 <b>\"<%= magicWord %>\"</b> into the text box below.",
"API": "API",

View File

@@ -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;

View File

@@ -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 });