feat: api tokens
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/3733
This commit is contained in:
commit
28f2551d87
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
data-input
|
data-input
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
ref="root"
|
ref="root"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,39 +20,39 @@ type Options = flatpickr.Options.Options
|
|||||||
type DateOption = flatpickr.Options.DateOption
|
type DateOption = flatpickr.Options.DateOption
|
||||||
|
|
||||||
function camelToKebab(string: string) {
|
function camelToKebab(string: string) {
|
||||||
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayify<T = unknown>(obj: T) {
|
function arrayify<T = unknown>(obj: T) {
|
||||||
return obj instanceof Array
|
return obj instanceof Array
|
||||||
? obj
|
? obj
|
||||||
: [obj]
|
: [obj]
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullify<T = unknown>(value: T) {
|
function nullify<T = unknown>(value: T) {
|
||||||
return (value && (value as unknown[]).length)
|
return (value && (value as unknown[]).length)
|
||||||
? value
|
? value
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events to emit, copied from flatpickr source
|
// Events to emit, copied from flatpickr source
|
||||||
const includedEvents = [
|
const includedEvents = [
|
||||||
'onChange',
|
'onChange',
|
||||||
'onClose',
|
'onClose',
|
||||||
'onDestroy',
|
'onDestroy',
|
||||||
'onMonthChange',
|
'onMonthChange',
|
||||||
'onOpen',
|
'onOpen',
|
||||||
'onYearChange',
|
'onYearChange',
|
||||||
] as HookKey[]
|
] as HookKey[]
|
||||||
|
|
||||||
// Let's not emit these events by default
|
// Let's not emit these events by default
|
||||||
const excludedEvents = [
|
const excludedEvents = [
|
||||||
'onValueUpdate',
|
'onValueUpdate',
|
||||||
'onDayCreate',
|
'onDayCreate',
|
||||||
'onParseConfig',
|
'onParseConfig',
|
||||||
'onReady',
|
'onReady',
|
||||||
'onPreCalendarPosition',
|
'onPreCalendarPosition',
|
||||||
'onKeyDown',
|
'onKeyDown',
|
||||||
] as HookKey[]
|
] as HookKey[]
|
||||||
|
|
||||||
// Keep a copy of all events for later use
|
// Keep a copy of all events for later use
|
||||||
@ -100,19 +100,19 @@ const attrs = useAttrs()
|
|||||||
|
|
||||||
const root = ref<HTMLInputElement | null>(null)
|
const root = ref<HTMLInputElement | null>(null)
|
||||||
const fp = ref<flatpickr.Instance | null>(null)
|
const fp = ref<flatpickr.Instance | null>(null)
|
||||||
const safeConfig = ref<Options>({ ...props.config })
|
const safeConfig = ref<Options>({...props.config})
|
||||||
|
|
||||||
function prepareConfig() {
|
function prepareConfig() {
|
||||||
// Don't mutate original object on parent component
|
// Don't mutate original object on parent component
|
||||||
const newConfig: Options = { ...props.config }
|
const newConfig: Options = {...props.config}
|
||||||
|
|
||||||
props.events.forEach((hook) => {
|
props.events.forEach((hook) => {
|
||||||
// Respect global callbacks registered via setDefault() method
|
// Respect global callbacks registered via setDefault() method
|
||||||
const globalCallbacks = flatpickr.defaultConfig[hook] || []
|
const globalCallbacks = flatpickr.defaultConfig[hook] || []
|
||||||
|
|
||||||
// Inject our own method along with user callback
|
// Inject our own method along with user callback
|
||||||
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
|
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
|
||||||
|
|
||||||
// Overwrite with merged array
|
// Overwrite with merged array
|
||||||
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
|
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
|
||||||
globalCallbacks,
|
globalCallbacks,
|
||||||
@ -147,9 +147,9 @@ onMounted(() => {
|
|||||||
prepareConfig()
|
prepareConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the HTML node where flatpickr to be attached
|
* Get the HTML node where flatpickr to be attached
|
||||||
* Bind on parent element if wrap is true
|
* Bind on parent element if wrap is true
|
||||||
*/
|
*/
|
||||||
const element = props.config.wrap
|
const element = props.config.wrap
|
||||||
? root.value.parentNode
|
? root.value.parentNode
|
||||||
: root.value
|
: root.value
|
||||||
@ -179,7 +179,7 @@ watch(config, () => {
|
|||||||
fp.value.set(name, safeConfig.value[name])
|
fp.value.set(name, safeConfig.value[name])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, {deep:true})
|
}, {deep: true})
|
||||||
|
|
||||||
const fpInput = computed(() => {
|
const fpInput = computed(() => {
|
||||||
if (!fp.value) return
|
if (!fp.value) return
|
||||||
@ -198,8 +198,8 @@ watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
|
|||||||
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))
|
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watch for the disabled property and sets the value to the real input.
|
* Watch for the disabled property and sets the value to the real input.
|
||||||
*/
|
*/
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (disabled.value) {
|
if (disabled.value) {
|
||||||
fpInput.value?.setAttribute('disabled', '')
|
fpInput.value?.setAttribute('disabled', '')
|
||||||
|
@ -139,6 +139,30 @@
|
|||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"dark": "Dark"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"apiTokens": {
|
||||||
|
"title": "API Tokens",
|
||||||
|
"general": "API tokens allow you to use Vikunja's API without user credentials.",
|
||||||
|
"apiDocs": "Check out the api docs",
|
||||||
|
"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.",
|
||||||
|
"titleRequired": "The title is required",
|
||||||
|
"expired": "This token has expired {ago}.",
|
||||||
|
"delete": {
|
||||||
|
"header": "Delete this token",
|
||||||
|
"text1": "Are you sure you want to delete the token \"{token}\"?",
|
||||||
|
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"title": "Title",
|
||||||
|
"titlePlaceholder": "Enter a title you will recognize later",
|
||||||
|
"expiresAt": "Expires at",
|
||||||
|
"permissions": "Permissions"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deletion": {
|
"deletion": {
|
||||||
|
14
src/modelTypes/IApiToken.ts
Normal file
14
src/modelTypes/IApiToken.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||||
|
|
||||||
|
export interface IApiPermission {
|
||||||
|
[key: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApiToken extends IAbstract {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
token: string
|
||||||
|
permissions: IApiPermission
|
||||||
|
expiresAt: Date
|
||||||
|
created: Date
|
||||||
|
}
|
21
src/models/apiTokenModel.ts
Normal file
21
src/models/apiTokenModel.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import AbstractModel from '@/models/abstractModel'
|
||||||
|
import type {IApiToken} from '@/modelTypes/IApiToken'
|
||||||
|
|
||||||
|
export default class ApiTokenModel extends AbstractModel<IApiToken> {
|
||||||
|
id = 0
|
||||||
|
title = ''
|
||||||
|
token = ''
|
||||||
|
permissions = null
|
||||||
|
expiresAt: Date = null
|
||||||
|
created: Date = null
|
||||||
|
|
||||||
|
constructor(data: Partial<IApiToken> = {}) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.assignData(data)
|
||||||
|
|
||||||
|
this.expiresAt = new Date(this.expiresAt)
|
||||||
|
this.created = new Date(this.created)
|
||||||
|
this.updated = new Date(this.updated)
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,7 @@ const UserSettingsEmailUpdateComponent = () => import('@/views/user/settings/Ema
|
|||||||
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
|
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
|
||||||
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
|
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
|
||||||
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
|
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
|
||||||
|
const UserSettingsApiTokensComponent = () => import('@/views/user/settings/ApiTokens.vue')
|
||||||
|
|
||||||
// Project Handling
|
// Project Handling
|
||||||
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
|
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
|
||||||
@ -183,6 +184,11 @@ const router = createRouter({
|
|||||||
name: 'user.settings.totp',
|
name: 'user.settings.totp',
|
||||||
component: UserSettingsTOTPComponent,
|
component: UserSettingsTOTPComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/user/settings/api-tokens',
|
||||||
|
name: 'user.settings.apiTokens',
|
||||||
|
component: UserSettingsApiTokensComponent,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
36
src/services/apiToken.ts
Normal file
36
src/services/apiToken.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
import type {IApiToken} from '@/modelTypes/IApiToken'
|
||||||
|
import ApiTokenModel from '@/models/apiTokenModel'
|
||||||
|
|
||||||
|
export default class ApiTokenService extends AbstractService<IApiToken> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
create: '/tokens',
|
||||||
|
getAll: '/tokens',
|
||||||
|
delete: '/tokens/{id}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
processModel(model: IApiToken) {
|
||||||
|
return {
|
||||||
|
...model,
|
||||||
|
expiresAt: new Date(model.expiresAt).toISOString(),
|
||||||
|
created: new Date(model.created).toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,10 @@ const navigationItems = computed(() => {
|
|||||||
routeName: 'user.settings.caldav',
|
routeName: 'user.settings.caldav',
|
||||||
condition: caldavEnabled.value,
|
condition: caldavEnabled.value,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('user.settings.apiTokens.title'),
|
||||||
|
routeName: 'user.settings.apiTokens',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('user.deletion.title'),
|
title: t('user.deletion.title'),
|
||||||
routeName: 'user.settings.deletion',
|
routeName: 'user.settings.deletion',
|
||||||
|
254
src/views/user/settings/ApiTokens.vue
Normal file
254
src/views/user/settings/ApiTokens.vue
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ApiTokenService from '@/services/apiToken'
|
||||||
|
import {computed, onMounted, ref} from 'vue'
|
||||||
|
import {formatDateShort, formatDateSince} 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'
|
||||||
|
import flatPickr from 'vue-flatpickr-component'
|
||||||
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
|
import {useI18n} from 'vue-i18n'
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
|
||||||
|
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 newTokenExpiryCustom = ref(new Date())
|
||||||
|
const newTokenPermissions = ref({})
|
||||||
|
const newTokenTitleValid = ref(true)
|
||||||
|
const apiTokenTitle = ref()
|
||||||
|
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
|
const tokenToDelete = ref(null)
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const flatPickerConfig = computed(() => ({
|
||||||
|
altFormat: t('date.altFormatLong'),
|
||||||
|
altInput: true,
|
||||||
|
dateFormat: 'Y-m-d H:i',
|
||||||
|
enableTime: true,
|
||||||
|
time_24hr: true,
|
||||||
|
locale: {
|
||||||
|
firstDayOfWeek: authStore.settings.weekStart,
|
||||||
|
},
|
||||||
|
minDate: now,
|
||||||
|
}))
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteToken() {
|
||||||
|
await service.delete(tokenToDelete.value)
|
||||||
|
showDeleteModal.value = false
|
||||||
|
tokenToDelete.value = null
|
||||||
|
const index = tokens.value.findIndex(el => el.id === tokenToDelete.value.id)
|
||||||
|
if (index === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokens.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createToken() {
|
||||||
|
if (!newTokenTitleValid.value) {
|
||||||
|
apiTokenTitle.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
newToken.value.expiresAt = new Date(newTokenExpiryCustom.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
newToken.value.permissions = {}
|
||||||
|
Object.entries(newTokenPermissions.value).forEach(([key, ps]) => {
|
||||||
|
const all = Object.entries(ps)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
.filter(([_, v]) => v)
|
||||||
|
.map(p => p[0])
|
||||||
|
if (all.length > 0) {
|
||||||
|
newToken.value.permissions[key] = all
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = await service.create(newToken.value)
|
||||||
|
newToken.value = new ApiTokenModel()
|
||||||
|
newTokenExpiry.value = 30
|
||||||
|
newTokenExpiryCustom.value = new Date()
|
||||||
|
resetPermissions()
|
||||||
|
tokens.value.push(token)
|
||||||
|
showCreateForm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPermissionTitle(title: string): string {
|
||||||
|
return title.replaceAll('_', ' ')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<card :title="$t('user.settings.apiTokens.title')">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ $t('user.settings.apiTokens.general') }}
|
||||||
|
<BaseButton :href="apiDocsUrl">{{ $t('user.settings.apiTokens.apiDocs') }}</BaseButton>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table" v-if="tokens.length > 0">
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('misc.id') }}</th>
|
||||||
|
<th>{{ $t('user.settings.apiTokens.attributes.title') }}</th>
|
||||||
|
<th>{{ $t('user.settings.apiTokens.attributes.permissions') }}</th>
|
||||||
|
<th>{{ $t('user.settings.apiTokens.attributes.expiresAt') }}</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>{{ tk.title }}</td>
|
||||||
|
<td class="is-capitalized">
|
||||||
|
<template v-for="(v, p) in tk.permissions" :key="'permission-' + p">
|
||||||
|
<strong>{{ formatPermissionTitle(p) }}:</strong>
|
||||||
|
{{ v.map(formatPermissionTitle).join(', ') }}
|
||||||
|
<br/>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ formatDateShort(tk.expiresAt) }}
|
||||||
|
<p v-if="tk.expiresAt < new Date()" class="has-text-danger">
|
||||||
|
{{ $t('user.settings.apiTokens.expired', {ago: formatDateSince(tk.expiresAt)}) }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td>{{ formatDateShort(tk.created) }}</td>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<x-button variant="secondary" @click="() => {tokenToDelete = tk; showDeleteModal = true}">
|
||||||
|
{{ $t('misc.delete') }}
|
||||||
|
</x-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
ref="apiTokenTitle"
|
||||||
|
type="text"
|
||||||
|
v-focus
|
||||||
|
:placeholder="$t('user.settings.apiTokens.attributes.titlePlaceholder')"
|
||||||
|
v-model="newToken.title"
|
||||||
|
@keyup="() => newTokenTitleValid = newToken.title !== ''"
|
||||||
|
@focusout="() => newTokenTitleValid = newToken.title !== ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" v-if="!newTokenTitleValid">
|
||||||
|
{{ $t('user.settings.apiTokens.titleRequired') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expiry -->
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="apiTokenExpiry">
|
||||||
|
{{ $t('user.settings.apiTokens.attributes.expiresAt') }}
|
||||||
|
</label>
|
||||||
|
<div class="is-flex">
|
||||||
|
<div class="control select">
|
||||||
|
<select class="select" v-model="newTokenExpiry" id="apiTokenExpiry">
|
||||||
|
<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>
|
||||||
|
<flat-pickr
|
||||||
|
v-if="newTokenExpiry === 'custom'"
|
||||||
|
class="ml-2"
|
||||||
|
:config="flatPickerConfig"
|
||||||
|
v-model="newTokenExpiryCustom"
|
||||||
|
/>
|
||||||
|
</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 class="is-capitalized">{{ formatPermissionTitle(group) }}</strong><br/>
|
||||||
|
<fancycheckbox
|
||||||
|
v-for="(paths, route) in routes"
|
||||||
|
:key="group+'-'+route"
|
||||||
|
class="mr-2 is-capitalized"
|
||||||
|
v-model="newTokenPermissions[group][route]"
|
||||||
|
>
|
||||||
|
{{ formatPermissionTitle(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>
|
||||||
|
|
||||||
|
<modal
|
||||||
|
:enabled="showDeleteModal"
|
||||||
|
@close="showDeleteModal = false"
|
||||||
|
@submit="deleteToken()"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
{{ $t('user.settings.apiTokens.delete.header') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #text>
|
||||||
|
<p>
|
||||||
|
{{ $t('user.settings.apiTokens.delete.text1', {token: tokenToDelete.title}) }}<br/>
|
||||||
|
{{ $t('user.settings.apiTokens.delete.text2') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</card>
|
||||||
|
</template>
|
Loading…
x
Reference in New Issue
Block a user