User accounts can now be deleted with some UI to prevent accidental deletion

This commit is contained in:
Stefan Zermatten
2021-02-25 14:28:51 +02:00
parent a5460bba0b
commit 2a983b0a94
10 changed files with 268 additions and 8 deletions

View File

@@ -28,10 +28,14 @@ const removeCreature = new ValidatedMethod({
},
run({charId}) {
assertOwnership(charId, this.userId)
Creatures.remove(charId);
this.unblock();
removeRelatedDocuments(charId);
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}
export default removeCreature;

View File

@@ -118,10 +118,14 @@ const removeLibrary = new ValidatedMethod({
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
Libraries.remove(_id);
this.unblock();
LibraryNodes.remove({'ancestors.id': _id});
this.unblock();
removeLibaryWork(_id)
}
})
});
export function removeLibaryWork(libraryId){
Libraries.remove(libraryId);
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary };

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js';
const userSchema = new SimpleSchema({
username: {

View File

@@ -0,0 +1,61 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js';
import Creatures from '/imports/api/creature/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/removeCreature.js';
Meteor.users.deleteMyAccount = new ValidatedMethod({
name: 'users.deleteMyAccount',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run(){
let userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
'You must be logged into to delete your account');
// Delete all creatures
let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch();
creatures.forEach(creature => removeCreatureWork(creature._id));
// Remove permissions from all creatures
Creatures.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// Delete all libraries
let libraries = Libraries.find({owner: userId}, {fields: {_id: 1}}).fetch();
libraries.forEach(library => removeLibaryWork(library._id));
// Remove permissions from all creatures
Libraries.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// delete the account
Meteor.users.remove(userId);
}
});

View File

@@ -7,3 +7,4 @@ import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'
import '/imports/server/publications/ownedDocuments.js'

View File

@@ -0,0 +1,15 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
Meteor.publish('ownedDocuments', function(){
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
return [
Creatures.find({owner: userId}),
Libraries.find({owner: userId}),
]
});
});

View File

@@ -4,6 +4,7 @@ import CreaturePropertyCreationDialog from '/imports/ui/creature/creaturePropert
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'
import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue';
import DeleteUserAccountDialog from '/imports/ui/user/DeleteUserAccountDialog.vue';
import ExperienceInsertDialog from '/imports/ui/creature/experiences/ExperienceInsertDialog.vue';
import ExperienceListDialog from '/imports/ui/creature/experiences/ExperienceListDialog.vue';
import InviteDialog from '/imports/ui/user/InviteDialog.vue';
@@ -26,6 +27,7 @@ export default {
CreaturePropertyDialog,
CreaturePropertyFromLibraryDialog,
DeleteConfirmationDialog,
DeleteUserAccountDialog,
ExperienceInsertDialog,
ExperienceListDialog,
InviteDialog,

View File

@@ -13,7 +13,7 @@
name="toolbar"
/>
<v-toolbar
v-if="!$route.matched[0].components.toolbar"
v-if="!$route.matched[0] || !$route.matched[0].components.toolbar"
app
color="secondary"
dark

View File

@@ -141,6 +141,19 @@
</template>
</v-list>
</template>
<v-layout
row
justify-end
class="mt-3"
>
<v-btn
color="error"
data-id="delete-account-btn"
@click="deleteAccount"
>
Delete Account
</v-btn>
</v-layout>
</v-card>
</div>
</template>
@@ -270,6 +283,12 @@
this.updatePatreonLoading = false;
if (error) this.updatePatreonError = error;
});
},
deleteAccount(){
this.$store.commit('pushDialogStack', {
component: 'delete-user-account-dialog',
elementId: 'delete-account-btn',
});
}
},
}

View File

@@ -0,0 +1,153 @@
<template lang="html">
<dialog-base>
<v-toolbar-title slot="toolbar">
Delete User Account
</v-toolbar-title>
<div>
<h2>Are you sure you want to delete your account?</h2>
<v-alert
:value="true"
icon="warning"
color="error"
outline
>
Deleted accounts can not be recovered
</v-alert>
<p>We will immediately delete your account and all of your data</p>
<p>Your username will become available to anyone on DiceCloud</p>
<template v-if="characters.length">
<h3 v-if="characters.length > 1">
These {{ characters.length }} characters will be deleted:
</h3>
<h3 v-else>
This character will be deleted:
</h3>
<v-list>
<creature-list-tile
v-for="character in characters"
:key="character._id"
:model="character"
/>
</v-list>
</template>
<template v-if="libraries.length">
<h3 v-if="libraries.length > 1">
These {{ libraries.length }} libraries will be deleted:
</h3>
<h3 v-else>
This library will be deleted:
</h3>
<v-list>
<creature-list-tile
v-for="library in libraries"
:key="library._id"
:model="library"
/>
</v-list>
</template>
<v-layout
column
align-start
>
<v-text-field
v-if="user.username"
v-model="usernameInput"
label="Type your username or email"
style="width: 350px;"
:error-messages="usernameInputValid ? undefined : ' '"
:append-icon="usernameInputValid ? 'done' : undefined"
/>
<v-text-field
v-model="verificationInput"
label="To verify type 'delete my account'"
style="width: 350px;"
:error-messages="verificationInputValid ? undefined : ' '"
:append-icon="verificationInputValid ? 'done' : undefined"
/>
<v-btn
class="mt-4"
color="error"
:disabled="!valid"
@click="deleteAccount"
>
Permanently delete account
</v-btn>
</v-layout>
</div>
<div
slot="actions"
class="layout row justify-end"
>
<v-btn
flat
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
</div>
</dialog-base>
</template>
<script>
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
import CreatureListTile from '/imports/ui/creature/CreatureListTile.vue';
export default {
components: {
DialogBase,
CreatureListTile,
},
data(){return {
usernameInput: '',
verificationInput: '',
};},
meteor: {
$subscribe: {
'ownedDocuments'(){
return [];
},
},
characters(){
return Creatures.find({owner: Meteor.userId()});
},
libraries(){
return Libraries.find({owner: Meteor.userId()});
},
user(){
return Meteor.user();
},
},
computed:{
usernameInputValid(){
let username = this.user.username;
if (!username) return true;
let input = this.usernameInput;
if (!input) return false;
if (input.toLowerCase() === username.toLowerCase()){
return true;
} else {
return false;
}
},
verificationInputValid(){
let input = this.verificationInput || '';
return input.toLowerCase() === 'delete my account'
},
valid(){
return this.usernameInputValid && this.verificationInputValid;
}
},
methods: {
deleteAccount(){
this.$router.push('/');
Meteor.users.deleteMyAccount.call();
this.$store.dispatch('popDialogStack');
},
},
}
</script>
<style lang="css" scoped>
</style>