Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

6 changes: 0 additions & 6 deletions apps/dokploy/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import "@/styles/globals.css";
import type { NextPage } from "next";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import { ThemeProvider } from "next-themes";
import NextTopLoader from "nextjs-toploader";
import type { ReactElement, ReactNode } from "react";
import { SearchCommand } from "@/components/dashboard/search-command";
import { WhitelabelingProvider } from "@/components/proprietary/whitelabeling/whitelabeling-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { api } from "@/utils/api";
Expand Down Expand Up @@ -39,9 +37,6 @@ const MyApp = ({
}
`}
</style>
<Head>
<title>Dokploy</title>
</Head>
<TooltipProvider>
<ThemeProvider
attribute="class"
Expand All @@ -51,7 +46,6 @@ const MyApp = ({
forcedTheme={Component.theme}
>
<NextTopLoader color="hsl(var(--sidebar-ring))" />
<WhitelabelingProvider />
<Toaster richColors />
<SearchCommand />
{getLayout(<Component {...pageProps} />)}
Expand Down
148 changes: 145 additions & 3 deletions apps/dokploy/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,104 @@
import { Head, Html, Main, NextScript } from "next/document";
import { getPublicWhitelabelingConfig } from "@dokploy/server";
import NextDocument, {
type DocumentContext,
type DocumentInitialProps,
Head,
Html,
Main,
NextScript,
} from "next/document";

export default function Document() {
interface WhitelabelingDocumentProps {
metaTitle: string | null;
faviconHref: string | null;
customCss: string | null;
}

// Cache the resolved favicon (inlined as a data URI) so we don't re-fetch the
// remote image on every server render. Keyed by the configured favicon URL.
const FAVICON_CACHE_TTL = 60 * 60 * 1000; // 1 hour

const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute

declare global {
var __SETTINGS_CACHE: {
data: {
metaTitle: string | null;
faviconHref: string | null;
customCss: string | null;
};
expiresAt: number;
} | null;
var __FAVICON_CACHE: Map<string, { href: string; expiresAt: number }>;
}

const faviconCache =
globalThis.__FAVICON_CACHE ||
new Map<string, { href: string; expiresAt: number }>();
globalThis.__FAVICON_CACHE = faviconCache;

/**
* Resolve the favicon to an inline data URI so it is present in the initial
* HTML and renders without a network round-trip (no flash of the default
* favicon). Falls back to the raw URL if the image can't be fetched.
*/
async function resolveFaviconHref(
faviconUrl: string | null | undefined,
): Promise<string | null> {
if (!faviconUrl) return null;

const cached = faviconCache.get(faviconUrl);
if (cached && cached.expiresAt > Date.now()) {
return cached.href;
}

// Default to the raw URL so the custom favicon still loads if inlining fails.
let href = faviconUrl;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(faviconUrl, { signal: controller.signal });
clearTimeout(timeout);

if (response.ok) {
const buffer = Buffer.from(await response.arrayBuffer());
// Avoid embedding very large images directly in the HTML.
if (buffer.byteLength <= 512 * 1024) {
const contentType = response.headers.get("content-type") || "image/png";
href = `data:${contentType};base64,${buffer.toString("base64")}`;
}
}
} catch {
// Keep the raw URL fallback.
}

faviconCache.set(faviconUrl, {
href,
expiresAt: Date.now() + FAVICON_CACHE_TTL,
});
return href;
}

export default function Document({
metaTitle,
faviconHref,
customCss,
}: WhitelabelingDocumentProps) {
const title = metaTitle || "Dokploy";
return (
<Html lang="en" className="font-sans">
<Head>
<link rel="icon" href="/icon.svg" />
{/* Rendered on the server so the correct branding is present on first
paint (and for social scrapers), avoiding a flash of / fallback to
the default Dokploy branding. */}
<title>{title}</title>
<link rel="icon" href={faviconHref || "/icon.svg"} />
{customCss && (
<style
id="whitelabeling-styles"
dangerouslySetInnerHTML={{ __html: customCss }}
/>
)}
</Head>
<body className="flex h-full w-full flex-col font-sans">
<Main />
Expand All @@ -13,3 +107,51 @@ export default function Document() {
</Html>
);
}

Document.getInitialProps = async (
ctx: DocumentContext,
): Promise<DocumentInitialProps & WhitelabelingDocumentProps> => {
const initialProps = await NextDocument.getInitialProps(ctx);

let metaTitle: string | null = null;
let faviconHref: string | null = null;
let customCss: string | null = null;

if (
globalThis.__SETTINGS_CACHE &&
globalThis.__SETTINGS_CACHE.expiresAt > Date.now() &&
globalThis.__SETTINGS_CACHE.data
) {
return {
...initialProps,
...globalThis.__SETTINGS_CACHE.data,
};
}

try {
const config = await getPublicWhitelabelingConfig();
if (config) {
metaTitle = config.metaTitle;
customCss = config.customCss;
faviconHref = await resolveFaviconHref(config.faviconUrl);
}
} catch {
// Fall back to defaults if settings can't be read (e.g. DB not ready)
}

globalThis.__SETTINGS_CACHE = {
data: {
metaTitle,
faviconHref,
customCss,
},
expiresAt: Date.now() + SETTINGS_CACHE_TTL,
};

return {
...initialProps,
metaTitle,
faviconHref,
customCss,
};
};
28 changes: 27 additions & 1 deletion apps/dokploy/pages/_error.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { NextPageContext } from "next";
import Link from "next/link";
import superjson from "superjson";
import { Logo } from "@/components/shared/logo";
import { buttonVariants } from "@/components/ui/button";
import { appRouter } from "@/server/api/root";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";

interface Props {
Expand Down Expand Up @@ -102,7 +105,30 @@ export default function Custom404({ statusCode, error }: Props) {
}

// @ts-ignore
Error.getInitialProps = ({ res, err }: NextPageContext) => {
Error.getInitialProps = async ({ res, err, req }: NextPageContext) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;

// getInitialProps runs on the client too (e.g. client-side errors) where
// req/res are undefined. Only prefetch branding when running server-side.
if (req && res) {
try {
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: null as any,
user: null as any,
},
transformer: superjson,
});
await helpers.whitelabeling.getPublic.prefetch();
return { statusCode, error: err, trpcState: helpers.dehydrate() };
} catch {
// If branding prefetch fails, fall through to the default return.
}
}

return { statusCode, error: err };
};
20 changes: 20 additions & 0 deletions apps/dokploy/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
} from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import superjson from "superjson";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github";
Expand Down Expand Up @@ -45,6 +47,7 @@ import {
} from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";

Expand Down Expand Up @@ -430,6 +433,21 @@ Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: context.req as any,
res: context.res as any,
db: null as any,
session: null as any,
user: null as any,
},
transformer: superjson,
});
// Prefetch the public branding so the login/onboarding logo and app name
// render correctly on the server (no flash of default branding).
await helpers.whitelabeling.getPublic.prefetch();

if (IS_CLOUD) {
try {
const { user } = await validateRequest(context.req);
Expand All @@ -445,6 +463,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {

return {
props: {
trpcState: helpers.dehydrate(),
IS_CLOUD: IS_CLOUD,
enforceSSO: false,
},
Expand Down Expand Up @@ -476,6 +495,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {

return {
props: {
trpcState: helpers.dehydrate(),
hasAdmin,
enforceSSO: webServerSettings?.enforceSSO ?? false,
},
Expand Down
20 changes: 20 additions & 0 deletions apps/dokploy/pages/invitation.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { getUserByToken, IS_CLOUD } from "@dokploy/server";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import superjson from "superjson";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
Expand All @@ -22,6 +24,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";

Expand Down Expand Up @@ -326,6 +329,21 @@ Invitation.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: ctx.req as any,
res: ctx.res as any,
db: null as any,
session: null as any,
user: null as any,
},
transformer: superjson,
});
// Prefetch the public branding so the invitation logo and app name render
// correctly on the server (no flash of default branding).
await helpers.whitelabeling.getPublic.prefetch();

const { query } = ctx;

const token = query.token;
Expand Down Expand Up @@ -354,6 +372,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (invitation.userAlreadyExists) {
return {
props: {
trpcState: helpers.dehydrate(),
isCloud: IS_CLOUD,
token: token,
invitation: invitation,
Expand All @@ -373,6 +392,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {

return {
props: {
trpcState: helpers.dehydrate(),
isCloud: IS_CLOUD,
token: token,
invitation: invitation,
Expand Down
Loading