mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Delete Account with Social Auth (#8796)
* feat(accounts): delete social accts * test(integration): social auth delete
This commit is contained in:
@@ -18,10 +18,13 @@ import {
|
|||||||
} from '../../../../../website/server/libs/password';
|
} from '../../../../../website/server/libs/password';
|
||||||
import * as email from '../../../../../website/server/libs/email';
|
import * as email from '../../../../../website/server/libs/email';
|
||||||
|
|
||||||
|
const DELETE_CONFIRMATION = 'DELETE';
|
||||||
|
|
||||||
describe('DELETE /user', () => {
|
describe('DELETE /user', () => {
|
||||||
let user;
|
let user;
|
||||||
let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
|
let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
|
||||||
|
|
||||||
|
context('user with local auth', async () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = await generateUser({balance: 10});
|
user = await generateUser({balance: 10});
|
||||||
});
|
});
|
||||||
@@ -46,6 +49,13 @@ describe('DELETE /user', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deletes the user', async () => {
|
||||||
|
await user.del('/user', {
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns an error if excessive feedback is supplied', async () => {
|
it('returns an error if excessive feedback is supplied', async () => {
|
||||||
let feedbackText = 'spam feedback ';
|
let feedbackText = 'spam feedback ';
|
||||||
let feedback = feedbackText;
|
let feedback = feedbackText;
|
||||||
@@ -117,13 +127,6 @@ describe('DELETE /user', () => {
|
|||||||
expect(challenge.memberCount).to.eql(1);
|
expect(challenge.memberCount).to.eql(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the user', async () => {
|
|
||||||
await user.del('/user', {
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sends feedback to the admin email', async () => {
|
it('sends feedback to the admin email', async () => {
|
||||||
sandbox.spy(email, 'sendTxn');
|
sandbox.spy(email, 'sendTxn');
|
||||||
|
|
||||||
@@ -280,4 +283,63 @@ describe('DELETE /user', () => {
|
|||||||
expect(userInGroup).to.not.exist;
|
expect(userInGroup).to.not.exist;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('user with Facebook auth', async () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser({
|
||||||
|
auth: {
|
||||||
|
facebook: {
|
||||||
|
id: 'facebook-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if confirmation phrase is wrong', async () => {
|
||||||
|
await expect(user.del('/user', {
|
||||||
|
password: 'just-do-it',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('incorrectDeletePhrase'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if confirmation phrase is not supplied', async () => {
|
||||||
|
await expect(user.del('/user', {
|
||||||
|
password: '',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('missingPassword'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a Facebook user', async () => {
|
||||||
|
await user.del('/user', {
|
||||||
|
password: DELETE_CONFIRMATION,
|
||||||
|
});
|
||||||
|
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('user with Google auth', async () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser({
|
||||||
|
auth: {
|
||||||
|
google: {
|
||||||
|
id: 'google-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a Google user', async () => {
|
||||||
|
await user.del('/user', {
|
||||||
|
password: DELETE_CONFIRMATION,
|
||||||
|
});
|
||||||
|
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -246,6 +246,7 @@
|
|||||||
"missingNewPassword": "Missing new password.",
|
"missingNewPassword": "Missing new password.",
|
||||||
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
|
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
|
||||||
"wrongPassword": "Wrong password.",
|
"wrongPassword": "Wrong password.",
|
||||||
|
"incorrectDeletePhrase": "Please type DELETE in all caps to delete your account.",
|
||||||
"notAnEmail": "Invalid email address.",
|
"notAnEmail": "Invalid email address.",
|
||||||
"emailTaken": "Email address is already used in an account.",
|
"emailTaken": "Email address is already used in an account.",
|
||||||
"newEmailRequired": "Missing new email address.",
|
"newEmailRequired": "Missing new email address.",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
"resetText1": "WARNING! This resets many parts of your account. This is highly discouraged, but some people find it useful in the beginning after playing with the site for a short time.",
|
"resetText1": "WARNING! This resets many parts of your account. This is highly discouraged, but some people find it useful in the beginning after playing with the site for a short time.",
|
||||||
"resetText2": "You will lose all your levels, gold, and experience points. All your tasks (except those from challenges) will be deleted permanently and you will lose all of their historical data. You will lose all your equipment but you will be able to buy it all back, including all limited edition equipment or subscriber Mystery items that you already own (you will need to be in the correct class to re-buy class-specific gear). You will keep your current class and your pets and mounts. You might prefer to use an Orb of Rebirth instead, which is a much safer option and which will preserve your tasks and equipment.",
|
"resetText2": "You will lose all your levels, gold, and experience points. All your tasks (except those from challenges) will be deleted permanently and you will lose all of their historical data. You will lose all your equipment but you will be able to buy it all back, including all limited edition equipment or subscriber Mystery items that you already own (you will need to be in the correct class to re-buy class-specific gear). You will keep your current class and your pets and mounts. You might prefer to use an Orb of Rebirth instead, which is a much safer option and which will preserve your tasks and equipment.",
|
||||||
"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.",
|
"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 \"DELETE\" into the text box below.",
|
||||||
"API": "API",
|
"API": "API",
|
||||||
"APIv3": "API v3",
|
"APIv3": "API v3",
|
||||||
"APIText": "Copy these for use in third party applications. However, think of your API Token like a password, and do not share it publicly. You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.",
|
"APIText": "Copy these for use in third party applications. However, think of your API Token like a password, and do not share it publicly. You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import nconf from 'nconf';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
||||||
|
const DELETE_CONFIRMATION = 'DELETE';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine UserNotFound
|
* @apiDefine UserNotFound
|
||||||
@@ -303,14 +304,15 @@ api.deleteUser = {
|
|||||||
let password = req.body.password;
|
let password = req.body.password;
|
||||||
if (!password) throw new BadRequest(res.t('missingPassword'));
|
if (!password) throw new BadRequest(res.t('missingPassword'));
|
||||||
|
|
||||||
let feedback = req.body.feedback;
|
if (user.auth.local.hashed_password && user.auth.local.email) {
|
||||||
if (feedback && feedback.length > 10000) throw new BadRequest(`Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email ${TECH_ASSISTANCE_EMAIL}.`);
|
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
|
||||||
if (validationErrors) throw validationErrors;
|
|
||||||
|
|
||||||
let isValidPassword = await passwordUtils.compare(user, password);
|
let isValidPassword = await passwordUtils.compare(user, password);
|
||||||
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
||||||
|
} else if ((user.auth.facebook.id || user.auth.google.id) && password !== DELETE_CONFIRMATION) {
|
||||||
|
throw new NotAuthorized(res.t('incorrectDeletePhrase'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let feedback = req.body.feedback;
|
||||||
|
if (feedback && feedback.length > 10000) throw new BadRequest(`Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email ${TECH_ASSISTANCE_EMAIL}.`);
|
||||||
|
|
||||||
if (plan && plan.customerId && !plan.dateTerminated) {
|
if (plan && plan.customerId && !plan.dateTerminated) {
|
||||||
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
||||||
|
|||||||
@@ -183,4 +183,5 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
|||||||
span=env.t('dangerZone')
|
span=env.t('dangerZone')
|
||||||
.panel-body
|
.panel-body
|
||||||
a.btn.btn-danger(ng-click='openModal("reset", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount')
|
a.btn.btn-danger(ng-click='openModal("reset", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount')
|
||||||
a.btn.btn-danger(ng-click='openModal("delete", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount')
|
a.btn.btn-danger(ng-if='user.auth.local.email' ng-click='openModal("deletelocal", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount')
|
||||||
|
a.btn.btn-danger(ng-if='!user.auth.local.email', ng-click='openModal("deletesocial", {controller:"SettingsCtrl"})', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount')
|
||||||
@@ -57,7 +57,7 @@ script(type='text/ng-template', id='modals/restore.html')
|
|||||||
button.btn.btn-default(ng-click='$close()')=env.t('discardChanges')
|
button.btn.btn-default(ng-click='$close()')=env.t('discardChanges')
|
||||||
button.btn.btn-primary(ng-click='restore()')=env.t('saveAndClose')
|
button.btn.btn-primary(ng-click='restore()')=env.t('saveAndClose')
|
||||||
|
|
||||||
script(type='text/ng-template', id='modals/delete.html')
|
script(type='text/ng-template', id='modals/deletelocal.html')
|
||||||
.modal-header
|
.modal-header
|
||||||
h4=env.t('deleteAccount')
|
h4=env.t('deleteAccount')
|
||||||
.modal-body
|
.modal-body
|
||||||
@@ -74,3 +74,16 @@ script(type='text/ng-template', id='modals/delete.html')
|
|||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-default(ng-click='$close()')=env.t('neverMind')
|
button.btn.btn-default(ng-click='$close()')=env.t('neverMind')
|
||||||
button.btn.btn-danger(ng-disabled='!_deleteAccount', ng-click='$close(); delete(_deleteAccount, feedback)')=env.t('deleteDo')
|
button.btn.btn-danger(ng-disabled='!_deleteAccount', ng-click='$close(); delete(_deleteAccount, feedback)')=env.t('deleteDo')
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='modals/deletesocial.html')
|
||||||
|
.modal-header
|
||||||
|
h4=env.t('deleteAccount')
|
||||||
|
.modal-body
|
||||||
|
p!=env.t('deleteSocialAccountText')
|
||||||
|
br
|
||||||
|
.row
|
||||||
|
.col-md-6
|
||||||
|
input.form-control(type='text', ng-model='_deleteAccount')
|
||||||
|
.modal-footer
|
||||||
|
button.btn.btn-default(ng-click='$close()')=env.t('neverMind')
|
||||||
|
button.btn.btn-danger(ng-disabled='!_deleteAccount', ng-click='$close(); delete(_deleteAccount, feedback)')=env.t('deleteDo')
|
||||||
|
|||||||
Reference in New Issue
Block a user