vat_wms/resources/js/Pages/API/Partials/APITokenManager.tsx
2025-05-13 12:32:55 +02:00

351 lines
12 KiB
TypeScript

import { useForm } from '@inertiajs/react';
import classNames from 'classnames';
import React, { useState } from 'react';
import useRoute from '@/Hooks/useRoute';
import ActionMessage from '@/Components/ActionMessage';
import ActionSection from '@/Components/ActionSection';
import Checkbox from '@/Components/Checkbox';
import ConfirmationModal from '@/Components/ConfirmationModal';
import DangerButton from '@/Components/DangerButton';
import DialogModal from '@/Components/DialogModal';
import FormSection from '@/Components/FormSection';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import SecondaryButton from '@/Components/SecondaryButton';
import SectionBorder from '@/Components/SectionBorder';
import { ApiToken } from '@/types';
import useTypedPage from '@/Hooks/useTypedPage';
interface Props {
tokens: ApiToken[];
availablePermissions: string[];
defaultPermissions: string[];
}
export default function APITokenManager({
tokens,
availablePermissions,
defaultPermissions,
}: Props) {
const route = useRoute();
const createApiTokenForm = useForm({
name: '',
permissions: defaultPermissions,
});
const updateApiTokenForm = useForm({
permissions: [] as string[],
});
const deleteApiTokenForm = useForm({});
const [displayingToken, setDisplayingToken] = useState(false);
const [managingPermissionsFor, setManagingPermissionsFor] =
useState<ApiToken | null>(null);
const [apiTokenBeingDeleted, setApiTokenBeingDeleted] =
useState<ApiToken | null>(null);
const page = useTypedPage();
function createApiToken() {
createApiTokenForm.post(route('api-tokens.store'), {
preserveScroll: true,
onSuccess: () => {
setDisplayingToken(true);
createApiTokenForm.reset();
},
});
}
function manageApiTokenPermissions(token: ApiToken) {
updateApiTokenForm.setData('permissions', token.abilities);
setManagingPermissionsFor(token);
}
function updateApiToken() {
if (!managingPermissionsFor) {
return;
}
updateApiTokenForm.put(
route('api-tokens.update', [managingPermissionsFor]),
{
preserveScroll: true,
preserveState: true,
onSuccess: () => setManagingPermissionsFor(null),
},
);
}
function confirmApiTokenDeletion(token: ApiToken) {
setApiTokenBeingDeleted(token);
}
function deleteApiToken() {
if (!apiTokenBeingDeleted) {
return;
}
deleteApiTokenForm.delete(
route('api-tokens.destroy', [apiTokenBeingDeleted]),
{
preserveScroll: true,
preserveState: true,
onSuccess: () => setApiTokenBeingDeleted(null),
},
);
}
return (
<div>
{/* <!-- Generate API Token --> */}
<FormSection
onSubmit={createApiToken}
title={'Create API Token'}
description={
'API tokens allow third-party services to authenticate with our application on your behalf.'
}
renderActions={() => (
<>
<ActionMessage
on={createApiTokenForm.recentlySuccessful}
className="mr-3"
>
Created.
</ActionMessage>
<PrimaryButton
className={classNames({
'opacity-25': createApiTokenForm.processing,
})}
disabled={createApiTokenForm.processing}
>
Create
</PrimaryButton>
</>
)}
>
{/* <!-- Token Name --> */}
<div className="col-span-6 sm:col-span-4">
<InputLabel htmlFor="name">Name</InputLabel>
<TextInput
id="name"
type="text"
className="mt-1 block w-full"
value={createApiTokenForm.data.name}
onChange={e =>
createApiTokenForm.setData('name', e.currentTarget.value)
}
autoFocus
/>
<InputError
message={createApiTokenForm.errors.name}
className="mt-2"
/>
</div>
{/* <!-- Token Permissions --> */}
{availablePermissions.length > 0 && (
<div className="col-span-6">
<InputLabel htmlFor="permissions">Permissions</InputLabel>
<div className="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
{availablePermissions.map(permission => (
<div key={permission}>
<label className="flex items-center">
<Checkbox
value={permission}
checked={createApiTokenForm.data.permissions.includes(
permission,
)}
onChange={e => {
if (
createApiTokenForm.data.permissions.includes(
e.currentTarget.value,
)
) {
createApiTokenForm.setData(
'permissions',
createApiTokenForm.data.permissions.filter(
p => p !== e.currentTarget.value,
),
);
} else {
createApiTokenForm.setData('permissions', [
e.currentTarget.value,
...createApiTokenForm.data.permissions,
]);
}
}}
/>
<span className="ml-2 text-sm text-gray-600 dark:text-gray-400">
{permission}
</span>
</label>
</div>
))}
</div>
</div>
)}
</FormSection>
{tokens.length > 0 ? (
<div>
<SectionBorder />
{/* <!-- Manage API Tokens --> */}
<div className="mt-10 sm:mt-0">
<ActionSection
title={'Manage API Tokens'}
description={
'You may delete any of your existing tokens if they are no longer needed.'
}
>
{/* <!-- API Token List --> */}
<div className="space-y-6">
{tokens.map(token => (
<div
className="flex items-center justify-between"
key={token.id}
>
<div className="break-all dark:text-white">
{token.name}
</div>
<div className="flex items-center">
{token.last_used_ago && (
<div className="text-sm text-gray-400">
Last used {token.last_used_ago}
</div>
)}
{availablePermissions.length > 0 ? (
<PrimaryButton
className="cursor-pointer ml-6 text-sm text-gray-400 underline"
onClick={() => manageApiTokenPermissions(token)}
>
Permissions
</PrimaryButton>
) : null}
<PrimaryButton
className="cursor-pointer ml-6 text-sm text-red-500"
onClick={() => confirmApiTokenDeletion(token)}
>
Delete
</PrimaryButton>
</div>
</div>
))}
</div>
</ActionSection>
</div>
</div>
) : null}
{/* <!-- Token Value Modal --> */}
<DialogModal
isOpen={displayingToken}
onClose={() => setDisplayingToken(false)}
>
<DialogModal.Content title={'API Token'}>
<div>
Please copy your new API token. For your security, it won't be shown
again.
</div>
<div className="mt-4 bg-gray-100 dark:bg-gray-900 px-4 py-2 rounded-sm font-mono text-sm text-gray-500">
{page.props?.jetstream?.flash?.token}
</div>
</DialogModal.Content>
<DialogModal.Footer>
<SecondaryButton onClick={() => setDisplayingToken(false)}>
Close
</SecondaryButton>
</DialogModal.Footer>
</DialogModal>
{/* <!-- API Token Permissions Modal --> */}
<DialogModal
isOpen={!!managingPermissionsFor}
onClose={() => setManagingPermissionsFor(null)}
>
<DialogModal.Content title={'API Token Permissions'}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{availablePermissions.map(permission => (
<div key={permission}>
<label className="flex items-center">
<Checkbox
value={permission}
checked={updateApiTokenForm.data.permissions.includes(
permission,
)}
onChange={e => {
if (
updateApiTokenForm.data.permissions.includes(
e.currentTarget.value,
)
) {
updateApiTokenForm.setData(
'permissions',
updateApiTokenForm.data.permissions.filter(
p => p !== e.currentTarget.value,
),
);
} else {
updateApiTokenForm.setData('permissions', [
e.currentTarget.value,
...updateApiTokenForm.data.permissions,
]);
}
}}
/>
<span className="ml-2 text-sm text-gray-600 dark:text-gray-400">
{permission}
</span>
</label>
</div>
))}
</div>
</DialogModal.Content>
<DialogModal.Footer>
<SecondaryButton onClick={() => setManagingPermissionsFor(null)}>
Cancel
</SecondaryButton>
<PrimaryButton
onClick={updateApiToken}
className={classNames('ml-2', {
'opacity-25': updateApiTokenForm.processing,
})}
disabled={updateApiTokenForm.processing}
>
Save
</PrimaryButton>
</DialogModal.Footer>
</DialogModal>
{/* <!-- Delete Token Confirmation Modal --> */}
<ConfirmationModal
isOpen={!!apiTokenBeingDeleted}
onClose={() => setApiTokenBeingDeleted(null)}
>
<ConfirmationModal.Content title={'Delete API Token'}>
Are you sure you would like to delete this API token?
</ConfirmationModal.Content>
<ConfirmationModal.Footer>
<SecondaryButton onClick={() => setApiTokenBeingDeleted(null)}>
Cancel
</SecondaryButton>
<DangerButton
onClick={deleteApiToken}
className={classNames('ml-2', {
'opacity-25': deleteApiTokenForm.processing,
})}
disabled={deleteApiTokenForm.processing}
>
Delete
</DangerButton>
</ConfirmationModal.Footer>
</ConfirmationModal>
</div>
);
}