mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 02:36:38 +00:00
✨ apply auth branding to resource auth page
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user