'use client' import { useState, useEffect, Suspense } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import Link from 'next/link' import Image from 'next/image' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { Loader2, ArrowRight, Eye, EyeOff, CheckCircle } from 'lucide-react' import clsx from 'clsx' // Logo Component function Logo() { return ( ) } // OAuth Icons function GoogleIcon({ className }: { className?: string }) { return ( ) } function GitHubIcon({ className }: { className?: string }) { return ( ) } function LoginForm() { const router = useRouter() const searchParams = useSearchParams() const { login } = useStore() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [oauthProviders, setOauthProviders] = useState({ google_enabled: false, github_enabled: false }) const [verified, setVerified] = useState(false) const sanitizeRedirect = (value: string | null | undefined): string => { const fallback = '/terminal/hunt' if (!value) return fallback const v = value.trim() if (!v.startsWith('/')) return fallback if (v.startsWith('//')) return fallback if (v.includes('://')) return fallback if (v.includes('\\')) return fallback if (v.length > 2048) return fallback return v } // Get redirect URL from query params or localStorage (set during registration) const paramRedirect = searchParams.get('redirect') const [redirectTo, setRedirectTo] = useState(sanitizeRedirect(paramRedirect)) // Check localStorage for redirect (set during registration before email verification) useEffect(() => { const storedRedirect = localStorage.getItem('pounce_redirect_after_login') if (storedRedirect && !paramRedirect) { setRedirectTo(sanitizeRedirect(storedRedirect)) } }, [paramRedirect]) // Check for verified status useEffect(() => { if (searchParams.get('verified') === 'true') { setVerified(true) } if (searchParams.get('error')) { setError(searchParams.get('error') === 'oauth_failed' ? 'OAuth authentication failed. Please try again.' : 'Authentication failed') } }, [searchParams]) // Load OAuth providers useEffect(() => { api.getOAuthProviders().then(setOauthProviders).catch(() => {}) }, []) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) setLoading(true) try { await login(email, password) // Clear stored redirect (was set during registration) localStorage.removeItem('pounce_redirect_after_login') // Redirect to intended destination or dashboard // Note: Email verification is enforced by the backend if REQUIRE_EMAIL_VERIFICATION=true router.push(sanitizeRedirect(redirectTo)) } catch (err: unknown) { console.error('Login error:', err) if (err instanceof Error) { setError(err.message || 'Authentication failed') } else if (typeof err === 'object' && err !== null) { if ('detail' in err) { setError(String((err as { detail: unknown }).detail)) } else if ('message' in err) { setError(String((err as { message: unknown }).message)) } else { setError('Authentication failed. Please try again.') } } else if (typeof err === 'string') { setError(err) } else { setError('Authentication failed. Please try again.') } } finally { setLoading(false) } } // Generate register link with redirect preserved const registerLink = redirectTo !== '/terminal/hunt' ? `/register?redirect=${encodeURIComponent(redirectTo)}` : '/register' return ( {/* Card Container */} {/* Tech Corners */} {/* Logo */} {/* Header */} Access Granted Welcome back. Authenticate to access the terminal. {/* Verified Message */} {verified && ( Email verified. System access ready. )} {/* Form */} {error && ( {error} )} Email Address setEmail(e.target.value)} placeholder="OPERATOR@POUNCE.IO" required autoComplete="email" className="w-full bg-[#0A0A0A] border border-white/10 px-4 py-3 text-white font-mono text-sm placeholder:text-white/20 focus:outline-none focus:border-accent transition-all rounded-none" /> Passcode setPassword(e.target.value)} placeholder="••••••••" required minLength={8} autoComplete="current-password" className="w-full bg-[#0A0A0A] border border-white/10 px-4 py-3 text-white font-mono text-sm placeholder:text-white/20 focus:outline-none focus:border-accent transition-all rounded-none pr-12" /> setShowPassword(!showPassword)} className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white transition-colors" aria-label={showPassword ? 'Hide password' : 'Show password'} > {showPassword ? ( ) : ( )} Lost Credentials? {loading ? ( ) : ( <> Initialize Session > )} {/* OAuth Buttons */} {(oauthProviders.google_enabled || oauthProviders.github_enabled) && ( Alternative Access {oauthProviders.google_enabled && ( Google )} {oauthProviders.github_enabled && ( GitHub )} )} {/* Register Link */} No clearance?{' '} Request Access ) } export default function LoginPage() { return ( {/* Living Background Atmosphere */} {/* Animated Orbs */} {/* Grid Overlay */} }> ) }
Authenticate to access the terminal.
Email verified. System access ready.
{error}
No clearance?{' '} Request Access