"use client"; import { useEffect, useState } from "react"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/ui/select"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { Check, ChevronsUpDown } from "lucide-react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { toast } from "@app/hooks/useToast"; import { useTranslations } from "next-intl"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { ListSitesResponse } from "@server/routers/site"; import { ListRolesResponse } from "@server/routers/role"; import { ListUsersResponse } from "@server/routers/user"; import { ListClientsResponse } from "@server/routers/client/listClients"; import { cn } from "@app/lib/cn"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Separator } from "@app/components/ui/separator"; import { AxiosResponse } from "axios"; import { UserType } from "@server/types/UserTypes"; type Site = ListSitesResponse["sites"][0]; type CreateInternalResourceDialogProps = { open: boolean; setOpen: (val: boolean) => void; orgId: string; sites: Site[]; onSuccess?: () => void; }; export default function CreateInternalResourceDialog({ open, setOpen, orgId, sites, onSuccess }: CreateInternalResourceDialogProps) { const t = useTranslations(); const api = createApiClient(useEnvContext()); const [isSubmitting, setIsSubmitting] = useState(false); const formSchema = z.object({ name: z .string() .min(1, t("createInternalResourceDialogNameRequired")) .max(255, t("createInternalResourceDialogNameMaxLength")), // mode: z.enum(["host", "cidr", "port"]), mode: z.enum(["host", "cidr"]), destination: z.string().min(1), siteId: z.int().positive(t("createInternalResourceDialogPleaseSelectSite")), protocol: z.enum(["tcp", "udp"]), // proxyPort: z.int() // .positive() // .min(1, t("createInternalResourceDialogProxyPortMin")) // .max(65535, t("createInternalResourceDialogProxyPortMax")), // destinationPort: z.int() // .positive() // .min(1, t("createInternalResourceDialogDestinationPortMin")) // .max(65535, t("createInternalResourceDialogDestinationPortMax")) // .nullish(), alias: z.string().nullish(), roles: z.array( z.object({ id: z.string(), text: z.string() }) ).optional(), users: z.array( z.object({ id: z.string(), text: z.string() }) ).optional(), clients: z.array( z.object({ id: z.string(), text: z.string() }) ).optional() }) // .refine( // (data) => { // if (data.mode === "port") { // return data.protocol !== undefined && data.protocol !== null; // } // return true; // }, // { // error: t("createInternalResourceDialogProtocol") + " is required for port mode", // path: ["protocol"] // } // ) // .refine( // (data) => { // if (data.mode === "port") { // return data.proxyPort !== undefined && data.proxyPort !== null; // } // return true; // }, // { // error: t("createInternalResourceDialogSitePort") + " is required for port mode", // path: ["proxyPort"] // } // ) // .refine( // (data) => { // if (data.mode === "port") { // return data.destinationPort !== undefined && data.destinationPort !== null; // } // return true; // }, // { // error: t("targetPort") + " is required for port mode", // path: ["destinationPort"] // } // ); type FormData = z.infer; const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]); const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>([]); const [allClients, setAllClients] = useState<{ id: string; text: string }[]>([]); const [activeRolesTagIndex, setActiveRolesTagIndex] = useState(null); const [activeUsersTagIndex, setActiveUsersTagIndex] = useState(null); const [activeClientsTagIndex, setActiveClientsTagIndex] = useState(null); const [hasMachineClients, setHasMachineClients] = useState(false); const availableSites = sites.filter( (site) => site.type === "newt" && site.subnet ); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: "", siteId: availableSites[0]?.siteId || 0, mode: "host", protocol: "tcp", // proxyPort: undefined, destination: "", // destinationPort: undefined, alias: "", roles: [], users: [], clients: [] } }); const mode = form.watch("mode"); useEffect(() => { if (open && availableSites.length > 0) { form.reset({ name: "", siteId: availableSites[0].siteId, mode: "host", protocol: "tcp", // proxyPort: undefined, destination: "", // destinationPort: undefined, alias: "", roles: [], users: [], clients: [] }); } }, [open]); useEffect(() => { const fetchRolesUsersAndClients = async () => { try { const [rolesResponse, usersResponse, clientsResponse] = await Promise.all([ api.get>(`/org/${orgId}/roles`), api.get>(`/org/${orgId}/users`), api.get>(`/org/${orgId}/clients?filter=machine&limit=1000`) ]); setAllRoles( rolesResponse.data.data.roles .map((role) => ({ id: role.roleId.toString(), text: role.name })) .filter((role) => role.text !== "Admin") ); setAllUsers( usersResponse.data.data.users.map((user) => ({ id: user.id.toString(), text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` })) ); const machineClients = clientsResponse.data.data.clients .filter((client) => !client.userId) .map((client) => ({ id: client.clientId.toString(), text: client.name })); setAllClients(machineClients); setHasMachineClients(machineClients.length > 0); } catch (error) { console.error("Error fetching roles, users, and clients:", error); } }; if (open) { fetchRolesUsersAndClients(); } }, [open, orgId]); const handleSubmit = async (data: FormData) => { setIsSubmitting(true); try { const response = await api.put>( `/org/${orgId}/site/${data.siteId}/resource`, { name: data.name, mode: data.mode, protocol: data.mode === "port" ? data.protocol : undefined, // proxyPort: data.mode === "port" ? data.proxyPort : undefined, // destinationPort: data.mode === "port" ? data.destinationPort : undefined, destination: data.destination, enabled: true, alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined } ); const siteResourceId = response.data.data.siteResourceId; // Set roles and users if provided if (data.roles && data.roles.length > 0) { await api.post(`/site-resource/${siteResourceId}/roles`, { roleIds: data.roles.map((r) => parseInt(r.id)) }); } if (data.users && data.users.length > 0) { await api.post(`/site-resource/${siteResourceId}/users`, { userIds: data.users.map((u) => u.id) }); } if (data.clients && data.clients.length > 0) { await api.post(`/site-resource/${siteResourceId}/clients`, { clientIds: data.clients.map((c) => parseInt(c.id)) }); } toast({ title: t("createInternalResourceDialogSuccess"), description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"), variant: "default" }); onSuccess?.(); setOpen(false); } catch (error) { console.error("Error creating internal resource:", error); toast({ title: t("createInternalResourceDialogError"), description: formatAxiosError( error, t("createInternalResourceDialogFailedToCreateInternalResource") ), variant: "destructive" }); } finally { setIsSubmitting(false); } }; if (availableSites.length === 0) { return ( {t("createInternalResourceDialogNoSitesAvailable")} {t("createInternalResourceDialogNoSitesAvailableDescription")} ); } return ( {t("createInternalResourceDialogCreateClientResource")} {t("createInternalResourceDialogCreateClientResourceDescription")}
{/* Resource Properties Form */}

{t("createInternalResourceDialogResourceProperties")}

( {t("createInternalResourceDialogName")} )} /> ( {t("createInternalResourceDialogSite")} {t("createInternalResourceDialogNoSitesFound")} {availableSites.map((site) => ( { field.onChange(site.siteId); }} > {site.name} ))} )} /> ( {t("createInternalResourceDialogMode")} )} /> {/* {mode === "port" && ( <>
( {t("createInternalResourceDialogProtocol")} )} /> ( {t("createInternalResourceDialogSitePort")} field.onChange( e.target.value === "" ? undefined : parseInt(e.target.value) ) } /> )} />
)} */}
{/* Target Configuration Form */}

