"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; import { useState, useRef, useActionState, type ComponentRef } from "react"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { formatAxiosError } from "@app/lib/api"; import { useRouter } from "next/navigation"; import { SettingsContainer, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionForm } from "@app/components/Settings"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { SwitchInput } from "@app/components/SwitchInput"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import type { OrgContextType } from "@app/contexts/orgContext"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; // Session length options in hours const SESSION_LENGTH_OPTIONS = [ { value: null, labelKey: "unenforced" }, { value: 1, labelKey: "1Hour" }, { value: 3, labelKey: "3Hours" }, { value: 6, labelKey: "6Hours" }, { value: 12, labelKey: "12Hours" }, { value: 24, labelKey: "1DaySession" }, { value: 72, labelKey: "3Days" }, { value: 168, labelKey: "7Days" }, { value: 336, labelKey: "14Days" }, { value: 720, labelKey: "30DaysSession" }, { value: 2160, labelKey: "90DaysSession" }, { value: 4320, labelKey: "180DaysSession" } ]; // Password expiry options in days - will be translated in component const PASSWORD_EXPIRY_OPTIONS = [ { value: null, labelKey: "neverExpire" }, { value: 1, labelKey: "1Day" }, { value: 30, labelKey: "30Days" }, { value: 60, labelKey: "60Days" }, { value: 90, labelKey: "90Days" }, { value: 180, labelKey: "180Days" }, { value: 365, labelKey: "1Year" } ]; // Schema for security organization settings const SecurityFormSchema = z.object({ requireTwoFactor: z.boolean().optional(), maxSessionLengthHours: z.number().nullable().optional(), passwordExpiryDays: z.number().nullable().optional(), settingsLogRetentionDaysRequest: z.number(), settingsLogRetentionDaysAccess: z.number(), settingsLogRetentionDaysAction: z.number() }); const LOG_RETENTION_OPTIONS = [ { label: "logRetentionDisabled", value: 0 }, { label: "logRetention3Days", value: 3 }, { label: "logRetention7Days", value: 7 }, { label: "logRetention14Days", value: 14 }, { label: "logRetention30Days", value: 30 }, { label: "logRetention90Days", value: 90 }, ...(build != "saas" ? [ { label: "logRetentionForever", value: -1 }, { label: "logRetentionEndOfFollowingYear", value: 9001 } ] : []) ]; type SectionFormProps = { org: OrgContextType["org"]["org"]; }; export default function SecurityPage() { const { org } = useOrgContext(); const { env } = useEnvContext(); return ( {!env.flags.disableEnterpriseFeatures && ( )} ); } function LogRetentionSectionForm({ org }: SectionFormProps) { const form = useForm({ resolver: zodResolver( SecurityFormSchema.pick({ settingsLogRetentionDaysRequest: true, settingsLogRetentionDaysAccess: true, settingsLogRetentionDaysAction: true }) ), defaultValues: { settingsLogRetentionDaysRequest: org.settingsLogRetentionDaysRequest ?? 15, settingsLogRetentionDaysAccess: org.settingsLogRetentionDaysAccess ?? 15, settingsLogRetentionDaysAction: org.settingsLogRetentionDaysAction ?? 15 }, mode: "onChange" }); const router = useRouter(); const t = useTranslations(); const { isPaidUser, subscriptionTier } = usePaidStatus(); const [, formAction, loadingSave] = useActionState(performSave, null); const { env } = useEnvContext(); const api = createApiClient({ env }); async function performSave() { const isValid = await form.trigger(); if (!isValid) return; const data = form.getValues(); try { const reqData = { settingsLogRetentionDaysRequest: data.settingsLogRetentionDaysRequest, settingsLogRetentionDaysAccess: data.settingsLogRetentionDaysAccess, settingsLogRetentionDaysAction: data.settingsLogRetentionDaysAction } as any; // Update organization await api.post(`/org/${org.orgId}`, reqData); toast({ title: t("orgUpdated"), description: t("orgUpdatedDescription") }); router.refresh(); } catch (e) { toast({ variant: "destructive", title: t("orgErrorUpdate"), description: formatAxiosError(e, t("orgErrorUpdateMessage")) }); } } return ( {t("logRetention")} {t("logRetentionDescription")}
( {t("logRetentionRequestLabel")} )} /> {!env.flags.disableEnterpriseFeatures && ( <> { const isDisabled = !isPaidUser( tierMatrix.accessLogs ); return ( {t( "logRetentionAccessLabel" )} ); }} /> { const isDisabled = !isPaidUser( tierMatrix.actionLogs ); return ( {t( "logRetentionActionLabel" )} ); }} /> )}
); } function SecuritySettingsSectionForm({ org }: SectionFormProps) { const router = useRouter(); const form = useForm({ resolver: zodResolver( SecurityFormSchema.pick({ requireTwoFactor: true, maxSessionLengthHours: true, passwordExpiryDays: true }) ), defaultValues: { requireTwoFactor: org.requireTwoFactor || false, maxSessionLengthHours: org.maxSessionLengthHours || null, passwordExpiryDays: org.passwordExpiryDays || null }, mode: "onChange" }); const t = useTranslations(); const { isPaidUser } = usePaidStatus(); // Track initial security policy values const initialSecurityValues = { requireTwoFactor: org.requireTwoFactor || false, maxSessionLengthHours: org.maxSessionLengthHours || null, passwordExpiryDays: org.passwordExpiryDays || null }; const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] = useState(false); // Check if security policies have changed const hasSecurityPolicyChanged = () => { const currentValues = form.getValues(); return ( currentValues.requireTwoFactor !== initialSecurityValues.requireTwoFactor || currentValues.maxSessionLengthHours !== initialSecurityValues.maxSessionLengthHours || currentValues.passwordExpiryDays !== initialSecurityValues.passwordExpiryDays ); }; const [, formAction, loadingSave] = useActionState(onSubmit, null); const api = createApiClient(useEnvContext()); const formRef = useRef>(null); async function onSubmit() { // Check if security policies have changed if (hasSecurityPolicyChanged()) { setIsSecurityPolicyConfirmOpen(true); return; } await performSave(); } async function performSave() { const isValid = await form.trigger(); if (!isValid) return; const data = form.getValues(); try { const reqData = { requireTwoFactor: data.requireTwoFactor || false, maxSessionLengthHours: data.maxSessionLengthHours, passwordExpiryDays: data.passwordExpiryDays } as any; // Update organization await api.post(`/org/${org.orgId}`, reqData); toast({ title: t("orgUpdated"), description: t("orgUpdatedDescription") }); router.refresh(); } catch (e) { toast({ variant: "destructive", title: t("orgErrorUpdate"), description: formatAxiosError(e, t("orgErrorUpdateMessage")) }); } } return ( <>

{t("securityPolicyChangeDescription")}

} buttonText={t("saveSettings")} onConfirm={performSave} string={t("securityPolicyChangeConfirmMessage")} title={t("securityPolicyChangeWarning")} warningText={t("securityPolicyChangeWarningText")} /> {t("securitySettings")} {t("securitySettingsDescription")}
{ const isDisabled = !isPaidUser( tierMatrix.twoFactorEnforcement ); return (
{ if ( !isDisabled ) { form.setValue( "requireTwoFactor", val ); } }} />
{t( "requireTwoFactorDescription" )}
); }} /> { const isDisabled = !isPaidUser( tierMatrix.sessionDurationPolicies ); return ( {t("maxSessionLength")} {t( "maxSessionLengthDescription" )} ); }} /> { const isDisabled = !isPaidUser( tierMatrix.passwordExpirationPolicies ); return ( {t("passwordExpiryDays")} {t( "editPasswordExpiryDescription" )} ); }} />
); }