From 8058b6207ad4be1d646182b67b810c397097c054 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:14:15 +0100 Subject: [PATCH] feat: add api key delete (#2210) Co-authored-by: Meier Lukas --- .../manage/tools/api/components/api-keys.tsx | 45 ++++++++++++++++--- packages/api/src/router/apiKeys.ts | 10 ++++- packages/translation/src/lang/en.json | 9 +++- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx b/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx index 660a5597a..110b3aa5a 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx @@ -1,14 +1,15 @@ "use client"; -import { useMemo } from "react"; -import { Button, Group, Stack, Text, Title } from "@mantine/core"; +import { useCallback, useMemo } from "react"; +import { ActionIcon, Button, Group, Stack, Text, Title } from "@mantine/core"; +import { IconTrash } from "@tabler/icons-react"; import type { MRT_ColumnDef } from "mantine-react-table"; import { MantineReactTable, useMantineReactTable } from "mantine-react-table"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; -import { useModalAction } from "@homarr/modals"; +import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; import { UserAvatar } from "@homarr/ui"; @@ -20,7 +21,8 @@ interface ApiKeysManagementProps { export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => { const { openModal } = useModalAction(CopyApiKeyModal); - const { mutate, isPending } = clientApi.apiKeys.create.useMutation({ + const { openConfirmModal } = useConfirmModal(); + const { mutate: mutateCreate, isPending: isPendingCreate } = clientApi.apiKeys.create.useMutation({ async onSuccess(data) { openModal({ apiKey: data.apiKey, @@ -28,7 +30,26 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => { await revalidatePathActionAsync("/manage/tools/api"); }, }); + const { mutateAsync: mutateDeleteAsync, isPending: isPendingDelete } = clientApi.apiKeys.delete.useMutation({ + async onSuccess() { + await revalidatePathActionAsync("/manage/tools/api"); + }, + }); + const t = useScopedI18n("management.page.tool.api.tab.apiKey"); + const handleDelete = useCallback( + (id: string) => { + openConfirmModal({ + title: t("modal.delete.title"), + children: t("modal.delete.text"), + // eslint-disable-next-line no-restricted-syntax + async onConfirm() { + await mutateDeleteAsync({ apiKeyId: id }); + }, + }); + }, + [t, openConfirmModal, mutateDeleteAsync], + ); const columns = useMemo[]>( () => [ @@ -46,8 +67,18 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => { ), }, + { + header: t("table.header.actions"), + Cell: ({ row }) => ( + + handleDelete(row.original.id)} loading={isPendingDelete} c="red"> + + + + ), + }, ], - [t], + [t, handleDelete, isPendingDelete], ); const table = useMantineReactTable({ @@ -56,9 +87,9 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => { renderTopToolbarCustomActions: () => ( diff --git a/packages/api/src/router/apiKeys.ts b/packages/api/src/router/apiKeys.ts index da903d695..345d0a705 100644 --- a/packages/api/src/router/apiKeys.ts +++ b/packages/api/src/router/apiKeys.ts @@ -1,6 +1,8 @@ +import { z } from "zod"; + import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import { generateSecureRandomToken } from "@homarr/common/server"; -import { createId, db } from "@homarr/db"; +import { createId, db, eq } from "@homarr/db"; import { apiKeys } from "@homarr/db/schema"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; @@ -39,4 +41,10 @@ export const apiKeysRouter = createTRPCRouter({ apiKey: `${id}.${randomToken}`, }; }), + delete: permissionRequiredProcedure + .requiresPermission("admin") + .input(z.object({ apiKeyId: z.string() })) + .mutation(async ({ ctx, input }) => { + await ctx.db.delete(apiKeys).where(eq(apiKeys.id, input.apiKeyId)).limit(1); + }), }); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 28d8b495d..379fcd243 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2584,10 +2584,17 @@ "button": { "createApiToken": "Create API token" }, + "modal": { + "delete": { + "title": "Delete API token", + "text": "This will permanently delete the API token. API clients using this token can no longer authenticate and perform API requests. This action cannot be undone." + } + }, "table": { "header": { "id": "ID", - "createdBy": "Created by" + "createdBy": "Created by", + "actions": "Actions" } } }