From 29a52f6ac4da98c018c57cac780ea7c02fe7e4f4 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 05:43:17 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Apply=20branding=20to=20auth=20p?= =?UTF-8?q?age=20when=20not=20authenticated=20not=20only=20when=20authed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/private/routers/internal.ts | 1 + server/private/routers/loginPage/index.ts | 1 + .../loginPage/loadLoginPageBranding.ts | 161 ++++++++++++++++++ server/routers/loginPage/types.ts | 4 + src/app/auth/resource/[resourceGuid]/page.tsx | 42 +++-- src/components/ResourceAuthPortal.tsx | 11 +- 6 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 server/private/routers/loginPage/loadLoginPageBranding.ts diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts index b393b884..49596a1f 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -28,6 +28,7 @@ internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps); internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier); internalRouter.get("/login-page", loginPage.loadLoginPage); +internalRouter.get("/login-page-branding", loginPage.loadLoginPageBranding); internalRouter.post( "/get-session-transfer-token", diff --git a/server/private/routers/loginPage/index.ts b/server/private/routers/loginPage/index.ts index d3ae6540..1bfe6e16 100644 --- a/server/private/routers/loginPage/index.ts +++ b/server/private/routers/loginPage/index.ts @@ -20,3 +20,4 @@ export * from "./deleteLoginPage"; export * from "./upsertLoginPageBranding"; export * from "./deleteLoginPageBranding"; export * from "./getLoginPageBranding"; +export * from "./loadLoginPageBranding"; diff --git a/server/private/routers/loginPage/loadLoginPageBranding.ts b/server/private/routers/loginPage/loadLoginPageBranding.ts new file mode 100644 index 00000000..94632639 --- /dev/null +++ b/server/private/routers/loginPage/loadLoginPageBranding.ts @@ -0,0 +1,161 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + idpOrg, + loginPage, + loginPageBranding, + loginPageBrandingOrg, + loginPageOrg, + resources +} from "@server/db"; +import { eq, and, type InferSelectModel } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types"; + +const querySchema = z.object({ + resourceId: z.coerce.number().int().positive().optional(), + idpId: z.coerce.number().int().positive().optional(), + orgId: z.string().min(1).optional(), + fullDomain: z.string().min(1).optional() +}); + +async function query(orgId?: string, fullDomain?: string) { + let orgLink: InferSelectModel | null = null; + if (orgId !== undefined) { + [orgLink] = await db + .select() + .from(loginPageBrandingOrg) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + } else if (fullDomain) { + const [res] = await db + .select() + .from(loginPage) + .where(eq(loginPage.fullDomain, fullDomain)) + .innerJoin( + loginPageOrg, + eq(loginPage.loginPageId, loginPageOrg.loginPageId) + ) + .innerJoin( + loginPageBrandingOrg, + eq(loginPageBrandingOrg.orgId, loginPageOrg.orgId) + ) + .limit(1); + + orgLink = res.loginPageBrandingOrg; + } + + if (!orgLink) { + return null; + } + + const [res] = await db + .select() + .from(loginPageBranding) + .where( + and( + eq( + loginPageBranding.loginPageBrandingId, + orgLink.loginPageBrandingId + ) + ) + ) + .limit(1); + return { + ...res, + orgId: orgLink.orgId + }; +} + +export async function loadLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { resourceId, idpId, fullDomain } = parsedQuery.data; + + let orgId: string | undefined = undefined; + if (resourceId) { + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource not found") + ); + } + + orgId = resource.orgId; + } else if (idpId) { + const [idpOrgLink] = await db + .select() + .from(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + + if (!idpOrgLink) { + return next( + createHttpError(HttpCode.NOT_FOUND, "IdP not found") + ); + } + + orgId = idpOrgLink.orgId; + } else if (parsedQuery.data.orgId) { + orgId = parsedQuery.data.orgId; + } + + const branding = await query(orgId, fullDomain); + + if (!branding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Branding for Login page not found" + ) + ); + } + + return response(res, { + data: branding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index 652ed4e9..6ef9ca81 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -10,4 +10,8 @@ export type UpdateLoginPageResponse = LoginPage; export type LoadLoginPageResponse = LoginPage & { orgId: string }; +export type LoadLoginPageBrandingResponse = LoginPageBranding & { + orgId: string; +}; + export type GetLoginPageBrandingResponse = LoginPageBranding; diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index 32b70298..9dcba710 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -19,9 +19,9 @@ import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import AutoLoginHandler from "@app/components/AutoLoginHandler"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { - GetLoginPageBrandingResponse, - GetLoginPageResponse +import type { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse } from "@server/routers/loginPage/types"; import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; @@ -93,9 +93,9 @@ export default async function ResourceAuthPage(props: { redirect(env.app.dashboardUrl); } - let loginPage: GetLoginPageResponse | undefined; + let loginPage: LoadLoginPageResponse | undefined; try { - const res = await priv.get>( + const res = await priv.get>( `/login-page?resourceId=${authInfo.resourceId}&fullDomain=${host}` ); @@ -110,6 +110,7 @@ export default async function ResourceAuthPage(props: { } let redirectUrl = authInfo.url; + if (searchParams.redirect) { try { const serverResourceHost = new URL(authInfo.url).host; @@ -261,20 +262,13 @@ export default async function ResourceAuthPage(props: { } } - let loginPageBranding: Omit< - GetLoginPageBrandingResponse, - "loginPageBrandingId" - > | null = null; + let branding: LoadLoginPageBrandingResponse | null = null; try { - const res = await internal.get< - AxiosResponse - >( - `/org/${authInfo.orgId}/login-page-branding`, - await authCookieHeader() - ); + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${authInfo.orgId}`); if (res.status === 200) { - const { loginPageBrandingId, ...rest } = res.data.data; - loginPageBranding = rest; + branding = res.data.data; } } catch (error) {} @@ -300,7 +294,19 @@ export default async function ResourceAuthPage(props: { redirect={redirectUrl} idps={loginIdps} orgId={build === "saas" ? authInfo.orgId : undefined} - branding={loginPageBranding} + branding={ + !branding || build === "oss" + ? undefined + : { + logoHeight: branding.logoHeight, + logoUrl: branding.logoUrl, + logoWidth: branding.logoWidth, + primaryColor: branding.primaryColor, + resourceTitle: branding.resourceTitle, + resourceSubtitle: + branding.resourceSubtitle + } + } /> )} diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 56acc967..66be584b 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -87,7 +87,14 @@ type ResourceAuthPortalProps = { redirect: string; idps?: LoginFormIDP[]; orgId?: string; - branding?: Omit | null; + branding?: { + logoUrl: string; + logoWidth: number; + logoHeight: number; + primaryColor: string | null; + resourceTitle: string; + resourceSubtitle: string | null; + }; }; /** @@ -342,8 +349,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { function getTitle(resourceName: string) { if ( - isUnlocked() && build !== "oss" && + isUnlocked() && (!!env.branding.resourceAuthPage?.titleText || !!props.branding?.resourceTitle) ) {