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:
konrad 2022-04-02 15:51:42 +00:00
commit 0b31cce567
5 changed files with 138 additions and 38 deletions

View file

@ -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",

View file

@ -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
View 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)
}
}
}

View 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)
}
}

View file

@ -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>