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

This commit is contained in:
Owen committed 2026-10-16 12:24:33 -04:00
commit 62a82aa4e2
4 files changed
+1317 -1165

No files matched your search

+77 -60
View File
@@ -36,7 +36,9 @@ import {
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
SettingsSectionFooter,
SettingsFormGrid,
SettingsFormCell
} from "@app/components/Settings";
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl";
@@ -242,7 +244,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<SettingsSectionForm variant="half">
<Form {...form}>
<form
onSubmit={(e) => {
@@ -251,71 +253,86 @@ function GeneralSectionForm({ org }: SectionFormProps) {
formAction();
});
}}
className="grid gap-4"
id="org-general-settings-form"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{t("orgDisplayName")}
</FormDescription>
</FormItem>
)}
/>
<SettingsFormGrid>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("name")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
{t("orgDisplayName")}
</FormDescription>
</FormItem>
)}
/>
</SettingsFormCell>
<PaidFeaturesAlert
tiers={tierMatrix.newtAutoUpdate}
/>
{!env.flags.disableEnterpriseFeatures && (
<FormField
control={form.control}
name="settingsEnableGlobalNewtAutoUpdate"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="settings-enable-global-newt-auto-update"
label={t("newtAutoUpdate")}
checked={field.value}
onCheckedChange={
field.onChange
}
disabled={
!hasAutoUpdateFeature
}
/>
</FormControl>
<FormDescription>
{t("newtAutoUpdateDescription")}{" "}
<a
href="https://docs.pangolin.net/manage/sites/auto-update"
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 span="full">
<PaidFeaturesAlert
tiers={tierMatrix.newtAutoUpdate}
/>
</SettingsFormCell>
{!env.flags.disableEnterpriseFeatures && (
<SettingsFormCell span="full">
<FormField
control={form.control}
name="settingsEnableGlobalNewtAutoUpdate"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="settings-enable-global-newt-auto-update"
label={t(
"newtAutoUpdate"
)}
checked={
field.value
}
onCheckedChange={
field.onChange
}
disabled={
!hasAutoUpdateFeature
}
/>
</FormControl>
<FormDescription>
{t(
"newtAutoUpdateDescription"
)}{" "}
<a
href="https://docs.pangolin.net/manage/sites/auto-update"
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>
</SettingsSectionForm>
</SettingsSectionBody>
<div className="flex justify-end gap-2 mt-4">
<SettingsSectionFooter>
<Button
type="submit"
form="org-general-settings-form"
@@ -324,7 +341,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
>
{t("saveSettings")}
</Button>
</div>
</SettingsSectionFooter>
</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 z from "zod";
import {
SettingsFormCell,
SettingsFormGrid,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
SettingsSectionTitle,
SettingsSubsectionDescription,
SettingsSubsectionHeader,
SettingsSubsectionTitle
} from "./Settings";
import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -176,89 +182,228 @@ export default function AuthPageBrandingForm({
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<PaidFeaturesAlert
tiers={tierMatrix.loginPageBranding}
/>
<SettingsSectionForm variant="half">
<Form {...form}>
<form
action={updateFormAction}
id="auth-page-branding-form"
className="flex flex-col space-y-4 items-stretch"
>
<FormField
control={form.control}
name="primaryColor"
render={({ field }) => (
<FormItem className="">
<FormLabel>
{t("brandingPrimaryColor")}
</FormLabel>
<SettingsFormGrid>
<SettingsFormCell span="full">
<PaidFeaturesAlert
tiers={tierMatrix.loginPageBranding}
/>
</SettingsFormCell>
<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>
)}
/>
<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">
<SettingsFormCell span="half">
<FormField
control={form.control}
name="logoWidth"
name="primaryColor"
render={({ field }) => (
<FormItem className="grow">
<FormItem>
<FormLabel>
{t("brandingLogoWidth")}
{t(
"brandingPrimaryColor"
)}
</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>
<Input {...field} />
</FormControl>
@@ -266,19 +411,16 @@ export default function AuthPageBrandingForm({
</FormItem>
)}
/>
<span className="relative top-8">
<XIcon className="text-muted-foreground size-4" />
</span>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="logoHeight"
name="resourceSubtitle"
render={({ field }) => (
<FormItem className="grow">
<FormItem>
<FormLabel>
{t(
"brandingLogoHeight"
"brandingResourceSubtitle"
)}
</FormLabel>
<FormControl>
@@ -288,116 +430,14 @@ export default function AuthPageBrandingForm({
</FormItem>
)}
/>
</div>
</div>
{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>
</SettingsFormCell>
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<div className="flex justify-end gap-2 mt-6 items-center">
<SettingsSectionFooter>
{branding && (
<form action={deleteFormAction}>
<Button
@@ -427,7 +467,7 @@ export default function AuthPageBrandingForm({
>
{t("saveAuthPageBranding")}
</Button>
</div>
</SettingsSectionFooter>
</SettingsSection>
</>
);
+150 -125
View File
@@ -20,7 +20,10 @@ import {
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm
SettingsSectionForm,
SettingsSectionFooter,
SettingsFormGrid,
SettingsFormCell
} from "@app/components/Settings";
import { useTranslations } from "next-intl";
import { GetLoginPageResponse } from "@server/routers/loginPage/types";
@@ -284,145 +287,167 @@ function AuthPageSettings({
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<PaidFeaturesAlert tiers={tierMatrix.loginPageDomain} />
<SettingsSectionForm variant="half">
<Form {...form}>
<form
action={formAction}
className="space-y-4"
id="auth-page-settings-form"
>
<div className="space-y-3">
<Label>{t("authPageDomain")}</Label>
<div className="border p-2 rounded-md flex items-center justify-between">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<Globe size="14" />
{loginPage &&
!loginPage.domainId ? (
<InfoPopup
info={t(
"domainNotFoundDescription"
<SettingsFormGrid>
<SettingsFormCell span="full">
<PaidFeaturesAlert
tiers={tierMatrix.loginPageDomain}
/>
</SettingsFormCell>
<SettingsFormCell span="full">
<div className="space-y-3">
<Label>{t("authPageDomain")}</Label>
<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")}
/>
) : 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(
</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"
);
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
)
}
>
<Trash2 size="14" />
</Button>
)}
</div>
</div>
? t("changeDomain")
: t("selectDomain")}
</Button>
{form.watch(
"authPageDomainId"
) && (
<Button
variant="destructive"
type="button"
size="sm"
onClick={
clearAuthPageDomain
}
disabled={
!isPaidUser(
tierMatrix.loginPageDomain
)
}
>
<Trash2 size="14" />
</Button>
)}
</div>
</div>
{!form.watch("authPageDomainId") && (
<div className="text-sm text-muted-foreground">
{t(
"addDomainToEnableCustomAuthPages"
{!form.watch(
"authPageDomainId"
) && (
<div className="text-sm text-muted-foreground">
{t(
"addDomainToEnableCustomAuthPages"
)}
</div>
)}
</div>
)}
{(build === "enterprise" ||
!isPaidUser(
tierMatrix.loginPageDomain
)) &&
loginPage?.domainId &&
loginPage?.fullDomain &&
!hasUnsavedChanges && (
<CertificateStatus
orgId={org?.org.orgId || ""}
domainId={loginPage.domainId}
fullDomain={
loginPage.fullDomain
}
showLabel={true}
polling={true}
/>
)}
</div>
{(build === "enterprise" ||
!isPaidUser(
tierMatrix.loginPageDomain
)) &&
loginPage?.domainId &&
loginPage?.fullDomain &&
!hasUnsavedChanges && (
<CertificateStatus
orgId={
org?.org.orgId || ""
}
domainId={
loginPage.domainId
}
fullDomain={
loginPage.fullDomain
}
showLabel={true}
polling={true}
/>
)}
</div>
</SettingsFormCell>
</SettingsFormGrid>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<div className="flex justify-end mt-6">
<SettingsSectionFooter>
<Button
type="submit"
form="auth-page-settings-form"
@@ -435,7 +460,7 @@ function AuthPageSettings({
>
{t("saveAuthPageDomain")}
</Button>
</div>
</SettingsSectionFooter>
</SettingsSection>
{/* Domain Picker Modal */}