'use client' import { useCallback, useEffect, useRef, useState, FormEvent } from 'react' import Image from 'next/image' import { Button } from '@/components/ui/button' interface GoogleCredentialResponse { credential?: string } interface GoogleAccountsIdApi { initialize(config: { client_id: string callback: (response: GoogleCredentialResponse) => void }): void prompt(): void } interface GoogleApi { accounts: { id: GoogleAccountsIdApi } } type LoginRequestBody = | { username: string; password: string } | { credential?: string } type LoginErrorPayload = { code?: string error?: string hint?: string } function readLoginErrorPayload(value: unknown): LoginErrorPayload { if (!value || typeof value !== 'object') return {} const record = value as Record return { code: typeof record.code === 'string' ? record.code : undefined, error: typeof record.error === 'string' ? record.error : undefined, hint: typeof record.hint === 'string' ? record.hint : undefined, } } declare global { interface Window { google?: GoogleApi } } function GoogleIcon({ className }: { className?: string }) { return ( ) } export default function LoginPage() { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [pendingApproval, setPendingApproval] = useState(false) const [needsSetup, setNeedsSetup] = useState(false) const [loading, setLoading] = useState(false) const [googleLoading, setGoogleLoading] = useState(false) const [googleReady, setGoogleReady] = useState(false) const googleCallbackRef = useRef<((response: GoogleCredentialResponse) => void) | null>(null) const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '' // Check if first-time setup is needed on page load — auto-redirect to /setup useEffect(() => { fetch('/api/setup') .then((res) => res.json()) .then((data) => { if (data.needsSetup) { window.location.href = '/setup' } }) .catch(() => { // Ignore — setup check is best-effort }) }, []) const completeLogin = useCallback(async (path: string, body: LoginRequestBody) => { const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) { const data = readLoginErrorPayload(await res.json().catch(() => null)) if (data.code === 'PENDING_APPROVAL') { setPendingApproval(true) setNeedsSetup(false) setError('') setLoading(false) setGoogleLoading(false) return false } if (data.code === 'NO_USERS') { setNeedsSetup(true) setError('') setLoading(false) setGoogleLoading(false) return false } setError(data.error || 'Login failed') setPendingApproval(false) setNeedsSetup(false) setLoading(false) setGoogleLoading(false) return false } // Full reload ensures the session cookie is sent on all subsequent requests. // router.push() + refresh() can race and use stale RSC payloads. window.location.href = '/' return true }, []) async function handleSubmit(e: FormEvent) { e.preventDefault() setError('') setLoading(true) // Read DOM values directly to handle browser autofill (which doesn't fire onChange) const form = e.target as HTMLFormElement const formUsername = (form.elements.namedItem('username') as HTMLInputElement)?.value || username const formPassword = (form.elements.namedItem('password') as HTMLInputElement)?.value || password try { await completeLogin('/api/auth/login', { username: formUsername, password: formPassword }) } catch { setError('Network error') setLoading(false) } } // Initialize Google Sign-In SDK (hidden prompt mode) useEffect(() => { if (!googleClientId) return const onScriptLoad = () => { if (!window.google) return googleCallbackRef.current = async (response: GoogleCredentialResponse) => { setError('') setGoogleLoading(true) try { const ok = await completeLogin('/api/auth/google', { credential: response?.credential }) if (!ok) return } catch { setError('Google sign-in failed') setGoogleLoading(false) } } window.google.accounts.id.initialize({ client_id: googleClientId, callback: (response: GoogleCredentialResponse) => googleCallbackRef.current?.(response), }) setGoogleReady(true) } const existing = document.querySelector('script[data-google-gsi="1"]') as HTMLScriptElement | null if (existing) { if (window.google) onScriptLoad() return } const script = document.createElement('script') script.src = 'https://accounts.google.com/gsi/client' script.async = true script.defer = true script.setAttribute('data-google-gsi', '1') script.onload = onScriptLoad script.onerror = () => setError('Failed to load Google Sign-In') document.head.appendChild(script) }, [googleClientId, completeLogin]) const handleGoogleSignIn = () => { if (!window.google || !googleReady) return window.google.accounts.id.prompt() } return (
Mission Control logo

Mission Control

Sign in to continue

{pendingApproval && (
Access Request Submitted

Your request has been sent to an administrator for review. You'll be able to sign in once approved.

)} {needsSetup && (
No admin account created yet

Set up your admin account to get started with Mission Control.

)} {error && (
{error}
)} {/* Google Sign-In button — shown only when client ID is configured */} {googleClientId && (
{!googleReady && (

Loading Google Sign-In...

)} {/* Divider */}
or
)}
setUsername(e.target.value)} className="w-full h-10 px-3 rounded-lg bg-secondary border border-border text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-smooth" placeholder="Enter username" autoComplete="username" autoFocus required aria-required="true" />
setPassword(e.target.value)} className="w-full h-10 px-3 rounded-lg bg-secondary border border-border text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-smooth" placeholder="Enter password" autoComplete="current-password" required aria-required="true" />

OpenClaw Agent Orchestration

) }