From b54eff648e591d08fcbb3ff7532cf63cbb9d7436 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 19 Jun 2026 03:56:26 +0530
Subject: [PATCH 1/2] feat: implement runtime authentication handling
- Added a new proxy function to manage runtime authentication types and set cookies accordingly.
- Introduced runtime authentication configuration to dynamically adjust UI based on the selected auth type.
- Updated global styles to hide specific authentication buttons based on the current auth type.
- Refactored sign-in button and hero section components to utilize the new runtime authentication logic.
- Created a new utility file for runtime authentication configuration and initialization script.
---
surfsense_web/app/globals.css | 5 ++
surfsense_web/app/layout.tsx | 15 +++++-
.../components/auth/sign-in-button.tsx | 46 ++++++++--------
.../components/homepage/hero-section.tsx | 28 +++++-----
surfsense_web/lib/runtime-auth-config.ts | 52 +++++++++++++++++++
surfsense_web/proxy.ts | 24 +++++++++
6 files changed, 130 insertions(+), 40 deletions(-)
create mode 100644 surfsense_web/lib/runtime-auth-config.ts
create mode 100644 surfsense_web/proxy.ts
diff --git a/surfsense_web/app/globals.css b/surfsense_web/app/globals.css
index 3cdb34bff..4a29edfa6 100644
--- a/surfsense_web/app/globals.css
+++ b/surfsense_web/app/globals.css
@@ -58,6 +58,11 @@
--highlight: oklch(0.852 0.199 91.936);
}
+html[data-surfsense-auth-type="GOOGLE"] .runtime-auth-local,
+html[data-surfsense-auth-type="LOCAL"] .runtime-auth-google {
+ display: none;
+}
+
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx
index 1e9c9eebe..46182f40e 100644
--- a/surfsense_web/app/layout.tsx
+++ b/surfsense_web/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
import "./globals.css";
import { RootProvider } from "fumadocs-ui/provider/next";
import { Roboto } from "next/font/google";
+import Script from "next/script";
import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider";
import { DesktopUpdateToast } from "@/components/desktop/desktop-update-toast";
import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider";
@@ -16,8 +17,13 @@ import {
import { ThemeProvider } from "@/components/theme/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { LocaleProvider } from "@/contexts/LocaleContext";
+import { BUILD_TIME_AUTH_TYPE } from "@/lib/env-config";
import { PlatformProvider } from "@/contexts/platform-context";
import { ReactQueryClientProvider } from "@/lib/query-client/query-client.provider";
+import {
+ getRuntimeAuthInitScript,
+ resolveRuntimeAuthUiMode,
+} from "@/lib/runtime-auth-config";
import { cn } from "@/lib/utils";
const roboto = Roboto({
@@ -131,8 +137,15 @@ export default function RootLayout({
// Language can be switched dynamically through LanguageSwitcher component
// Locale state is managed by LocaleContext and persisted in localStorage
return (
-
+
+
diff --git a/surfsense_web/components/auth/sign-in-button.tsx b/surfsense_web/components/auth/sign-in-button.tsx
index 581e37603..d0a563a54 100644
--- a/surfsense_web/components/auth/sign-in-button.tsx
+++ b/surfsense_web/components/auth/sign-in-button.tsx
@@ -3,7 +3,7 @@
import Link from "next/link";
import { useState } from "react";
import { Button } from "@/components/ui/button";
-import { BUILD_TIME_AUTH_TYPE, buildBackendUrl } from "@/lib/env-config";
+import { buildBackendUrl } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
@@ -46,7 +46,6 @@ interface SignInButtonProps {
}
export const SignInButton = ({ variant = "desktop" }: SignInButtonProps) => {
- const isGoogleAuth = BUILD_TIME_AUTH_TYPE === "GOOGLE";
const [isRedirecting, setIsRedirecting] = useState(false);
const handleGoogleLogin = () => {
@@ -56,44 +55,45 @@ export const SignInButton = ({ variant = "desktop" }: SignInButtonProps) => {
window.location.href = buildBackendUrl("/auth/google/authorize-redirect");
};
- const getClassName = () => {
+ const getGoogleClassName = () => {
if (variant === "desktop") {
- return isGoogleAuth
- ? "hidden rounded-full border border-white bg-white px-5 py-2 text-sm font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] md:flex dark:border-white"
- : "hidden rounded-full bg-black px-8 py-2 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] md:block dark:bg-white dark:text-black";
+ return "hidden rounded-full border border-white bg-white px-5 py-2 text-sm font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] md:flex dark:border-white";
}
if (variant === "compact") {
- return isGoogleAuth
- ? "rounded-full border border-white bg-white px-4 py-1.5 text-sm font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-white"
- : "rounded-full bg-black px-6 py-1.5 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black";
+ return "rounded-full border border-white bg-white px-4 py-1.5 text-sm font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-white";
}
// mobile
- return isGoogleAuth
- ? "w-full rounded-lg border border-white bg-white px-8 py-2.5 font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-white touch-manipulation"
- : "w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation";
+ return "w-full rounded-lg border border-white bg-white px-8 py-2.5 font-medium text-[#1f1f1f] shadow-sm hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-white touch-manipulation";
};
- if (isGoogleAuth) {
- return (
+ const getLocalClassName = () => {
+ if (variant === "desktop") {
+ return "hidden rounded-full bg-black px-8 py-2 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] md:block dark:bg-white dark:text-black";
+ }
+ if (variant === "compact") {
+ return "rounded-full bg-black px-6 py-1.5 text-sm font-bold text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black";
+ }
+ return "w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation";
+ };
+
+ return (
+ <>
- );
- }
-
- return (
-
- Sign In
-
+
+ Sign In
+
+ >
);
};
diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx
index 0f3bfe1aa..c9430f098 100644
--- a/surfsense_web/components/homepage/hero-section.tsx
+++ b/surfsense_web/components/homepage/hero-section.tsx
@@ -37,7 +37,7 @@ import {
getAssetLabel,
usePrimaryDownload,
} from "@/lib/desktop-download-utils";
-import { BUILD_TIME_AUTH_TYPE, buildBackendUrl } from "@/lib/env-config";
+import { buildBackendUrl } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
@@ -314,7 +314,6 @@ export function HeroSection() {
}
function GetStartedButton() {
- const isGoogleAuth = BUILD_TIME_AUTH_TYPE === "GOOGLE";
const [isRedirecting, setIsRedirecting] = useState(false);
const handleGoogleLogin = () => {
@@ -324,29 +323,26 @@ function GetStartedButton() {
window.location.href = buildBackendUrl("/auth/google/authorize-redirect");
};
- if (isGoogleAuth) {
- return (
+ return (
+ <>
- );
- }
-
- return (
-
+
+ >
);
}
diff --git a/surfsense_web/lib/runtime-auth-config.ts b/surfsense_web/lib/runtime-auth-config.ts
new file mode 100644
index 000000000..9e8d1921d
--- /dev/null
+++ b/surfsense_web/lib/runtime-auth-config.ts
@@ -0,0 +1,52 @@
+export const RUNTIME_AUTH_TYPE_COOKIE_NAME = "surfsense_auth_type";
+
+export type RuntimeAuthUiMode = "GOOGLE" | "LOCAL";
+
+export function resolveRuntimeAuthUiMode(
+ value: string | null | undefined,
+ fallback: string | null | undefined = "GOOGLE"
+): RuntimeAuthUiMode {
+ const candidate = value?.trim().toUpperCase();
+ if (candidate === "GOOGLE") return "GOOGLE";
+ if (candidate === "LOCAL") return "LOCAL";
+
+ const fallbackCandidate = fallback?.trim().toUpperCase();
+ return fallbackCandidate === "GOOGLE" ? "GOOGLE" : "LOCAL";
+}
+
+export function getRuntimeAuthInitScript(fallbackAuthType: string): string {
+ const fallback = resolveRuntimeAuthUiMode(fallbackAuthType);
+ const cookieName = JSON.stringify(RUNTIME_AUTH_TYPE_COOKIE_NAME);
+ const fallbackValue = JSON.stringify(fallback);
+
+ return `
+(function() {
+ try {
+ var cookieName = ${cookieName};
+ var fallback = ${fallbackValue};
+ var prefix = cookieName + "=";
+ var rawValue = fallback;
+ var cookies = document.cookie ? document.cookie.split(";") : [];
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = cookies[i].trim();
+ if (cookie.indexOf(prefix) === 0) {
+ rawValue = decodeURIComponent(cookie.slice(prefix.length));
+ break;
+ }
+ }
+ var normalized = String(rawValue || fallback).toUpperCase() === "GOOGLE" ? "GOOGLE" : "LOCAL";
+ window.__SURFSENSE_AUTH_TYPE__ = normalized;
+ document.documentElement.setAttribute("data-surfsense-auth-type", normalized);
+ } catch (_) {
+ window.__SURFSENSE_AUTH_TYPE__ = ${fallbackValue};
+ document.documentElement.setAttribute("data-surfsense-auth-type", ${fallbackValue});
+ }
+})();
+`;
+}
+
+declare global {
+ interface Window {
+ __SURFSENSE_AUTH_TYPE__?: RuntimeAuthUiMode;
+ }
+}
diff --git a/surfsense_web/proxy.ts b/surfsense_web/proxy.ts
new file mode 100644
index 000000000..b53ce68a7
--- /dev/null
+++ b/surfsense_web/proxy.ts
@@ -0,0 +1,24 @@
+import { NextResponse, type NextRequest } from "next/server";
+import { BUILD_TIME_AUTH_TYPE } from "@/lib/env-config";
+import {
+ RUNTIME_AUTH_TYPE_COOKIE_NAME,
+ resolveRuntimeAuthUiMode,
+} from "@/lib/runtime-auth-config";
+
+export function proxy(request: NextRequest) {
+ const response = NextResponse.next();
+ const authType = resolveRuntimeAuthUiMode(process.env.AUTH_TYPE, BUILD_TIME_AUTH_TYPE);
+
+ response.cookies.set(RUNTIME_AUTH_TYPE_COOKIE_NAME, authType, {
+ path: "/",
+ maxAge: 60 * 60 * 24 * 365,
+ sameSite: "lax",
+ secure: request.nextUrl.protocol === "https:",
+ });
+
+ return response;
+}
+
+export const config = {
+ matcher: ["/((?!api|auth|_next/static|_next/image|favicon.ico|.*\\..*).*)"],
+};
From 6bab6df819c769233f1fbcc68af2d7c15e8617b5 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 19 Jun 2026 12:04:52 +0530
Subject: [PATCH 2/2] chore: update .env.example for runtime configuration
---
surfsense_web/.env.example | 32 +++++++++++++++++++++-----------
1 file changed, 21 insertions(+), 11 deletions(-)
diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example
index 11646c948..7d03cf498 100644
--- a/surfsense_web/.env.example
+++ b/surfsense_web/.env.example
@@ -14,7 +14,10 @@ SURFSENSE_BACKEND_INTERNAL_URL=http://backend:8000
# ─────────────────────────────────────────────────────────────────────────────
# Runtime configuration (read at runtime by the server, no rebuild needed)
# ─────────────────────────────────────────────────────────────────────────────
-
+# Configure these plain variables for runtime behavior. They are read by server
+# code when the app starts/serves requests, so changing them requires restarting
+# the web process but not rebuilding the frontend bundle.
+#
# Authentication method: LOCAL (email/password) or GOOGLE (OAuth).
AUTH_TYPE=LOCAL
# Document parsing backend: DOCLING, LLAMACLOUD, etc.
@@ -22,16 +25,6 @@ ETL_SERVICE=DOCLING
# Deployment mode: self-hosted or cloud.
DEPLOYMENT_MODE=self-hosted
-# ─────────────────────────────────────────────────────────────────────────────
-# Build-time fallbacks for packaged clients (e.g. Electron) without a runtime
-# config provider. Optional; Docker reads the plain runtime vars above first.
-# ─────────────────────────────────────────────────────────────────────────────
-# NEXT_PUBLIC_AUTH_TYPE=GOOGLE
-# NEXT_PUBLIC_ETL_SERVICE=DOCLING
-# NEXT_PUBLIC_DEPLOYMENT_MODE=self-hosted
-# Overrides the app version shown in the UI (defaults to package.json version).
-# NEXT_PUBLIC_APP_VERSION=
-
# ─────────────────────────────────────────────────────────────────────────────
# Database (Contact Form, optional)
# ─────────────────────────────────────────────────────────────────────────────
@@ -72,3 +65,20 @@ NEXT_PUBLIC_GOOGLE_ADSENSE_SLOT_FREE_HUB_BEFORE_FAQ=
# ─────────────────────────────────────────────────────────────────────────────
NEXT_PUBLIC_GLOBAL_ANNOUNCEMENT_ENABLED=false
NEXT_PUBLIC_GLOBAL_ANNOUNCEMENT_MESSAGE=
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Internal build-time fallbacks
+# ─────────────────────────────────────────────────────────────────────────────
+#
+# Most deployments should leave these unset.
+#
+# These are only for SurfSense-managed production/cloud builds or packaged
+# clients that do not have the normal server runtime config available.
+#
+# NEXT_PUBLIC_* values are embedded into the browser bundle during `next build`.
+# Changing them after the bundle is built has no effect.
+
+# NEXT_PUBLIC_AUTH_TYPE=GOOGLE
+# NEXT_PUBLIC_ETL_SERVICE=DOCLING
+# NEXT_PUBLIC_DEPLOYMENT_MODE=self-hosted
+# NEXT_PUBLIC_APP_VERSION=
\ No newline at end of file