From a3fa12f0e4933e6ba07f0d4bc5b36da1fc9017f6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 18 Jan 2026 12:02:51 -0800 Subject: [PATCH] split org security settings to new tab --- src/app/[orgId]/settings/general/layout.tsx | 4 + src/app/[orgId]/settings/general/page.tsx | 712 +---------------- .../settings/general/security/page.tsx | 751 ++++++++++++++++++ 3 files changed, 757 insertions(+), 710 deletions(-) create mode 100644 src/app/[orgId]/settings/general/security/page.tsx diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 3ed9f0b2..53d03918 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -51,6 +51,10 @@ export default async function GeneralSettingsPage({ title: t("general"), href: `/{orgId}/settings/general`, exact: true + }, + { + title: t("security"), + href: `/{orgId}/settings/general/security` } ]; if (build !== "oss") { diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 3e78adc3..30285ff8 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -1,19 +1,12 @@ "use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import AuthPageSettings, { - AuthPageSettingsRef -} from "@app/components/private/AuthPageSettings"; - import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; -import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; import { toast } from "@app/hooks/useToast"; import { useState, - useRef, useTransition, - useActionState, - type ComponentRef + useActionState } from "react"; import { Form, @@ -25,13 +18,6 @@ import { FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@/components/ui/select"; import { z } from "zod"; import { useForm } from "react-hook-form"; @@ -55,79 +41,19 @@ import { import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import type { t } from "@faker-js/faker/dist/airline-DF6RqYmq"; import type { OrgContextType } from "@app/contexts/orgContext"; -// 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 general organization settings const GeneralFormSchema = z.object({ name: z.string(), - subnet: z.string().optional(), - requireTwoFactor: z.boolean().optional(), - maxSessionLengthHours: z.number().nullable().optional(), - passwordExpiryDays: z.number().nullable().optional(), - settingsLogRetentionDaysRequest: z.number(), - settingsLogRetentionDaysAccess: z.number(), - settingsLogRetentionDaysAction: z.number() + subnet: z.string().optional() }); -type GeneralFormValues = z.infer; - -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 } - ] - : []) -]; - export default function GeneralPage() { const { org } = useOrgContext(); return ( - - - - {build !== "oss" && } {build !== "saas" && } ); @@ -340,637 +266,3 @@ function GeneralSectionForm({ org }: SectionFormProps) { ); } -function LogRetentionSectionForm({ org }: SectionFormProps) { - const form = useForm({ - resolver: zodResolver( - GeneralFormSchema.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, hasSaasSubscription } = usePaidStatus(); - - const [, formAction, loadingSave] = useActionState(performSave, null); - const api = createApiClient(useEnvContext()); - - 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")} - - - - - - - )} - /> - - {build !== "oss" && ( - <> - - - { - const isDisabled = !isPaidUser; - - return ( - - - {t( - "logRetentionAccessLabel" - )} - - - - - - - ); - }} - /> - { - const isDisabled = !isPaidUser; - - return ( - - - {t( - "logRetentionActionLabel" - )} - - - - - - - ); - }} - /> - - )} - - -
-
- -
- -
-
- ); -} - -function SecuritySettingsSectionForm({ org }: SectionFormProps) { - const router = useRouter(); - const form = useForm({ - resolver: zodResolver( - GeneralFormSchema.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; - - return ( - -
- - { - if ( - !isDisabled - ) { - form.setValue( - "requireTwoFactor", - val - ); - } - }} - /> - -
- - - {t( - "requireTwoFactorDescription" - )} - -
- ); - }} - /> - { - const isDisabled = !isPaidUser; - - return ( - - - {t("maxSessionLength")} - - - - - - - {t( - "maxSessionLengthDescription" - )} - - - ); - }} - /> - { - const isDisabled = !isPaidUser; - - return ( - - - {t("passwordExpiryDays")} - - - - - - - {t( - "editPasswordExpiryDescription" - )} - - - ); - }} - /> - - -
-
- -
- -
-
- - ); -} diff --git a/src/app/[orgId]/settings/general/security/page.tsx b/src/app/[orgId]/settings/general/security/page.tsx new file mode 100644 index 00000000..716e35d6 --- /dev/null +++ b/src/app/[orgId]/settings/general/security/page.tsx @@ -0,0 +1,751 @@ +"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"; + +// 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(); + return ( + + + {build !== "oss" && } + + ); +} + +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, hasSaasSubscription } = usePaidStatus(); + + const [, formAction, loadingSave] = useActionState(performSave, null); + const api = createApiClient(useEnvContext()); + + 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")} + + + + + + + )} + /> + + {build !== "oss" && ( + <> + + + { + const isDisabled = !isPaidUser; + + return ( + + + {t( + "logRetentionAccessLabel" + )} + + + + + + + ); + }} + /> + { + const isDisabled = !isPaidUser; + + 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; + + return ( + +
+ + { + if ( + !isDisabled + ) { + form.setValue( + "requireTwoFactor", + val + ); + } + }} + /> + +
+ + + {t( + "requireTwoFactorDescription" + )} + +
+ ); + }} + /> + { + const isDisabled = !isPaidUser; + + return ( + + + {t("maxSessionLength")} + + + + + + + {t( + "maxSessionLengthDescription" + )} + + + ); + }} + /> + { + const isDisabled = !isPaidUser; + + return ( + + + {t("passwordExpiryDays")} + + + + + + + {t( + "editPasswordExpiryDescription" + )} + + + ); + }} + /> + + +
+
+ +
+ +
+
+ + ); +}