mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-09 16:52:17 +01:00
324 lines
13 KiB
Vue
324 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import FormSection from '@/Components/FormSection.vue';
|
|
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
|
import { Field, FieldLabel, FieldDescription, FieldError } from '@/packages/ui/src/field';
|
|
import { api, type ApiToken, type CreateApiTokenBody } from '@/packages/api/src';
|
|
import SectionBorder from '@/Components/SectionBorder.vue';
|
|
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
|
|
import TextInput from '../../../packages/ui/src/Input/TextInput.vue';
|
|
import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vue';
|
|
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
|
import ActionMessage from '@/Components/ActionMessage.vue';
|
|
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
|
|
import ActionSection from '@/Components/ActionSection.vue';
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
|
|
import { useNotificationsStore } from '@/utils/notification';
|
|
import { useClipboard } from '@vueuse/core';
|
|
import { formatDateTimeLocalized } from '../../../packages/ui/src/utils/time';
|
|
import { ClockIcon } from '@heroicons/vue/20/solid';
|
|
import type { Organization } from '@/packages/api/src';
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const apiTokenBeingDeleted = ref<ApiToken | null>(null);
|
|
const apiTokenBeingRevoked = ref<ApiToken | null>(null);
|
|
|
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
|
const newToken = ref('');
|
|
|
|
const { copy, copied, isSupported } = useClipboard();
|
|
|
|
const organization = inject<ComputedRef<Organization>>('organization');
|
|
|
|
async function createApiToken() {
|
|
await handleApiRequestNotifications(
|
|
() =>
|
|
createApiTokenMutation.mutateAsync({
|
|
name: createApiTokenForm.name,
|
|
}),
|
|
'API Token successfully created',
|
|
'There was an error while creating the API Token',
|
|
(response) => {
|
|
createApiTokenForm.name = '';
|
|
displayingToken.value = true;
|
|
newToken.value = response.data.access_token;
|
|
}
|
|
);
|
|
}
|
|
|
|
const createApiTokenForm = useForm({
|
|
name: '',
|
|
});
|
|
|
|
function confirmApiTokenDeletion(token: ApiToken) {
|
|
apiTokenBeingDeleted.value = token;
|
|
}
|
|
|
|
function confirmApiTokenRevocation(token: ApiToken) {
|
|
apiTokenBeingRevoked.value = token;
|
|
}
|
|
|
|
const displayingToken = ref(false);
|
|
|
|
async function deleteApiToken() {
|
|
if (apiTokenBeingDeleted.value) {
|
|
await handleApiRequestNotifications(
|
|
() => deleteApiTokenMutation.mutateAsync(apiTokenBeingDeleted.value!.id),
|
|
'API Token successfully deleted',
|
|
'There was an error while deleting the API Token',
|
|
() => {
|
|
apiTokenBeingDeleted.value = null;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
async function revokeApiToken() {
|
|
if (apiTokenBeingRevoked.value) {
|
|
await handleApiRequestNotifications(
|
|
() => revokeApiTokenMutation.mutateAsync(apiTokenBeingRevoked.value!.id),
|
|
'API Token successfully revoked',
|
|
'There was an error while revoking the API Token',
|
|
() => {
|
|
apiTokenBeingRevoked.value = null;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
const { data: sharedReportResponseData } = useQuery({
|
|
queryKey: ['api-tokens'],
|
|
queryFn: () => api.getApiTokens(),
|
|
});
|
|
|
|
const tokens = computed(() => {
|
|
return sharedReportResponseData.value?.data ?? [];
|
|
});
|
|
|
|
const createApiTokenMutation = useMutation({
|
|
mutationFn: async (apiToken: CreateApiTokenBody) => {
|
|
return await api.createApiToken(apiToken);
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['api-tokens'] });
|
|
},
|
|
});
|
|
|
|
const deleteApiTokenMutation = useMutation({
|
|
mutationFn: async (apiTokenId: string) => {
|
|
return await api.deleteApiToken(undefined, {
|
|
params: {
|
|
apiToken: apiTokenId,
|
|
},
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['api-tokens'] });
|
|
},
|
|
});
|
|
|
|
const revokeApiTokenMutation = useMutation({
|
|
mutationFn: async (apiTokenId: string) => {
|
|
return await api.revokeApiToken(undefined, {
|
|
params: {
|
|
apiToken: apiTokenId,
|
|
},
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['api-tokens'] });
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Generate API Token -->
|
|
<FormSection @submitted="createApiToken">
|
|
<template #title> Create API Token </template>
|
|
|
|
<template #description>
|
|
API tokens allow third-party services to authenticate with our application on your
|
|
behalf.
|
|
</template>
|
|
|
|
<template #form>
|
|
<!-- Token Name -->
|
|
<Field class="col-span-6 sm:col-span-4">
|
|
<FieldLabel for="api_key_name">API Key Name</FieldLabel>
|
|
<TextInput
|
|
id="api_key_name"
|
|
v-model="createApiTokenForm.name"
|
|
type="text"
|
|
class="block w-full" />
|
|
<FieldError v-if="createApiTokenForm.errors.name">{{
|
|
createApiTokenForm.errors.name
|
|
}}</FieldError>
|
|
<FieldDescription>
|
|
<span class="flex space-x-1.5 items-center text-text-tertiary font-medium">
|
|
<ClockIcon class="w-4"></ClockIcon>
|
|
<span> API Tokens are valid for 1 year </span>
|
|
</span>
|
|
</FieldDescription>
|
|
</Field>
|
|
</template>
|
|
|
|
<template #actions>
|
|
<ActionMessage :on="createApiTokenForm.recentlySuccessful" class="me-3">
|
|
Created.
|
|
</ActionMessage>
|
|
|
|
<PrimaryButton
|
|
:class="{ 'opacity-25': createApiTokenForm.processing }"
|
|
:disabled="createApiTokenForm.processing">
|
|
Create API Key
|
|
</PrimaryButton>
|
|
</template>
|
|
</FormSection>
|
|
|
|
<div v-if="tokens.length > 0">
|
|
<SectionBorder />
|
|
|
|
<!-- Manage API Tokens -->
|
|
<div class="mt-10 sm:mt-0">
|
|
<ActionSection>
|
|
<template #title> Manage API Tokens </template>
|
|
|
|
<template #description>
|
|
You may delete or revoke any of your existing tokens if they are no longer
|
|
needed.
|
|
</template>
|
|
|
|
<!-- API Token List -->
|
|
<template #content>
|
|
<div class="divide-border-secondary divide-y">
|
|
<div
|
|
v-for="token in tokens"
|
|
:key="token.id"
|
|
class="flex items-center py-2.5 justify-between">
|
|
<div class="break-all text-text-primary">
|
|
<div>{{ token.name }}</div>
|
|
<div class="text-sm text-text-tertiary space-x-3">
|
|
<span v-if="token.created_at">
|
|
Created at
|
|
{{
|
|
formatDateTimeLocalized(
|
|
token.created_at,
|
|
organization?.date_format,
|
|
organization?.time_format
|
|
)
|
|
}}
|
|
</span>
|
|
<span v-if="token.expires_at">
|
|
Expires at
|
|
{{
|
|
formatDateTimeLocalized(
|
|
token.expires_at,
|
|
organization?.date_format,
|
|
organization?.time_format
|
|
)
|
|
}}
|
|
</span>
|
|
<span v-if="token.revoked"> Revoked </span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center ms-2">
|
|
<div v-if="token.last_used_ago" class="text-sm text-gray-400">
|
|
Last used {{ token.last_used_ago }}
|
|
</div>
|
|
<button
|
|
v-if="!token.revoked"
|
|
class="cursor-pointer ms-6 text-sm text-text-secondary"
|
|
:aria-label="'Revoke API Token ' + token.name"
|
|
@click="confirmApiTokenRevocation(token)">
|
|
Revoke
|
|
</button>
|
|
<button
|
|
class="cursor-pointer ms-6 text-sm text-red-500"
|
|
:aria-label="'Delete API Token ' + token.name"
|
|
@click="confirmApiTokenDeletion(token)">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</ActionSection>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Token Value Modal -->
|
|
<DialogModal :show="displayingToken" @close="displayingToken = false">
|
|
<template #title> API Token created successfully </template>
|
|
|
|
<template #content>
|
|
<div>
|
|
Please copy your new API token. For your security, it won't be shown again.
|
|
<strong>This token is valid for one year</strong> unless you revoke it.
|
|
</div>
|
|
|
|
<div></div>
|
|
|
|
<div class="flex gap-2 pt-6 w-full">
|
|
<TextInput
|
|
v-if="newToken"
|
|
disabled
|
|
:model-value="newToken"
|
|
class="flex-1 text-gray-500"></TextInput>
|
|
<PrimaryButton v-if="isSupported" @click="copy(newToken)">{{
|
|
copied ? 'Copied!' : 'Copy Token'
|
|
}}</PrimaryButton>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<SecondaryButton @click="displayingToken = false"> Close </SecondaryButton>
|
|
</template>
|
|
</DialogModal>
|
|
|
|
<!-- Delete Token Confirmation Modal -->
|
|
<ConfirmationModal
|
|
:show="apiTokenBeingDeleted != null"
|
|
@close="apiTokenBeingDeleted = null">
|
|
<template #title> Delete API Token </template>
|
|
|
|
<template #content> Are you sure you would like to delete this API token? </template>
|
|
|
|
<template #footer>
|
|
<SecondaryButton @click="apiTokenBeingDeleted = null"> Cancel </SecondaryButton>
|
|
|
|
<DangerButton
|
|
class="ms-3"
|
|
:class="{ 'opacity-25': createApiTokenMutation.isPending.value }"
|
|
:disabled="createApiTokenMutation.isPending.value"
|
|
@click="deleteApiToken">
|
|
Delete
|
|
</DangerButton>
|
|
</template>
|
|
</ConfirmationModal>
|
|
|
|
<ConfirmationModal
|
|
:show="apiTokenBeingRevoked != null"
|
|
@close="apiTokenBeingRevoked = null">
|
|
<template #title> Revoke API Token </template>
|
|
|
|
<template #content> Are you sure you would like to revoke this API token? </template>
|
|
|
|
<template #footer>
|
|
<SecondaryButton @click="apiTokenBeingRevoked = null"> Cancel </SecondaryButton>
|
|
|
|
<DangerButton
|
|
class="ms-3"
|
|
:class="{ 'opacity-25': revokeApiTokenMutation.isPending.value }"
|
|
:disabled="revokeApiTokenMutation.isPending.value"
|
|
@click="revokeApiToken">
|
|
Revoke
|
|
</DangerButton>
|
|
</template>
|
|
</ConfirmationModal>
|
|
</div>
|
|
</template>
|