{t("createInternalResourceDialogTargetConfiguration")}

( {t("createInternalResourceDialogDestination")} {mode === "host" && t("createInternalResourceDialogDestinationHostDescription")} {mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")} {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} )} /> {/* {mode === "port" && ( ( {t("targetPort")} field.onChange( e.target.value === "" ? undefined : parseInt(e.target.value) ) } /> {t("createInternalResourceDialogDestinationPortDescription")} )} /> )} */}
{/* Alias */} {mode !== "cidr" && (
( {t("createInternalResourceDialogAlias")} {t("createInternalResourceDialogAliasDescription")} )} />
)} {/* Access Control Section */}

{t("resourceUsersRoles")}

( {t("roles")} { form.setValue( "roles", newRoles as [Tag, ...Tag[]] ); }} enableAutocomplete={true} autocompleteOptions={allRoles} allowDuplicates={false} restrictTagsToAutocompleteOptions={true} sortTags={true} /> {t("resourceRoleDescription")} )} /> ( {t("users")} { form.setValue( "users", newUsers as [Tag, ...Tag[]] ); }} enableAutocomplete={true} autocompleteOptions={allUsers} allowDuplicates={false} restrictTagsToAutocompleteOptions={true} sortTags={true} /> )} /> {hasMachineClients && ( ( {t("clients")} { form.setValue( "clients", newClients as [Tag, ...Tag[]] ); }} enableAutocomplete={true} autocompleteOptions={allClients} allowDuplicates={false} restrictTagsToAutocompleteOptions={true} sortTags={true} /> {t("resourceClientDescription") || "Machine clients that can access this resource"} )} /> )}
); }