- 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
236 lines
7 KiB
TypeScript
236 lines
7 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 { RegisterRequest } 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 registerSchema = z
|
|
.object({
|
|
email: z.string().email('Email invalide'),
|
|
username: z
|
|
.string()
|
|
.min(3, "Le nom d'utilisateur doit contenir au moins 3 caractères"),
|
|
password: z
|
|
.string()
|
|
.min(12, 'Le mot de passe doit contenir au moins 12 caractères'),
|
|
password_confirm: z.string().min(1, 'La confirmation est requise'),
|
|
})
|
|
.refine((data) => data.password === data.password_confirm, {
|
|
message: 'Les mots de passe ne correspondent pas',
|
|
path: ['password_confirm'],
|
|
});
|
|
|
|
type RegisterFormData = z.infer<typeof registerSchema>;
|
|
|
|
/**
|
|
* RegisterForm with Kōdō Design System
|
|
* MIGRATED: Now using Kōdō Input and Button components
|
|
*/
|
|
export const RegisterForm = () => {
|
|
const navigate = useNavigate();
|
|
const { register: registerStore, isLoading, error } = useAuthStore();
|
|
const { toast } = useToast();
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
watch,
|
|
} = useForm<RegisterFormData>({
|
|
resolver: zodResolver(registerSchema),
|
|
defaultValues: {
|
|
email: '',
|
|
username: '',
|
|
password: '',
|
|
password_confirm: '',
|
|
},
|
|
});
|
|
|
|
// Action 5.2.1.2: Pre-validation with backend
|
|
const formData = watch();
|
|
const { validate, errors: backendErrors } = useFormValidation({
|
|
type: 'RegisterRequest',
|
|
debounceMs: 300,
|
|
});
|
|
|
|
// Validate on form data change (debounced)
|
|
useEffect(() => {
|
|
const hasData =
|
|
formData.email || formData.username || formData.password || formData.password_confirm;
|
|
if (hasData) {
|
|
// Backend expects password_confirmation, not password_confirm
|
|
const registerData = {
|
|
email: formData.email || '',
|
|
username: formData.username || '',
|
|
password: formData.password || '',
|
|
password_confirmation: formData.password_confirm || '',
|
|
};
|
|
validate(registerData);
|
|
}
|
|
}, [formData.email, formData.username, formData.password, formData.password_confirm, validate]);
|
|
|
|
const onSubmit = async (data: RegisterFormData) => {
|
|
try {
|
|
const registerRequest: RegisterRequest = {
|
|
email: data.email,
|
|
username: data.username,
|
|
password: data.password,
|
|
password_confirm: data.password_confirm,
|
|
};
|
|
await registerStore(registerRequest);
|
|
|
|
// Redirection après succès
|
|
// Si l'utilisateur est authentifié (token reçu), aller au dashboard
|
|
// Sinon (email vérification requise), aller au login
|
|
const { isAuthenticated } = useAuthStore.getState();
|
|
if (isAuthenticated) {
|
|
navigate('/dashboard');
|
|
} else {
|
|
navigate('/login');
|
|
}
|
|
} 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('Register 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 d'inscription",
|
|
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 d'inscription",
|
|
description: errorMessage,
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
}, [error, toast]);
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
<h2 className="text-2xl font-heading font-bold text-center text-white">
|
|
Inscription
|
|
</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>
|
|
<Input
|
|
label="Email"
|
|
type="email"
|
|
{...register('email')}
|
|
placeholder="votre@email.com"
|
|
/>
|
|
{errors.email && (
|
|
<p className="text-kodo-red text-xs mt-1">{errors.email.message}</p>
|
|
)}
|
|
{backendErrors
|
|
.filter((e) => e.field === 'email')
|
|
.map((e, i) => (
|
|
<p key={i} className="text-kodo-red text-xs mt-1">
|
|
{e.message}
|
|
</p>
|
|
))}
|
|
</div>
|
|
|
|
<div>
|
|
<Input
|
|
label="Nom d'utilisateur"
|
|
type="text"
|
|
{...register('username')}
|
|
placeholder="utilisateur123"
|
|
/>
|
|
{errors.username && (
|
|
<p className="text-kodo-red text-xs mt-1">
|
|
{errors.username.message}
|
|
</p>
|
|
)}
|
|
{backendErrors
|
|
.filter((e) => e.field === 'username')
|
|
.map((e, i) => (
|
|
<p key={i} className="text-kodo-red text-xs mt-1">
|
|
{e.message}
|
|
</p>
|
|
))}
|
|
</div>
|
|
|
|
<div>
|
|
<Input
|
|
label="Mot de passe"
|
|
type="password"
|
|
{...register('password')}
|
|
placeholder="••••••••••••"
|
|
/>
|
|
{errors.password && (
|
|
<p className="text-kodo-red text-xs mt-1">
|
|
{errors.password.message}
|
|
</p>
|
|
)}
|
|
{backendErrors
|
|
.filter((e) => e.field === 'password')
|
|
.map((e, i) => (
|
|
<p key={i} className="text-kodo-red text-xs mt-1">
|
|
{e.message}
|
|
</p>
|
|
))}
|
|
</div>
|
|
|
|
<div>
|
|
<Input
|
|
label="Confirmer le mot de passe"
|
|
type="password"
|
|
{...register('password_confirm')}
|
|
placeholder="••••••••••••"
|
|
/>
|
|
{errors.password_confirm && (
|
|
<p className="text-kodo-red text-xs mt-1">
|
|
{errors.password_confirm.message}
|
|
</p>
|
|
)}
|
|
{backendErrors
|
|
.filter((e) => e.field === 'password_confirmation' || e.field === 'password_confirm')
|
|
.map((e, i) => (
|
|
<p key={i} className="text-kodo-red text-xs mt-1">
|
|
{e.message}
|
|
</p>
|
|
))}
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
variant="primary"
|
|
className="w-full"
|
|
>
|
|
{isLoading ? 'Inscription...' : "S'inscrire"}
|
|
</Button>
|
|
</form>
|
|
);
|
|
};
|