mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Optional feedback on account deletion (#8750)
* Fixed rebase. * Removed commented out mail sending to pass linting. Styles from settings.styl still not propagating to app.css * fix(feedback): address PR comments * fix(style): linting errors
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
describe('DELETE /user', () => {
|
||||
let user;
|
||||
@@ -25,7 +26,7 @@ describe('DELETE /user', () => {
|
||||
user = await generateUser({balance: 10});
|
||||
});
|
||||
|
||||
it('returns an errors if password is wrong', async () => {
|
||||
it('returns an error if password is wrong', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: 'wrong-password',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
@@ -35,6 +36,33 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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'});
|
||||
|
||||
@@ -96,6 +124,32 @@ describe('DELETE /user', () => {
|
||||
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();
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
@import "./options.styl"
|
||||
@import "./no-script.styl"
|
||||
@import "./loading-screen.styl"
|
||||
@import "./settings.styl"
|
||||
|
||||
html,body,p,h1,ul,li,table,tr,th,td
|
||||
margin: 0
|
||||
|
||||
2
website/client-old/css/settings.styl
Normal file
2
website/client-old/css/settings.styl
Normal file
@@ -0,0 +1,2 @@
|
||||
#feedback label
|
||||
font-weight: normal
|
||||
@@ -187,11 +187,11 @@ habitrpg.controller('SettingsCtrl',
|
||||
$rootScope.$state.go('tasks');
|
||||
}
|
||||
|
||||
$scope['delete'] = function(password) {
|
||||
$scope.delete = function(password, feedback) {
|
||||
$http({
|
||||
url: ApiUrl.get() + '/api/v3/user',
|
||||
method: 'DELETE',
|
||||
data: {password: password},
|
||||
data: {password: password, feedback: feedback},
|
||||
})
|
||||
.then(function(res, code) {
|
||||
localStorage.clear();
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"resetAccPop": "Start over, removing all levels, gold, gear, history, and tasks.",
|
||||
"deleteAccount": "Delete Account",
|
||||
"deleteAccPop": "Cancel and remove your Habitica account.",
|
||||
"feedback": "If you'd like to give us feedback, please enter it below - we'd love to know what you liked or didn't like about Habitica! It will be anonymous unless you choose to enter your contact details. Don't speak English well? No problem! Use the language you prefer.",
|
||||
"qrCode": "QR Code",
|
||||
"dataExport": "Data Export",
|
||||
"saveData": "Here are a few options for saving your data.",
|
||||
|
||||
@@ -14,6 +14,13 @@ import { model as User } from '../../models/user';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import {
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from '../../libs/email';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
||||
|
||||
/**
|
||||
* @apiDefine UserNotFound
|
||||
@@ -252,6 +259,7 @@ api.updateUser = {
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam {String} password The user's password if the account uses local authentication
|
||||
* @apiParam {String} feedback User's optional feedback explaining reasons for deletion
|
||||
*
|
||||
* @apiSuccess {Object} data An empty Object
|
||||
*
|
||||
@@ -262,6 +270,7 @@ api.updateUser = {
|
||||
* }
|
||||
*
|
||||
* @apiError {BadRequest} MissingPassword The password was not included in the request
|
||||
* @apiError {BadRequest} LengthExceeded The feedback provided is longer than 10K
|
||||
* @apiError {BadRequest} NotAuthorized There is no account that uses those credentials.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
@@ -286,16 +295,15 @@ api.deleteUser = {
|
||||
let user = res.locals.user;
|
||||
let plan = user.purchased.plan;
|
||||
|
||||
req.checkBody({
|
||||
password: {
|
||||
notEmpty: {errorMessage: res.t('missingPassword')},
|
||||
},
|
||||
});
|
||||
let password = req.body.password;
|
||||
if (!password) throw new BadRequest(res.t('missingPassword'));
|
||||
|
||||
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}.`);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let password = req.body.password;
|
||||
let isValidPassword = await passwordUtils.compare(user, password);
|
||||
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
||||
|
||||
@@ -320,6 +328,16 @@ api.deleteUser = {
|
||||
|
||||
await user.remove();
|
||||
|
||||
if (feedback) {
|
||||
txnEmail(TECH_ASSISTANCE_EMAIL, 'admin-feedback', [
|
||||
{name: 'PROFILE_NAME', content: user.profile.name},
|
||||
{name: 'UUID', content: user._id},
|
||||
{name: 'EMAIL', content: getUserInfo(user, ['email']).email},
|
||||
{name: 'FEEDBACK_SOURCE', content: 'from deletion form'},
|
||||
{name: 'FEEDBACK', content: feedback},
|
||||
]);
|
||||
}
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,11 +61,16 @@ script(type='text/ng-template', id='modals/delete.html')
|
||||
.modal-header
|
||||
h4=env.t('deleteAccount')
|
||||
.modal-body
|
||||
p!=env.t('deleteLocalAccountText')
|
||||
strong=env.t('deleteLocalAccountText')
|
||||
br
|
||||
.row
|
||||
.col-md-6
|
||||
input.form-control(type='password', ng-model='_deleteAccount')
|
||||
br
|
||||
.row
|
||||
#feedback.col-xs-12.form-group
|
||||
label(for='feedbackTextArea')=env.t('feedback')
|
||||
textarea#feedbackTextArea.form-control(ng-model='feedback')
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click='$close()')=env.t('neverMind')
|
||||
button.btn.btn-danger(ng-disabled='!_deleteAccount', ng-click='$close(); delete(_deleteAccount)')=env.t('deleteDo')
|
||||
button.btn.btn-danger(ng-disabled='!_deleteAccount', ng-click='$close(); delete(_deleteAccount, feedback)')=env.t('deleteDo')
|
||||
|
||||
Reference in New Issue
Block a user