apply auth branding to resource auth page

This commit is contained in:
Fred KISSIE
2025-11-13 03:24:47 +01:00
parent 228481444f
commit 4beed9d464
5 changed files with 99 additions and 25 deletions

View File

@@ -38,8 +38,8 @@ const paramsSchema = z
const bodySchema = z const bodySchema = z
.object({ .object({
logoUrl: z.string().url(), logoUrl: z.string().url(),
logoWidth: z.number().min(1), logoWidth: z.coerce.number().min(1),
logoHeight: z.number().min(1), logoHeight: z.coerce.number().min(1),
title: z.string(), title: z.string(),
subtitle: z.string().optional(), subtitle: z.string().optional(),
resourceTitle: z.string(), resourceTitle: z.string(),

View File

@@ -19,7 +19,10 @@ import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
import AutoLoginHandler from "@app/components/AutoLoginHandler"; import AutoLoginHandler from "@app/components/AutoLoginHandler";
import { build } from "@server/build"; import { build } from "@server/build";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { GetLoginPageResponse } from "@server/routers/loginPage/types"; import {
GetLoginPageBrandingResponse,
GetLoginPageResponse
} from "@server/routers/loginPage/types";
import { GetOrgTierResponse } from "@server/routers/billing/types"; import { GetOrgTierResponse } from "@server/routers/billing/types";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { CheckOrgUserAccessResponse } from "@server/routers/org"; import { CheckOrgUserAccessResponse } from "@server/routers/org";
@@ -261,6 +264,23 @@ export default async function ResourceAuthPage(props: {
} }
} }
let loginPageBranding: Omit<
GetLoginPageBrandingResponse,
"loginPageBrandingId"
> | null = null;
try {
const res = await internal.get<
AxiosResponse<GetLoginPageBrandingResponse>
>(
`/org/${authInfo.orgId}/login-page-branding`,
await authCookieHeader()
);
if (res.status === 200) {
const { loginPageBrandingId, ...rest } = res.data.data;
loginPageBranding = rest;
}
} catch (error) {}
return ( return (
<> <>
{userIsUnauthorized && isSSOOnly ? ( {userIsUnauthorized && isSSOOnly ? (
@@ -283,6 +303,7 @@ export default async function ResourceAuthPage(props: {
redirect={redirectUrl} redirect={redirectUrl}
idps={loginIdps} idps={loginIdps}
orgId={build === "saas" ? authInfo.orgId : undefined} orgId={build === "saas" ? authInfo.orgId : undefined}
branding={loginPageBranding}
/> />
</div> </div>
)} )}

View File

@@ -69,8 +69,8 @@ const AuthPageFormSchema = z.object({
message: "Invalid logo URL, must be a valid image URL" message: "Invalid logo URL, must be a valid image URL"
} }
), ),
logoWidth: z.number().min(1), logoWidth: z.coerce.number().min(1),
logoHeight: z.number().min(1), logoHeight: z.coerce.number().min(1),
title: z.string(), title: z.string(),
subtitle: z.string().optional(), subtitle: z.string().optional(),
resourceTitle: z.string(), resourceTitle: z.string(),
@@ -102,8 +102,8 @@ export default function AuthPageBrandingForm({
resolver: zodResolver(AuthPageFormSchema), resolver: zodResolver(AuthPageFormSchema),
defaultValues: { defaultValues: {
logoUrl: branding?.logoUrl ?? "", logoUrl: branding?.logoUrl ?? "",
logoWidth: branding?.logoWidth ?? 500, logoWidth: branding?.logoWidth ?? 100,
logoHeight: branding?.logoHeight ?? 500, logoHeight: branding?.logoHeight ?? 100,
title: branding?.title ?? `Log in to {{orgName}}`, title: branding?.title ?? `Log in to {{orgName}}`,
subtitle: branding?.subtitle ?? `Log in to {{orgName}}`, subtitle: branding?.subtitle ?? `Log in to {{orgName}}`,
resourceTitle: resourceTitle:
@@ -240,7 +240,7 @@ export default function AuthPageBrandingForm({
<FormField <FormField
control={form.control} control={form.control}
name="logoWidth" name="logoHeight"
render={({ field }) => ( render={({ field }) => (
<FormItem className="grow"> <FormItem className="grow">
<FormLabel> <FormLabel>

View File

@@ -7,6 +7,7 @@ import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type BrandingLogoProps = { type BrandingLogoProps = {
logoPath?: string;
width: number; width: number;
height: number; height: number;
}; };
@@ -38,16 +39,17 @@ export default function BrandingLogo(props: BrandingLogoProps) {
if (isUnlocked() && env.branding.logo?.darkPath) { if (isUnlocked() && env.branding.logo?.darkPath) {
return env.branding.logo.darkPath; return env.branding.logo.darkPath;
} }
return "/logo/word_mark_white.png"; return "/logo/word_mark_white.png";
} }
const path = getPath(); setPath(props.logoPath ?? getPath());
setPath(path); }, [theme, env, props.logoPath]);
}, [theme, env]);
const Component = props.logoPath ? "img" : Image;
return ( return (
path && ( path && (
<Image <Component
src={path} src={path}
alt="Logo" alt="Logo"
width={props.width} width={props.width}

View File

@@ -23,7 +23,7 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@/components/ui/form"; } from "@/components/ui/form";
import { LockIcon, Binary, Key, User, Send, AtSign } from "lucide-react"; import { LockIcon, Binary, Key, User, Send, AtSign, Regex } from "lucide-react";
import { import {
InputOTP, InputOTP,
InputOTPGroup, InputOTPGroup,
@@ -88,6 +88,15 @@ type ResourceAuthPortalProps = {
redirect: string; redirect: string;
idps?: LoginFormIDP[]; idps?: LoginFormIDP[];
orgId?: string; orgId?: string;
branding?: {
title: string;
logoUrl: string;
logoWidth: number;
logoHeight: number;
subtitle: string | null;
resourceTitle: string;
resourceSubtitle: string | null;
} | null;
}; };
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
@@ -104,7 +113,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
return colLength; return colLength;
}; };
const [numMethods, setNumMethods] = useState(getNumMethods()); const [numMethods] = useState(() => getNumMethods());
const [passwordError, setPasswordError] = useState<string | null>(null); const [passwordError, setPasswordError] = useState<string | null>(null);
const [pincodeError, setPincodeError] = useState<string | null>(null); const [pincodeError, setPincodeError] = useState<string | null>(null);
@@ -309,13 +318,37 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
} }
} }
function getTitle() { function replacePlaceholder(
stringWithPlaceholder: string,
data: Record<string, string>
) {
let newString = stringWithPlaceholder;
const keys = Object.keys(data);
for (const key of keys) {
newString = newString.replace(
new RegExp(`{{${key}}}`, "gm"),
data[key]
);
}
return newString;
}
function getTitle(resourceName: string) {
if ( if (
isUnlocked() && isUnlocked() &&
build !== "oss" && build !== "oss" &&
env.branding.resourceAuthPage?.titleText (!!env.branding.resourceAuthPage?.titleText ||
!!props.branding?.resourceTitle)
) { ) {
return env.branding.resourceAuthPage.titleText; if (props.branding?.resourceTitle) {
return replacePlaceholder(props.branding?.resourceTitle, {
resourceName
});
}
return env.branding.resourceAuthPage?.titleText;
} }
return t("authenticationRequired"); return t("authenticationRequired");
} }
@@ -324,10 +357,16 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
if ( if (
isUnlocked() && isUnlocked() &&
build !== "oss" && build !== "oss" &&
env.branding.resourceAuthPage?.subtitleText (env.branding.resourceAuthPage?.subtitleText ||
props.branding?.resourceSubtitle)
) { ) {
return env.branding.resourceAuthPage.subtitleText if (props.branding?.resourceSubtitle) {
.split("{{resourceName}}") return replacePlaceholder(props.branding?.resourceSubtitle, {
resourceName
});
}
return env.branding.resourceAuthPage?.subtitleText
?.split("{{resourceName}}")
.join(resourceName); .join(resourceName);
} }
return numMethods > 1 return numMethods > 1
@@ -335,8 +374,16 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
: t("authenticationRequest", { name: resourceName }); : t("authenticationRequest", { name: resourceName });
} }
const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 100 : 100; const logoWidth = isUnlocked()
const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 100 : 100; ? (props.branding?.logoWidth ??
env.branding.logo?.authPage?.width ??
100)
: 100;
const logoHeight = isUnlocked()
? (props.branding?.logoHeight ??
env.branding.logo?.authPage?.height ??
100)
: 100;
return ( return (
<div> <div>
@@ -377,15 +424,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
<CardHeader> <CardHeader>
{isUnlocked() && {isUnlocked() &&
build !== "oss" && build !== "oss" &&
env.branding?.resourceAuthPage?.showLogo && ( (env.branding?.resourceAuthPage?.showLogo ||
props.branding) && (
<div className="flex flex-row items-center justify-center mb-3"> <div className="flex flex-row items-center justify-center mb-3">
<BrandingLogo <BrandingLogo
height={logoHeight} height={logoHeight}
width={logoWidth} width={logoWidth}
logoPath={props.branding?.logoUrl}
/> />
</div> </div>
)} )}
<CardTitle>{getTitle()}</CardTitle> <CardTitle>
{getTitle(props.resource.name)}
</CardTitle>
<CardDescription> <CardDescription>
{getSubtitle(props.resource.name)} {getSubtitle(props.resource.name)}
</CardDescription> </CardDescription>