diff --git a/src/App.vue b/src/App.vue
index 4525a079..87101e17 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -36,6 +36,7 @@ import ContentAuth from './components/home/contentAuth'
import ContentLinkShare from './components/home/contentLinkShare'
import ContentNoAuth from './components/home/contentNoAuth'
import {setLanguage} from './i18n/setup'
+import AccountDeleteService from '@/services/accountDelete'
export default {
name: 'app',
@@ -51,6 +52,7 @@ export default {
this.setupOnlineStatus()
this.setupPasswortResetRedirect()
this.setupEmailVerificationRedirect()
+ this.setupAccountDeletionVerification()
},
beforeCreate() {
this.$store.dispatch('config/update')
@@ -95,6 +97,17 @@ export default {
this.$router.push({name: 'user.login'})
}
},
+ setupAccountDeletionVerification() {
+ if (typeof this.$route.query.accountDeletionConfirm !== 'undefined') {
+ const accountDeletionService = new AccountDeleteService()
+ accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
+ .then(() => {
+ this.success({message: this.$t('user.deletion.confirmSuccess')})
+ this.$store.dispatch('auth/refreshUserInfo')
+ })
+ .catch(e => this.error(e))
+ }
+ },
},
}
diff --git a/src/components/tasks/partials/listSearch.vue b/src/components/tasks/partials/listSearch.vue
index 3cb83e89..1424babf 100644
--- a/src/components/tasks/partials/listSearch.vue
+++ b/src/components/tasks/partials/listSearch.vue
@@ -1,7 +1,6 @@
+
+
+
+
+
+ {{ $t('user.deletion.scheduledCancelConfirm') }}
+
+
+
+
+
+
+ {{ $t('user.deletion.confirm') }}
+
+
+
+
+
+
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json
index 161edaf1..3cfc31e7 100644
--- a/src/i18n/lang/en.json
+++ b/src/i18n/lang/en.json
@@ -96,6 +96,20 @@
"statusUpdateSuccess": "Avatar status was updated successfully!",
"setSuccess": "The avatar has been set successfully!"
}
+ },
+ "deletion": {
+ "title": "Delete your Vikunja Account",
+ "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, lists, tasks and everything associated with it.",
+ "text2": "To proceed, please enter your password. You will receive an email with further instructions.",
+ "confirm": "Delete my account",
+ "requestSuccess": "The request was successful. You'll receive an email with further instructions.",
+ "passwordRequired": "Please enter your password.",
+ "confirmSuccess": "You've successfully confirmed the deletion of your account. We will delete your account in three days.",
+ "scheduled": "We will delete your Vikunja account at {date} ({dateSince}).",
+ "scheduledCancel": "To cancel the deletion of your account, click here.",
+ "scheduledCancelText": "To cancel the deletion of your account, please enter your password below:",
+ "scheduledCancelConfirm": "Cancel the deletion of my account",
+ "scheduledCancelSuccess": "We will not delete your account."
}
},
"list": {
diff --git a/src/services/abstractService.js b/src/services/abstractService.js
index 86a8abda..7855421c 100644
--- a/src/services/abstractService.js
+++ b/src/services/abstractService.js
@@ -72,12 +72,14 @@ export default class AbstractService {
this.http.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
- this.paths = {
- create: paths.create !== undefined ? paths.create : '',
- get: paths.get !== undefined ? paths.get : '',
- getAll: paths.getAll !== undefined ? paths.getAll : '',
- update: paths.update !== undefined ? paths.update : '',
- delete: paths.delete !== undefined ? paths.delete : '',
+ if (paths) {
+ this.paths = {
+ create: paths.create !== undefined ? paths.create : '',
+ get: paths.get !== undefined ? paths.get : '',
+ getAll: paths.getAll !== undefined ? paths.getAll : '',
+ update: paths.update !== undefined ? paths.update : '',
+ delete: paths.delete !== undefined ? paths.delete : '',
+ }
}
}
diff --git a/src/services/accountDelete.js b/src/services/accountDelete.js
new file mode 100644
index 00000000..c4f443cf
--- /dev/null
+++ b/src/services/accountDelete.js
@@ -0,0 +1,15 @@
+import AbstractService from './abstractService'
+
+export default class AccountDeleteService extends AbstractService {
+ request(password) {
+ return this.post('/user/deletion/request', {password: password})
+ }
+
+ confirm(token) {
+ return this.post('/user/deletion/confirm', {token: token})
+ }
+
+ cancel(password) {
+ return this.post('/user/deletion/cancel', {password: password})
+ }
+}
\ No newline at end of file
diff --git a/src/store/modules/auth.js b/src/store/modules/auth.js
index 767ee2b1..2dfbd509 100644
--- a/src/store/modules/auth.js
+++ b/src/store/modules/auth.js
@@ -181,27 +181,8 @@ export default {
ctx.commit('info', info)
if (authenticated) {
- const HTTP = HTTPFactory()
- // We're not returning the promise here to prevent blocking the initial ui render if the user is
- // accessing the site with a token in local storage
- HTTP.get('user', {
- headers: {
- Authorization: `Bearer ${jwt}`,
- },
- })
- .then(r => {
- const info = new UserModel(r.data)
- info.type = ctx.state.info.type
- info.email = ctx.state.info.email
- info.exp = ctx.state.info.exp
-
- ctx.commit('info', info)
- ctx.commit('authenticated', authenticated)
- ctx.commit('lastUserRefresh')
- })
- .catch(e => {
- console.error('Error while refreshing user info:', e)
- })
+ ctx.dispatch('refreshUserInfo')
+ ctx.commit('authenticated', authenticated)
}
}
@@ -212,6 +193,33 @@ export default {
return Promise.resolve()
},
+ refreshUserInfo(ctx) {
+ const jwt = getToken()
+ if (!jwt) {
+ return
+ }
+
+ const HTTP = HTTPFactory()
+ // We're not returning the promise here to prevent blocking the initial ui render if the user is
+ // accessing the site with a token in local storage
+ HTTP.get('user', {
+ headers: {
+ Authorization: `Bearer ${jwt}`,
+ },
+ })
+ .then(r => {
+ const info = new UserModel(r.data)
+ info.type = ctx.state.info.type
+ info.email = ctx.state.info.email
+ info.exp = ctx.state.info.exp
+
+ ctx.commit('info', info)
+ ctx.commit('lastUserRefresh')
+ })
+ .catch(e => {
+ console.error('Error while refreshing user info:', e)
+ })
+ },
// Renews the api token and saves it to local storage
renewToken(ctx) {
// Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
diff --git a/src/store/modules/config.js b/src/store/modules/config.js
index 1c0b5b34..60017fed 100644
--- a/src/store/modules/config.js
+++ b/src/store/modules/config.js
@@ -23,6 +23,7 @@ export default {
privacyPolicyUrl: '',
},
caldavEnabled: false,
+ userDeletionEnabled: true,
auth: {
local: {
enabled: true,
@@ -49,6 +50,7 @@ export default {
state.legal.imprintUrl = config.legal.imprint_url
state.legal.privacyPolicyUrl = config.legal.privacy_policy_url
state.caldavEnabled = config.caldav_enabled
+ state.userDeletionEnabled = config.user_deletion_enabled
const auth = objectToCamelCase(config.auth)
state.auth.local.enabled = auth.local.enabled
state.auth.openidConnect.enabled = auth.openidConnect.enabled
diff --git a/src/views/Home.vue b/src/views/Home.vue
index 25d874aa..50240f80 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -3,6 +3,17 @@
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
+
+ {{
+ $t('user.deletion.scheduled', {
+ date: formatDateShort(deletionScheduledAt),
+ dateSince: formatDateSince(deletionScheduledAt),
+ })
+ }}
+
+ {{ $t('user.deletion.scheduledCancel') }}
+
+
0
},
loading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks',
+ deletionScheduledAt: state => parseDateOrNull(state.auth.info.deletionScheduledAt),
}),
},
methods: {
diff --git a/src/views/user/Settings.vue b/src/views/user/Settings.vue
index a3f4bc2a..a66ac649 100644
--- a/src/views/user/Settings.vue
+++ b/src/views/user/Settings.vue
@@ -245,6 +245,9 @@
+
+
+
@@ -289,6 +292,7 @@ import {mapState} from 'vuex'
import AvatarSettings from '../../components/user/avatar-settings.vue'
import copy from 'copy-to-clipboard'
import ListSearch from '@/components/tasks/partials/listSearch.vue'
+import UserSettingsDeletion from '../../components/user/settings/deletion'
export default {
name: 'Settings',
@@ -318,6 +322,7 @@ export default {
}
},
components: {
+ UserSettingsDeletion,
ListSearch,
AvatarSettings,
},
@@ -342,6 +347,7 @@ export default {
},
mounted() {
this.setTitle(this.$t('user.settings.title'))
+ this.anchorHashCheck()
},
computed: {
caldavUrl() {
@@ -452,6 +458,14 @@ export default {
copy(text) {
copy(text)
},
+ anchorHashCheck() {
+ if (window.location.hash === this.$route.hash) {
+ const el = document.getElementById(this.$route.hash.slice(1))
+ if (el) {
+ window.scrollTo(0, el.offsetTop)
+ }
+ }
+ },
},
}