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:
parent
546785ef67
commit
b1756f1320
444
e-voting-system/INTEGRATION_SETUP.md
Normal file
444
e-voting-system/INTEGRATION_SETUP.md
Normal 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
|
||||
@ -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..."
|
||||
) : (
|
||||
<>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
146
e-voting-system/frontend/lib/validation.ts
Normal file
146
e-voting-system/frontend/lib/validation.ts
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user