Merge branch 'dev' of github.com:fosrl/pangolin into dev

This commit is contained in:
Owen
2026-10-16 12:24:33 -04:00
4 changed files with 1317 additions and 1165 deletions
+77 -60
View File
@@ -36,7 +36,9 @@ import {
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionForm, SettingsSectionForm,
SettingsSectionFooter SettingsSectionFooter,
SettingsFormGrid,
SettingsFormCell
} from "@app/components/Settings"; } from "@app/components/Settings";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -242,7 +244,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm variant="half">
<Form {...form}> <Form {...form}>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
@@ -251,71 +253,86 @@ function GeneralSectionForm({ org }: SectionFormProps) {
formAction(); formAction();
}); });
}} }}
className="grid gap-4"
id="org-general-settings-form" id="org-general-settings-form"
> >
<FormField <SettingsFormGrid>
control={form.control} <SettingsFormCell span="half">
name="name" <FormField
render={({ field }) => ( control={form.control}
<FormItem> name="name"
<FormLabel>{t("name")}</FormLabel> render={({ field }) => (
<FormControl> <FormItem>
<Input {...field} /> <FormLabel>
</FormControl> {t("name")}
<FormMessage /> </FormLabel>
<FormDescription> <FormControl>
{t("orgDisplayName")} <Input {...field} />
</FormDescription> </FormControl>
</FormItem> <FormMessage />
)} <FormDescription>
/> {t("orgDisplayName")}
</FormDescription>
</FormItem>
)}
/>
</SettingsFormCell>
<PaidFeaturesAlert <SettingsFormCell span="full">
tiers={tierMatrix.newtAutoUpdate} <PaidFeaturesAlert
/> tiers={tierMatrix.newtAutoUpdate}
{!env.flags.disableEnterpriseFeatures && ( />
<FormField </SettingsFormCell>
control={form.control} {!env.flags.disableEnterpriseFeatures && (
name="settingsEnableGlobalNewtAutoUpdate" <SettingsFormCell span="full">
render={({ field }) => ( <FormField
<FormItem> control={form.control}
<FormControl> name="settingsEnableGlobalNewtAutoUpdate"
<SwitchInput render={({ field }) => (
id="settings-enable-global-newt-auto-update" <FormItem>
label={t("newtAutoUpdate")} <FormControl>
checked={field.value} <SwitchInput
onCheckedChange={ id="settings-enable-global-newt-auto-update"
field.onChange label={t(
} "newtAutoUpdate"
disabled={ )}
!hasAutoUpdateFeature checked={
} field.value
/> }
</FormControl> onCheckedChange={
<FormDescription> field.onChange
{t("newtAutoUpdateDescription")}{" "} }
<a disabled={
href="https://docs.pangolin.net/manage/sites/auto-update" !hasAutoUpdateFeature
target="_blank" }
rel="noopener noreferrer" />
className="text-primary hover:underline inline-flex items-center gap-1" </FormControl>
> <FormDescription>
{t("learnMore")} {t(
<ExternalLink className="size-3.5 shrink-0" /> "newtAutoUpdateDescription"
</a> )}{" "}
</FormDescription> <a
<FormMessage /> href="https://docs.pangolin.net/manage/sites/auto-update"
</FormItem> target="_blank"
)} rel="noopener noreferrer"
/> className="text-primary hover:underline inline-flex items-center gap-1"
)} >
{t("learnMore")}
<ExternalLink className="size-3.5 shrink-0" />
</a>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
)}
</SettingsFormGrid>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
<div className="flex justify-end gap-2 mt-4"> <SettingsSectionFooter>
<Button <Button
type="submit" type="submit"
form="org-general-settings-form" form="org-general-settings-form"
@@ -324,7 +341,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
> >
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>
</div> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
); );
} }
File diff suppressed because it is too large Load Diff
+227 -187
View File
@@ -15,12 +15,18 @@ import { useActionState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import z from "zod"; import z from "zod";
import { import {
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm, SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "./Settings"; } from "./Settings";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -176,89 +182,228 @@ export default function AuthPageBrandingForm({
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm variant="half">
<PaidFeaturesAlert
tiers={tierMatrix.loginPageBranding}
/>
<Form {...form}> <Form {...form}>
<form <form
action={updateFormAction} action={updateFormAction}
id="auth-page-branding-form" id="auth-page-branding-form"
className="flex flex-col space-y-4 items-stretch"
> >
<FormField <SettingsFormGrid>
control={form.control} <SettingsFormCell span="full">
name="primaryColor" <PaidFeaturesAlert
render={({ field }) => ( tiers={tierMatrix.loginPageBranding}
<FormItem className=""> />
<FormLabel> </SettingsFormCell>
{t("brandingPrimaryColor")}
</FormLabel>
<div className="flex items-center gap-2"> <SettingsFormCell span="half">
<label
className="size-8 rounded-sm"
aria-hidden="true"
style={{
backgroundColor:
field.value
}}
>
<input
type="color"
{...field}
className="sr-only"
/>
</label>
<FormControl>
<Input {...field} />
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
<div className="grid md:grid-cols-5 gap-3 items-start">
<FormField
control={form.control}
name="logoUrl"
render={({ field }) => (
<FormItem className="md:col-span-3">
<FormLabel>
{build === "enterprise"
? t(
"brandingLogoURLOrPath"
)
: t("brandingLogoURL")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{build === "enterprise"
? t(
"brandingLogoPathDescription"
)
: t(
"brandingLogoURLDescription"
)}
</FormDescription>
</FormItem>
)}
/>
<div className="md:col-span-2 flex gap-3 items-start">
<FormField <FormField
control={form.control} control={form.control}
name="logoWidth" name="primaryColor"
render={({ field }) => ( render={({ field }) => (
<FormItem className="grow"> <FormItem>
<FormLabel> <FormLabel>
{t("brandingLogoWidth")} {t(
"brandingPrimaryColor"
)}
</FormLabel> </FormLabel>
<div className="flex items-center gap-2">
<label
className="size-8 rounded-sm"
aria-hidden="true"
style={{
backgroundColor:
field.value
}}
>
<input
type="color"
{...field}
className="sr-only"
/>
</label>
<FormControl>
<Input {...field} />
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell
span="half"
className="md:col-start-1"
>
<FormField
control={form.control}
name="logoUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{build === "enterprise"
? t(
"brandingLogoURLOrPath"
)
: t(
"brandingLogoURL"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{build === "enterprise"
? t(
"brandingLogoPathDescription"
)
: t(
"brandingLogoURLDescription"
)}
</FormDescription>
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<div className="flex gap-3 items-start">
<FormField
control={form.control}
name="logoWidth"
render={({ field }) => (
<FormItem className="grow">
<FormLabel>
{t(
"brandingLogoWidth"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<span className="relative top-8">
<XIcon className="text-muted-foreground size-4" />
</span>
<FormField
control={form.control}
name="logoHeight"
render={({ field }) => (
<FormItem className="grow">
<FormLabel>
{t(
"brandingLogoHeight"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</SettingsFormCell>
{build === "saas" ||
env.env.app.identityProviderMode ===
"org" ? (
<>
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t(
"organizationLoginPageTitle"
)}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t(
"organizationLoginPageDescription"
)}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="orgTitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"brandingOrgTitle"
)}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="orgSubtitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"brandingOrgSubtitle"
)}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
</>
) : null}
<SettingsFormCell span="full">
<SettingsSubsectionHeader>
<SettingsSubsectionTitle>
{t("resourceLoginPageTitle")}
</SettingsSubsectionTitle>
<SettingsSubsectionDescription>
{t(
"resourceLoginPageDescription"
)}
</SettingsSubsectionDescription>
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="resourceTitle"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"brandingResourceTitle"
)}
</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
@@ -266,19 +411,16 @@ export default function AuthPageBrandingForm({
</FormItem> </FormItem>
)} )}
/> />
</SettingsFormCell>
<span className="relative top-8"> <SettingsFormCell span="half">
<XIcon className="text-muted-foreground size-4" />
</span>
<FormField <FormField
control={form.control} control={form.control}
name="logoHeight" name="resourceSubtitle"
render={({ field }) => ( render={({ field }) => (
<FormItem className="grow"> <FormItem>
<FormLabel> <FormLabel>
{t( {t(
"brandingLogoHeight" "brandingResourceSubtitle"
)} )}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -288,116 +430,14 @@ export default function AuthPageBrandingForm({
</FormItem> </FormItem>
)} )}
/> />
</div> </SettingsFormCell>
</div> </SettingsFormGrid>
{build === "saas" ||
env.env.app.identityProviderMode === "org" ? (
<>
<div className="mt-3 mb-6">
<SettingsSectionTitle>
{t(
"organizationLoginPageTitle"
)}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t(
"organizationLoginPageDescription"
)}
</SettingsSectionDescription>
</div>
<div className="flex flex-col gap-5">
<FormField
control={form.control}
name="orgTitle"
render={({ field }) => (
<FormItem className="md:col-span-3">
<FormLabel>
{t(
"brandingOrgTitle"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="orgSubtitle"
render={({ field }) => (
<FormItem className="md:col-span-3">
<FormLabel>
{t(
"brandingOrgSubtitle"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</>
) : null}
<div className="mt-3 mb-6">
<SettingsSectionTitle>
{t("resourceLoginPageTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceLoginPageDescription")}
</SettingsSectionDescription>
</div>
<div className="flex flex-col gap-5">
<FormField
control={form.control}
name="resourceTitle"
render={({ field }) => (
<FormItem className="md:col-span-3">
<FormLabel>
{t("brandingResourceTitle")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="resourceSubtitle"
render={({ field }) => (
<FormItem className="md:col-span-3">
<FormLabel>
{t(
"brandingResourceSubtitle"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
<div className="flex justify-end gap-2 mt-6 items-center"> <SettingsSectionFooter>
{branding && ( {branding && (
<form action={deleteFormAction}> <form action={deleteFormAction}>
<Button <Button
@@ -427,7 +467,7 @@ export default function AuthPageBrandingForm({
> >
{t("saveAuthPageBranding")} {t("saveAuthPageBranding")}
</Button> </Button>
</div> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
</> </>
); );
+150 -125
View File
@@ -20,7 +20,10 @@ import {
SettingsSectionTitle, SettingsSectionTitle,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionForm SettingsSectionForm,
SettingsSectionFooter,
SettingsFormGrid,
SettingsFormCell
} from "@app/components/Settings"; } from "@app/components/Settings";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { GetLoginPageResponse } from "@server/routers/loginPage/types"; import { GetLoginPageResponse } from "@server/routers/loginPage/types";
@@ -284,145 +287,167 @@ function AuthPageSettings({
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm variant="half">
<PaidFeaturesAlert tiers={tierMatrix.loginPageDomain} />
<Form {...form}> <Form {...form}>
<form <form
action={formAction} action={formAction}
className="space-y-4"
id="auth-page-settings-form" id="auth-page-settings-form"
> >
<div className="space-y-3"> <SettingsFormGrid>
<Label>{t("authPageDomain")}</Label> <SettingsFormCell span="full">
<div className="border p-2 rounded-md flex items-center justify-between"> <PaidFeaturesAlert
<span className="text-sm text-muted-foreground flex items-center gap-2"> tiers={tierMatrix.loginPageDomain}
<Globe size="14" /> />
{loginPage && </SettingsFormCell>
!loginPage.domainId ? ( <SettingsFormCell span="full">
<InfoPopup <div className="space-y-3">
info={t( <Label>{t("authPageDomain")}</Label>
"domainNotFoundDescription" <div className="border p-2 rounded-md flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<Globe size="14" />
{loginPage &&
!loginPage.domainId ? (
<InfoPopup
info={t(
"domainNotFoundDescription"
)}
text={t(
"domainNotFound"
)}
/>
) : loginPage?.fullDomain ? (
<a
href={`${window.location.protocol}//${loginPage.fullDomain}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{`${window.location.protocol}//${loginPage.fullDomain}`}
</a>
) : form.watch(
"authPageDomainId"
) ? (
// Show selected domain from form state when no loginPage exists yet
(() => {
const selectedDomainId =
form.watch(
"authPageDomainId"
);
const selectedSubdomain =
form.watch(
"authPageSubdomain"
);
const domain =
baseDomains.find(
(d) =>
d.domainId ===
selectedDomainId
);
if (domain) {
const sanitizedSubdomain =
selectedSubdomain
? finalizeSubdomainSanitize(
selectedSubdomain
)
: "";
const fullDomain =
sanitizedSubdomain
? `${sanitizedSubdomain}.${domain.baseDomain}`
: domain.baseDomain;
return fullDomain;
}
return t(
"noDomainSet"
);
})()
) : (
t("noDomainSet")
)} )}
text={t("domainNotFound")} </span>
/> <div className="flex items-center gap-2">
) : loginPage?.fullDomain ? ( <Button
<a variant="secondary"
href={`${window.location.protocol}//${loginPage.fullDomain}`} type="button"
target="_blank" size="sm"
rel="noopener noreferrer" onClick={() =>
className="hover:underline" setEditDomainOpen(
> true
{`${window.location.protocol}//${loginPage.fullDomain}`} )
</a> }
) : form.watch( disabled={
"authPageDomainId" !isPaidUser(
) ? ( tierMatrix.loginPageDomain
// Show selected domain from form state when no loginPage exists yet )
(() => { }
const selectedDomainId = >
form.watch( {form.watch(
"authPageDomainId" "authPageDomainId"
);
const selectedSubdomain =
form.watch(
"authPageSubdomain"
);
const domain =
baseDomains.find(
(d) =>
d.domainId ===
selectedDomainId
);
if (domain) {
const sanitizedSubdomain =
selectedSubdomain
? finalizeSubdomainSanitize(
selectedSubdomain
)
: "";
const fullDomain =
sanitizedSubdomain
? `${sanitizedSubdomain}.${domain.baseDomain}`
: domain.baseDomain;
return fullDomain;
}
return t("noDomainSet");
})()
) : (
t("noDomainSet")
)}
</span>
<div className="flex items-center gap-2">
<Button
variant="secondary"
type="button"
size="sm"
onClick={() =>
setEditDomainOpen(true)
}
disabled={
!isPaidUser(
tierMatrix.loginPageDomain
)
}
>
{form.watch("authPageDomainId")
? t("changeDomain")
: t("selectDomain")}
</Button>
{form.watch("authPageDomainId") && (
<Button
variant="destructive"
type="button"
size="sm"
onClick={
clearAuthPageDomain
}
disabled={
!isPaidUser(
tierMatrix.loginPageDomain
) )
} ? t("changeDomain")
> : t("selectDomain")}
<Trash2 size="14" /> </Button>
</Button> {form.watch(
)} "authPageDomainId"
</div> ) && (
</div> <Button
variant="destructive"
type="button"
size="sm"
onClick={
clearAuthPageDomain
}
disabled={
!isPaidUser(
tierMatrix.loginPageDomain
)
}
>
<Trash2 size="14" />
</Button>
)}
</div>
</div>
{!form.watch("authPageDomainId") && ( {!form.watch(
<div className="text-sm text-muted-foreground"> "authPageDomainId"
{t( ) && (
"addDomainToEnableCustomAuthPages" <div className="text-sm text-muted-foreground">
{t(
"addDomainToEnableCustomAuthPages"
)}
</div>
)} )}
</div>
)}
{(build === "enterprise" || {(build === "enterprise" ||
!isPaidUser( !isPaidUser(
tierMatrix.loginPageDomain tierMatrix.loginPageDomain
)) && )) &&
loginPage?.domainId && loginPage?.domainId &&
loginPage?.fullDomain && loginPage?.fullDomain &&
!hasUnsavedChanges && ( !hasUnsavedChanges && (
<CertificateStatus <CertificateStatus
orgId={org?.org.orgId || ""} orgId={
domainId={loginPage.domainId} org?.org.orgId || ""
fullDomain={ }
loginPage.fullDomain domainId={
} loginPage.domainId
showLabel={true} }
polling={true} fullDomain={
/> loginPage.fullDomain
)} }
</div> showLabel={true}
polling={true}
/>
)}
</div>
</SettingsFormCell>
</SettingsFormGrid>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
<div className="flex justify-end mt-6"> <SettingsSectionFooter>
<Button <Button
type="submit" type="submit"
form="auth-page-settings-form" form="auth-page-settings-form"
@@ -435,7 +460,7 @@ function AuthPageSettings({
> >
{t("saveAuthPageDomain")} {t("saveAuthPageDomain")}
</Button> </Button>
</div> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
{/* Domain Picker Modal */} {/* Domain Picker Modal */}