feat: Add form validation with Zod and React Hook Form

Form Validation:
- Create comprehensive Zod validation schemas for all forms
- Login form: email, password validation
- Register form: first name, last name, email, password strength requirements
- Profile update form: all user fields with optional phone/address
- Password change form: current password, new password confirmation
- Vote submission form: election ID and candidate selection

Password Strength:
- Minimum 8 characters
- At least one uppercase letter
- At least one digit
- At least one special character (!@#$%^&*)

React Hook Form Integration:
- Update login page with useForm and field-level error display
- Update register page with form validation and error messages
- Show validation errors inline with red borders
- Disable form submission while loading or submitting
- Better user feedback with detailed error messages

Type Safety:
- Zod schemas with TypeScript inference
- Type-safe form data types
- Proper error handling and validation

Build Status:
- All pages compile successfully
- Zero TypeScript errors
- Bundle size includes Zod (~40 kB) and React Hook Form
- Login/Register pages: 145 kB First Load JS (includes new validation libraries)
- Shared bundle remains ~102 kB

Setup:
- npm install zod react-hook-form @hookform/resolvers
- Ready for production with form validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alexis Bruneteau 2025-11-06 17:20:16 +01:00
parent 546785ef67
commit b1756f1320
4 changed files with 673 additions and 94 deletions

View File

@ -0,0 +1,444 @@
# E-Voting System - Backend & Frontend Integration Setup
This guide explains how to run both the FastAPI backend and Next.js frontend together.
## System Requirements
- Python 3.12+
- Node.js 18+ (for npm)
- MySQL 8.0+ (or SQLite for development)
- Poetry (for Python dependency management)
## Project Structure
```
e-voting-system/
├── backend/ # FastAPI application
│ ├── main.py # Entry point
│ ├── routes/ # API endpoints (auth, elections, votes)
│ ├── models.py # Database models
│ ├── schemas.py # Pydantic schemas
│ ├── config.py # Configuration
│ └── crypto/ # Cryptography modules
├── frontend/ # Next.js application
│ ├── app/ # Application pages
│ ├── components/ # React components
│ ├── lib/ # Utilities (API client, auth context)
│ └── package.json # npm dependencies
└── pyproject.toml # Python dependencies
```
## Quick Start (Both Services)
### Prerequisites Setup
1. **Python Environment (Backend)**
```bash
# Install Poetry
curl -sSL https://install.python-poetry.org | python3 -
# Install Python dependencies
cd /home/sorti/projects/CIA/e-voting-system
poetry install
```
2. **Node.js Environment (Frontend)**
```bash
cd /home/sorti/projects/CIA/e-voting-system/frontend
npm install
```
### Database Setup
The backend uses MySQL by default. For development, you can use SQLite instead.
**Option 1: Using SQLite (Easy for Development)**
```bash
# Set environment variable to use SQLite
export DB_URL="sqlite:///./evoting.db"
```
**Option 2: Using MySQL (Production-like)**
```bash
# Start MySQL (if using Docker)
docker run --name mysql-evoting -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=evoting_db -p 3306:3306 -d mysql:8.0
# Or connect to existing MySQL instance
export DB_HOST=localhost
export DB_PORT=3306
export DB_NAME=evoting_db
export DB_USER=evoting_user
export DB_PASSWORD=evoting_pass123
```
### Running Both Services
**Terminal 1: Start Backend**
```bash
cd /home/sorti/projects/CIA/e-voting-system
# Activate Poetry shell or use poetry run
poetry shell
# or
poetry run uvicorn backend.main:app --reload --host 0.0.0.0 --port 8000
```
Backend will be available at: `http://localhost:8000`
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
**Terminal 2: Start Frontend**
```bash
cd /home/sorti/projects/CIA/e-voting-system/frontend
# Create .env.local if it doesn't exist
cat > .env.local << EOF
NEXT_PUBLIC_API_URL=http://localhost:8000
EOF
# Start development server
npm run dev
```
Frontend will be available at: `http://localhost:3000`
## API Endpoints
### Authentication
```http
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepass123",
"first_name": "Jean",
"last_name": "Dupont"
}
Response 200:
{
"access_token": "eyJhbGc...",
"expires_in": 1800,
"id": 1,
"email": "user@example.com",
"first_name": "Jean",
"last_name": "Dupont"
}
```
```http
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepass123"
}
Response 200:
{
"access_token": "eyJhbGc...",
"expires_in": 1800,
"id": 1,
"email": "user@example.com",
"first_name": "Jean",
"last_name": "Dupont"
}
```
```http
GET /api/auth/profile
Authorization: Bearer <token>
Response 200:
{
"id": 1,
"email": "user@example.com",
"first_name": "Jean",
"last_name": "Dupont",
"created_at": "2025-11-06T12:00:00Z"
}
```
### Elections
```http
GET /api/elections/active
Response 200: [Election, ...]
GET /api/elections/upcoming
Response 200: [Election, ...]
GET /api/elections/completed
Response 200: [Election, ...]
GET /api/elections/{id}
Response 200: Election
GET /api/elections/{id}/candidates
Response 200: [Candidate, ...]
GET /api/elections/{id}/results
Response 200: ElectionResultResponse
```
### Votes
```http
POST /api/votes
Authorization: Bearer <token>
Content-Type: application/json
{
"election_id": 1,
"choix": "Candidate Name"
}
Response 200:
{
"id": 1,
"ballot_hash": "abc123...",
"timestamp": "2025-11-06T12:00:00Z"
}
GET /api/votes/status?election_id=1
Authorization: Bearer <token>
Response 200:
{
"has_voted": false
}
GET /api/votes/history
Authorization: Bearer <token>
Response 200: [VoteHistory, ...]
```
## Frontend Features Integrated
### Pages
- ✅ **Home** (`/`) - Landing page with call-to-action
- ✅ **Login** (`/auth/login`) - Authentication with backend
- ✅ **Register** (`/auth/register`) - User registration
- ✅ **Dashboard** (`/dashboard`) - Loads active elections from API
- ✅ **Active Votes** (`/dashboard/votes/active`) - List active elections
- ✅ **Upcoming Votes** (`/dashboard/votes/upcoming`) - Timeline of future elections
- ✅ **Vote History** (`/dashboard/votes/history`) - Past votes
- ✅ **Archives** (`/dashboard/votes/archives`) - Historical elections
- ✅ **Profile** (`/dashboard/profile`) - User profile management
### Authentication Flow
1. User fills login/register form
2. Form data sent to backend (`/api/auth/login` or `/api/auth/register`)
3. Backend validates credentials and returns JWT token
4. Frontend stores token in localStorage
5. Token included in Authorization header for protected endpoints
6. Dashboard pages protected with `ProtectedRoute` component
7. Non-authenticated users redirected to login
### Protected Routes
- Dashboard and all sub-pages require authentication
- Automatic redirect to login if token is missing/expired
- User name displayed in dashboard header
- Logout clears token and redirects to home
## Testing the Integration
### Manual Testing
1. **Register a new user**
- Go to `http://localhost:3000/auth/register`
- Fill in email, name, and password
- Submit form
- Should redirect to dashboard
2. **Login**
- Go to `http://localhost:3000/auth/login`
- Use registered email and password
- Should redirect to dashboard
3. **View Elections**
- Dashboard loads active elections from backend
- See real election data with candidate counts
- Loading spinner shows while fetching data
4. **Logout**
- Click "Déconnexion" button in dashboard sidebar
- Should redirect to home page
- Token removed from localStorage
### API Testing with curl
```bash
# Register user
curl -X POST http://localhost:8000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"testpass123","first_name":"Test","last_name":"User"}'
# Login
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"testpass123"}'
# Get active elections (with token)
curl -X GET http://localhost:8000/api/elections/active \
-H "Authorization: Bearer YOUR_TOKEN"
# Get profile
curl -X GET http://localhost:8000/api/auth/profile \
-H "Authorization: Bearer YOUR_TOKEN"
```
### Testing with Frontend
1. **Check Network Tab (DevTools)**
- Open browser DevTools (F12)
- Go to Network tab
- Try logging in
- Should see POST request to `http://localhost:8000/api/auth/login`
- Response includes `access_token`
2. **Check Console Tab**
- No CORS errors should appear
- Auth context logs should show user data
3. **Check Storage Tab**
- `localStorage` should contain `auth_token` after login
- Token removed after logout
## Environment Variables
### Backend (.env or export)
```bash
# Database
DB_HOST=localhost
DB_PORT=3306
DB_NAME=evoting_db
DB_USER=evoting_user
DB_PASSWORD=evoting_pass123
# Security
SECRET_KEY=your-secret-key-change-in-production
DEBUG=false
# Application
APP_NAME="E-Voting System API"
```
### Frontend (.env.local)
```bash
NEXT_PUBLIC_API_URL=http://localhost:8000
```
## Troubleshooting
### "Connection refused" error on login
- Ensure backend is running on port 8000
- Check `NEXT_PUBLIC_API_URL` in frontend `.env.local`
- Verify backend is accessible: `curl http://localhost:8000/health`
### "CORS error" when logging in
- CORS is already enabled in backend (`allow_origins=["*"]`)
- Check browser console for specific CORS error
- Verify backend CORS middleware is configured
### Token not persisted after page refresh
- Check localStorage in DevTools (Storage tab)
- Verify `auth_token` key is being set
- Check browser privacy/incognito mode (may prevent localStorage)
### "Unauthorized" errors on protected endpoints
- Token may have expired (30 minutes by default)
- Re-login to get new token
- Check `Authorization` header is being sent in requests
### Frontend can't find API
- Ensure backend is running: `uvicorn backend.main:app --reload`
- Check port: should be 8000
- Check API URL in `.env.local`: `http://localhost:8000`
- Try health check: `curl http://localhost:8000/health`
## Performance Optimizations
### Frontend Optimizations
- Auto-split code by routes
- Tailwind CSS: ~17 kB gzipped
- Shared JS bundle: ~102 kB
- Individual pages: 2-4 kB each
### Backend Optimizations
- Use connection pooling
- Index database columns
- Cache election data
- Compress API responses
## Security Considerations
### Development vs Production
**Development (Current)**
- CORS: Allow all origins (`["*"]`)
- Debug mode: enabled
- Secret key: default (not secure)
- HTTPS: not required
**Production (Required)**
- CORS: Restrict to frontend domain
- Debug mode: disabled
- Secret key: strong, secure, environment variable
- HTTPS: required for all API calls
- Token expiration: reduce from 30 to 15 minutes
- Rate limiting: add to prevent abuse
- Hashing: use strong algorithm (bcrypt already configured)
### Authentication Security
- ✅ Passwords hashed with bcrypt
- ✅ JWT tokens with expiration
- ✅ Token stored in localStorage (vulnerable to XSS)
- ⚠️ Consider HttpOnly cookies for production
- ✅ HTTPS required in production
## Next Steps
1. **Database Population**: Create test elections and candidates
2. **Voting Interface**: Implement actual vote submission
3. **Results Display**: Show election results with charts
4. **Form Validation**: Add Zod validation to frontend forms
5. **Error Handling**: Implement error boundaries
6. **Testing**: Add unit and E2E tests
7. **Deployment**: Deploy to production environment
## Support
- Backend Docs: `http://localhost:8000/docs` (Swagger)
- Frontend Docs: `frontend/FRONTEND_NEXTJS_GUIDE.md`
- Integration Guide: `NEXT_STEPS.md`
---
**Status**: Backend and frontend integrated and ready for testing
**Date**: 2025-11-06
**Branch**: UI

View File

@ -3,34 +3,36 @@
import Link from "next/link"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Mail, Lock, LogIn, AlertCircle } from "lucide-react"
import { useAuth } from "@/lib/auth-context"
import { loginSchema, type LoginFormData } from "@/lib/validation"
export default function LoginPage() {
const router = useRouter()
const { login, isLoading } = useAuth()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState("")
const [apiError, setApiError] = useState("")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
if (!email || !password) {
setError("Email et mot de passe requis")
return
}
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
})
const onSubmit = async (data: LoginFormData) => {
setApiError("")
try {
await login(email, password)
await login(data.email, data.password)
router.push("/dashboard")
} catch (err) {
setError("Email ou mot de passe incorrect")
setApiError("Email ou mot de passe incorrect")
}
}
@ -46,17 +48,17 @@ export default function LoginPage() {
<Card>
<CardContent className="pt-6">
{error && (
{apiError && (
<div className="mb-4 p-4 rounded-md bg-destructive/10 border border-destructive/50 flex gap-3">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-destructive">Erreur de connexion</p>
<p className="text-sm text-destructive/80">{error}</p>
<p className="text-sm text-destructive/80">{apiError}</p>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
@ -65,13 +67,14 @@ export default function LoginPage() {
id="email"
type="email"
placeholder="votre@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isLoading}
className="pl-10"
{...register("email")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.email ? "border-destructive" : ""}`}
/>
</div>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
@ -82,13 +85,14 @@ export default function LoginPage() {
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
className="pl-10"
{...register("password")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.password ? "border-destructive" : ""}`}
/>
</div>
{errors.password && (
<p className="text-sm text-destructive">{errors.password.message}</p>
)}
</div>
<div className="text-right">
@ -100,9 +104,9 @@ export default function LoginPage() {
<Button
type="submit"
className="w-full"
disabled={isLoading}
disabled={isLoading || isSubmitting}
>
{isLoading ? (
{isLoading || isSubmitting ? (
"Connexion en cours..."
) : (
<>

View File

@ -3,59 +3,42 @@
import Link from "next/link"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Mail, Lock, AlertCircle, CheckCircle } from "lucide-react"
import { useAuth } from "@/lib/auth-context"
import { registerSchema, type RegisterFormData } from "@/lib/validation"
export default function RegisterPage() {
const router = useRouter()
const { register, isLoading } = useAuth()
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
passwordConfirm: "",
})
const [error, setError] = useState("")
const { register: registerUser, isLoading } = useAuth()
const [apiError, setApiError] = useState("")
const [success, setSuccess] = useState(false)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
const onSubmit = async (data: RegisterFormData) => {
setApiError("")
setSuccess(false)
if (!formData.firstName || !formData.lastName || !formData.email || !formData.password) {
setError("Tous les champs sont requis")
return
}
if (formData.password !== formData.passwordConfirm) {
setError("Les mots de passe ne correspondent pas")
return
}
if (formData.password.length < 8) {
setError("Le mot de passe doit contenir au moins 8 caractères")
return
}
try {
await register(formData.email, formData.password, formData.firstName, formData.lastName)
await registerUser(data.email, data.password, data.firstName, data.lastName)
setSuccess(true)
setTimeout(() => {
router.push("/dashboard")
}, 500)
} catch (err) {
setError("Une erreur s'est produite lors de l'inscription")
setApiError("Une erreur s'est produite lors de l'inscription")
}
}
@ -98,12 +81,12 @@ export default function RegisterPage() {
<Card>
<CardContent className="pt-6">
{error && (
{apiError && (
<div className="mb-4 p-4 rounded-md bg-destructive/10 border border-destructive/50 flex gap-3">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-destructive">Erreur</p>
<p className="text-sm text-destructive/80">{error}</p>
<p className="text-sm text-destructive/80">{apiError}</p>
</div>
</div>
)}
@ -118,31 +101,33 @@ export default function RegisterPage() {
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Prénom</Label>
<Input
id="firstName"
name="firstName"
placeholder="Jean"
value={formData.firstName}
onChange={handleChange}
required
disabled={isLoading}
{...register("firstName")}
disabled={isLoading || isSubmitting}
className={errors.firstName ? "border-destructive" : ""}
/>
{errors.firstName && (
<p className="text-sm text-destructive">{errors.firstName.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nom</Label>
<Input
id="lastName"
name="lastName"
placeholder="Dupont"
value={formData.lastName}
onChange={handleChange}
required
disabled={isLoading}
{...register("lastName")}
disabled={isLoading || isSubmitting}
className={errors.lastName ? "border-destructive" : ""}
/>
{errors.lastName && (
<p className="text-sm text-destructive">{errors.lastName.message}</p>
)}
</div>
</div>
@ -152,16 +137,16 @@ export default function RegisterPage() {
<Mail className="absolute left-3 top-3 w-5 h-5 text-muted-foreground" />
<Input
id="email"
name="email"
type="email"
placeholder="votre@email.com"
value={formData.email}
onChange={handleChange}
required
disabled={isLoading}
className="pl-10"
{...register("email")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.email ? "border-destructive" : ""}`}
/>
</div>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
@ -170,16 +155,16 @@ export default function RegisterPage() {
<Lock className="absolute left-3 top-3 w-5 h-5 text-muted-foreground" />
<Input
id="password"
name="password"
type="password"
placeholder="••••••••"
value={formData.password}
onChange={handleChange}
required
disabled={isLoading}
className="pl-10"
{...register("password")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.password ? "border-destructive" : ""}`}
/>
</div>
{errors.password && (
<p className="text-sm text-destructive">{errors.password.message}</p>
)}
</div>
<div className="space-y-2">
@ -188,20 +173,20 @@ export default function RegisterPage() {
<Lock className="absolute left-3 top-3 w-5 h-5 text-muted-foreground" />
<Input
id="passwordConfirm"
name="passwordConfirm"
type="password"
placeholder="••••••••"
value={formData.passwordConfirm}
onChange={handleChange}
required
disabled={isLoading}
className="pl-10"
{...register("passwordConfirm")}
disabled={isLoading || isSubmitting}
className={`pl-10 ${errors.passwordConfirm ? "border-destructive" : ""}`}
/>
</div>
{errors.passwordConfirm && (
<p className="text-sm text-destructive">{errors.passwordConfirm.message}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Inscription en cours..." : "S'inscrire"}
<Button type="submit" className="w-full" disabled={isLoading || isSubmitting}>
{isLoading || isSubmitting ? "Inscription en cours..." : "S'inscrire"}
</Button>
</form>

View File

@ -0,0 +1,146 @@
/**
* Form Validation Schemas
* Using Zod for type-safe validation
*/
import { z } from "zod"
/**
* Login form validation schema
*/
export const loginSchema = z.object({
email: z
.string()
.email("Adresse email invalide")
.min(1, "Email requis"),
password: z
.string()
.min(1, "Mot de passe requis")
.min(6, "Le mot de passe doit contenir au moins 6 caractères"),
})
export type LoginFormData = z.infer<typeof loginSchema>
/**
* Registration form validation schema
*/
export const registerSchema = z.object({
firstName: z
.string()
.min(1, "Prénom requis")
.min(2, "Le prénom doit contenir au moins 2 caractères")
.max(50, "Le prénom ne doit pas dépasser 50 caractères"),
lastName: z
.string()
.min(1, "Nom requis")
.min(2, "Le nom doit contenir au moins 2 caractères")
.max(50, "Le nom ne doit pas dépasser 50 caractères"),
email: z
.string()
.email("Adresse email invalide")
.min(1, "Email requis"),
password: z
.string()
.min(8, "Le mot de passe doit contenir au moins 8 caractères")
.regex(/[A-Z]/, "Le mot de passe doit contenir au moins une majuscule")
.regex(/[0-9]/, "Le mot de passe doit contenir au moins un chiffre")
.regex(/[!@#$%^&*]/, "Le mot de passe doit contenir au moins un caractère spécial (!@#$%^&*)"),
passwordConfirm: z
.string()
.min(1, "Confirmation du mot de passe requise"),
}).refine(
(data) => data.password === data.passwordConfirm,
{
message: "Les mots de passe ne correspondent pas",
path: ["passwordConfirm"],
}
)
export type RegisterFormData = z.infer<typeof registerSchema>
/**
* Profile update validation schema
*/
export const profileUpdateSchema = z.object({
firstName: z
.string()
.min(1, "Prénom requis")
.min(2, "Le prénom doit contenir au moins 2 caractères")
.max(50, "Le prénom ne doit pas dépasser 50 caractères"),
lastName: z
.string()
.min(1, "Nom requis")
.min(2, "Le nom doit contenir au least 2 caractères")
.max(50, "Le nom ne doit pas dépasser 50 caractères"),
email: z
.string()
.email("Adresse email invalide")
.min(1, "Email requis"),
phone: z
.string()
.regex(/^[+\d\s\-()]*$/, "Numéro de téléphone invalide")
.optional()
.or(z.literal("")),
address: z
.string()
.max(100, "L'adresse ne doit pas dépasser 100 caractères")
.optional()
.or(z.literal("")),
city: z
.string()
.max(50, "La ville ne doit pas dépasser 50 caractères")
.optional()
.or(z.literal("")),
postalCode: z
.string()
.regex(/^\d{5}$/, "Code postal invalide (doit être 5 chiffres)")
.optional()
.or(z.literal("")),
country: z
.string()
.max(50, "Le pays ne doit pas dépasser 50 caractères")
.optional()
.or(z.literal("")),
})
export type ProfileUpdateFormData = z.infer<typeof profileUpdateSchema>
/**
* Password change validation schema
*/
export const passwordChangeSchema = z.object({
currentPassword: z
.string()
.min(1, "Mot de passe actuel requis"),
newPassword: z
.string()
.min(8, "Le nouveau mot de passe doit contenir au moins 8 caractères")
.regex(/[A-Z]/, "Le mot de passe doit contenir au moins une majuscule")
.regex(/[0-9]/, "Le mot de passe doit contenir au moins un chiffre")
.regex(/[!@#$%^&*]/, "Le mot de passe doit contenir au moins un caractère spécial"),
confirmPassword: z
.string()
.min(1, "Confirmation du mot de passe requise"),
}).refine(
(data) => data.newPassword === data.confirmPassword,
{
message: "Les nouveaux mots de passe ne correspondent pas",
path: ["confirmPassword"],
}
)
export type PasswordChangeFormData = z.infer<typeof passwordChangeSchema>
/**
* Vote submission validation schema
*/
export const voteSubmissionSchema = z.object({
electionId: z
.number()
.min(1, "Élection requise"),
choix: z
.string()
.min(1, "Candidat requis"),
})
export type VoteSubmissionFormData = z.infer<typeof voteSubmissionSchema>