mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-21 12:26:40 +00:00
469 lines
22 KiB
TypeScript
469 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage
|
|
} from "@app/components/ui/form";
|
|
import { Input } from "@app/components/ui/input";
|
|
import {
|
|
OptionSelect,
|
|
type OptionSelectOption
|
|
} from "@app/components/OptionSelect";
|
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
import { useTranslations } from "next-intl";
|
|
import { useEffect } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
|
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
|
import { CheckboxWithLabel } from "./ui/checkbox";
|
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|
import type { Role } from "@server/db";
|
|
|
|
export const SSH_SUDO_MODE_VALUES = ["none", "full", "commands"] as const;
|
|
export type SshSudoMode = (typeof SSH_SUDO_MODE_VALUES)[number];
|
|
|
|
function parseRoleJsonArray(value: string | null | undefined): string[] {
|
|
if (value == null || value === "") return [];
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function toSshSudoMode(value: string | null | undefined): SshSudoMode {
|
|
if (value === "none" || value === "full" || value === "commands")
|
|
return value;
|
|
return "none";
|
|
}
|
|
|
|
export type RoleFormValues = {
|
|
name: string;
|
|
description?: string;
|
|
requireDeviceApproval?: boolean;
|
|
allowSsh?: boolean;
|
|
sshSudoMode: SshSudoMode;
|
|
sshSudoCommands?: string;
|
|
sshCreateHomeDir?: boolean;
|
|
sshUnixGroups?: string;
|
|
};
|
|
|
|
type RoleFormProps = {
|
|
variant: "create" | "edit";
|
|
role?: Role;
|
|
onSubmit: (values: RoleFormValues) => void | Promise<void>;
|
|
formId?: string;
|
|
};
|
|
|
|
export function RoleForm({
|
|
variant,
|
|
role,
|
|
onSubmit,
|
|
formId = "create-role-form"
|
|
}: RoleFormProps) {
|
|
const t = useTranslations();
|
|
const { isPaidUser } = usePaidStatus();
|
|
const { env } = useEnvContext();
|
|
|
|
const formSchema = z.object({
|
|
name: z
|
|
.string({ message: t("nameRequired") })
|
|
.min(1)
|
|
.max(32),
|
|
description: z.string().max(255).optional(),
|
|
requireDeviceApproval: z.boolean().optional(),
|
|
allowSsh: z.boolean().optional(),
|
|
sshSudoMode: z.enum(SSH_SUDO_MODE_VALUES),
|
|
sshSudoCommands: z.string().optional(),
|
|
sshCreateHomeDir: z.boolean().optional(),
|
|
sshUnixGroups: z.string().optional()
|
|
});
|
|
|
|
const defaultValues: RoleFormValues = role
|
|
? {
|
|
name: role.name,
|
|
description: role.description ?? "",
|
|
requireDeviceApproval: role.requireDeviceApproval ?? false,
|
|
allowSsh:
|
|
(role as Role & { allowSsh?: boolean }).allowSsh ?? false,
|
|
sshSudoMode: toSshSudoMode(role.sshSudoMode),
|
|
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
|
|
", "
|
|
),
|
|
sshCreateHomeDir: role.sshCreateHomeDir ?? false,
|
|
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ")
|
|
}
|
|
: {
|
|
name: "",
|
|
description: "",
|
|
requireDeviceApproval: false,
|
|
allowSsh: false,
|
|
sshSudoMode: "none",
|
|
sshSudoCommands: "",
|
|
sshCreateHomeDir: true,
|
|
sshUnixGroups: ""
|
|
};
|
|
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (variant === "edit" && role) {
|
|
form.reset({
|
|
name: role.name,
|
|
description: role.description ?? "",
|
|
requireDeviceApproval: role.requireDeviceApproval ?? false,
|
|
allowSsh:
|
|
(role as Role & { allowSsh?: boolean }).allowSsh ?? false,
|
|
sshSudoMode: toSshSudoMode(role.sshSudoMode),
|
|
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
|
|
", "
|
|
),
|
|
sshCreateHomeDir: role.sshCreateHomeDir ?? false,
|
|
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ")
|
|
});
|
|
}
|
|
}, [variant, role, form]);
|
|
|
|
const sshDisabled = !isPaidUser(tierMatrix.sshPam);
|
|
const sshSudoMode = form.watch("sshSudoMode");
|
|
const isAdminRole = variant === "edit" && role?.isAdmin === true;
|
|
|
|
useEffect(() => {
|
|
if (sshDisabled) {
|
|
form.setValue("allowSsh", false);
|
|
}
|
|
}, [sshDisabled, form]);
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit((values) => onSubmit(values))}
|
|
className="space-y-4"
|
|
id={formId}
|
|
>
|
|
{env.flags.disableEnterpriseFeatures ? (
|
|
<div className="space-y-4 mt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t("accessRoleName")}</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
disabled={isAdminRole}
|
|
readOnly={isAdminRole}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t("description")}</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
disabled={isAdminRole}
|
|
readOnly={isAdminRole}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<HorizontalTabs
|
|
clientSide={true}
|
|
defaultTab={0}
|
|
items={[
|
|
{ title: t("general"), href: "#" },
|
|
...(env.flags.disableEnterpriseFeatures
|
|
? []
|
|
: [{ title: t("sshAccess"), href: "#" }])
|
|
]}
|
|
>
|
|
{/* General tab */}
|
|
<div className="space-y-4 mt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{t("accessRoleName")}
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
disabled={isAdminRole}
|
|
readOnly={isAdminRole}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{t("description")}
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
disabled={isAdminRole}
|
|
readOnly={isAdminRole}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<PaidFeaturesAlert
|
|
tiers={tierMatrix.deviceApprovals}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="requireDeviceApproval"
|
|
render={({ field }) => (
|
|
<FormItem className="my-2">
|
|
<FormControl>
|
|
<CheckboxWithLabel
|
|
{...field}
|
|
disabled={
|
|
!isPaidUser(
|
|
tierMatrix.deviceApprovals
|
|
)
|
|
}
|
|
value="on"
|
|
checked={form.watch(
|
|
"requireDeviceApproval"
|
|
)}
|
|
onCheckedChange={(checked) => {
|
|
if (
|
|
checked !==
|
|
"indeterminate"
|
|
) {
|
|
form.setValue(
|
|
"requireDeviceApproval",
|
|
checked
|
|
);
|
|
}
|
|
}}
|
|
label={t(
|
|
"requireDeviceApproval"
|
|
)}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
"requireDeviceApprovalDescription"
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{/* SSH tab - hidden when enterprise features are disabled */}
|
|
{!env.flags.disableEnterpriseFeatures && (
|
|
<div className="space-y-4 mt-4">
|
|
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
|
|
<FormField
|
|
control={form.control}
|
|
name="allowSsh"
|
|
render={({ field }) => {
|
|
const allowSshOptions: OptionSelectOption<"allow" | "disallow">[] = [
|
|
{
|
|
value: "allow",
|
|
label: t("roleAllowSshAllow")
|
|
},
|
|
{
|
|
value: "disallow",
|
|
label: t("roleAllowSshDisallow")
|
|
}
|
|
];
|
|
return (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{t("roleAllowSsh")}
|
|
</FormLabel>
|
|
<OptionSelect<"allow" | "disallow">
|
|
options={allowSshOptions}
|
|
value={
|
|
sshDisabled
|
|
? "disallow"
|
|
: field.value
|
|
? "allow"
|
|
: "disallow"
|
|
}
|
|
onChange={(v) => {
|
|
if (sshDisabled) return;
|
|
field.onChange(v === "allow");
|
|
}}
|
|
cols={2}
|
|
disabled={sshDisabled}
|
|
/>
|
|
<FormDescription>
|
|
{t(
|
|
"roleAllowSshDescription"
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="sshSudoMode"
|
|
render={({ field }) => {
|
|
const sudoOptions: OptionSelectOption<SshSudoMode>[] =
|
|
[
|
|
{
|
|
value: "none",
|
|
label: t("sshSudoModeNone")
|
|
},
|
|
{
|
|
value: "full",
|
|
label: t("sshSudoModeFull")
|
|
},
|
|
{
|
|
value: "commands",
|
|
label: t(
|
|
"sshSudoModeCommands"
|
|
)
|
|
}
|
|
];
|
|
return (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{t("sshSudoMode")}
|
|
</FormLabel>
|
|
<OptionSelect<SshSudoMode>
|
|
options={sudoOptions}
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
cols={3}
|
|
disabled={sshDisabled}
|
|
/>
|
|
<FormMessage />
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
{sshSudoMode === "commands" && (
|
|
<FormField
|
|
control={form.control}
|
|
name="sshSudoCommands"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{t("sshSudoCommands")}
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
disabled={sshDisabled}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
"sshSudoCommandsDescription"
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="sshUnixGroups"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{t("sshUnixGroups")}
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
disabled={sshDisabled}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t("sshUnixGroupsDescription")}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="sshCreateHomeDir"
|
|
render={({ field }) => (
|
|
<FormItem className="my-2">
|
|
<FormControl>
|
|
<CheckboxWithLabel
|
|
{...field}
|
|
value="on"
|
|
checked={form.watch(
|
|
"sshCreateHomeDir"
|
|
)}
|
|
onCheckedChange={(
|
|
checked
|
|
) => {
|
|
if (
|
|
checked !==
|
|
"indeterminate"
|
|
) {
|
|
form.setValue(
|
|
"sshCreateHomeDir",
|
|
checked
|
|
);
|
|
}
|
|
}}
|
|
label={t(
|
|
"sshCreateHomeDir"
|
|
)}
|
|
disabled={sshDisabled}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</HorizontalTabs>
|
|
)}
|
|
</form>
|
|
</Form>
|
|
);
|
|
}
|