refactored Authentication

This commit is contained in:
2026-02-17 16:51:47 +00:00
parent 13e8866875
commit d32d0f5c03
45 changed files with 1322 additions and 66 deletions
@@ -0,0 +1,63 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/Card';
import { useForm } from 'hooks/useForm';
export function Bootstrap() {
const authClient = useClient('/api/auth');
const [isSubmitting, setIsSubmitting] = useState(false);
const [done, setDone] = useState(false);
const { formRef, state, isValid } = useForm({}, (state) => !!state.email);
const handleBootstrap = async () => {
const trimmed = state.email.trim();
if (!trimmed || isSubmitting) return;
setIsSubmitting(true);
try {
await authClient.post('/bootstrap', { email: trimmed });
setDone(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Bootstrap failed. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<Card>
{done ? (
<div className="text-center">
<h2 className="text-2xl font-bold text-duck-dark mb-2">Check your email</h2>
<div className="text-sm text-duck-dark/60">
<p>A verification link has been sent to <strong>{state.email}</strong>.</p>
<p>Click it to activate your account.</p>
</div>
</div>
) : (
<>
<h2 className="text-2xl font-bold text-duck-dark mb-2">Welcome, Admin</h2>
<p className="text-sm text-duck-dark/60 mb-6">Enter your email to create your administrator account.</p>
<form ref={formRef} className="flex gap-2">
<input
type="email"
name="email"
placeholder="admin@example.com"
className="flex-1 px-4 py-2.5 rounded-lg border-2 border-duck-dark/20 text-sm text-duck-dark placeholder:!text-duck-dark/30 focus:outline-none focus:border-duck-teal/50"
/>
<Button
onClick={handleBootstrap}
disabled={!isValid || isSubmitting}
className="bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold px-6 py-2.5 rounded-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Sending...' : 'Bootstrap'}
</Button>
</form>
</>
)}
</Card>
);
}
@@ -0,0 +1,27 @@
import { OfficerSvgLogo } from '@/components/Logos';
import { useLandingPage } from '@/state/useLandingPage';
import { Bootstrap } from './Bootstrap';
import { Login } from './Login';
export function LandingPage() {
const { registrationOpen, isLoading } = useLandingPage();
if (isLoading) return null;
return (
<div className="relative overflow-hidden h-dvh outline-none inset-0">
<header className="pt-12 md:pt-20 lg:pt-24 xl:pt-0 z-20 flex justify-center">
<div className="hidden md:block md:pt-28">
<OfficerSvgLogo size="2xl" />
</div>
<div className="block md:hidden">
<OfficerSvgLogo size="lg" />
</div>
</header>
<div className="absolute inset-x-0 bottom-0 z-20 flex justify-center pb-0 md:pb-12">
{registrationOpen ? <Bootstrap /> : <Login />}
</div>
</div>
);
}
@@ -0,0 +1,103 @@
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Link } from 'react-router';
import { Card } from '@/components/Card';
import { useNavigate } from 'react-router';
import { useState } from 'react';
import { toast } from 'sonner';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
export function Login() {
const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false);
const { state, formRef, update, isValid } = useForm<LoginFormState>({}, validateForm);
const { signin } = useAuth();
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting) return;
setIsSubmitting(true);
try {
await signin({ email: state.email!, password: state.password! });
window.location.href = '/';
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Login failed. Please try again.');
update({ ...state, password: '' });
} finally {
setIsSubmitting(false);
}
};
return (
<Card className="md:py-12 md:px-24 flex flex-col gap-6">
<div className="text-center">
<div className="text-duck-dark text-2xl font-bold">Welcome Back</div>
<div className="text-duck-dark/60">Sign in to your account</div>
</div>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="email"
name="email"
placeholder="you@example.com"
autoComplete="email"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="password"
placeholder="Your password"
autoComplete="current-password"
/>
</Label>
<div className="pt-2">
<Button
type="submit"
disabled={!isValid || isSubmitting}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</div>
<div className="text-center text-sm text-duck-dark/60 grid gap-1">
{/* <p> */}
{/* Don't have an account?{' '} */}
{/* <Link to="/auth/register" className="text-duck-forest underline hover:text-duck-forest/80"> */}
{/* Register */}
{/* </Link> */}
{/* </p> */}
<p>
<Link to="/auth/forgot-password" className="text-duck-forest underline hover:text-duck-forest/80">
Forgot password?
</Link>
</p>
</div>
</form>
</Card>
);
}
type LoginFormState = {
email?: string;
password?: string;
};
const validateForm = (state: Partial<LoginFormState>) => {
const { email, password } = state;
if (!email || !password) return false;
return true;
};
@@ -0,0 +1 @@
export * from './LandingPage';