mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
add frontend support for api token create, delete and revoke
This commit is contained in:
committed by
Constantin Graf
parent
bbed618fdc
commit
4ea55e5867
@@ -37,6 +37,8 @@ class ApiTokenController extends Controller
|
||||
* The response will contain the access token that can be used to send authenticated API requests.
|
||||
* Please note that the access token is only shown in this response and cannot be retrieved later.
|
||||
*
|
||||
* @operationId createApiToken
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function store(ApiTokenStoreRequest $request): ApiTokenWithAccessTokenResource
|
||||
@@ -53,6 +55,8 @@ class ApiTokenController extends Controller
|
||||
/**
|
||||
* Revoke an api token
|
||||
*
|
||||
* @operationId revokeApiToken
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function revoke(string $apiTokenId): JsonResponse
|
||||
@@ -69,6 +73,8 @@ class ApiTokenController extends Controller
|
||||
/**
|
||||
* Delete an api token
|
||||
*
|
||||
* @operationId deleteApiToken
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function destroy(string $apiTokenId): JsonResponse
|
||||
|
||||
@@ -39,7 +39,7 @@ async function resendInvitation() {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.resendInvitationEmail(
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
params: {
|
||||
invitation: props.invitation.id,
|
||||
|
||||
@@ -32,7 +32,7 @@ async function invitePlaceholder(id: string) {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.invitePlaceholder(
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
params: {
|
||||
organization: organizationId,
|
||||
|
||||
317
resources/js/Pages/Profile/Partials/ApiTokensForm.vue
Normal file
317
resources/js/Pages/Profile/Partials/ApiTokensForm.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
|
||||
import {computed, ref} from 'vue';
|
||||
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
|
||||
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 InputError from "@/packages/ui/src/Input/InputError.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";
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
// @ts-expect-error temporary fix until openapi docs type is fixed
|
||||
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: {
|
||||
apiTokenId: apiTokenId,
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['api-tokens'] })
|
||||
},
|
||||
});
|
||||
|
||||
const revokeApiTokenMutation = useMutation({
|
||||
mutationFn: async (apiTokenId: string) => {
|
||||
return await api.revokeApiToken(undefined, {
|
||||
params: {
|
||||
apiTokenId: 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 -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="name" value="Name" />
|
||||
<TextInput
|
||||
id="name"
|
||||
v-model="createApiTokenForm.name"
|
||||
type="text"
|
||||
class="mt-1 block w-full" />
|
||||
<InputError
|
||||
:message="createApiTokenForm.errors.name"
|
||||
class="mt-2" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<ActionMessage
|
||||
:on="createApiTokenForm.recentlySuccessful"
|
||||
class="me-3">
|
||||
Created.
|
||||
</ActionMessage>
|
||||
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': createApiTokenForm.processing }"
|
||||
:disabled="createApiTokenForm.processing">
|
||||
Create
|
||||
</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-white">
|
||||
<div>{{ token.name }}</div>
|
||||
<div class="text-sm text-text-tertiary space-x-3">
|
||||
<span v-if="token.created_at">
|
||||
Created on {{ formatDateTimeLocalized(token.created_at) }}
|
||||
</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"
|
||||
@click="confirmApiTokenRevocation(token)">
|
||||
Revoke
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer ms-6 text-sm text-red-500"
|
||||
@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 </template>
|
||||
|
||||
<template #content>
|
||||
<div>
|
||||
Please copy your new API token. For your security, it won't
|
||||
be shown again.
|
||||
</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>
|
||||
@@ -9,6 +9,7 @@ import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfile
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import type { User } from '@/types/models';
|
||||
import type { Session } from '@/types/jetstream';
|
||||
import ApiTokensForm from "@/Pages/Profile/Partials/ApiTokensForm.vue";
|
||||
|
||||
defineProps<{
|
||||
confirmsTwoFactorAuthentication: boolean;
|
||||
@@ -65,6 +66,9 @@ const page = usePage<{
|
||||
<LogoutOtherBrowserSessionsForm
|
||||
:sessions="sessions"
|
||||
class="mt-10 sm:mt-0" />
|
||||
<SectionBorder />
|
||||
|
||||
<ApiTokensForm></ApiTokensForm>
|
||||
|
||||
<template
|
||||
v-if="page.props.jetstream.hasAccountDeletionFeatures">
|
||||
|
||||
@@ -29,7 +29,7 @@ async function exportData() {
|
||||
const response = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.exportOrganization(
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
params: {
|
||||
organization: organizationId,
|
||||
|
||||
@@ -162,6 +162,15 @@ export type UpdateReportBody = ZodiosBodyByAlias<SolidTimeApi, 'updateReport'>;
|
||||
export type CreateReportBodyProperties = CreateReportBody['properties'];
|
||||
export type Report = ReportIndexResponse['data'][0];
|
||||
|
||||
export type ApiTokenIndexResponse = ZodiosResponseByAlias<
|
||||
SolidTimeApi,
|
||||
'getApiTokens'
|
||||
>;
|
||||
|
||||
export type CreateApiTokenBody = ZodiosBodyByAlias<SolidTimeApi, 'createApiToken'>;
|
||||
export type ApiToken = ApiTokenIndexResponse['data'][0];
|
||||
|
||||
|
||||
const api = createApiClient('/api', { validate: 'none' });
|
||||
|
||||
export { createApiClient, api };
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { makeApi, Zodios, type ZodiosOptions } from '@zodios/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ApiTokenResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
revoked: z.string(),
|
||||
scopes: z.string(),
|
||||
created_at: z.union([z.string(), z.null()]),
|
||||
expires_at: z.union([z.string(), z.null()]),
|
||||
})
|
||||
.passthrough();
|
||||
const ApiTokenCollection = z.array(ApiTokenResource);
|
||||
const ApiTokenStoreRequest = z
|
||||
.object({ name: z.string().min(1).max(255) })
|
||||
.passthrough();
|
||||
const ApiTokenWithAccessTokenResource = z.string();
|
||||
const ClientResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
@@ -26,9 +41,11 @@ const ImportRequest = z
|
||||
const InvitationResource = z
|
||||
.object({ id: z.string(), email: z.string(), role: z.string() })
|
||||
.passthrough();
|
||||
const Role = z.enum(['owner', 'admin', 'manager', 'employee', 'placeholder']);
|
||||
const InvitationStoreRequest = z
|
||||
.object({ email: z.string().email(), role: Role })
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
role: z.enum(['admin', 'manager', 'employee']),
|
||||
})
|
||||
.passthrough();
|
||||
const MemberResource = z
|
||||
.object({
|
||||
@@ -41,6 +58,7 @@ const MemberResource = z
|
||||
billable_rate: z.union([z.number(), z.null()]),
|
||||
})
|
||||
.passthrough();
|
||||
const Role = z.enum(['owner', 'admin', 'manager', 'employee', 'placeholder']);
|
||||
const MemberUpdateRequest = z
|
||||
.object({ role: Role, billable_rate: z.union([z.number(), z.null()]) })
|
||||
.partial()
|
||||
@@ -190,13 +208,6 @@ const ReportStoreRequest = z
|
||||
timezone: z.union([z.string(), z.null()]).optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
'properties.member_ids': z.string().optional(),
|
||||
'properties.client_ids': z.string().optional(),
|
||||
'properties.project_ids': z.string().optional(),
|
||||
'properties.tag_ids': z.string().optional(),
|
||||
'properties.task_ids': z.string().optional(),
|
||||
'properties.week_start': z.string().optional(),
|
||||
'properties.timezone': z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
const DetailedReportResource = z
|
||||
@@ -459,18 +470,21 @@ const PersonalMembershipResource = z
|
||||
role: z.string(),
|
||||
})
|
||||
.passthrough();
|
||||
const PersonalMembershipCollection = z.array(PersonalMembershipResource);
|
||||
|
||||
export const schemas = {
|
||||
ApiTokenResource,
|
||||
ApiTokenCollection,
|
||||
ApiTokenStoreRequest,
|
||||
ApiTokenWithAccessTokenResource,
|
||||
ClientResource,
|
||||
ClientCollection,
|
||||
ClientStoreRequest,
|
||||
ClientUpdateRequest,
|
||||
ImportRequest,
|
||||
InvitationResource,
|
||||
Role,
|
||||
InvitationStoreRequest,
|
||||
MemberResource,
|
||||
Role,
|
||||
MemberUpdateRequest,
|
||||
OrganizationResource,
|
||||
OrganizationUpdateRequest,
|
||||
@@ -502,7 +516,6 @@ export const schemas = {
|
||||
TimeEntryUpdateRequest,
|
||||
UserResource,
|
||||
PersonalMembershipResource,
|
||||
PersonalMembershipCollection,
|
||||
};
|
||||
|
||||
const endpoints = makeApi([
|
||||
@@ -786,11 +799,6 @@ const endpoints = makeApi([
|
||||
alias: 'exportOrganization',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: z.object({}).partial().passthrough(),
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
@@ -1122,11 +1130,6 @@ const endpoints = makeApi([
|
||||
alias: 'resendInvitationEmail',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: z.object({}).partial().passthrough(),
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
@@ -1345,11 +1348,6 @@ const endpoints = makeApi([
|
||||
alias: 'invitePlaceholder',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: z.object({}).partial().passthrough(),
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
@@ -1397,11 +1395,6 @@ const endpoints = makeApi([
|
||||
alias: 'v1.members.make-placeholder',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: z.object({}).partial().passthrough(),
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'Path',
|
||||
@@ -2614,6 +2607,11 @@ Users with the permission `time-entries:view:own` can only use this en
|
||||
type: 'Query',
|
||||
schema: z.enum(['true', 'false']).optional(),
|
||||
},
|
||||
{
|
||||
name: 'user_id',
|
||||
type: 'Query',
|
||||
schema: z.string().optional(),
|
||||
},
|
||||
{
|
||||
name: 'member_ids',
|
||||
type: 'Query',
|
||||
@@ -2639,11 +2637,6 @@ Users with the permission `time-entries:view:own` can only use this en
|
||||
type: 'Query',
|
||||
schema: z.array(z.string()).min(1).optional(),
|
||||
},
|
||||
{
|
||||
name: 'user_id',
|
||||
type: 'Query',
|
||||
schema: z.string().optional(),
|
||||
},
|
||||
],
|
||||
response: z
|
||||
.object({
|
||||
@@ -3438,6 +3431,120 @@ The report is considered public if the `is_public` field is set to &#x
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/users/me/api-tokens',
|
||||
alias: 'getApiTokens',
|
||||
description: `This endpoint is independent of organization.`,
|
||||
requestFormat: 'json',
|
||||
response: z.object({ data: ApiTokenCollection }).passthrough(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
description: `Unauthenticated`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/v1/users/me/api-tokens',
|
||||
alias: 'createApiToken',
|
||||
description: `The response will contain the access token that can be used to send authenticated API requests.
|
||||
Please note that the access token is only shown in this response and cannot be retrieved later.`,
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: z
|
||||
.object({ name: z.string().min(1).max(255) })
|
||||
.passthrough(),
|
||||
},
|
||||
],
|
||||
response: z
|
||||
.object({ data: ApiTokenWithAccessTokenResource })
|
||||
.passthrough(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
description: `Unauthenticated`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 422,
|
||||
description: `Validation error`,
|
||||
schema: z
|
||||
.object({
|
||||
message: z.string(),
|
||||
errors: z.record(z.array(z.string())),
|
||||
})
|
||||
.passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'delete',
|
||||
path: '/v1/users/me/api-tokens/:apiTokenId',
|
||||
alias: 'deleteApiToken',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'apiTokenId',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
description: `Unauthenticated`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/v1/users/me/api-tokens/:apiTokenId/revoke',
|
||||
alias: 'revokeApiToken',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'apiTokenId',
|
||||
type: 'Path',
|
||||
schema: z.string(),
|
||||
},
|
||||
],
|
||||
response: z.null(),
|
||||
errors: [
|
||||
{
|
||||
status: 401,
|
||||
description: `Unauthenticated`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
description: `Authorization error`,
|
||||
schema: z.object({ message: z.string() }).passthrough(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/users/me/memberships',
|
||||
@@ -3445,7 +3552,7 @@ The report is considered public if the `is_public` field is set to &#x
|
||||
description: `This endpoint is independent of organization.`,
|
||||
requestFormat: 'json',
|
||||
response: z
|
||||
.object({ data: PersonalMembershipCollection })
|
||||
.object({ data: z.array(PersonalMembershipResource) })
|
||||
.passthrough(),
|
||||
errors: [
|
||||
{
|
||||
|
||||
@@ -99,6 +99,10 @@ export function formatDateLocalized(date: string): string {
|
||||
return getLocalizedDayJs(date).format('DD.MM.YYYY');
|
||||
}
|
||||
|
||||
export function formatDateTimeLocalized(date: string): string {
|
||||
return getLocalizedDayJs(date).format('DD.MM.YYYY HH:mm');
|
||||
}
|
||||
|
||||
export function formatWeek(date: string | null): string {
|
||||
return 'Week ' + getDayJsInstance()(date).week();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user