import { useEffect, useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import ms from "ms"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, FormLabel, IconButton, Input, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { useCreateSshHost, useGetSshHostById, useGetWorkspaceUsers, useListWorkspaceGroups, useListWorkspaceSshHosts, useUpdateSshHost } from "@app/hooks/api"; import { LoginMappingSource } from "@app/hooks/api/sshHost/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { popUp: UsePopUpState<["sshHost"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshHost"]>, state?: boolean) => void; }; const schema = z .object({ hostname: z.string().trim(), alias: z.string().trim(), userCertTtl: z .string() .trim() .refine( (val) => ms(val) > 0, "TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..." ) .default("8h"), loginMappings: z .object({ loginUser: z.string().trim().min(1), allowedPrincipals: z .array( z.object({ type: z.enum(["user", "group"]), value: z.string().trim().min(1) }) ) .default([]), source: z.nativeEnum(LoginMappingSource) }) .array() .default([]) }) .required(); export type FormData = z.infer; export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; const { data: sshHosts } = useListWorkspaceSshHosts(currentWorkspace.id); const { data: members = [] } = useGetWorkspaceUsers(projectId); const { data: groups = [] } = useListWorkspaceGroups(projectId); const [expandedMappings, setExpandedMappings] = useState>({}); const { data: sshHost } = useGetSshHostById( (popUp?.sshHost?.data as { sshHostId: string })?.sshHostId || "" ); const { mutateAsync: createMutateAsync } = useCreateSshHost(); const { mutateAsync: updateMutateAsync } = useUpdateSshHost(); const { control, handleSubmit, reset, getValues, setValue, formState: { isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { hostname: "", alias: "", userCertTtl: "8h", loginMappings: [] } }); const loginMappingsFormFields = useFieldArray({ control, name: "loginMappings" }); useEffect(() => { if (sshHost) { reset({ hostname: sshHost.hostname, alias: sshHost.alias ?? "", userCertTtl: sshHost.userCertTtl, loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals, source }) => ({ loginUser, allowedPrincipals: [ ...(allowedPrincipals.usernames || []).map((username) => ({ type: "user" as const, value: username })), ...(allowedPrincipals.groups || []).map((group) => ({ type: "group" as const, value: group })) ], source })) }); setExpandedMappings( Object.fromEntries(sshHost.loginMappings.map((_, index) => [index, false])) ); } else { reset({ hostname: "", alias: "", userCertTtl: "8h", loginMappings: [] }); } }, [sshHost]); const onFormSubmit = async ({ hostname, alias, userCertTtl, loginMappings }: FormData) => { try { if (!projectId) return; // Filter out login mappings that are from host groups const hostLoginMappings = loginMappings.filter( (mapping) => mapping.source === LoginMappingSource.HOST ); // check if there is already a different host with the same hostname const existingHostnames = sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || []; if (existingHostnames.includes(hostname.trim())) { createNotification({ text: "A host with this hostname already exists.", type: "error" }); return; } const trimmedAlias = alias.trim(); // check if there is already a different host with the same non-null alias if (trimmedAlias) { const existingAliases = sshHosts?.filter((h) => h.id !== sshHost?.id && h.alias !== null).map((h) => h.alias) || []; if (existingAliases.includes(trimmedAlias)) { createNotification({ text: "A host with this alias already exists.", type: "error" }); return; } } const transformedLoginMappings = hostLoginMappings.map(({ loginUser, allowedPrincipals }) => { const usernames = allowedPrincipals .filter((p) => p.type === "user" && p.value) .map((p) => p.value); const groupNames = allowedPrincipals .filter((p) => p.type === "group" && p.value) .map((p) => p.value); return { loginUser, allowedPrincipals: { usernames, groups: groupNames } }; }); if (sshHost) { await updateMutateAsync({ sshHostId: sshHost.id, hostname, alias: trimmedAlias, userCertTtl, loginMappings: transformedLoginMappings }); } else { await createMutateAsync({ projectId, hostname, alias: trimmedAlias, userCertTtl, loginMappings: transformedLoginMappings }); } reset(); handlePopUpToggle("sshHost", false); createNotification({ text: `Successfully ${sshHost ? "updated" : "added"} SSH host`, type: "success" }); } catch (err) { console.error(err); createNotification({ text: `Failed to ${sshHost ? "update" : "add"} SSH host`, type: "error" }); } }; const toggleMapping = (index: number) => { setExpandedMappings((prev) => ({ ...prev, [index]: !prev[index] })); }; const isPrincipalDuplicate = ( mappingIndex: number, principalType: string, principalValue: string ) => { const principals = getValues(`loginMappings.${mappingIndex}.allowedPrincipals`) || []; return principals.some((p) => p.type === principalType && p.value === principalValue); }; return ( { reset(); handlePopUpToggle("sshHost", isOpen); }} >
{sshHost && ( )} ( )} /> ( )} /> ( )} />
{loginMappingsFormFields.fields.map(({ id: metadataFieldId }, i) => (
loginMappingsFormFields.remove(i)} isDisabled={ loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST_GROUP } >
{expandedMappings[i] && ( <>
Login User ( { if ( loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST_GROUP ) return; const newValue = e.target.value; const loginMappings = getValues("loginMappings"); const isDuplicate = loginMappings.some( (mapping, index) => index !== i && mapping.loginUser === newValue && mapping.source === LoginMappingSource.HOST ); if (isDuplicate) { createNotification({ text: "This login user already exists", type: "error" }); return; } field.onChange(e); }} /> )} />
{loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST && ( )}
(
{value.map((principal, principalIndex) => (
{ if ( loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST_GROUP ) return; const newPrincipals = value.filter( (_, idx) => idx !== principalIndex ); onChange([...newPrincipals]); }} isDisabled={ loginMappingsFormFields.fields[i].source === LoginMappingSource.HOST_GROUP } >
))} {error && {error.message}}
)} />
)}
))}
); };