mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-03 01:06:39 +00:00
✨apply branding to org auth page
This commit is contained in:
@@ -13,16 +13,8 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { db, loginPageBranding, loginPageBrandingOrg, orgs } from "@server/db";
|
||||||
db,
|
import { eq, and } from "drizzle-orm";
|
||||||
idpOrg,
|
|
||||||
loginPage,
|
|
||||||
loginPageBranding,
|
|
||||||
loginPageBrandingOrg,
|
|
||||||
loginPageOrg,
|
|
||||||
resources
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, and, type InferSelectModel } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -31,37 +23,15 @@ import { fromError } from "zod-validation-error";
|
|||||||
import type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types";
|
import type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
resourceId: z.coerce.number().int().positive().optional(),
|
orgId: z.string().min(1)
|
||||||
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) {
|
async function query(orgId: string) {
|
||||||
let orgLink: InferSelectModel<typeof loginPageBrandingOrg> | null = null;
|
const [orgLink] = await db
|
||||||
if (orgId !== undefined) {
|
.select()
|
||||||
[orgLink] = await db
|
.from(loginPageBrandingOrg)
|
||||||
.select()
|
.where(eq(loginPageBrandingOrg.orgId, orgId))
|
||||||
.from(loginPageBrandingOrg)
|
.innerJoin(orgs, eq(loginPageBrandingOrg.orgId, orgs.orgId));
|
||||||
.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) {
|
if (!orgLink) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -73,14 +43,15 @@ async function query(orgId?: string, fullDomain?: string) {
|
|||||||
and(
|
and(
|
||||||
eq(
|
eq(
|
||||||
loginPageBranding.loginPageBrandingId,
|
loginPageBranding.loginPageBrandingId,
|
||||||
orgLink.loginPageBrandingId
|
orgLink.loginPageBrandingOrg.loginPageBrandingId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
orgId: orgLink.orgId
|
orgId: orgLink.orgs.orgId,
|
||||||
|
orgName: orgLink.orgs.name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,41 +71,9 @@ export async function loadLoginPageBranding(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resourceId, idpId, fullDomain } = parsedQuery.data;
|
const { orgId } = parsedQuery.data;
|
||||||
|
|
||||||
let orgId: string | undefined = undefined;
|
const branding = await query(orgId);
|
||||||
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) {
|
if (!branding) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type LoadLoginPageResponse = LoginPage & { orgId: string };
|
|||||||
|
|
||||||
export type LoadLoginPageBrandingResponse = LoginPageBranding & {
|
export type LoadLoginPageBrandingResponse = LoginPageBranding & {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
orgName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetLoginPageBrandingResponse = LoginPageBranding;
|
export type GetLoginPageBrandingResponse = LoginPageBranding;
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { LoginFormIDP } from "@app/components/LoginForm";
|
|||||||
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
|
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
|
import {
|
||||||
|
LoadLoginPageBrandingResponse,
|
||||||
|
LoadLoginPageResponse
|
||||||
|
} from "@server/routers/loginPage/types";
|
||||||
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
|
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -26,6 +29,7 @@ import ValidateSessionTransferToken from "@app/components/private/ValidateSessio
|
|||||||
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 { getCachedSubscription } from "@app/lib/api/getCachedSubscription";
|
import { getCachedSubscription } from "@app/lib/api/getCachedSubscription";
|
||||||
|
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -33,7 +37,6 @@ export default async function OrgAuthPage(props: {
|
|||||||
params: Promise<{}>;
|
params: Promise<{}>;
|
||||||
searchParams: Promise<{ token?: string }>;
|
searchParams: Promise<{ token?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
const env = pullEnv();
|
const env = pullEnv();
|
||||||
@@ -122,12 +125,10 @@ export default async function OrgAuthPage(props: {
|
|||||||
|
|
||||||
let loginIdps: LoginFormIDP[] = [];
|
let loginIdps: LoginFormIDP[] = [];
|
||||||
if (build === "saas") {
|
if (build === "saas") {
|
||||||
const idpsRes = await cache(
|
const idpsRes = await priv.get<AxiosResponse<ListOrgIdpsResponse>>(
|
||||||
async () =>
|
`/org/${loginPage.orgId}/idp`
|
||||||
await priv.get<AxiosResponse<ListOrgIdpsResponse>>(
|
);
|
||||||
`/org/${loginPage!.orgId}/idp`
|
|
||||||
)
|
|
||||||
)();
|
|
||||||
loginIdps = idpsRes.data.data.idps.map((idp) => ({
|
loginIdps = idpsRes.data.data.idps.map((idp) => ({
|
||||||
idpId: idp.idpId,
|
idpId: idp.idpId,
|
||||||
name: idp.name,
|
name: idp.name,
|
||||||
@@ -135,6 +136,16 @@ export default async function OrgAuthPage(props: {
|
|||||||
})) as LoginFormIDP[];
|
})) as LoginFormIDP[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let branding: LoadLoginPageBrandingResponse | null = null;
|
||||||
|
try {
|
||||||
|
const res = await priv.get<
|
||||||
|
AxiosResponse<LoadLoginPageBrandingResponse>
|
||||||
|
>(`/login-page-branding?orgId=${loginPage.orgId}`);
|
||||||
|
if (res.status === 200) {
|
||||||
|
branding = res.data.data;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
@@ -152,11 +163,30 @@ export default async function OrgAuthPage(props: {
|
|||||||
</div>
|
</div>
|
||||||
<Card className="shadow-md w-full max-w-md">
|
<Card className="shadow-md w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("orgAuthSignInTitle")}</CardTitle>
|
{branding?.logoUrl && (
|
||||||
|
<div className="flex flex-row items-center justify-center mb-3">
|
||||||
|
<img
|
||||||
|
src={branding.logoUrl}
|
||||||
|
height={branding.logoHeight}
|
||||||
|
width={branding.logoWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CardTitle>
|
||||||
|
{branding?.orgTitle
|
||||||
|
? replacePlaceholder(branding.orgTitle, {
|
||||||
|
orgName: branding.orgName
|
||||||
|
})
|
||||||
|
: t("orgAuthSignInTitle")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{loginIdps.length > 0
|
{branding?.orgSubtitle
|
||||||
? t("orgAuthChooseIdpDescription")
|
? replacePlaceholder(branding.orgSubtitle, {
|
||||||
: ""}
|
orgName: branding.orgName
|
||||||
|
})
|
||||||
|
: loginIdps.length > 0
|
||||||
|
? t("orgAuthChooseIdpDescription")
|
||||||
|
: ""}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types";
|
import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types";
|
||||||
|
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
||||||
|
|
||||||
const pinSchema = z.object({
|
const pinSchema = z.object({
|
||||||
pin: z
|
pin: z
|
||||||
@@ -329,24 +330,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function getTitle(resourceName: string) {
|
||||||
if (
|
if (
|
||||||
build !== "oss" &&
|
build !== "oss" &&
|
||||||
@@ -399,7 +382,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
// @ts-expect-error
|
// @ts-expect-error CSS variable
|
||||||
"--primary": isUnlocked() ? props.branding?.primaryColor : null
|
"--primary": isUnlocked() ? props.branding?.primaryColor : null
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
17
src/lib/replacePlaceholder.ts
Normal file
17
src/lib/replacePlaceholder.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user