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 };
+}