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.
This commit is contained in:
Anish Sarkar 2026-06-19 03:56:26 +05:30
parent 03e57bdf7e
commit b54eff648e
6 changed files with 130 additions and 40 deletions

View file

@ -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);

View file

@ -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 (
<html lang="en" suppressHydrationWarning>
<html
lang="en"
data-surfsense-auth-type={resolveRuntimeAuthUiMode(BUILD_TIME_AUTH_TYPE)}
suppressHydrationWarning
>
<head>
<Script id="surfsense-runtime-auth-init" strategy="beforeInteractive">
{getRuntimeAuthInitScript(BUILD_TIME_AUTH_TYPE)}
</Script>
<link rel="preconnect" href="https://api.github.com" />
<OrganizationJsonLd />
<WebSiteJsonLd />

View file

@ -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 (
<>
<Button
type="button"
variant="ghost"
onClick={handleGoogleLogin}
disabled={isRedirecting}
className={cn(
"flex items-center justify-center gap-2 transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50",
getClassName()
"runtime-auth-google flex items-center justify-center gap-2 transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50",
getGoogleClassName()
)}
>
<GoogleLogo className="h-4 w-4" />
<span>Sign In</span>
</Button>
);
}
return (
<Link href="/login" className={getClassName()}>
Sign In
</Link>
<Link href="/login" className={cn("runtime-auth-local", getLocalClassName())}>
Sign In
</Link>
</>
);
};

View file

@ -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 (
<>
<Button
type="button"
variant="ghost"
onClick={handleGoogleLogin}
disabled={isRedirecting}
className="h-14 w-full cursor-pointer gap-3 rounded-lg border border-white bg-white text-center text-base font-medium text-[#1f1f1f] shadow-sm transition duration-150 hover:bg-zinc-100 hover:text-[#1f1f1f] sm:w-56 dark:border-white"
className="runtime-auth-google h-14 w-full cursor-pointer gap-3 rounded-lg border border-white bg-white text-center text-base font-medium text-[#1f1f1f] shadow-sm transition duration-150 hover:bg-zinc-100 hover:text-[#1f1f1f] sm:w-56 dark:border-white"
>
<GoogleLogo className="h-5 w-5" />
<span>Continue with Google</span>
</Button>
);
}
return (
<Button
asChild
variant="ghost"
className="h-14 w-full rounded-lg bg-black text-center text-base font-medium text-white shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 hover:bg-black sm:w-52 dark:bg-white dark:text-black dark:hover:bg-white"
>
<Link href="/login">Get Started</Link>
</Button>
<Button
asChild
variant="ghost"
className="runtime-auth-local h-14 w-full rounded-lg bg-black text-center text-base font-medium text-white shadow-sm ring-1 shadow-black/10 ring-black/10 transition duration-150 active:scale-98 hover:bg-black sm:w-52 dark:bg-white dark:text-black dark:hover:bg-white"
>
<Link href="/login">Get Started</Link>
</Button>
</>
);
}

View file

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

24
surfsense_web/proxy.ts Normal file
View file

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