diff --git a/app/setup/page.tsx b/app/setup/page.tsx index 27070e7e..d40f0534 100644 --- a/app/setup/page.tsx +++ b/app/setup/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, type FormEvent, type ReactNode } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { CheckCircle2, AlertTriangle, AlertCircle, Server, ShieldCheck, KeyRound, FileText, Palette, Lock } from 'lucide-react'; +import { CheckCircle2, AlertTriangle, AlertCircle, Server, ShieldCheck, KeyRound, FileText, Palette, Lock, ShieldAlert } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; type State = 'bootstrap' | 'configured' | 'env-managed'; @@ -101,9 +101,20 @@ export default function SetupWizardPage() { const [config, setConfig] = useState(EMPTY_CONFIG); const [stepIndex, setStepIndex] = useState(0); const [completed, setCompleted] = useState(false); + // Detect synchronously on first client render so we don't flash the loading + // screen before the warning appears. The session cookie is set with the + // Secure flag in production, which browsers silently drop over plain HTTP - + // every subsequent step call then 401s with "Wizard session required". + const [insecureContext] = useState(detectInsecureContext); // ─── Initial status load ──────────────────────────────────────────────── useEffect(() => { + // Skip the status fetch entirely when we're going to render the HTTPS + // notice - the wizard cookie can't survive an HTTP origin anyway. + if (insecureContext) { + setBootstrapping(false); + return; + } let cancelled = false; (async () => { try { @@ -141,7 +152,7 @@ export default function SetupWizardPage() { return () => { cancelled = true; }; - }, [router]); + }, [router, insecureContext]); // ─── Token submit (welcome step) ──────────────────────────────────────── async function submitToken(token: string) { @@ -173,6 +184,10 @@ export default function SetupWizardPage() { } // ─── Render shell ─────────────────────────────────────────────────────── + if (insecureContext) { + return ; + } + if (bootstrapping) { return

Loading…

; } @@ -347,6 +362,44 @@ function CompletedScreen() { ); } +function InsecureContextScreen() { + const httpsUrl = + typeof window !== 'undefined' + ? `https://${window.location.host}${window.location.pathname}${window.location.search}` + : ''; + return ( + +
+
+ +
+

HTTPS required for setup

+

+ The setup wizard signs you in with a Secure cookie, + which your browser will only accept over HTTPS. Loading this page over plain HTTP causes every + step to fail with Wizard session required. +

+
+
+

To continue, do one of the following:

+
    +
  • Reach this page over HTTPS (terminate TLS on the container or a reverse proxy in front of it).
  • +
  • If you already have a reverse proxy, make sure it forwards to the webmail and forwards the + X-Forwarded-Proto header.
  • +
+
+ {httpsUrl && ( + + Open over HTTPS + + )} +
+ ); +} + function AlreadyConfiguredScreen() { return ( @@ -1729,6 +1782,18 @@ function isInsecureHttpUrl(url: string): boolean { return /^http:\/\//i.test(url.trim()); } +function detectInsecureContext(): boolean { + if (typeof window === 'undefined') return false; + if (window.location.protocol !== 'http:') return false; + // Browsers treat localhost/loopback as "potentially trustworthy" and accept + // Secure cookies even without TLS, so the wizard still works there. + const host = window.location.hostname; + if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') { + return false; + } + return true; +} + function humanError(e: unknown): string { if (e instanceof Error) return e.message; if (typeof e === 'string') return e;