'use client'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { useState } from 'react'; import { Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { BrandedAuthShell } from '@/components/shared/branded-auth-shell'; /** * Validate the `?next=` post-login redirect target. auth-flow-auditor M10: * an unvalidated `next` lets `/portal/login?next=https://evil.example` * navigate cross-site after sign-in. Only allow same-origin paths * scoped to the portal surface - anything else falls back to the * dashboard. */ function safeNextPath(raw: string | null): string { const fallback = '/portal/dashboard'; if (!raw) return fallback; // Reject absolute URLs (http://, https://, //evil.example) and // protocol-relative URLs. Only `/portal/...` paths are kept. if (!raw.startsWith('/portal/')) return fallback; if (raw.startsWith('//')) return fallback; return raw; } export default function PortalLoginPage() { const router = useRouter(); const search = useSearchParams(); // The middleware backstop (src/proxy.ts) redirects unauthenticated // portal visitors with `?redirect=`; older links / manual callers may // still use `?next=`. Accept either, preferring `redirect`. const next = safeNextPath(search.get('redirect') ?? search.get('next')); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(''); setLoading(true); try { const res = await fetch('/api/portal/auth/sign-in', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); setError((data as { error?: string }).error ?? 'Invalid email or password'); return; } // typedRoutes: `next` is a runtime string we can't statically check. router.replace(next as never); router.refresh(); } catch { setError('Unable to connect. Please try again.'); } finally { setLoading(false); } } return (

Client Portal

Sign in to your account

setEmail(e.target.value)} required autoFocus autoComplete="email" disabled={loading} />
Forgot password?
setPassword(e.target.value)} required autoComplete="current-password" disabled={loading} />
{error &&

{error}

}

This portal is for existing clients only.

); }