diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts index 4c7a39097..3bab6d7c0 100644 --- a/backend/src/ee/routes/v1/kmip-router.ts +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -208,21 +208,17 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { - query: { projectId } - } = req; - const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({ - projectId, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + ...req.query }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId, + projectId: req.query.projectId, event: { type: EventType.GET_KMIP_CLIENTS, metadata: { diff --git a/frontend/src/hooks/api/kmip/index.ts b/frontend/src/hooks/api/kmip/index.ts new file mode 100644 index 000000000..f4e57d7cc --- /dev/null +++ b/frontend/src/hooks/api/kmip/index.ts @@ -0,0 +1,2 @@ +export * from "./mutation"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/kmip/mutation.ts b/frontend/src/hooks/api/kmip/mutation.ts new file mode 100644 index 000000000..736c4c5cb --- /dev/null +++ b/frontend/src/hooks/api/kmip/mutation.ts @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { kmipKeys } from "./queries"; +import { TCreateKmipClient, TDeleteKmipClient, TUpdateKmipClient } from "./types"; + +export const useCreateKmipClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload: TCreateKmipClient) => { + const { data } = await apiRequest.post("/api/v1/kmip/clients", payload); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useUpdateKmipClient = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, name, description, permissions }: TUpdateKmipClient) => { + const { data } = await apiRequest.patch(`/api/v1/kmip/clients/${id}`, { + name, + description, + permissions + }); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; + +export const useDeleteKmipClients = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id }: TDeleteKmipClient) => { + const { data } = await apiRequest.delete(`/api/v1/kmip/clients/${id}`); + + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: kmipKeys.getKmipClientsByProjectId({ projectId }) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/kmip/queries.tsx b/frontend/src/hooks/api/kmip/queries.tsx new file mode 100644 index 000000000..2816e6b75 --- /dev/null +++ b/frontend/src/hooks/api/kmip/queries.tsx @@ -0,0 +1,52 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { OrderByDirection } from "../generic/types"; +import { KmipClientOrderBy, TListProjectKmipClientsDTO, TProjectKmipClientList } from "./types"; + +export const kmipKeys = { + getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) => + [projectId, filters] as const +}; + +export const useGetKmipClientsByProjectId = ( + { + projectId, + offset = 0, + limit = 100, + orderBy = KmipClientOrderBy.Name, + orderDirection = OrderByDirection.ASC, + search = "" + }: TListProjectKmipClientsDTO, + options?: Omit< + UseQueryOptions< + TProjectKmipClientList, + unknown, + TProjectKmipClientList, + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: kmipKeys.getKmipClientsByProjectId({ + projectId, + offset, + limit, + orderBy, + orderDirection, + search + }), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/kmip/clients", { + params: { projectId, offset, limit, search, orderBy, orderDirection } + }); + + return data; + }, + enabled: Boolean(projectId) && (options?.enabled ?? true), + placeholderData: (previousData) => previousData, + ...options + }); +}; diff --git a/frontend/src/hooks/api/kmip/types.ts b/frontend/src/hooks/api/kmip/types.ts new file mode 100644 index 000000000..fd2450174 --- /dev/null +++ b/frontend/src/hooks/api/kmip/types.ts @@ -0,0 +1,46 @@ +import { OrderByDirection } from "../generic/types"; + +export enum KmipPermission { + Create = "create", + Locate = "locate", + Check = "check", + Get = "get" +} + +export type TKmipClient = { + id: string; + name: string; + description?: string; + permissions: KmipPermission[]; + projectId: string; +}; + +type ProjectRef = { projectId: string }; +type KeyRef = { id: string }; + +export type TCreateKmipClient = Pick & + ProjectRef; + +export type TUpdateKmipClient = KeyRef & + Partial> & + ProjectRef; + +export type TProjectKmipClientList = { + kmipClients: TKmipClient[]; + totalCount: number; +}; + +export type TDeleteKmipClient = KeyRef & ProjectRef; + +export type TListProjectKmipClientsDTO = { + projectId: string; + offset?: number; + limit?: number; + orderBy?: KmipClientOrderBy; + orderDirection?: OrderByDirection; + search?: string; +}; + +export enum KmipClientOrderBy { + Name = "name" +} diff --git a/frontend/src/pages/kms/KmipPage/KmipPage.tsx b/frontend/src/pages/kms/KmipPage/KmipPage.tsx index 7fa9fca75..6297c5eef 100644 --- a/frontend/src/pages/kms/KmipPage/KmipPage.tsx +++ b/frontend/src/pages/kms/KmipPage/KmipPage.tsx @@ -3,7 +3,9 @@ import { useTranslation } from "react-i18next"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context"; + +import { KmipClientTable } from "./components/KmipClientTable"; export const KmipPage = () => { const { t } = useTranslation(); @@ -15,14 +17,17 @@ export const KmipPage = () => {
- + -
KMIP clients here
+
diff --git a/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx new file mode 100644 index 000000000..d9f833543 --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/DeleteKmipClientModal.tsx @@ -0,0 +1,53 @@ +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal } from "@app/components/v2"; +import { useDeleteKmipClients } from "@app/hooks/api/kmip"; +import { TKmipClient } from "@app/hooks/api/kmip/types"; + +type Props = { + kmipClient: TKmipClient; + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => { + const deleteKmipClients = useDeleteKmipClients(); + + if (!kmipClient) return null; + + const { id, projectId, name } = kmipClient; + + const handleDeleteKmipClient = async () => { + try { + await deleteKmipClients.mutateAsync({ + id, + projectId + }); + + createNotification({ + text: "KMIP client successfully deleted", + type: "success" + }); + + onOpenChange(false); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to delete KMIP client"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx new file mode 100644 index 000000000..74a0143ce --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientModal.tsx @@ -0,0 +1,184 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Checkbox, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + TextArea +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip"; +import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types"; + +const KMIP_PERMISSIONS_OPTIONS = [ + { value: KmipPermission.Check, label: "Check" }, + { value: KmipPermission.Create, label: "Create" }, + { value: KmipPermission.Get, label: "Get" }, + { value: KmipPermission.Locate, label: "Locate" } +] as const; + +const formSchema = z.object({ + name: z.string().trim().min(1), + description: z.string().max(500).optional(), + permissions: z.object({ + [KmipPermission.Check]: z.boolean().optional(), + [KmipPermission.Create]: z.boolean().optional(), + [KmipPermission.Get]: z.boolean().optional(), + [KmipPermission.Locate]: z.boolean().optional() + }) +}); + +export type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + kmipClient?: TKmipClient | null; +}; + +type FormProps = Pick & { + onComplete: () => void; +}; + +const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => { + const createKmipClient = useCreateKmipClient(); + const updateKmipClient = useUpdateKmipClient(); + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const isUpdate = !!kmipClient; + + const { + control, + handleSubmit, + register, + formState: { isSubmitting, errors } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: kmipClient?.name, + description: kmipClient?.description, + permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true])) + } + }); + + const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => { + const mutation = isUpdate + ? updateKmipClient.mutateAsync({ + id: kmipClient.id, + projectId, + name, + description, + permissions: Object.entries(permissions) + .filter(([, value]) => value) + .map(([key]) => key as KmipPermission) + }) + : createKmipClient.mutateAsync({ + projectId, + name, + description, + permissions: Object.entries(permissions) + .filter(([, value]) => value) + .map(([key]) => key as KmipPermission) + }); + + try { + await mutation; + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`, + type: "success" + }); + onComplete(); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`, + type: "error" + }); + } + }; + + return ( +
+ + + + +