"use client"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import z from "zod"; import { type PolicyFormValues, createPolicySchema } from "."; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { orgs, type ResourcePolicy } from "@server/db"; import type { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { InfoPopup } from "@app/components/ui/info-popup"; import { Input } from "@app/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/ui/select"; import { Switch } from "@app/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@app/components/ui/table"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@app/components/ui/input-otp"; import { MAJOR_ASNS } from "@server/db/asns"; import { COUNTRIES } from "@server/db/countries"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators"; import { ColumnDef, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table"; import { ArrowUpDown, Binary, Bot, Check, ChevronsUpDown, InfoIcon, Key, Plus } from "lucide-react"; import { useCallback, useMemo, useState, useActionState } from "react"; import { UseFormReturn, useForm, useWatch } from "react-hook-form"; import router from "next/navigation"; import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── export type EditPolicyFormProps = { hidePolicyNameForm?: boolean; }; export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { const { org } = useOrgContext(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); // const [, formAction, isSubmitting] = useActionState(onSubmit, null); const { isPaidUser } = usePaidStatus(); const router = useRouter(); const isMaxmindAvailable = !!( env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0 ); const isMaxmindASNAvailable = !!( env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0 ); const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( orgQueries.roles({ orgId: org.org.orgId }) ); const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( orgQueries.users({ orgId: org.org.orgId }) ); const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( orgQueries.identityProviders({ orgId: org.org.orgId, useOrgOnlyIdp: env.app.identityProviderMode === "org" }) ); // const form = useForm({ // resolver: zodResolver(createPolicySchema) as any, // defaultValues: { // name: "", // sso: true, // skipToIdpId: null, // emailWhitelistEnabled: false, // roles: [], // users: [], // emails: [], // applyRules: false, // rules: [], // password: null, // headerAuth: null, // pincode: null // } // }); // async function onSubmit() { // return; // // const isValid = await form.trigger(); // // if (!isValid) return; // // const payload = form.getValues(); // // try { // // const res = await api // // .post>( // // `/org/${org.org.orgId}/resource-policy/`, // // { // // name: payload.name, // // sso: payload.sso, // // roleIds: payload.roles.map((r) => r.id), // // userIds: payload.users.map((u) => u.id) // // } // // ) // // .catch((e) => { // // toast({ // // variant: "destructive", // // title: t("policyErrorCreate"), // // description: formatAxiosError( // // e, // // t("policyErrorCreateDescription") // // ) // // }); // // }); // // if (res && res.status === 201) { // // const id = res.data.data.resourcePolicyId; // // const niceId = res.data.data.niceId; // // router.push(`/${org.org.orgId}/settings/policies/resources/`); // // // should redirect to the details page // // // router.push( // // // `/${org.org.orgId}/settings/policies/resources/${niceId}` // // // ); // // toast({ // // title: t("success"), // // description: t("policyCreatedSuccess") // // }); // // } // // } catch (e) { // // toast({ // // variant: "destructive", // // title: t("policyErrorCreate"), // // description: t("policyErrorCreateMessageDescription") // // }); // // } // } const allRoles = useMemo( () => orgRoles .map((role) => ({ id: role.roleId.toString(), text: role.name })) .filter((role) => role.text !== "Admin"), [orgRoles] ); const allUsers = useMemo( () => orgUsers.map((user) => ({ id: user.id.toString(), text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` })), [orgUsers] ); const allIdps = useMemo(() => { if (build === "saas") { if (isPaidUser(tierMatrix.orgOidc)) { return orgIdps.map((idp) => ({ id: idp.idpId, text: idp.name })); } } else { return orgIdps.map((idp) => ({ id: idp.idpId, text: idp.name })); } return []; }, [orgIdps, isPaidUser]); if (isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps) { return <>; } return ( //
// {/* Name */} {!hidePolicyNameForm && } {/* */} {/* */} //
// ); } // ─── PolicyNameSection ────────────────────────────────────────────────── export function PolicyNameSection() { const t = useTranslations(); const api = createApiClient(useEnvContext()); const { policy } = useResourcePolicyContext(); const { org } = useOrgContext(); const form = useForm({ resolver: zodResolver( z.object({ name: z.string() }) ), defaultValues: { name: policy.name } }); const [, formAction, isSubmitting] = useActionState(onSubmit, null); async function onSubmit() { const isValid = await form.trigger(); if (!isValid) return; const payload = form.getValues(); try { const res = await api .put>( `/resource-policy/${policy.resourcePolicyId}`, { name: payload.name } ) .catch((e) => { toast({ variant: "destructive", title: t("policyErrorUpdate"), description: formatAxiosError( e, t("policyErrorUpdateDescription") ) }); }); if (res && res.status === 200) { toast({ title: t("success"), description: t("policyUpdatedSuccess") }); } } catch (e) { toast({ variant: "destructive", title: t("policyErrorUpdate"), description: t("policyErrorUpdateMessageDescription") }); } } return (
{t("resourcePolicyName")} {t("resourcePolicyNameDescription")} ( {t("name")} )} />
); } // ─── PolicyUsersRolesSection ────────────────────────────────────────────────── type PolicyUsersRolesSectionProps = { form: UseFormReturn; allRoles: { id: string; text: string }[]; allUsers: { id: string; text: string }[]; allIdps: { id: number; text: string }[]; }; export function PolicyUsersRolesSection({ form, allRoles, allUsers, allIdps }: PolicyUsersRolesSectionProps) { const t = useTranslations(); const ssoEnabled = useWatch({ control: form.control, name: "sso" }); const selectedIdpId = useWatch({ control: form.control, name: "skipToIdpId" }); const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< number | null >(null); const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< number | null >(null); return ( {t("resourceUsersRoles")} {t("resourcePolicyUsersRolesDescription")} { console.log(`form.setValue("sso", ${val})`); form.setValue("sso", val); }} /> {ssoEnabled && ( <> ( {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} /> )} /> )} {ssoEnabled && allIdps.length > 0 && (

