feat(api tokens): add token creation form

This commit is contained in:
kolaente 2023-09-01 12:47:32 +02:00
parent a20eef2453
commit e47ad021a3
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
5 changed files with 134 additions and 17 deletions

View File

@ -142,9 +142,14 @@
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's api without user credentials.",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createToken": "Create a token",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"attributes": {
"title": "Title",
"expiresAt": "Expires at",

View File

@ -10,4 +10,4 @@ export interface IApiToken extends IAbstract {
permissions: IApiPermission
expiresAt: Date
created: Date
}
}

View File

@ -8,7 +8,7 @@ export default class ApiTokenModel extends AbstractModel<IApiToken> {
expiresAt: Date = null
created: Date = null
constructor(data: Partial<IApiToken>) {
constructor(data: Partial<IApiToken> = {}) {
super()
this.assignData(data)

View File

@ -22,4 +22,15 @@ export default class ApiTokenService extends AbstractService<IApiToken> {
modelFactory(data: Partial<IApiToken>) {
return new ApiTokenModel(data)
}
async getAvailableRoutes() {
const cancel = this.setLoading()
try {
const response = await this.http.get('/routes')
return response.data
} finally {
cancel()
}
}
}

View File

@ -1,34 +1,78 @@
<script setup lang="ts">
import ApiTokenService from '@/services/apiToken'
import {computed, onMounted, ref} from 'vue'
import { formatDateShort } from '@/helpers/time/formatDate'
import {formatDateShort} from '@/helpers/time/formatDate'
import XButton from '@/components/input/button.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import ApiTokenModel from '@/models/apiTokenModel'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {MILLISECONDS_A_DAY} from '@/constants/date'
const service = new ApiTokenService()
const tokens = ref([])
const apiDocsUrl = window.API_URL + '/docs'
const showCreateForm = ref(false)
const availableRoutes = ref(null)
const newToken = ref(new ApiTokenModel())
const newTokenExpiry = ref<string | number>(30)
const newTokenPermissions = ref({})
onMounted(async () => {
tokens.value = await service.getAll()
availableRoutes.value = await service.getAvailableRoutes()
resetPermissions()
})
function resetPermissions() {
newTokenPermissions.value = {}
Object.entries(availableRoutes.value).forEach(entry => {
const [group, routes] = entry
newTokenPermissions.value[group] = {}
Object.keys(routes).forEach(r => {
newTokenPermissions.value[group][r] = false
})
})
}
function deleteToken() {
}
function createToken() {
async function createToken() {
const expiry = Number(newTokenExpiry.value)
if(!isNaN(expiry)) {
// if it's a number, we assume it's the number of days in the future
newToken.value.expiresAt = new Date((new Date()) + expiry * MILLISECONDS_A_DAY)
}
newToken.value.permissions = {}
Object.entries(newTokenPermissions.value).forEach(([key, ps]) => {
const all = Object.entries(ps)
.filter(([_, v]) => v)
.map(p => p[0])
console.log({all})
if (all.length > 0) {
newToken.value.permissions[key] = all
}
})
const token = await service.create(newToken.value)
newToken.value = new ApiTokenModel()
newTokenExpiry.value = 30
resetPermissions()
tokens.value.push(token)
showCreateForm.value = false
}
</script>
<template>
<card :title="$t('user.settings.apiTokens.title')">
<p>
{{ $t('user.settings.apiTokens.general') }}
<BaseButton :href="apiDocsUrl">{{ $t('user.settings.apiTokens.apiDocs') }}</BaseButton>.
<BaseButton :href="apiDocsUrl">{{ $t('user.settings.apiTokens.apiDocs') }}</BaseButton>
.
</p>
<table class="table" v-if="tokens.length > 0">
<tr>
<th>{{ $t('misc.id') }}</th>
@ -57,13 +101,70 @@ function createToken() {
</td>
</tr>
</table>
<x-button icon="plus" class="mb-4" @click="createToken" :loading="service.loading">
{{ $t('user.settings.apiTokens.createToken') }}
<form
v-if="showCreateForm"
@submit.prevent="createToken"
>
<!-- Title -->
<div class="field">
<label class="label" for="apiTokenTitle">{{ $t('user.settings.apiTokens.attributes.title') }}</label>
<div class="control">
<input
class="input"
id="apiTokenTitle"
type="text"
v-focus
v-model="newToken.title"/>
</div>
</div>
<!-- Expiry -->
<div class="field">
<label class="label" for="apiTokenTitle">{{
$t('user.settings.apiTokens.attributes.expiresAt')
}}</label>
<div class="control select">
<select class="select" v-model="newTokenExpiry">
<option value="30">{{ $t('user.settings.apiTokens.30d') }}</option>
<option value="60">{{ $t('user.settings.apiTokens.60d') }}</option>
<option value="90">{{ $t('user.settings.apiTokens.90d') }}</option>
<option value="custom">{{ $t('misc.custom') }}</option>
</select>
</div>
</div>
<!-- Permissions -->
<div class="field">
<label class="label">{{ $t('user.settings.apiTokens.attributes.permissions') }}</label>
<p>{{ $t('user.settings.apiTokens.permissionExplanation') }}</p>
<div v-for="(routes, group) in availableRoutes" class="mb-2" :key="group">
<strong>{{ group }}</strong><br/>
<fancycheckbox
v-for="(paths, route) in routes"
:key="group+'-'+route"
class="mr-2"
v-model="newTokenPermissions[group][route]"
>
{{ route }}
</fancycheckbox>
<br/>
</div>
</div>
<x-button :loading="service.loading" @click="createToken">
{{ $t('user.settings.apiTokens.createToken') }}
</x-button>
</form>
<x-button
v-else
icon="plus"
class="mb-4"
@click="() => showCreateForm = true"
:loading="service.loading"
>
{{ $t('user.settings.apiTokens.createAToken') }}
</x-button>
</card>
</template>
<style scoped lang="scss">
</style>