mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 07:29:28 -08:00
refactor: api key service (#24779)
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IconButton, type ActionItem } from '@immich/ui';
|
import { IconButton, type ActionItem, type Size } from '@immich/ui';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
action: ActionItem;
|
action: ActionItem;
|
||||||
|
size?: Size;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { action }: Props = $props();
|
const { action, size }: Props = $props();
|
||||||
const { title, icon, onAction } = $derived(action);
|
const { title, icon, onAction } = $derived(action);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if action.$if?.() ?? true}
|
{#if action.$if?.() ?? true}
|
||||||
<IconButton shape="round" color="primary" {icon} aria-label={title} onclick={() => onAction(action)} />
|
<IconButton {size} shape="round" color="primary" {icon} aria-label={title} onclick={() => onAction(action)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,68 +1,47 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
|
import TableButton from '$lib/components/TableButton.svelte';
|
||||||
import { dateFormats } from '$lib/constants';
|
import { dateFormats } from '$lib/constants';
|
||||||
import ApiKeyCreateModal from '$lib/modals/ApiKeyCreateModal.svelte';
|
import { getApiKeyActions, getApiKeysActions } from '$lib/services/api-key.service';
|
||||||
import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte';
|
|
||||||
import ApiKeyUpdateModal from '$lib/modals/ApiKeyUpdateModal.svelte';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { getApiKeys, type ApiKeyResponseDto } from '@immich/sdk';
|
||||||
import { deleteApiKey, getApiKeys, type ApiKeyResponseDto } from '@immich/sdk';
|
import { Button } from '@immich/ui';
|
||||||
import { Button, IconButton, modalManager, toastManager } from '@immich/ui';
|
|
||||||
import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
keys: ApiKeyResponseDto[];
|
keys: ApiKeyResponseDto[];
|
||||||
}
|
};
|
||||||
|
|
||||||
let { keys = $bindable() }: Props = $props();
|
let { keys = $bindable() }: Props = $props();
|
||||||
|
|
||||||
async function refreshKeys() {
|
const onApiKeyCreate = async () => {
|
||||||
keys = await getApiKeys();
|
keys = await getApiKeys();
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
const secret = await modalManager.show(ApiKeyCreateModal);
|
|
||||||
|
|
||||||
if (!secret) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await modalManager.show(ApiKeySecretModal, { secret });
|
|
||||||
await refreshKeys();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (key: ApiKeyResponseDto) => {
|
const onApiKeyUpdate = (update: ApiKeyResponseDto) => {
|
||||||
const success = await modalManager.show(ApiKeyUpdateModal, {
|
for (const key of keys) {
|
||||||
apiKey: key,
|
if (key.id === update.id) {
|
||||||
});
|
Object.assign(key, update);
|
||||||
|
}
|
||||||
if (success) {
|
|
||||||
await refreshKeys();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (key: ApiKeyResponseDto) => {
|
const onApiKeyDelete = ({ id }: ApiKeyResponseDto) => {
|
||||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('delete_api_key_prompt') });
|
keys = keys.filter((apiKey) => apiKey.id !== id);
|
||||||
if (!isConfirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteApiKey({ id: key.id });
|
|
||||||
toastManager.success($t('removed_api_key', { values: { name: key.name } }));
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_remove_api_key'));
|
|
||||||
} finally {
|
|
||||||
await refreshKeys();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { Create } = $derived(getApiKeysActions($t));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<OnEvents {onApiKeyCreate} {onApiKeyUpdate} {onApiKeyDelete} />
|
||||||
|
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
<div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
|
||||||
<div class="mb-2 flex justify-end">
|
<div class="mb-2 flex justify-end">
|
||||||
<Button shape="round" size="small" onclick={() => handleCreate()}>{$t('new_api_key')}</Button>
|
<Button leadingIcon={Create.icon} shape="round" size="small" onclick={() => Create.onAction(Create)}
|
||||||
|
>{Create.title}</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if keys.length > 0}
|
{#if keys.length > 0}
|
||||||
@@ -79,6 +58,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||||
{#each keys as key (key.id)}
|
{#each keys as key (key.id)}
|
||||||
|
{@const { Update, Delete } = getApiKeyActions($t, key)}
|
||||||
<tr
|
<tr
|
||||||
class="flex h-20 w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
class="flex h-20 w-full place-items-center text-center dark:text-immich-dark-fg even:bg-subtle/20 odd:bg-subtle/80"
|
||||||
>
|
>
|
||||||
@@ -91,22 +71,8 @@
|
|||||||
>{new Date(key.createdAt).toLocaleDateString($locale, dateFormats.settings)}
|
>{new Date(key.createdAt).toLocaleDateString($locale, dateFormats.settings)}
|
||||||
</td>
|
</td>
|
||||||
<td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/4">
|
<td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/4">
|
||||||
<IconButton
|
<TableButton action={Update} size="small" />
|
||||||
shape="round"
|
<TableButton action={Delete} size="small" />
|
||||||
color="primary"
|
|
||||||
icon={mdiPencilOutline}
|
|
||||||
aria-label={$t('edit_key')}
|
|
||||||
size="small"
|
|
||||||
onclick={() => handleUpdate(key)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
shape="round"
|
|
||||||
color="primary"
|
|
||||||
icon={mdiTrashCanOutline}
|
|
||||||
aria-label={$t('delete_key')}
|
|
||||||
size="small"
|
|
||||||
onclick={() => handleDelete(key)}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ThemeSetting } from '$lib/managers/theme-manager.svelte';
|
|||||||
import type { ReleaseEvent } from '$lib/types';
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
import type {
|
import type {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
|
ApiKeyResponseDto,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
QueueResponseDto,
|
QueueResponseDto,
|
||||||
@@ -19,6 +20,10 @@ export type Events = {
|
|||||||
LanguageChange: [{ name: string; code: string; rtl?: boolean }];
|
LanguageChange: [{ name: string; code: string; rtl?: boolean }];
|
||||||
ThemeChange: [ThemeSetting];
|
ThemeChange: [ThemeSetting];
|
||||||
|
|
||||||
|
ApiKeyCreate: [ApiKeyResponseDto];
|
||||||
|
ApiKeyUpdate: [ApiKeyResponseDto];
|
||||||
|
ApiKeyDelete: [ApiKeyResponseDto];
|
||||||
|
|
||||||
AssetReplace: [{ oldAssetId: string; newAssetId: string }];
|
AssetReplace: [{ oldAssetId: string; newAssetId: string }];
|
||||||
|
|
||||||
AlbumDelete: [AlbumResponseDto];
|
AlbumDelete: [AlbumResponseDto];
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
|
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleCreateApiKey } from '$lib/services/api-key.service';
|
||||||
import { createApiKey, Permission } from '@immich/sdk';
|
import { Permission } from '@immich/sdk';
|
||||||
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
|
import { Field, FormModal, Input } from '@immich/ui';
|
||||||
import { mdiKeyVariant } from '@mdi/js';
|
import { mdiKeyVariant } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = { onClose: (secret?: string) => void };
|
type Props = { onClose: () => void };
|
||||||
|
|
||||||
const { onClose }: Props = $props();
|
const { onClose }: Props = $props();
|
||||||
|
|
||||||
@@ -14,47 +14,22 @@
|
|||||||
let selectedPermissions = $state<Permission[]>([]);
|
let selectedPermissions = $state<Permission[]>([]);
|
||||||
const isAllPermissions = $derived(selectedPermissions.length === Object.keys(Permission).length - 1);
|
const isAllPermissions = $derived(selectedPermissions.length === Object.keys(Permission).length - 1);
|
||||||
|
|
||||||
const onsubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (!name) {
|
const success = await handleCreateApiKey({
|
||||||
toastManager.warning($t('api_key_empty'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedPermissions.length === 0) {
|
|
||||||
toastManager.warning($t('permission_empty'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { secret } = await createApiKey({
|
|
||||||
apiKeyCreateDto: {
|
|
||||||
name,
|
name,
|
||||||
permissions: isAllPermissions ? [Permission.All] : selectedPermissions,
|
permissions: isAllPermissions ? [Permission.All] : selectedPermissions,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
onClose(secret);
|
if (success) {
|
||||||
} catch (error) {
|
onClose();
|
||||||
handleError(error, $t('errors.unable_to_create_api_key'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('new_api_key')} icon={mdiKeyVariant} {onClose} size="giant">
|
<FormModal title={$t('new_api_key')} icon={mdiKeyVariant} {onClose} {onSubmit} submitText={$t('create')} size="giant">
|
||||||
<ModalBody>
|
|
||||||
<form {onsubmit} autocomplete="off" id="api-key-form">
|
|
||||||
<div class="mb-4 flex flex-col gap-2">
|
<div class="mb-4 flex flex-col gap-2">
|
||||||
<Field label={$t('name')}>
|
<Field label={$t('name')}>
|
||||||
<Input bind:value={name} />
|
<Input bind:value={name} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<ApiKeyPermissionsPicker bind:selectedPermissions />
|
<ApiKeyPermissionsPicker bind:selectedPermissions />
|
||||||
</form>
|
</FormModal>
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack fullWidth>
|
|
||||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
|
||||||
<Button shape="round" type="submit" fullWidth form="api-key-form">{$t('create')}</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
|
|||||||
@@ -1,69 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
|
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleUpdateApiKey } from '$lib/services/api-key.service';
|
||||||
import { Permission, updateApiKey } from '@immich/sdk';
|
import { Permission } from '@immich/sdk';
|
||||||
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
|
import { Field, FormModal, Input } from '@immich/ui';
|
||||||
import { mdiKeyVariant } from '@mdi/js';
|
import { mdiKeyVariant } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
apiKey: { id: string; name: string; permissions: Permission[] };
|
apiKey: { id: string; name: string; permissions: Permission[] };
|
||||||
onClose: (success?: true) => void;
|
onClose: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
let { apiKey, onClose }: Props = $props();
|
let { apiKey, onClose }: Props = $props();
|
||||||
|
|
||||||
|
const isAllPermissions = (permissions: Permission[]) => permissions.length === Object.keys(Permission).length - 1;
|
||||||
|
|
||||||
const mapPermissions = (permissions: Permission[]) =>
|
const mapPermissions = (permissions: Permission[]) =>
|
||||||
permissions.includes(Permission.All)
|
permissions.includes(Permission.All)
|
||||||
? Object.values(Permission).filter((permission) => permission !== Permission.All)
|
? Object.values(Permission).filter((permission) => permission !== Permission.All)
|
||||||
: permissions;
|
: permissions;
|
||||||
const isAllPermissions = (permissions: Permission[]) => permissions.length === Object.keys(Permission).length - 1;
|
|
||||||
|
|
||||||
let name = $state(apiKey.name);
|
let name = $state(apiKey.name);
|
||||||
let selectedPermissions = $state<Permission[]>(mapPermissions(apiKey.permissions));
|
let selectedPermissions = $state<Permission[]>(mapPermissions(apiKey.permissions));
|
||||||
|
|
||||||
const onsubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (!name) {
|
const success = await handleUpdateApiKey(apiKey, {
|
||||||
toastManager.warning($t('api_key_empty'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selectedPermissions.length === 0) {
|
|
||||||
toastManager.warning($t('permission_empty'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateApiKey({
|
|
||||||
id: apiKey.id,
|
|
||||||
apiKeyUpdateDto: {
|
|
||||||
name,
|
name,
|
||||||
permissions: isAllPermissions(selectedPermissions) ? [Permission.All] : selectedPermissions,
|
permissions: isAllPermissions(selectedPermissions) ? [Permission.All] : selectedPermissions,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
toastManager.success($t('saved_api_key'));
|
if (success) {
|
||||||
onClose(true);
|
onClose();
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_save_api_key'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('api_key')} icon={mdiKeyVariant} {onClose} size="giant">
|
<FormModal title={$t('api_key')} icon={mdiKeyVariant} {onClose} {onSubmit} size="giant" submitText={$t('save')}>
|
||||||
<ModalBody>
|
|
||||||
<form {onsubmit} autocomplete="off" id="api-key-form">
|
|
||||||
<div class="mb-4 flex flex-col gap-2">
|
<div class="mb-4 flex flex-col gap-2">
|
||||||
<Field label={$t('name')}>
|
<Field label={$t('name')}>
|
||||||
<Input bind:value={name} />
|
<Input bind:value={name} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<ApiKeyPermissionsPicker bind:selectedPermissions />
|
<ApiKeyPermissionsPicker bind:selectedPermissions />
|
||||||
</form>
|
</FormModal>
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack fullWidth>
|
|
||||||
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
|
||||||
<Button shape="round" type="submit" fullWidth form="api-key-form">{$t('save')}</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
|
|||||||
110
web/src/lib/services/api-key.service.ts
Normal file
110
web/src/lib/services/api-key.service.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import ApiKeyCreateModal from '$lib/modals/ApiKeyCreateModal.svelte';
|
||||||
|
import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte';
|
||||||
|
import ApiKeyUpdateModal from '$lib/modals/ApiKeyUpdateModal.svelte';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import {
|
||||||
|
createApiKey,
|
||||||
|
deleteApiKey,
|
||||||
|
updateApiKey,
|
||||||
|
type ApiKeyCreateDto,
|
||||||
|
type ApiKeyResponseDto,
|
||||||
|
type ApiKeyUpdateDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
|
import { mdiPencilOutline, mdiPlus, mdiTrashCanOutline } from '@mdi/js';
|
||||||
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export const getApiKeysActions = ($t: MessageFormatter) => {
|
||||||
|
const Create: ActionItem = {
|
||||||
|
title: $t('new_api_key'),
|
||||||
|
icon: mdiPlus,
|
||||||
|
onAction: () => modalManager.show(ApiKeyCreateModal, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { Create };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApiKeyActions = ($t: MessageFormatter, apiKey: ApiKeyResponseDto) => {
|
||||||
|
const Update: ActionItem = {
|
||||||
|
title: $t('edit_key'),
|
||||||
|
icon: mdiPencilOutline,
|
||||||
|
onAction: () => modalManager.show(ApiKeyUpdateModal, { apiKey }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Delete: ActionItem = {
|
||||||
|
title: $t('delete_key'),
|
||||||
|
icon: mdiTrashCanOutline,
|
||||||
|
onAction: () => handleDeleteApiKey(apiKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { Update, Delete };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleCreateApiKey = async (dto: ApiKeyCreateDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!dto.name) {
|
||||||
|
toastManager.warning($t('api_key_empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.permissions.length === 0) {
|
||||||
|
toastManager.warning($t('permission_empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiKey, secret } = await createApiKey({ apiKeyCreateDto: dto });
|
||||||
|
|
||||||
|
eventManager.emit('ApiKeyCreate', apiKey);
|
||||||
|
|
||||||
|
// no nested modal
|
||||||
|
void modalManager.show(ApiKeySecretModal, { secret });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_create_api_key'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleUpdateApiKey = async (apiKey: { id: string }, dto: ApiKeyUpdateDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
if (!dto.name) {
|
||||||
|
toastManager.warning($t('api_key_empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.permissions && dto.permissions.length === 0) {
|
||||||
|
toastManager.warning($t('permission_empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateApiKey({ id: apiKey.id, apiKeyUpdateDto: dto });
|
||||||
|
eventManager.emit('ApiKeyUpdate', response);
|
||||||
|
toastManager.success($t('saved_api_key'));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_save_api_key'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleDeleteApiKey = async (apiKey: ApiKeyResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
const confirmed = await modalManager.showDialog({ prompt: $t('delete_api_key_prompt') });
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteApiKey({ id: apiKey.id });
|
||||||
|
eventManager.emit('ApiKeyDelete', apiKey);
|
||||||
|
toastManager.success($t('removed_api_key', { values: { name: apiKey.name } }));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_remove_api_key'));
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user