{t("defaultIdentityProviderDescription")}

)}
); } // ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── const setPasswordSchema = z.object({ password: z.string().min(4).max(100) }); const setPincodeSchema = z.object({ pincode: z.string().length(6) }); const setHeaderAuthSchema = z.object({ user: z.string().min(4).max(100), password: z.string().min(4).max(100), extendedCompatibility: z.boolean() }); type PolicyAuthMethodsSectionProps = { form: UseFormReturn; }; export function PolicyAuthMethodsSection({ form }: PolicyAuthMethodsSectionProps) { const t = useTranslations(); const [isOpen, setIsOpen] = useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); const password = form.watch("password"); const pincode = form.watch("pincode"); const headerAuth = form.watch("headerAuth"); const passwordForm = useForm({ resolver: zodResolver(setPasswordSchema), defaultValues: { password: "" } }); const pincodeForm = useForm({ resolver: zodResolver(setPincodeSchema), defaultValues: { pincode: "" } }); const headerAuthForm = useForm({ resolver: zodResolver(setHeaderAuthSchema), defaultValues: { user: "", password: "", extendedCompatibility: true } }); if (!isOpen) { return ( {t("resourceAuthMethods")} {t("resourcePolicyAuthMethodsDescription")} ); } return ( <> {/* Password Credenza */} { setIsSetPasswordOpen(val); if (!val) passwordForm.reset(); }} > {t("resourcePasswordSetupTitle")} {t("resourcePasswordSetupTitleDescription")}
{ form.setValue("password", data); setIsSetPasswordOpen(false); passwordForm.reset(); })} className="space-y-4" id="set-password-form" > ( {t("password")} )} />
{/* Pincode Credenza */} { setIsSetPincodeOpen(val); if (!val) pincodeForm.reset(); }} > {t("resourcePincodeSetupTitle")} {t("resourcePincodeSetupTitleDescription")}
{ form.setValue("pincode", data); setIsSetPincodeOpen(false); pincodeForm.reset(); })} className="space-y-4" id="set-pincode-form" > ( {t("resourcePincode")}
)} />
{/* Header Auth Credenza */} { setIsSetHeaderAuthOpen(val); if (!val) headerAuthForm.reset(); }} > {t("resourceHeaderAuthSetupTitle")} {t("resourceHeaderAuthSetupTitleDescription")}
{ form.setValue("headerAuth", data); setIsSetHeaderAuthOpen(false); headerAuthForm.reset(); } )} className="space-y-4" id="set-header-auth-form" > ( {t("user")} )} /> ( {t("password")} )} /> ( )} />
{t("resourceAuthMethods")} {t("resourcePolicyAuthMethodsDescription")} {/* Password row */}
{t("resourcePasswordProtection", { status: password ? t("enabled") : t("disabled") })}
{/* Pincode row */}
{t("resourcePincodeProtection", { status: pincode ? t("enabled") : t("disabled") })}
{/* Header auth row */}
{headerAuth ? t( "resourceHeaderAuthProtectionEnabled" ) : t( "resourceHeaderAuthProtectionDisabled" )}
); } // ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── type PolicyOtpEmailSectionProps = { form: UseFormReturn; emailEnabled: boolean; }; export function PolicyOtpEmailSection({ form, emailEnabled }: PolicyOtpEmailSectionProps) { const t = useTranslations(); const [isOpen, setIsOpen] = useState(false); const [whitelistEnabled, setWhitelistEnabled] = useState(false); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); if (!isOpen) { return ( {t("otpEmailTitle")} {t("otpEmailTitleDescription")} ); } return ( {t("otpEmailTitle")} {t("otpEmailTitleDescription")} {!emailEnabled && ( {t("otpEmailSmtpRequired")} {t("otpEmailSmtpRequiredDescription")} )} { setWhitelistEnabled(val); form.setValue("emailWhitelistEnabled", val); }} disabled={!emailEnabled} /> {whitelistEnabled && emailEnabled && ( ( {/* @ts-ignore */} { return z .email() .or( z .string() .regex( /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { message: t( "otpEmailErrorInvalid" ) } ) ) .safeParse(tag).success; }} setActiveTagIndex={ setActiveEmailTagIndex } placeholder={t("otpEmailEnter")} tags={form.getValues().emails} setTags={(newEmails) => { form.setValue( "emails", newEmails as [Tag, ...Tag[]] ); }} allowDuplicates={false} sortTags={true} /> {t("otpEmailEnterDescription")} )} /> )} ); } // ─── PolicyRulesSection ─────────────────────────────────────────────────────── const addRuleSchema = z.object({ action: z.enum(["ACCEPT", "DROP", "PASS"]), match: z.string(), value: z.string(), priority: z.coerce.number().int().optional() }); type LocalRule = { ruleId: number; action: "ACCEPT" | "DROP" | "PASS"; match: string; value: string; priority: number; enabled: boolean; new?: boolean; updated?: boolean; }; type PolicyRulesSectionProps = { form: UseFormReturn; isMaxmindAvailable: boolean; isMaxmindAsnAvailable: boolean; }; export function PolicyRulesSection({ form, isMaxmindAvailable, isMaxmindAsnAvailable }: PolicyRulesSectionProps) { const t = useTranslations(); const [isOpen, setIsOpen] = useState(false); const [rules, setRules] = useState([]); const [rulesEnabled, setRulesEnabled] = useState(false); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); const addRuleForm = useForm({ resolver: zodResolver(addRuleSchema), defaultValues: { action: "ACCEPT" as const, match: "IP", value: "" } }); const RuleAction = useMemo( () => ({ ACCEPT: t("alwaysAllow"), DROP: t("alwaysDeny"), PASS: t("passToAuth") }), [t] ); const RuleMatch = useMemo( () => ({ PATH: t("path"), IP: "IP", CIDR: t("ipAddressRange"), COUNTRY: t("country"), ASN: "ASN" }), [t] ); const syncFormRules = useCallback( (updatedRules: LocalRule[]) => { form.setValue( "rules", updatedRules.map( ({ action, match, value, priority, enabled }) => ({ action, match, value, priority, enabled }) ) ); }, [form] ); const addRule = useCallback( function addRule(data: z.infer) { const isDuplicate = rules.some( (rule) => rule.action === data.action && rule.match === data.match && rule.value === data.value ); if (isDuplicate) { toast({ variant: "destructive", title: t("rulesErrorDuplicate"), description: t("rulesErrorDuplicateDescription") }); return; } if (data.match === "CIDR" && !isValidCIDR(data.value)) { toast({ variant: "destructive", title: t("rulesErrorInvalidIpAddressRange"), description: t("rulesErrorInvalidIpAddressRangeDescription") }); return; } if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { toast({ variant: "destructive", title: t("rulesErrorInvalidUrl"), description: t("rulesErrorInvalidUrlDescription") }); return; } if (data.match === "IP" && !isValidIP(data.value)) { toast({ variant: "destructive", title: t("rulesErrorInvalidIpAddress"), description: t("rulesErrorInvalidIpAddressDescription") }); return; } if ( data.match === "COUNTRY" && !COUNTRIES.some((c) => c.code === data.value) ) { toast({ variant: "destructive", title: t("rulesErrorInvalidCountry"), description: t("rulesErrorInvalidCountryDescription") || "" }); return; } let priority = data.priority; if (priority === undefined) { priority = rules.reduce( (acc, rule) => rule.priority > acc ? rule.priority : acc, 0 ) + 1; } const updatedRules = [ ...rules, { ...data, ruleId: new Date().getTime(), new: true, priority, enabled: true } ]; setRules(updatedRules); syncFormRules(updatedRules); addRuleForm.reset(); }, [rules, t, addRuleForm, syncFormRules] ); const removeRule = useCallback( function removeRule(ruleId: number) { const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); setRules(updatedRules); syncFormRules(updatedRules); }, [rules, syncFormRules] ); const updateRule = useCallback( function updateRule(ruleId: number, data: Partial) { const updatedRules = rules.map((rule) => rule.ruleId === ruleId ? { ...rule, ...data, updated: true } : rule ); setRules(updatedRules); syncFormRules(updatedRules); }, [rules, syncFormRules] ); const getValueHelpText = useCallback( function getValueHelpText(type: string) { switch (type) { case "CIDR": return t("rulesMatchIpAddressRangeDescription"); case "IP": return t("rulesMatchIpAddress"); case "PATH": return t("rulesMatchUrl"); case "COUNTRY": return t("rulesMatchCountry"); case "ASN": return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; } }, [t] ); const columns: ColumnDef[] = useMemo( () => [ { accessorKey: "priority", header: ({ column }) => ( ), cell: ({ row }) => ( e.currentTarget.focus()} onBlur={(e) => { const parsed = z.coerce .number() .int() .optional() .safeParse(e.target.value); if (!parsed.success) { toast({ variant: "destructive", title: t("rulesErrorInvalidPriority"), description: t( "rulesErrorInvalidPriorityDescription" ) }); return; } updateRule(row.original.ruleId, { priority: parsed.data }); }} /> ) }, { accessorKey: "action", header: () => {t("rulesAction")}, cell: ({ row }) => ( ) }, { accessorKey: "match", header: () => ( {t("rulesMatchType")} ), cell: ({ row }) => ( ) }, { accessorKey: "value", header: () => {t("value")}, cell: ({ row }) => row.original.match === "COUNTRY" ? ( {t("noCountryFound")} {COUNTRIES.map((country) => ( updateRule( row.original.ruleId, { value: country.code } ) } > {country.name} ( {country.code}) ))} ) : row.original.match === "ASN" ? ( No ASN found. Enter a custom ASN below. {MAJOR_ASNS.map((asn) => ( updateRule( row.original.ruleId, { value: asn.code } ) } > {asn.name} ({asn.code}) ))}
asn.code === row.original.value ) ? row.original.value : "" } onKeyDown={(e) => { if (e.key === "Enter") { const value = e.currentTarget.value .toUpperCase() .replace(/^AS/, ""); if (/^\d+$/.test(value)) { updateRule( row.original.ruleId, { value: "AS" + value } ); } } }} className="text-sm" />
) : ( updateRule(row.original.ruleId, { value: e.target.value }) } /> ) }, { accessorKey: "enabled", header: () => {t("enabled")}, cell: ({ row }) => ( updateRule(row.original.ruleId, { enabled: val }) } /> ) }, { id: "actions", header: () => {t("actions")}, cell: ({ row }) => (
) } ], [ t, RuleAction, RuleMatch, isMaxmindAvailable, isMaxmindAsnAvailable, updateRule, removeRule ] ); const table = useReactTable({ data: rules, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), state: { pagination: { pageIndex: 0, pageSize: 1000 } } }); if (!isOpen) { return ( {t("rulesResource")} {t("rulesResourcePolicyDescription")} ); } return ( {t("rulesResource")} {t("rulesResourceDescription")}
{ setRulesEnabled(val); form.setValue("applyRules", val); }} />
( {t("rulesAction")} )} /> ( {t("rulesMatchType")} )} /> ( {addRuleForm.watch("match") === "COUNTRY" ? ( {t( "noCountryFound" )} {COUNTRIES.map( ( country ) => ( { field.onChange( country.code ); setOpenAddRuleCountrySelect( false ); }} > { country.name }{" "} ( { country.code } ) ) )} ) : addRuleForm.watch( "match" ) === "ASN" ? ( No ASN found. Use the custom input below. {MAJOR_ASNS.map( ( asn ) => ( { field.onChange( asn.code ); setOpenAddRuleAsnSelect( false ); }} > { asn.name }{" "} ( { asn.code } ) ) )}
{ if ( e.key === "Enter" ) { const value = e.currentTarget.value .toUpperCase() .replace( /^AS/, "" ); if ( /^\d+$/.test( value ) ) { field.onChange( "AS" + value ); setOpenAddRuleAsnSelect( false ); } } }} className="text-sm" />
) : ( )}
)} />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const isActionsColumn = header.column.id === "actions"; return ( {header.isPlaceholder ? null : flexRender( header.column .columnDef.header, header.getContext() )} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const isActionsColumn = cell.column.id === "actions"; return ( {flexRender( cell.column.columnDef .cell, cell.getContext() )} ); })} )) ) : ( {t("rulesNoOne")} )}
); }