Files
Charon/frontend/src/pages/Setup.tsx

168 lines
5.3 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getSetupStatus, performSetup, SetupRequest } from '../api/setup';
import client from '../api/client';
import { useAuth } from '../hooks/useAuth';
import { Input } from '../components/ui/Input';
import { Button } from '../components/ui/Button';
import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter';
import { isValidEmail } from '../utils/validation';
const Setup: React.FC = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { login, isAuthenticated } = useAuth();
const [formData, setFormData] = useState<SetupRequest>({
name: '',
email: '',
password: '',
});
const [error, setError] = useState<string | null>(null);
const [emailValid, setEmailValid] = useState<boolean | null>(null);
const { data: status, isLoading: statusLoading } = useQuery({
queryKey: ['setupStatus'],
queryFn: getSetupStatus,
retry: false,
});
useEffect(() => {
if (formData.email) {
setEmailValid(isValidEmail(formData.email));
} else {
setEmailValid(null);
}
}, [formData.email]);
useEffect(() => {
// Wait for setup status to load
if (statusLoading) return;
// If setup is required, stay on this page (ignore stale auth)
if (status?.setupRequired) {
return;
}
// If setup is NOT required, redirect based on auth
if (isAuthenticated) {
navigate('/');
} else {
navigate('/login');
}
}, [status, statusLoading, isAuthenticated, navigate]);
const mutation = useMutation({
mutationFn: async (data: SetupRequest) => {
// 1. Perform Setup
await performSetup(data);
// 2. Auto Login
await client.post('/auth/login', { email: data.email, password: data.password });
// 3. Update Auth Context
await login();
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['setupStatus'] });
navigate('/');
},
onError: (err: Error) => {
setError(err.message || 'Setup failed');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError(null);
mutation.mutate(formData);
};
if (statusLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
<div className="text-blue-500">Loading...</div>
</div>
);
}
if (status && !status.setupRequired) {
return null; // Will redirect in useEffect
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md">
<div className="flex flex-col items-center">
<img src="/logo.png" alt="Charon" className="h-12 w-auto mb-4" />
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
Welcome to Charon
</h2>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Create your administrator account to get started.
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
<Input
id="name"
name="name"
label="Name"
type="text"
required
placeholder="Admin User"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<div className="relative">
<Input
id="email"
name="email"
label="Email Address"
type="email"
required
placeholder="admin@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''}
/>
{emailValid === false && (
<p className="mt-1 text-xs text-red-500">Please enter a valid email address</p>
)}
</div>
<div className="space-y-1">
<Input
id="password"
name="password"
label="Password"
type="password"
required
placeholder="••••••••"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<PasswordStrengthMeter password={formData.password} />
</div>
</div>
{error && (
<div className="text-red-500 text-sm text-center">
{error}
</div>
)}
<div>
<Button
type="submit"
className="w-full"
isLoading={mutation.isPending}
>
Create Admin Account
</Button>
</div>
</form>
</div>
</div>
);
};
export default Setup;