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|.*\\..*).*)"],
+};