feat: manage caldav tokens (#1307)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1307 Reviewed-by: konrad <k@knt.li>
This commit is contained in:
commit
0b31cce567
5 changed files with 138 additions and 38 deletions
|
@ -103,9 +103,16 @@
|
||||||
"disableSuccess": "Two factor authentication was sucessfully disabled."
|
"disableSuccess": "Two factor authentication was sucessfully disabled."
|
||||||
},
|
},
|
||||||
"caldav": {
|
"caldav": {
|
||||||
"title": "Caldav",
|
"title": "CalDAV",
|
||||||
"howTo": "You can connect Vikunja to caldav clients to view and manage all tasks from different clients. Enter this url into your client:",
|
"howTo": "You can connect Vikunja to CalDAV clients to view and manage all tasks from different clients. Enter this url into your client:",
|
||||||
"more": "More information about caldav in Vikunja"
|
"more": "More information about CalDAV in Vikunja",
|
||||||
|
"tokens": "CalDAV Tokens",
|
||||||
|
"tokensHowTo": "You can use a CalDAV token to use instead of a password to log in the above endpoint.",
|
||||||
|
"createToken": "Create a token",
|
||||||
|
"tokenCreated": "Here is your token: {token}",
|
||||||
|
"wontSeeItAgain": "Write it down, you won't be able to see it again.",
|
||||||
|
"mustUseToken": "You need to create a CalDAV token if you want to use CalDAV with a third party client. Use the token as the password.",
|
||||||
|
"usernameIs": "Your username is: {0}"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"title": "Avatar",
|
"title": "Avatar",
|
||||||
|
@ -486,7 +493,10 @@
|
||||||
"hideMenu": "Hide the menu",
|
"hideMenu": "Hide the menu",
|
||||||
"forExample": "For example:",
|
"forExample": "For example:",
|
||||||
"welcomeBack": "Welcome Back!",
|
"welcomeBack": "Welcome Back!",
|
||||||
"custom": "Custom"
|
"custom": "Custom",
|
||||||
|
"id": "ID",
|
||||||
|
"created": "Created at",
|
||||||
|
"actions": "Actions"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -5,15 +5,13 @@ export default class AbstractModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The max right the user has on this object, as returned by the x-max-right header from the api.
|
* The max right the user has on this object, as returned by the x-max-right header from the api.
|
||||||
* @type {number|null}
|
|
||||||
*/
|
*/
|
||||||
maxRight = null
|
maxRight: number | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The abstract constructor takes an object and merges its data with the default data of this model.
|
* The abstract constructor takes an object and merges its data with the default data of this model.
|
||||||
* @param data
|
|
||||||
*/
|
*/
|
||||||
constructor(data) {
|
constructor(data : Object = {}) {
|
||||||
data = objectToCamelCase(data)
|
data = objectToCamelCase(data)
|
||||||
|
|
||||||
// Put all data in our model while overriding those with a value of null or undefined with their defaults
|
// Put all data in our model while overriding those with a value of null or undefined with their defaults
|
||||||
|
@ -26,9 +24,8 @@ export default class AbstractModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default attributes that define the "empty" state.
|
* Default attributes that define the "empty" state.
|
||||||
* @return {{}}
|
|
||||||
*/
|
*/
|
||||||
defaults() {
|
defaults(): Object {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
15
src/models/caldavToken.ts
Normal file
15
src/models/caldavToken.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import AbstractModel from './abstractModel'
|
||||||
|
|
||||||
|
export default class CaldavTokenModel extends AbstractModel {
|
||||||
|
constructor(data? : Object) {
|
||||||
|
super(data)
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
this.id
|
||||||
|
|
||||||
|
if (this.created) {
|
||||||
|
/** @type {Date} */
|
||||||
|
this.created = new Date(this.created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/services/caldavToken.js
Normal file
25
src/services/caldavToken.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
import CaldavTokenModel from '../models/caldavToken'
|
||||||
|
import AbstractService from './abstractService'
|
||||||
|
|
||||||
|
export default class CaldavTokenService extends AbstractService {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
getAll: '/user/settings/token/caldav',
|
||||||
|
create: '/user/settings/token/caldav',
|
||||||
|
delete: '/user/settings/token/caldav/{id}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
processModel(model) {
|
||||||
|
return {
|
||||||
|
...model,
|
||||||
|
created: formatISO(new Date(model.created)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelFactory(data) {
|
||||||
|
return new CaldavTokenModel(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,41 +16,94 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-5 mb-4 has-text-weight-bold">
|
||||||
|
{{ $t('user.settings.caldav.tokens') }}
|
||||||
|
</h5>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://vikunja.io/docs/caldav/" rel="noreferrer noopener nofollow" target="_blank">
|
{{ isLocalUser ? $t('user.settings.caldav.tokensHowTo') : $t('user.settings.caldav.mustUseToken') }}
|
||||||
|
<template v-if="!isLocalUser">
|
||||||
|
<br/>
|
||||||
|
<i18n-t keypath="user.settings.caldav.usernameIs">
|
||||||
|
<strong>{{ username }}</strong>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table" v-if="tokens.length > 0">
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('misc.id') }}</th>
|
||||||
|
<th>{{ $t('misc.created') }}</th>
|
||||||
|
<th class="has-text-right">{{ $t('misc.actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="tk in tokens" :key="tk.id">
|
||||||
|
<td>{{ tk.id }}</td>
|
||||||
|
<td>{{ formatDateShort(tk.created) }}</td>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<x-button type="secondary" @click="deleteToken(tk)">
|
||||||
|
{{ $t('misc.delete') }}
|
||||||
|
</x-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<Message v-if="newToken" class="mb-4">
|
||||||
|
{{ $t('user.settings.caldav.tokenCreated', {token: newToken.token}) }}<br/>
|
||||||
|
{{ $t('user.settings.caldav.wontSeeItAgain') }}
|
||||||
|
</Message>
|
||||||
|
|
||||||
|
<x-button icon="plus" class="mb-4" @click="createToken" :loading="service.loading">
|
||||||
|
{{ $t('user.settings.caldav.createToken') }}
|
||||||
|
</x-button>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<BaseButton :href="CALDAV_DOCS" target="_blank">
|
||||||
{{ $t('user.settings.caldav.more') }}
|
{{ $t('user.settings.caldav.more') }}
|
||||||
</a>
|
</BaseButton>
|
||||||
</p>
|
</p>
|
||||||
</card>
|
</card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import {defineComponent} from 'vue'
|
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import {mapState} from 'vuex'
|
import {computed, ref, shallowReactive} from 'vue'
|
||||||
import {CALDAV_DOCS} from '@/urls'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
import {useStore} from 'vuex'
|
||||||
|
|
||||||
export default defineComponent({
|
import {CALDAV_DOCS} from '@/urls'
|
||||||
name: 'user-settings-caldav',
|
import {useTitle} from '@/composables/useTitle'
|
||||||
data() {
|
import {success} from '@/message'
|
||||||
return {
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
caldavDocsUrl: CALDAV_DOCS,
|
import Message from '@/components/misc/message.vue'
|
||||||
}
|
import CaldavTokenService from '@/services/caldavToken'
|
||||||
},
|
import CaldavTokenModel from '@/models/caldavToken'
|
||||||
mounted() {
|
|
||||||
this.setTitle(`${this.$t('user.settings.caldav.title')} - ${this.$t('user.settings.title')}`)
|
const {t} = useI18n()
|
||||||
},
|
useTitle(() => `${t('user.settings.caldav.title')} - ${t('user.settings.title')}`)
|
||||||
computed: {
|
|
||||||
caldavUrl() {
|
const service = shallowReactive(new CaldavTokenService())
|
||||||
return `${this.$store.getters['config/apiBase']}/dav/principals/${this.userInfo.username}/`
|
const tokens = ref<CaldavTokenModel[]>([])
|
||||||
},
|
|
||||||
...mapState('config', ['caldavEnabled']),
|
service.getAll().then((result: CaldavTokenModel[]) => {
|
||||||
...mapState({
|
tokens.value = result
|
||||||
userInfo: state => state.auth.info,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
copy,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const newToken = ref<CaldavTokenModel>()
|
||||||
|
async function createToken() {
|
||||||
|
newToken.value = await service.create({}) as CaldavTokenModel
|
||||||
|
tokens.value.push(newToken.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteToken(token: CaldavTokenModel) {
|
||||||
|
const r = await service.delete(token)
|
||||||
|
tokens.value = tokens.value.filter(({id}) => id !== token.id)
|
||||||
|
success(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const username = computed(() => store.state.auth.info?.username)
|
||||||
|
const caldavUrl = computed(() => `${store.getters['config/apiBase']}/dav/principals/${username.value}/`)
|
||||||
|
const caldavEnabled = computed(() => store.state.config.caldavEnabled)
|
||||||
|
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue