feat: add 2FA prompt and OTP input to login flow

- Integrated 2FA verification into the login process.
- Added conditional rendering for OTP input.
- Updated UI to support dynamic switching between login and 2FA views.
- Introduced new state variables for managing 2FA logic.
This commit is contained in:
Mathis HERRIOT
2026-01-29 13:49:54 +01:00
parent 13ccdbc2ab
commit 0584c46190

View File

@@ -24,6 +24,12 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { useAuth } from "@/providers/auth-provider";
const loginSchema = z.object({
@@ -36,8 +42,11 @@ const loginSchema = z.object({
type LoginFormValues = z.infer<typeof loginSchema>;
export default function LoginPage() {
const { login } = useAuth();
const { login, verify2fa } = useAuth();
const [loading, setLoading] = React.useState(false);
const [show2fa, setShow2fa] = React.useState(false);
const [userId, setUserId] = React.useState<string | null>(null);
const [otpValue, setOtpValue] = React.useState("");
const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
@@ -50,7 +59,11 @@ export default function LoginPage() {
async function onSubmit(values: LoginFormValues) {
setLoading(true);
try {
await login(values.email, values.password);
const res = await login(values.email, values.password);
if (res.userId && res.message === "Please provide 2FA token") {
setUserId(res.userId);
setShow2fa(true);
}
} catch (_error) {
// Error is handled in useAuth via toast
} finally {
@@ -58,6 +71,20 @@ export default function LoginPage() {
}
}
async function onOtpSubmit(e: React.FormEvent) {
e.preventDefault();
if (!userId || otpValue.length !== 6) return;
setLoading(true);
try {
await verify2fa(userId, otpValue);
} catch (_error) {
// Error handled in useAuth
} finally {
setLoading(false);
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
<div className="w-full max-w-md space-y-4">
@@ -70,45 +97,82 @@ export default function LoginPage() {
</Link>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Connexion</CardTitle>
<CardTitle className="text-2xl">
{show2fa ? "Double Authentification" : "Connexion"}
</CardTitle>
<CardDescription>
Entrez vos identifiants pour accéder à votre compte MemeGoat.
{show2fa
? "Entrez le code à 6 chiffres de votre application d'authentification."
: "Entrez vos identifiants pour accéder à votre compte MemeGoat."}
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="goat@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Mot de passe</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Connexion en cours..." : "Se connecter"}
{show2fa ? (
<form onSubmit={onOtpSubmit} className="space-y-6 flex flex-col items-center">
<InputOTP
maxLength={6}
value={otpValue}
onChange={(value) => setOtpValue(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button type="submit" className="w-full" disabled={loading || otpValue.length !== 6}>
{loading ? "Vérification..." : "Vérifier le code"}
</Button>
<Button
variant="ghost"
className="w-full"
onClick={() => setShow2fa(false)}
disabled={loading}
>
Retour
</Button>
</form>
</Form>
) : (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="goat@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Mot de passe</FormLabel>
<FormControl>
<Input type="password" placeholder="" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Connexion en cours..." : "Se connecter"}
</Button>
</form>
</Form>
)}
</CardContent>
<CardFooter className="flex flex-col space-y-2">
<p className="text-sm text-center text-muted-foreground">