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
32 changes: 21 additions & 11 deletions surfsense_web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,17 @@ 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.
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)
# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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=
5 changes: 5 additions & 0 deletions surfsense_web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 14 additions & 1 deletion surfsense_web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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({
Expand Down Expand Up @@ -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 />
Expand Down
46 changes: 23 additions & 23 deletions surfsense_web/components/auth/sign-in-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 = () => {
Expand All @@ -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>
</>
);
};
28 changes: 12 additions & 16 deletions surfsense_web/components/homepage/hero-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -314,7 +314,6 @@ export function HeroSection() {
}

function GetStartedButton() {
const isGoogleAuth = BUILD_TIME_AUTH_TYPE === "GOOGLE";
const [isRedirecting, setIsRedirecting] = useState(false);

const handleGoogleLogin = () => {
Expand All @@ -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>
</>
);
}

Expand Down
52 changes: 52 additions & 0 deletions surfsense_web/lib/runtime-auth-config.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions surfsense_web/proxy.ts
Original file line number Diff line number Diff line change
@@ -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|.*\\..*).*)"],
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading