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