veza/apps/web/src/features/auth/components/LoginForm.tsx
senke 93c15f7cae fix: Corriger URL Swagger et finaliser implémentation DeveloperPage
- Ajouter fallback pour Swagger UI si doc.json ne fonctionne pas
- Améliorer message d'erreur avec bouton pour ouvrir Swagger UI directement
- Les fonctionnalités API Keys et Usage Stats sont maintenant complètes et fonctionnelles
- Tous les onglets de DeveloperPage sont maintenant implémentés
2026-01-18 13:55:28 +01:00

214 lines
6.4 KiB
TypeScript

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { formatErrorMessage } from '@/utils/apiErrorHandler';
import type { LoginRequest } from '@/services/api/auth';
import type { ApiError } from '@/schemas/apiSchemas';
import { Input, Button } from '@veza/design-system';
import { logger } from '@/utils/logger';
import { useFormValidation } from '@/hooks/useFormValidation';
import { useEffect } from 'react';
import { useToast } from '@/hooks/useToast';
const loginSchema = z.object({
email: z.string().email('Email invalide'),
password: z.string().min(1, 'Le mot de passe est requis'),
remember_me: z.boolean().optional(),
});
type LoginFormData = z.infer<typeof loginSchema>;
/**
* LoginForm with Kōdō Design System
* MIGRATED: Now using Kōdō Input and Button components
*/
export const LoginForm = () => {
const navigate = useNavigate();
const { login: loginStore, isLoading, error } = useAuthStore();
const { toast } = useToast();
const {
register,
handleSubmit,
formState: { errors },
watch,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
remember_me: false,
},
});
// Action 5.2.1.2: Pre-validation with backend
const formData = watch();
const { validate, errors: backendErrors } = useFormValidation({
type: 'LoginRequest',
debounceMs: 300,
});
// Validate on form data change (debounced)
useEffect(() => {
const hasData = formData.email || formData.password;
if (hasData) {
const loginData: LoginRequest = {
email: formData.email || '',
password: formData.password || '',
remember_me: formData.remember_me || false,
};
validate(loginData);
}
}, [formData.email, formData.password, formData.remember_me, validate]);
const onSubmit = async (data: LoginFormData) => {
try {
const loginRequest: LoginRequest = {
email: data.email,
password: data.password,
remember_me: data.remember_me || false,
};
await loginStore(loginRequest);
// Redirection après succès
navigate('/dashboard');
} catch (err) {
// L'erreur est déjà gérée par le store, mais afficher aussi un toast pour visibilité
const errorMessage = formatErrorMessage(err as ApiError);
logger.error('Login error', {
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
// UI_UX: Afficher un toast pour s'assurer que l'erreur est visible
toast({
title: 'Erreur de connexion',
description: errorMessage,
variant: 'destructive',
});
}
};
// Afficher un toast si une erreur apparaît dans le store (par exemple, erreur réseau)
useEffect(() => {
if (error) {
const errorMessage = formatErrorMessage(error);
toast({
title: 'Erreur de connexion',
description: errorMessage,
variant: 'destructive',
});
}
}, [error, toast]);
return (
<form onSubmit={handleSubmit(onSubmit)} className="w-full space-y-6" style={{ width: '100%', minWidth: '0', maxWidth: '100%', boxSizing: 'border-box' }}>
<h2 className="text-2xl font-heading font-bold text-center text-white">
Connexion
</h2>
{error && (
<div
className="bg-kodo-red/10 border border-kodo-red/50 text-kodo-red px-4 py-4 rounded-lg"
role="alert"
>
<span className="block sm:inline text-sm">
{formatErrorMessage(error as ApiError)}
</span>
</div>
)}
<div className="w-full" style={{ width: '100%', minWidth: '0', maxWidth: '100%', boxSizing: 'border-box' }}>
<Input
label="Email"
type="email"
autoComplete="email"
required
{...register('email')}
placeholder="votre@email.com"
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={errors.email ? 'true' : 'false'}
/>
{errors.email && (
<p
id="email-error"
className="text-kodo-red text-xs mt-1"
role="alert"
>
{errors.email.message}
</p>
)}
{backendErrors
.filter((e) => e.field === 'email')
.map((e, i) => (
<p
key={i}
id="email-error-backend"
className="text-kodo-red text-xs mt-1"
role="alert"
>
{e.message}
</p>
))}
</div>
<div className="w-full" style={{ width: '100%', minWidth: '0', maxWidth: '100%', boxSizing: 'border-box' }}>
<Input
label="Mot de passe"
type="password"
autoComplete="current-password"
required
{...register('password')}
placeholder="••••••••"
aria-describedby={errors.password ? 'password-error' : undefined}
aria-invalid={errors.password ? 'true' : 'false'}
/>
{errors.password && (
<p
id="password-error"
className="text-kodo-red text-xs mt-1"
role="alert"
>
{errors.password.message}
</p>
)}
{backendErrors
.filter((e) => e.field === 'password')
.map((e, i) => (
<p
key={i}
id="password-error-backend"
className="text-kodo-red text-xs mt-1"
role="alert"
>
{e.message}
</p>
))}
</div>
<div className="flex items-center">
<input
type="checkbox"
{...register('remember_me')}
id="remember_me"
className="h-4 w-4 text-kodo-steel focus:ring-kodo-steel border-kodo-steel rounded bg-kodo-graphite"
/>
<label
htmlFor="remember_me"
className="ml-2 block text-sm text-kodo-secondary"
>
Se souvenir de moi
</label>
</div>
<Button
type="submit"
disabled={isLoading}
variant="primary"
className="w-full"
>
{isLoading ? 'Connexion...' : 'Se connecter'}
</Button>
</form>
);
};