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,266 +18,328 @@ 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
|
||||||
|
|
||||||
beforeEach(async () => {
|
context('user with local auth', async () => {
|
||||||
user = await generateUser({balance: 10});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error if password is wrong', async () => {
|
|
||||||
await expect(user.del('/user', {
|
|
||||||
password: 'wrong-password',
|
|
||||||
})).to.eventually.be.rejected.and.eql({
|
|
||||||
code: 401,
|
|
||||||
error: 'NotAuthorized',
|
|
||||||
message: t('wrongPassword'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error if password is not supplied', async () => {
|
|
||||||
await expect(user.del('/user', {
|
|
||||||
password: '',
|
|
||||||
})).to.eventually.be.rejected.and.eql({
|
|
||||||
code: 400,
|
|
||||||
error: 'BadRequest',
|
|
||||||
message: t('missingPassword'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error if excessive feedback is supplied', async () => {
|
|
||||||
let feedbackText = 'spam feedback ';
|
|
||||||
let feedback = feedbackText;
|
|
||||||
while (feedback.length < 10000) {
|
|
||||||
feedback = feedback + feedbackText;
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(user.del('/user', {
|
|
||||||
password,
|
|
||||||
feedback,
|
|
||||||
})).to.eventually.be.rejected.and.eql({
|
|
||||||
code: 400,
|
|
||||||
error: 'BadRequest',
|
|
||||||
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error if user has active subscription', async () => {
|
|
||||||
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
|
|
||||||
|
|
||||||
await expect(userWithSubscription.del('/user', {
|
|
||||||
password,
|
|
||||||
})).to.be.rejected.and.to.eventually.eql({
|
|
||||||
code: 401,
|
|
||||||
error: 'NotAuthorized',
|
|
||||||
message: t('cannotDeleteActiveAccount'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes the user\'s tasks', async () => {
|
|
||||||
// gets the user's tasks ids
|
|
||||||
let ids = [];
|
|
||||||
each(user.tasksOrder, (idsForOrder) => {
|
|
||||||
ids.push(...idsForOrder);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
|
|
||||||
|
|
||||||
await user.del('/user', {
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
await Bluebird.all(map(ids, id => {
|
|
||||||
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reduces memberCount in challenges user is linked to', async () => {
|
|
||||||
let populatedGroup = await createAndPopulateGroup({
|
|
||||||
members: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
let group = populatedGroup.group;
|
|
||||||
let authorizedUser = populatedGroup.members[1];
|
|
||||||
|
|
||||||
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
|
|
||||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
|
||||||
|
|
||||||
await challenge.sync();
|
|
||||||
|
|
||||||
expect(challenge.memberCount).to.eql(2);
|
|
||||||
|
|
||||||
await authorizedUser.del('/user', {
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
await challenge.sync();
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
sandbox.spy(email, 'sendTxn');
|
|
||||||
|
|
||||||
let feedback = 'Reasons for Deletion';
|
|
||||||
await user.del('/user', {
|
|
||||||
password,
|
|
||||||
feedback,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(email.sendTxn).to.be.calledOnce;
|
|
||||||
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not send email if no feedback is supplied', async () => {
|
|
||||||
sandbox.spy(email, 'sendTxn');
|
|
||||||
|
|
||||||
await user.del('/user', {
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(email.sendTxn).to.not.be.called;
|
|
||||||
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes the user with a legacy sha1 password', async () => {
|
|
||||||
let textPassword = 'mySecretPassword';
|
|
||||||
let salt = sha1MakeSalt();
|
|
||||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
|
||||||
|
|
||||||
await user.update({
|
|
||||||
'auth.local.hashed_password': sha1HashedPassword,
|
|
||||||
'auth.local.passwordHashMethod': 'sha1',
|
|
||||||
'auth.local.salt': salt,
|
|
||||||
});
|
|
||||||
|
|
||||||
await user.sync();
|
|
||||||
|
|
||||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
|
||||||
expect(user.auth.local.salt).to.equal(salt);
|
|
||||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
|
||||||
|
|
||||||
// delete the user
|
|
||||||
await user.del('/user', {
|
|
||||||
password: textPassword,
|
|
||||||
});
|
|
||||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
context('last member of a party', () => {
|
|
||||||
let party;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
party = await generateGroup(user, {
|
user = await generateUser({balance: 10});
|
||||||
type: 'party',
|
});
|
||||||
privacy: 'private',
|
|
||||||
|
it('returns an error if password is wrong', async () => {
|
||||||
|
await expect(user.del('/user', {
|
||||||
|
password: 'wrong-password',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('wrongPassword'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes party when user is the only member', async () => {
|
it('returns an error if password 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 the user', async () => {
|
||||||
await user.del('/user', {
|
await user.del('/user', {
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
|
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
context('last member of a private guild', () => {
|
it('returns an error if excessive feedback is supplied', async () => {
|
||||||
let privateGuild;
|
let feedbackText = 'spam feedback ';
|
||||||
|
let feedback = feedbackText;
|
||||||
|
while (feedback.length < 10000) {
|
||||||
|
feedback = feedback + feedbackText;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
await expect(user.del('/user', {
|
||||||
privateGuild = await generateGroup(user, {
|
password,
|
||||||
type: 'guild',
|
feedback,
|
||||||
privacy: 'private',
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes guild when user is the only member', async () => {
|
it('returns an error if user has active subscription', async () => {
|
||||||
|
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
|
||||||
|
|
||||||
|
await expect(userWithSubscription.del('/user', {
|
||||||
|
password,
|
||||||
|
})).to.be.rejected.and.to.eventually.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('cannotDeleteActiveAccount'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes the user\'s tasks', async () => {
|
||||||
|
// gets the user's tasks ids
|
||||||
|
let ids = [];
|
||||||
|
each(user.tasksOrder, (idsForOrder) => {
|
||||||
|
ids.push(...idsForOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
|
||||||
|
|
||||||
await user.del('/user', {
|
await user.del('/user', {
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
|
|
||||||
|
await Bluebird.all(map(ids, id => {
|
||||||
|
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
context('groups user is leader of', () => {
|
it('reduces memberCount in challenges user is linked to', async () => {
|
||||||
let guild, oldLeader, newLeader;
|
let populatedGroup = await createAndPopulateGroup({
|
||||||
|
members: 2,
|
||||||
beforeEach(async () => {
|
|
||||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
|
||||||
groupDetails: {
|
|
||||||
type: 'guild',
|
|
||||||
privacy: 'public',
|
|
||||||
},
|
|
||||||
members: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
guild = group;
|
let group = populatedGroup.group;
|
||||||
newLeader = members[0];
|
let authorizedUser = populatedGroup.members[1];
|
||||||
oldLeader = groupLeader;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('chooses new group leader for any group user was the leader of', async () => {
|
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
|
||||||
await oldLeader.del('/user', {
|
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||||
|
|
||||||
|
await challenge.sync();
|
||||||
|
|
||||||
|
expect(challenge.memberCount).to.eql(2);
|
||||||
|
|
||||||
|
await authorizedUser.del('/user', {
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
let updatedGuild = await newLeader.get(`/groups/${guild._id}`);
|
await challenge.sync();
|
||||||
|
|
||||||
expect(updatedGuild.leader).to.exist;
|
expect(challenge.memberCount).to.eql(1);
|
||||||
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context('groups user is a part of', () => {
|
|
||||||
let group1, group2, userToDelete, otherUser;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
userToDelete = await generateUser({balance: 10});
|
|
||||||
|
|
||||||
group1 = await generateGroup(userToDelete, {
|
|
||||||
type: 'guild',
|
|
||||||
privacy: 'public',
|
|
||||||
});
|
|
||||||
|
|
||||||
let {group, members} = await createAndPopulateGroup({
|
|
||||||
groupDetails: {
|
|
||||||
type: 'guild',
|
|
||||||
privacy: 'public',
|
|
||||||
},
|
|
||||||
members: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
group2 = group;
|
|
||||||
otherUser = members[0];
|
|
||||||
|
|
||||||
await userToDelete.post(`/groups/${group2._id}/join`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes user from all groups user was a part of', async () => {
|
it('sends feedback to the admin email', async () => {
|
||||||
await userToDelete.del('/user', {
|
sandbox.spy(email, 'sendTxn');
|
||||||
|
|
||||||
|
let feedback = 'Reasons for Deletion';
|
||||||
|
await user.del('/user', {
|
||||||
|
password,
|
||||||
|
feedback,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(email.sendTxn).to.be.calledOnce;
|
||||||
|
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not send email if no feedback is supplied', async () => {
|
||||||
|
sandbox.spy(email, 'sendTxn');
|
||||||
|
|
||||||
|
await user.del('/user', {
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
let updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
|
expect(email.sendTxn).to.not.be.called;
|
||||||
let updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
|
|
||||||
let userInGroup = find(updatedGroup2Members, (member) => {
|
sandbox.restore();
|
||||||
return member._id === userToDelete._id;
|
});
|
||||||
|
|
||||||
|
it('deletes the user with a legacy sha1 password', async () => {
|
||||||
|
let textPassword = 'mySecretPassword';
|
||||||
|
let salt = sha1MakeSalt();
|
||||||
|
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
'auth.local.hashed_password': sha1HashedPassword,
|
||||||
|
'auth.local.passwordHashMethod': 'sha1',
|
||||||
|
'auth.local.salt': salt,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updatedGroup1Members).to.be.empty;
|
await user.sync();
|
||||||
expect(updatedGroup2Members).to.not.be.empty;
|
|
||||||
expect(userInGroup).to.not.exist;
|
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||||
|
expect(user.auth.local.salt).to.equal(salt);
|
||||||
|
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||||
|
|
||||||
|
// delete the user
|
||||||
|
await user.del('/user', {
|
||||||
|
password: textPassword,
|
||||||
|
});
|
||||||
|
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('last member of a party', () => {
|
||||||
|
let party;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
party = await generateGroup(user, {
|
||||||
|
type: 'party',
|
||||||
|
privacy: 'private',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes party when user is the only member', async () => {
|
||||||
|
await user.del('/user', {
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('last member of a private guild', () => {
|
||||||
|
let privateGuild;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
privateGuild = await generateGroup(user, {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes guild when user is the only member', async () => {
|
||||||
|
await user.del('/user', {
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('groups user is leader of', () => {
|
||||||
|
let guild, oldLeader, newLeader;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
guild = group;
|
||||||
|
newLeader = members[0];
|
||||||
|
oldLeader = groupLeader;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chooses new group leader for any group user was the leader of', async () => {
|
||||||
|
await oldLeader.del('/user', {
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedGuild = await newLeader.get(`/groups/${guild._id}`);
|
||||||
|
|
||||||
|
expect(updatedGuild.leader).to.exist;
|
||||||
|
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('groups user is a part of', () => {
|
||||||
|
let group1, group2, userToDelete, otherUser;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
userToDelete = await generateUser({balance: 10});
|
||||||
|
|
||||||
|
group1 = await generateGroup(userToDelete, {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
});
|
||||||
|
|
||||||
|
let {group, members} = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
group2 = group;
|
||||||
|
otherUser = members[0];
|
||||||
|
|
||||||
|
await userToDelete.post(`/groups/${group2._id}/join`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes user from all groups user was a part of', async () => {
|
||||||
|
await userToDelete.del('/user', {
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
|
||||||
|
let updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
|
||||||
|
let userInGroup = find(updatedGroup2Members, (member) => {
|
||||||
|
return member._id === userToDelete._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedGroup1Members).to.be.empty;
|
||||||
|
expect(updatedGroup2Members).to.not.be.empty;
|
||||||
|
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,15 +304,16 @@ 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'));
|
||||||
|
|
||||||
|
if (user.auth.local.hashed_password && user.auth.local.email) {
|
||||||
|
let isValidPassword = await passwordUtils.compare(user, password);
|
||||||
|
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;
|
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 (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);
|
|
||||||
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
|
||||||
|
|
||||||
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