import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useNavigate, useSearchParams } from 'react-router'; import { syncAdminCookie, useAuth } from '../components/auth/AuthProvider'; import { Wordmark } from '../components/Logo'; import { Button } from '../components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; import { Input } from '../components/ui/input'; import { useAuthSettings } from '../hooks/api'; type OAuthProvider = 'google' | 'microsoft' | 'github' | 'apple' | 'discord'; const providerConfig: Record = { google: { label: 'Google', icon: ( ), }, microsoft: { label: 'Microsoft', icon: ( ), }, github: { label: 'GitHub', icon: ( ), }, apple: { label: 'Apple', icon: ( ), }, discord: { label: 'Discord', icon: ( ), }, }; /** * Validate a redirect URL is safe (not an open redirect). * Allows relative paths and absolute URLs to sibling subdomains * (e.g., n8n.localhost when the app is on localhost). */ function getSafeRedirect(redirectParam: string | null): string { const fallback = '/dashboard'; if (!redirectParam) return fallback; // Relative paths: must start with / and not // (protocol-relative) if (redirectParam.startsWith('/') && !redirectParam.startsWith('//')) { return redirectParam; } // Absolute URLs: allow ONLY the current host or a subdomain of it. The login // page is served on the app's own origin, so anchoring on // window.location.hostname is exact — this preserves the admin-tool // ForwardAuth flow (login on the apex, redirect back to studio./grafana. // subdomains) while rejecting look-alikes. The previous "last two DNS // labels" heuristic was an OPEN REDIRECT on multi-part public suffixes: // on app.example.co.uk it treated `co.uk` as the base domain, so // `https://evil.co.uk` matched and post-login users could be sent there. try { const url = new URL(redirectParam); if (url.protocol !== 'http:' && url.protocol !== 'https:') return fallback; const host = window.location.hostname; const target = url.hostname; if (target === host || target.endsWith(`.${host}`)) { return redirectParam; // same origin or a subdomain of it } } catch { // Invalid URL } return fallback; } /** * Navigate to redirect target — uses window.location for absolute URLs * (cross-subdomain) and react-router navigate for relative paths. */ function performRedirect(redirect: string, navigate: ReturnType) { if (redirect.startsWith('http://') || redirect.startsWith('https://')) { window.location.href = redirect; } else { navigate(redirect, { replace: true }); } } export default function Login() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); const { user, signIn, signUp } = useAuth(); const { data: authSettings } = useAuthSettings(); // Default to signup mode if on /signup route const [isSignUp, setIsSignUp] = useState(location.pathname === '/signup'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [magicLinkSent, setMagicLinkSent] = useState(false); const [showMagicLink, setShowMagicLink] = useState(false); // Derive enabled providers from settings const providers = authSettings?.settings?.providers; const enabledProviders: OAuthProvider[] = providers ? (Object.entries(providers) as [OAuthProvider, boolean][]) .filter(([, enabled]) => enabled) .map(([provider]) => provider) : []; const magicLinkEnabled = authSettings?.settings?.magicLinkEnabled ?? true; // Sync isSignUp state with route useEffect(() => { setIsSignUp(location.pathname === '/signup'); }, [location.pathname]); // Redirect if already logged in. Await the ForwardAuth-cookie mint first: // this effect IS the bounce path when an admin's vc-admin-token expired on // a subdomain tool (ForwardAuth 302s here with ?redirect=), so the cookie // must be set before we send the browser back. useEffect(() => { if (user) { const safeRedirect = getSafeRedirect(searchParams.get('redirect')); syncAdminCookie().finally(() => performRedirect(safeRedirect, navigate)); } }, [user, navigate, searchParams]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { const safeRedirect = getSafeRedirect(searchParams.get('redirect')); if (isSignUp) { await signUp(email, password, name); performRedirect(safeRedirect, navigate); } else { const { requiresMfa } = await signIn.email(email, password); if (requiresMfa) { // Redirect to MFA verification with the intended destination navigate(`/mfa-verify?redirect=${encodeURIComponent(safeRedirect)}`); } else { // Cross-subdomain targets need the ForwardAuth cookie before we leave await syncAdminCookie(); performRedirect(safeRedirect, navigate); } } } catch (err) { setError(err instanceof Error ? err.message : t('auth.authFailed')); } finally { setLoading(false); } }; const handleMagicLink = async (e: React.FormEvent) => { e.preventDefault(); if (!email) return; setError(''); setLoading(true); try { await signIn.magicLink(email); setMagicLinkSent(true); } catch (err) { setError(err instanceof Error ? err.message : t('auth.magicLinkFailed')); } finally { setLoading(false); } }; const handleOAuth = async (provider: OAuthProvider) => { setLoading(true); setError(''); try { await signIn[provider](); } catch (err) { setError(err instanceof Error ? err.message : t('auth.oauthFailed')); setLoading(false); } }; return (
{/* Header */}
{/* Main content */}
{magicLinkSent ? t('auth.checkYourEmail') : isSignUp ? t('auth.createAccount') : t('auth.signIn')} {magicLinkSent ? t('auth.magicLinkSent', { email }) : t('auth.poweredBy')} {magicLinkSent ? (

{t('auth.magicLinkExpiry')}

) : showMagicLink ? ( <>
{error && (
{error}
)} setEmail(e.target.value)} required autoFocus />
) : ( <>
{error && (
{error}
)} {isSignUp && ( setName(e.target.value)} /> )} setEmail(e.target.value)} required autoFocus /> setPassword(e.target.value)} required minLength={8} />
{magicLinkEnabled && !isSignUp && ( )} {!isSignUp && ( {t('auth.forgotPassword')} )}
{enabledProviders.length > 0 && ( <>
{t('auth.orContinueWith')}
{enabledProviders.map((provider) => ( ))}
)}

{isSignUp ? t('auth.alreadyHaveAccount') : t('auth.dontHaveAccount')}{' '}

)}
); }