diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index d4b708fbf..721fd037b 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -22,7 +22,8 @@ import { CardTitle, CardDescription } from "@app/components/ui/card"; -import Link from "next/link"; +import BrandedAuthSurface from "@app/components/BrandedAuthSurface"; +import PoweredByPangolin from "@app/components/PoweredByPangolin"; declare module "react" { namespace JSX { @@ -60,10 +61,12 @@ const isIronError = (error: unknown): error is IronError => { export default function RdpClient({ target, - error + error, + primaryColor }: { target: GetBrowserTargetResponse | null; error: string | null; + primaryColor?: string | null; }) { const STORAGE_KEY = "pangolin_rdp_credentials"; @@ -315,20 +318,8 @@ export default function RdpClient({ if (error) { return ( -
-
- - Powered by{" "} - - Pangolin - - -
+ + RDP @@ -337,27 +328,15 @@ export default function RdpClient({

{error}

-
+ ); } return ( <> {showLogin && ( -
-
- - Powered by{" "} - - Pangolin - - -
+ + Sign in to Remote Desktop @@ -441,7 +420,7 @@ export default function RdpClient({
- + )}
- +
diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index 4ba1c9211..945963ec0 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -20,6 +20,8 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import type { SignSshKeyResponse } from "@server/routers/ssh/types"; import { useTranslations } from "next-intl"; +import BrandedAuthSurface from "@app/components/BrandedAuthSurface"; +import PoweredByPangolin from "@app/components/PoweredByPangolin"; type AuthTab = "password" | "privateKey"; @@ -40,12 +42,14 @@ export default function SshClient({ target, error, signedKeyData, - privateKey: signedPrivateKey + privateKey: signedPrivateKey, + primaryColor }: { target: GetBrowserTargetResponse | null; error: string | null; signedKeyData?: SignSshKeyResponse | null; privateKey?: string | null; + primaryColor?: string | null; }) { const STORAGE_KEY = "pangolin_ssh_credentials"; @@ -377,20 +381,8 @@ export default function SshClient({ if (error) { return ( -
-
- - {t("sshPoweredBy")}{" "} - - Pangolin - - -
+ + {t("sshTitle")} @@ -399,27 +391,15 @@ export default function SshClient({

{error}

-
+ ); } return ( <> {!connected && ( -
-
- - {t("sshPoweredBy")}{" "} - - Pangolin - - -
+ + {t("sshSignInTitle")} @@ -496,10 +476,10 @@ export default function SshClient({ href="https://docs.pangolin.net/" target="_blank" rel="noopener noreferrer" - className="underline inline-flex items-center gap-1" + className="text-primary hover:underline inline-flex items-center gap-1" > {t("sshLearnMore")} - +

-
+ )} {connected && ( diff --git a/src/app/ssh/page.tsx b/src/app/ssh/page.tsx index 44d5f1201..5e2e057b0 100644 --- a/src/app/ssh/page.tsx +++ b/src/app/ssh/page.tsx @@ -2,6 +2,7 @@ import { headers } from "next/headers"; import { priv } from "@app/lib/api"; import { generateBrowserGatewayMetadata } from "@app/lib/browserGatewayMetadata"; import { getBrowserTargetForRequest } from "@app/lib/getBrowserTargetForRequest"; +import { loadOrgLoginPageBranding } from "@app/lib/loadOrgLoginPageBranding"; import { AxiosResponse } from "axios"; import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget"; import SshClient from "./SshClient"; @@ -154,6 +155,10 @@ export default async function SshPage() { } } + const { primaryColor } = target + ? await loadOrgLoginPageBranding(target.orgId) + : { primaryColor: null }; + return (
@@ -163,6 +168,7 @@ export default async function SshPage() { error={error} signedKeyData={signedKeyData} privateKey={privateKey} + primaryColor={primaryColor} />
diff --git a/src/app/vnc/VncClient.tsx b/src/app/vnc/VncClient.tsx index 03857169e..7a93537fd 100644 --- a/src/app/vnc/VncClient.tsx +++ b/src/app/vnc/VncClient.tsx @@ -13,7 +13,8 @@ import { CardTitle, CardDescription } from "@app/components/ui/card"; -import Link from "next/link"; +import BrandedAuthSurface from "@app/components/BrandedAuthSurface"; +import PoweredByPangolin from "@app/components/PoweredByPangolin"; type FormState = { password: string; @@ -21,10 +22,12 @@ type FormState = { export default function VncClient({ target, - error + error, + primaryColor }: { target: GetBrowserTargetResponse | null; error: string | null; + primaryColor?: string | null; }) { const STORAGE_KEY = "pangolin_vnc_credentials"; @@ -152,20 +155,8 @@ export default function VncClient({ if (error) { return ( -
-
- - Powered by{" "} - - Pangolin - - -
+ + VNC @@ -174,27 +165,15 @@ export default function VncClient({

{error}

-
+ ); } return ( <> {!connected && ( -
-
- - Powered by{" "} - - Pangolin - - -
+ + VNC @@ -224,7 +203,7 @@ export default function VncClient({
-
+ )}
- +
diff --git a/src/components/BrandedAuthSurface.tsx b/src/components/BrandedAuthSurface.tsx new file mode 100644 index 000000000..2b12808aa --- /dev/null +++ b/src/components/BrandedAuthSurface.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; + +type BrandedAuthSurfaceProps = { + primaryColor?: string | null; + children: React.ReactNode; +}; + +export default function BrandedAuthSurface({ + primaryColor, + children +}: BrandedAuthSurfaceProps) { + const { isUnlocked } = useLicenseStatusContext(); + + return ( +
+ {children} +
+ ); +} diff --git a/src/components/OrgLoginPage.tsx b/src/components/OrgLoginPage.tsx index 3270b7cb4..d70c278cf 100644 --- a/src/components/OrgLoginPage.tsx +++ b/src/components/OrgLoginPage.tsx @@ -14,9 +14,10 @@ import { import { Button } from "@app/components/ui/button"; import Link from "next/link"; import { replacePlaceholder } from "@app/lib/replacePlaceholder"; +import PoweredByPangolin from "@app/components/PoweredByPangolin"; +import BrandedAuthSurface from "@app/components/BrandedAuthSurface"; import { getTranslations } from "next-intl/server"; import { pullEnv } from "@app/lib/pullEnv"; -import { build } from "@server/build"; type OrgLoginPageProps = { loginPage: LoadLoginPageResponse | undefined; @@ -52,22 +53,8 @@ export default async function OrgLoginPage({ const env = pullEnv(); const t = await getTranslations(); return ( -
- {build !== "enterprise" || !env.branding.hidePoweredBy ? ( -
- - {t("poweredBy")}{" "} - - {env.branding.appName || "Pangolin"} - - -
- ) : null} + + {branding?.logoUrl && ( @@ -127,6 +114,6 @@ export default async function OrgLoginPage({ {t("loginBack")}

-
+ ); } diff --git a/src/components/PoweredByPangolin.tsx b/src/components/PoweredByPangolin.tsx new file mode 100644 index 000000000..cca479a4f --- /dev/null +++ b/src/components/PoweredByPangolin.tsx @@ -0,0 +1,53 @@ +"use client"; + +import Link from "next/link"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useTranslations } from "next-intl"; +import { build } from "@server/build"; + +function PoweredByLabel({ brandName }: { brandName: string }) { + const t = useTranslations(); + + return ( +
+ + {t("poweredBy")}{" "} + {brandName === "Pangolin" ? ( + + Pangolin + + ) : ( + brandName + )} + +
+ ); +} + +export default function PoweredByPangolin() { + const { env } = useEnvContext(); + const { isUnlocked } = useLicenseStatusContext(); + + if (isUnlocked() && build === "enterprise") { + if ( + env.branding.resourceAuthPage?.hidePoweredBy || + env.branding.hidePoweredBy + ) { + return null; + } + + return ( + + ); + } + + return ; +} diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 64e1d2725..018a08179 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -41,8 +41,9 @@ import { } from "@app/actions/server"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; -import Link from "next/link"; import BrandingLogo from "@app/components/BrandingLogo"; +import BrandedAuthSurface from "@app/components/BrandedAuthSurface"; +import PoweredByPangolin from "@app/components/PoweredByPangolin"; import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; @@ -366,57 +367,20 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { : 100; return ( -
+ {!accessDenied ? (
- {isUnlocked() && build === "enterprise" ? ( - !env.branding.resourceAuthPage?.hidePoweredBy && - !env.branding.hidePoweredBy && ( -
- - {t("poweredBy")}{" "} - - {env.branding.appName || "Pangolin"} - - -
- ) - ) : ( -
- - {t("poweredBy")}{" "} - - Pangolin - - -
- )} + {isUnlocked() && build !== "oss" && - (env.branding?.resourceAuthPage?.showLogo || - props.branding) && ( + props.branding?.logoUrl && (
)} @@ -790,6 +754,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { ) : ( )} -
+
); } diff --git a/src/lib/loadOrgLoginPageBranding.ts b/src/lib/loadOrgLoginPageBranding.ts new file mode 100644 index 000000000..7e549622a --- /dev/null +++ b/src/lib/loadOrgLoginPageBranding.ts @@ -0,0 +1,31 @@ +import { priv } from "@app/lib/api"; +import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed"; +import { build } from "@server/build"; +import { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types"; +import { AxiosResponse } from "axios"; + +export async function loadOrgLoginPageBranding(orgId: string): Promise<{ + primaryColor: string | null; +}> { + if (build === "oss") { + return { primaryColor: null }; + } + + const subscribed = await isOrgSubscribed(orgId); + if (!subscribed) { + return { primaryColor: null }; + } + + try { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${orgId}`); + if (res.status === 200) { + return { primaryColor: res.data.data.primaryColor ?? null }; + } + } catch { + // ignore + } + + return { primaryColor: null }; +}