mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 12:42: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.
|
* 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.
|
* Please note that the access token is only shown in this response and cannot be retrieved later.
|
||||||
*
|
*
|
||||||
|
* @operationId createApiToken
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*/
|
*/
|
||||||
public function store(ApiTokenStoreRequest $request): ApiTokenWithAccessTokenResource
|
public function store(ApiTokenStoreRequest $request): ApiTokenWithAccessTokenResource
|
||||||
@@ -53,6 +55,8 @@ class ApiTokenController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Revoke an api token
|
* Revoke an api token
|
||||||
*
|
*
|
||||||
|
* @operationId revokeApiToken
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*/
|
*/
|
||||||
public function revoke(string $apiTokenId): JsonResponse
|
public function revoke(string $apiTokenId): JsonResponse
|
||||||
@@ -69,6 +73,8 @@ class ApiTokenController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete an api token
|
* Delete an api token
|
||||||
*
|
*
|
||||||
|
* @operationId deleteApiToken
|
||||||
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*/
|
*/
|
||||||
public function destroy(string $apiTokenId): JsonResponse
|
public function destroy(string $apiTokenId): JsonResponse
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async function resendInvitation() {
|
|||||||
await handleApiRequestNotifications(
|
await handleApiRequestNotifications(
|
||||||
() =>
|
() =>
|
||||||
api.resendInvitationEmail(
|
api.resendInvitationEmail(
|
||||||
{},
|
undefined,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
invitation: props.invitation.id,
|
invitation: props.invitation.id,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async function invitePlaceholder(id: string) {
|
|||||||
await handleApiRequestNotifications(
|
await handleApiRequestNotifications(
|
||||||
() =>
|
() =>
|
||||||
api.invitePlaceholder(
|
api.invitePlaceholder(
|
||||||
{},
|
undefined,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
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 { usePage } from '@inertiajs/vue3';
|
||||||
import type { User } from '@/types/models';
|
import type { User } from '@/types/models';
|
||||||
import type { Session } from '@/types/jetstream';
|
import type { Session } from '@/types/jetstream';
|
||||||
|
import ApiTokensForm from "@/Pages/Profile/Partials/ApiTokensForm.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
confirmsTwoFactorAuthentication: boolean;
|
confirmsTwoFactorAuthentication: boolean;
|
||||||
@@ -65,6 +66,9 @@ const page = usePage<{
|
|||||||
<LogoutOtherBrowserSessionsForm
|
<LogoutOtherBrowserSessionsForm
|
||||||
:sessions="sessions"
|
:sessions="sessions"
|
||||||
class="mt-10 sm:mt-0" />
|
class="mt-10 sm:mt-0" />
|
||||||
|
<SectionBorder />
|
||||||
|
|
||||||
|
<ApiTokensForm></ApiTokensForm>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-if="page.props.jetstream.hasAccountDeletionFeatures">
|
v-if="page.props.jetstream.hasAccountDeletionFeatures">
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ async function exportData() {
|
|||||||
const response = await handleApiRequestNotifications(
|
const response = await handleApiRequestNotifications(
|
||||||
() =>
|
() =>
|
||||||
api.exportOrganization(
|
api.exportOrganization(
|
||||||
{},
|
undefined,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
|
|||||||
@@ -162,6 +162,15 @@ export type UpdateReportBody = ZodiosBodyByAlias<SolidTimeApi, 'updateReport'>;
|
|||||||
export type CreateReportBodyProperties = CreateReportBody['properties'];
|
export type CreateReportBodyProperties = CreateReportBody['properties'];
|
||||||
export type Report = ReportIndexResponse['data'][0];
|
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' });
|
const api = createApiClient('/api', { validate: 'none' });
|
||||||
|
|
||||||
export { createApiClient, api };
|
export { createApiClient, api };
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
import { makeApi, Zodios, type ZodiosOptions } from '@zodios/core';
|
import { makeApi, Zodios, type ZodiosOptions } from '@zodios/core';
|
||||||
import { z } from 'zod';
|
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
|
const ClientResource = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -26,9 +41,11 @@ const ImportRequest = z
|
|||||||
const InvitationResource = z
|
const InvitationResource = z
|
||||||
.object({ id: z.string(), email: z.string(), role: z.string() })
|
.object({ id: z.string(), email: z.string(), role: z.string() })
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const Role = z.enum(['owner', 'admin', 'manager', 'employee', 'placeholder']);
|
|
||||||
const InvitationStoreRequest = z
|
const InvitationStoreRequest = z
|
||||||
.object({ email: z.string().email(), role: Role })
|
.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
role: z.enum(['admin', 'manager', 'employee']),
|
||||||
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const MemberResource = z
|
const MemberResource = z
|
||||||
.object({
|
.object({
|
||||||
@@ -41,6 +58,7 @@ const MemberResource = z
|
|||||||
billable_rate: z.union([z.number(), z.null()]),
|
billable_rate: z.union([z.number(), z.null()]),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
const Role = z.enum(['owner', 'admin', 'manager', 'employee', 'placeholder']);
|
||||||
const MemberUpdateRequest = z
|
const MemberUpdateRequest = z
|
||||||
.object({ role: Role, billable_rate: z.union([z.number(), z.null()]) })
|
.object({ role: Role, billable_rate: z.union([z.number(), z.null()]) })
|
||||||
.partial()
|
.partial()
|
||||||
@@ -190,13 +208,6 @@ const ReportStoreRequest = z
|
|||||||
timezone: z.union([z.string(), z.null()]).optional(),
|
timezone: z.union([z.string(), z.null()]).optional(),
|
||||||
})
|
})
|
||||||
.passthrough(),
|
.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();
|
.passthrough();
|
||||||
const DetailedReportResource = z
|
const DetailedReportResource = z
|
||||||
@@ -459,18 +470,21 @@ const PersonalMembershipResource = z
|
|||||||
role: z.string(),
|
role: z.string(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const PersonalMembershipCollection = z.array(PersonalMembershipResource);
|
|
||||||
|
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
|
ApiTokenResource,
|
||||||
|
ApiTokenCollection,
|
||||||
|
ApiTokenStoreRequest,
|
||||||
|
ApiTokenWithAccessTokenResource,
|
||||||
ClientResource,
|
ClientResource,
|
||||||
ClientCollection,
|
ClientCollection,
|
||||||
ClientStoreRequest,
|
ClientStoreRequest,
|
||||||
ClientUpdateRequest,
|
ClientUpdateRequest,
|
||||||
ImportRequest,
|
ImportRequest,
|
||||||
InvitationResource,
|
InvitationResource,
|
||||||
Role,
|
|
||||||
InvitationStoreRequest,
|
InvitationStoreRequest,
|
||||||
MemberResource,
|
MemberResource,
|
||||||
|
Role,
|
||||||
MemberUpdateRequest,
|
MemberUpdateRequest,
|
||||||
OrganizationResource,
|
OrganizationResource,
|
||||||
OrganizationUpdateRequest,
|
OrganizationUpdateRequest,
|
||||||
@@ -502,7 +516,6 @@ export const schemas = {
|
|||||||
TimeEntryUpdateRequest,
|
TimeEntryUpdateRequest,
|
||||||
UserResource,
|
UserResource,
|
||||||
PersonalMembershipResource,
|
PersonalMembershipResource,
|
||||||
PersonalMembershipCollection,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoints = makeApi([
|
const endpoints = makeApi([
|
||||||
@@ -786,11 +799,6 @@ const endpoints = makeApi([
|
|||||||
alias: 'exportOrganization',
|
alias: 'exportOrganization',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
|
||||||
name: 'body',
|
|
||||||
type: 'Body',
|
|
||||||
schema: z.object({}).partial().passthrough(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
@@ -1122,11 +1130,6 @@ const endpoints = makeApi([
|
|||||||
alias: 'resendInvitationEmail',
|
alias: 'resendInvitationEmail',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
|
||||||
name: 'body',
|
|
||||||
type: 'Body',
|
|
||||||
schema: z.object({}).partial().passthrough(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
@@ -1345,11 +1348,6 @@ const endpoints = makeApi([
|
|||||||
alias: 'invitePlaceholder',
|
alias: 'invitePlaceholder',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
|
||||||
name: 'body',
|
|
||||||
type: 'Body',
|
|
||||||
schema: z.object({}).partial().passthrough(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
@@ -1397,11 +1395,6 @@ const endpoints = makeApi([
|
|||||||
alias: 'v1.members.make-placeholder',
|
alias: 'v1.members.make-placeholder',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
|
||||||
name: 'body',
|
|
||||||
type: 'Body',
|
|
||||||
schema: z.object({}).partial().passthrough(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'organization',
|
name: 'organization',
|
||||||
type: 'Path',
|
type: 'Path',
|
||||||
@@ -2614,6 +2607,11 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.enum(['true', 'false']).optional(),
|
schema: z.enum(['true', 'false']).optional(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'user_id',
|
||||||
|
type: 'Query',
|
||||||
|
schema: z.string().optional(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'member_ids',
|
name: 'member_ids',
|
||||||
type: 'Query',
|
type: 'Query',
|
||||||
@@ -2639,11 +2637,6 @@ Users with the permission `time-entries:view:own` can only use this en
|
|||||||
type: 'Query',
|
type: 'Query',
|
||||||
schema: z.array(z.string()).min(1).optional(),
|
schema: z.array(z.string()).min(1).optional(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'user_id',
|
|
||||||
type: 'Query',
|
|
||||||
schema: z.string().optional(),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
response: z
|
response: z
|
||||||
.object({
|
.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',
|
method: 'get',
|
||||||
path: '/v1/users/me/memberships',
|
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.`,
|
description: `This endpoint is independent of organization.`,
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
response: z
|
response: z
|
||||||
.object({ data: PersonalMembershipCollection })
|
.object({ data: z.array(PersonalMembershipResource) })
|
||||||
.passthrough(),
|
.passthrough(),
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ export function formatDateLocalized(date: string): string {
|
|||||||
return getLocalizedDayJs(date).format('DD.MM.YYYY');
|
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 {
|
export function formatWeek(date: string | null): string {
|
||||||
return 'Week ' + getDayJsInstance()(date).week();
|
return 'Week ' + getDayJsInstance()(date).week();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user