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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user