diff --git a/backend/.env.example b/backend/.env.example index efb7104..2ca9d51 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,16 +4,10 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/the_tip_top_db" JWT_SECRET="your_secret_key" -APP_URL=http://localhost:5000 -MAIL_HOST=smtp.gmail.com -MAIL_PORT=587 -MAIL_USER=yourmail_adress@gmail.com -MAIL_PASS=_______________________ -MAIL_FROM="Thé Tip Top " +APP_URL="http://localhost:5000" +MAIL_HOST="smtp.gmail.com" +MAIL_PORT="587" +MAIL_USER="engongomo.emma27@gmail.com" +MAIL_PASS="rczo zqps hsnm msdm" +MAIL_FROM="Thé Tip Top " -# SMTP (email verification + password reset) -SMTP_HOST="smtp.gmail.com" -SMTP_PORT="587" -SMTP_USER="engongomo.emma27@gmail.com" -SMTP_PASS="your-gmail-app-password" -SMTP_FROM="BizManager " \ No newline at end of file diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 4773d9f..4810fd6 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -34,8 +34,8 @@ const register = async (req, res) => { const hashedPassword = await bcrypt.hash(password, 10); - const emailVerifyToken = crypto.randomBytes(32).toString("hex"); - const emailVerifyExpires = new Date(Date.now() + 1000 * 60 * 60); // 1h + const emailVerifyToken = String(Math.floor(1000 + Math.random() * 9000)); + const emailVerifyExpires = new Date(Date.now() + 1000 * 60 * 15); // 15 min const user = await prisma.user.create({ data: { @@ -62,23 +62,25 @@ const register = async (req, res) => { }, }); - const verifyUrl = `${process.env.APP_URL}/api/auth/verify-email/${emailVerifyToken}`; - await transporter.sendMail({ from: process.env.MAIL_FROM, to: email, - subject: "Validation de votre compte Thé Tip Top", + subject: "Votre code de validation Thé Tip Top", html: ` -

Bienvenue ${firstname}

-

Merci pour votre inscription.

-

Veuillez cliquer sur le lien ci-dessous pour valider votre compte :

- Valider mon compte -

Ce lien expire dans 1 heure.

+
+

Bienvenue ${firstname} !

+

Merci pour votre inscription sur Thé Tip Top.

+

Voici votre code de validation à 4 chiffres :

+
+ ${emailVerifyToken} +
+

Ce code expire dans 15 minutes.

+
`, }); return res.status(201).json({ - message: "Inscription réussie. Un email de validation a été envoyé.", + message: "Inscription réussie. Un code de validation a été envoyé par email.", user, }); } catch (error) { @@ -148,7 +150,7 @@ const login = async (req, res) => { }); } - if (!user.isEmailVerified) { + if (!user.isEmailVerified && user.role === "CLIENT") { return res.status(403).json({ message: "Veuillez valider votre adresse email avant de vous connecter.", }); @@ -298,12 +300,106 @@ const resetPassword = async (req, res) => { } }; +const verifyCode = async (req, res) => { + try { + const { email, code } = req.body; + + if (!email || !code) { + return res.status(400).json({ message: "Email et code requis." }); + } + + const user = await prisma.user.findUnique({ where: { email } }); + + if (!user) { + return res.status(404).json({ message: "Compte introuvable." }); + } + + if (user.isEmailVerified) { + return res.status(400).json({ message: "Ce compte est déjà vérifié." }); + } + + if (!user.emailVerifyToken || user.emailVerifyToken !== String(code)) { + return res.status(400).json({ message: "Code invalide." }); + } + + if (new Date() > user.emailVerifyExpires) { + return res.status(400).json({ message: "Code expiré. Veuillez vous réinscrire." }); + } + + await prisma.user.update({ + where: { id: user.id }, + data: { isEmailVerified: true, emailVerifyToken: null, emailVerifyExpires: null }, + }); + + const token = jwt.sign( + { id: user.id, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: "24h" } + ); + + return res.json({ + message: "Compte vérifié avec succès.", + token, + user: { + id: user.id, + firstname: user.firstname, + lastname: user.lastname, + email: user.email, + role: user.role, + }, + }); + } catch (error) { + console.error("Erreur verifyCode:", error); + return res.status(500).json({ message: "Erreur serveur lors de la vérification." }); + } +}; + const me = async (req, res) => { return res.json({ user: req.user, }); }; +const deleteAccount = async (req, res) => { + try { + const userId = req.user.id; + + await prisma.$transaction(async (tx) => { + // 1. Supprimer le grand gagnant si c'est lui + await tx.grandPrizeWinner.deleteMany({ where: { userId } }); + + // 2. Récupérer les participations pour supprimer leurs claims + const participations = await tx.participation.findMany({ + where: { userId }, + select: { id: true }, + }); + const participationIds = participations.map((p) => p.id); + + // 3. Supprimer les claims + if (participationIds.length > 0) { + await tx.claim.deleteMany({ where: { participationId: { in: participationIds } } }); + } + + // 4. Supprimer les participations + await tx.participation.deleteMany({ where: { userId } }); + + // 5. Libérer les tickets utilisés par ce user + await tx.ticket.updateMany({ + where: { userId }, + data: { userId: null, isUsed: false, usedAt: null }, + }); + + // 6. Supprimer l'utilisateur + await tx.user.delete({ where: { id: userId } }); + }); + + return res.json({ message: "Compte supprimé avec succès." }); + } catch (error) { + console.error("Erreur deleteAccount:", error); + return res.status(500).json({ message: "Erreur serveur lors de la suppression du compte." }); + } +}; + const changePassword = async (req, res) => { try { const { currentPassword, newPassword } = req.body; @@ -339,7 +435,9 @@ module.exports = { login, me, verifyEmail, + verifyCode, resetPassword, forgotPassword, changePassword, + deleteAccount, }; \ No newline at end of file diff --git a/backend/src/routes/auth.routes.js b/backend/src/routes/auth.routes.js index 7525474..6053cc4 100644 --- a/backend/src/routes/auth.routes.js +++ b/backend/src/routes/auth.routes.js @@ -1,5 +1,5 @@ const express = require("express"); -const { register, login, verifyEmail, forgotPassword, resetPassword, me, changePassword } = require("../controllers/auth.controller"); +const { register, login, verifyEmail, verifyCode, forgotPassword, resetPassword, me, changePassword, deleteAccount } = require("../controllers/auth.controller"); const authMiddleware = require("../middlewares/auth.middleware"); const validate = require("../middlewares/validate.middleware"); const { registerSchema, loginSchema } = require("../validations/auth.validation"); @@ -157,6 +157,8 @@ router.post("/forgot-password", forgotPassword); */ router.post("/reset-password/:token", resetPassword); +router.post("/verify-code", verifyCode); router.patch("/change-password", authMiddleware, changePassword); +router.delete("/account", authMiddleware, deleteAccount); module.exports = router; \ No newline at end of file diff --git a/backend/src/scripts/createAdmin.js b/backend/src/scripts/createAdmin.js index 1255a85..619d456 100644 --- a/backend/src/scripts/createAdmin.js +++ b/backend/src/scripts/createAdmin.js @@ -6,8 +6,13 @@ async function createAdmin() { const password = 'Admin@TipTop2026!' const existing = await prisma.user.findUnique({ where: { email } }) + if (existing) { - console.log('Admin déjà existant :', email) + await prisma.user.update({ + where: { email }, + data: { isEmailVerified: true, role: 'ADMIN' }, + }) + console.log('Admin mis à jour :', email) return } @@ -20,12 +25,13 @@ async function createAdmin() { email, password: hashed, role: 'ADMIN', + isEmailVerified: true, }, }) - console.log(' Admin créé avec succès') - console.log(' Email :', email) - console.log(' Mot de passe :', password) + console.log('Admin créé avec succès') + console.log(' Email :', email) + console.log(' Mot de passe:', password) } createAdmin() diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 477be20..6fe2b02 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { AuthProvider } from './context/AuthContext.jsx' import Layout from './components/Layout.jsx' import ProtectedRoute from './components/ProtectedRoute.jsx' +import CookieBanner from './components/CookieBanner.jsx' import Connexion from './pages/Connexion' import Inscription from './pages/Inscription' import Accueil from './pages/Accueil' @@ -24,7 +25,7 @@ export default function App() { - } /> + } /> } /> } /> @@ -75,6 +76,7 @@ export default function App() { } /> + ) diff --git a/frontend/src/components/CookieBanner.jsx b/frontend/src/components/CookieBanner.jsx new file mode 100644 index 0000000..e68ed36 --- /dev/null +++ b/frontend/src/components/CookieBanner.jsx @@ -0,0 +1,41 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' + +export default function CookieBanner() { + const [visible, setVisible] = useState(() => !localStorage.getItem('cookieConsent')) + + if (!visible) return null + + const handleChoice = (choice) => { + localStorage.setItem('cookieConsent', choice) + setVisible(false) + } + + return ( +
+
+
+

Ce site utilise des cookies

+

+ Nous utilisons des cookies essentiels au fonctionnement du site. En continuant votre navigation, vous acceptez leur utilisation conformément à notre{' '} + politique de confidentialité. +

+
+
+ + +
+
+
+ ) +} diff --git a/frontend/src/hooks/useHandleAuthError.js b/frontend/src/hooks/useHandleAuthError.js index be472df..4e9ddd0 100644 --- a/frontend/src/hooks/useHandleAuthError.js +++ b/frontend/src/hooks/useHandleAuthError.js @@ -1,3 +1,4 @@ +import { useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { useAuth } from '../context/AuthContext' @@ -5,12 +6,12 @@ export function useHandleAuthError() { const { logout } = useAuth() const navigate = useNavigate() - return function handleAuthError(err) { + return useCallback((err) => { if (err.status === 401) { logout() navigate('/connexion') return true } return false - } + }, [logout, navigate]) } diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index e55af6c..67fe444 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -1,24 +1,44 @@ import { useState, useEffect } from 'react' import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi } from '../services/admin' +import { searchTicketApi, deliverClaimApi } from '../services/employee' +import { useHandleAuthError } from '../hooks/useHandleAuthError' function StatCard({ label, value, sub }) { return (
-

{value ?? '—'}

+

{value ?? '—'}

{label}

{sub &&

{sub}

}
) } +function formatDate(dateStr) { + return new Date(dateStr).toLocaleDateString('fr-FR', { + day: '2-digit', month: 'long', year: 'numeric', + hour: '2-digit', minute: '2-digit', + }) +} + export default function Admin() { - const [stats, setStats] = useState(null) - const [users, setUsers] = useState([]) - const [winner, setWinner] = useState(undefined) - const [drawing, setDrawing] = useState(false) + const handleAuthError = useHandleAuthError() + const [stats, setStats] = useState(null) + const [users, setUsers] = useState([]) + const [winner, setWinner] = useState(undefined) + const [drawing, setDrawing] = useState(false) const [drawError, setDrawError] = useState('') - const [loading, setLoading] = useState(true) - const [error, setError] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [showConfirm, setShowConfirm] = useState(false) + + // Remise de lot + const [ticketCode, setTicketCode] = useState('') + const [ticketResult, setTicketResult] = useState(null) + const [ticketSearching, setTicketSearching] = useState(false) + const [ticketDelivering, setTicketDelivering] = useState(false) + const [ticketSearchErr, setTicketSearchErr] = useState('') + const [ticketDeliverErr, setTicketDeliverErr] = useState('') + const [ticketSuccess, setTicketSuccess] = useState(false) useEffect(() => { Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()]) @@ -27,24 +47,66 @@ export default function Admin() { setUsers(u.users) setWinner(w) }) - .catch((e) => setError(e.message)) + .catch((e) => { + if (!handleAuthError(e)) setError(e.message) + }) .finally(() => setLoading(false)) }, []) const handleDraw = async () => { - if (!window.confirm('Lancer le tirage au sort ? Cette action est irréversible.')) return + setShowConfirm(false) setDrawing(true) setDrawError('') try { const data = await drawWinnerApi() setWinner(data) } catch (e) { - setDrawError(e.message) + if (!handleAuthError(e)) setDrawError(e.message) } finally { setDrawing(false) } } + const handleTicketSearch = async (e) => { + e.preventDefault() + setTicketResult(null) + setTicketSearchErr('') + setTicketDeliverErr('') + setTicketSuccess(false) + setTicketSearching(true) + try { + const data = await searchTicketApi(ticketCode.trim()) + setTicketResult(data) + } catch (err) { + if (!handleAuthError(err)) setTicketSearchErr(err.message) + } finally { + setTicketSearching(false) + } + } + + const handleTicketDeliver = async () => { + if (!ticketResult?.claim?.id) return + setTicketDeliverErr('') + setTicketDelivering(true) + try { + await deliverClaimApi(ticketResult.claim.id) + setTicketResult((prev) => ({ + ...prev, + claim: { ...prev.claim, status: 'DELIVERED', claimedAt: new Date().toISOString() }, + })) + setTicketSuccess(true) + setStats((prev) => prev ? { + ...prev, + claimsPending: Math.max(0, (prev.claimsPending ?? 1) - 1), + claimsDelivered: (prev.claimsDelivered ?? 0) + 1, + } : prev) + } catch (err) { + if (!handleAuthError(err)) setTicketDeliverErr(err.message) + } finally { + setTicketDelivering(false) + } + } + if (loading) return (

Chargement...

@@ -57,6 +119,8 @@ export default function Admin() {
) + const winnerUser = winner?.winner?.user + return (
@@ -98,7 +162,7 @@ export default function Admin() { {lot.name} {lot.percentage}% {lot._count.tickets.toLocaleString('fr-FR')} - {lot._count.participations} + {lot._count.participations} ))} @@ -107,31 +171,147 @@ export default function Admin() {
)} - {/* Tirage au sort */} + {/* ── Remise de lot ── */}
-

Grand tirage au sort

+

Remise de lot

- {winner?.winner ? ( -
-

🏆 Gagnant tiré au sort

-

- {winner.winner.user.firstname} {winner.winner.user.lastname} -

-

{winner.winner.user.email}

-

- Lot : {winner.winner.prize} -

+
+ setTicketCode(e.target.value.toUpperCase())} + placeholder="Code ticket (10 caractères)" + maxLength={10} + className="flex-1 border border-gray-200 rounded-lg px-4 py-2.5 text-sm font-mono uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-[#006B6B] focus:border-[#006B6B]" + /> + +
+ + {ticketSearchErr && ( +

{ticketSearchErr}

+ )} + + {ticketResult && ( +
+ {ticketSuccess && ( +
+ + + +

Lot remis avec succès.

+
+ )} + +
+
+

{ticketResult.lot.name}

+

{ticketResult.lot.description}

+ {ticketResult.lot.value &&

{ticketResult.lot.value} €

} +
+ + {ticketResult.claim?.status === 'DELIVERED' ? 'Remis' + : ticketResult.claim?.status === 'CANCELLED' ? 'Expiré' + : 'À remettre'} + +
+ +
+
+ {(ticketResult.client.firstname?.[0] ?? '?').toUpperCase()}{(ticketResult.client.lastname?.[0] ?? '?').toUpperCase()} +
+
+

{ticketResult.client.firstname} {ticketResult.client.lastname}

+

{ticketResult.client.email}

+
+
+ +
+ {ticketDeliverErr &&

{ticketDeliverErr}

} + + {ticketResult.claim?.status === 'PENDING' && !ticketSuccess && ( + + )} + +
+
+ )} +
+ + {/* ── Tirage au sort ── */} +
+

Grand tirage au sort

+ + {winnerUser ? ( + /* Gagnant déjà tiré */ +
+
+
+ {(winnerUser.firstname?.[0] ?? '?').toUpperCase()}{(winnerUser.lastname?.[0] ?? '?').toUpperCase()} +
+
+

Gagnant du grand tirage

+

{winnerUser.firstname} {winnerUser.lastname}

+

{winnerUser.email}

+
+
+ + + +
+
+ {winner?.winner?.createdAt && ( +

+ Tirage effectué le {formatDate(winner.winner.createdAt)} +

+ )}
) : ( -
+ /* Pas encore de tirage */ +
+
+

+ {stats?.participations ?? 0} participations enregistrées +

+

+ {stats?.users ?? 0} participants éligibles +

+
+ + {drawError && ( +

{drawError}

+ )} + - {drawError &&

{drawError}

}
)}
@@ -164,7 +344,7 @@ export default function Admin() { {user.firstname} {user.lastname} {user.email} - + {user._count.participations} @@ -175,6 +355,51 @@ export default function Admin() {
+ + {/* ── Modal de confirmation du tirage ── */} + {showConfirm && ( +
+
+ +
+
+ + + +
+
+ +

Lancer le tirage au sort ?

+

+ Un gagnant sera sélectionné aléatoirement parmi les {stats?.users} participants. +

+ +
+ + + +

Cette action est irréversible. Le tirage ne peut être effectué qu'une seule fois.

+
+ +
+ + +
+ +
+
+ )} +
) } diff --git a/frontend/src/pages/Inscription.jsx b/frontend/src/pages/Inscription.jsx index 9f7521e..850fd0c 100644 --- a/frontend/src/pages/Inscription.jsx +++ b/frontend/src/pages/Inscription.jsx @@ -1,32 +1,30 @@ -import { useState, useEffect } from 'react' +import { useState, useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' -import { registerApi } from '../services/auth' +import { registerApi, verifyCodeApi } from '../services/auth' +import { useAuth } from '../context/AuthContext' export default function Inscription() { const navigate = useNavigate() - const [form, setForm] = useState({ email: '', lastName: '', firstName: '', password: '' }) + const { login } = useAuth() + + // Étape : 'form' | 'verify' + const [step, setStep] = useState('form') + const [email, setEmail] = useState('') + const [form, setForm] = useState({ email: '', lastName: '', firstName: '', password: '' }) const [acceptedTerms, setAcceptedTerms] = useState(false) - const [showPassword, setShowPassword] = useState(false) - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - const [successName, setSuccessName] = useState(null) - const [countdown, setCountdown] = useState(5) + const [showPassword, setShowPassword] = useState(false) + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) - useEffect(() => { - if (successName === null) return - if (countdown === 0) { - navigate('/connexion', { state: { registered: true } }) - return - } - const t = setTimeout(() => setCountdown((c) => c - 1), 1000) - return () => clearTimeout(t) - }, [successName, countdown, navigate]) + // Code à 4 chiffres + const [digits, setDigits] = useState(['', '', '', '']) + const [codeError, setCodeError] = useState('') + const [codeLoading, setCodeLoading] = useState(false) + const inputRefs = [useRef(), useRef(), useRef(), useRef()] - const handleChange = (e) => { - const { name, value } = e.target - setForm({ ...form, [name]: value }) - } + const handleChange = (e) => setForm({ ...form, [e.target.name]: e.target.value }) + // Étape 1 — Inscription const handleSubmit = async (e) => { e.preventDefault() setError('') @@ -38,7 +36,8 @@ export default function Inscription() { lastname: form.lastName, password: form.password, }) - setSuccessName(form.firstName) + setEmail(form.email) + setStep('verify') } catch (err) { setError(err.message) } finally { @@ -46,9 +45,106 @@ export default function Inscription() { } } + // Étape 2 — Saisie du code + const handleDigit = (index, value) => { + if (!/^\d?$/.test(value)) return + const next = [...digits] + next[index] = value + setDigits(next) + if (value && index < 3) inputRefs[index + 1].current?.focus() + } + + const handleDigitKeyDown = (index, e) => { + if (e.key === 'Backspace' && !digits[index] && index > 0) { + inputRefs[index - 1].current?.focus() + } + } + + const handleVerify = async (e) => { + e.preventDefault() + const code = digits.join('') + if (code.length !== 4) return + setCodeError('') + setCodeLoading(true) + try { + const data = await verifyCodeApi({ email, code }) + login(data.user, data.token) + navigate('/saisir-code-unique') + } catch (err) { + setCodeError(err.message) + setDigits(['', '', '', '']) + inputRefs[0].current?.focus() + } finally { + setCodeLoading(false) + } + } + + // ── Étape 2 : Vérification du code ── + if (step === 'verify') { + return ( +
+
+
+ + + +
+

Vérifiez votre email

+

+ Un code à 4 chiffres a été envoyé à
+ {email} +

+
+ + {codeError && ( +
+ {codeError} +
+ )} + +
+
+ {digits.map((d, i) => ( + handleDigit(i, e.target.value)} + onKeyDown={(e) => handleDigitKeyDown(i, e)} + className="w-14 h-14 text-center text-2xl font-bold border-2 border-gray-200 rounded-xl focus:outline-none focus:border-[#006B6B] focus:ring-1 focus:ring-[#006B6B] transition-colors" + /> + ))} +
+ + +
+ +

+ Le code expire dans 15 minutes.{' '} + +

+
+ ) + } + + // ── Étape 1 : Formulaire d'inscription ── return (
-

S'inscrire

Créer votre compte

@@ -62,70 +158,30 @@ export default function Inscription() {
- - + +
- - + +
- - + +
- +
- - @@ -183,28 +227,6 @@ export default function Inscription() { Vous avez déjà un compte ?{' '} Se connecter

- - {/* Modal succès */} - {successName !== null && ( -
-
-
-
- - - -
-
-

- Bienvenue {successName} !! -

-

- Votre compte a été créé avec succès. Un email de validation vous a été envoyé. Vous serez redirigé vers la connexion dans{' '} - {countdown} secondes. -

-
-
- )}
) } diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index da50573..60aa83b 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -1,12 +1,20 @@ import { useState } from 'react' +import { useNavigate } from 'react-router-dom' import UserNav from '../components/UserNav' -import { changePasswordApi } from '../services/auth' +import { changePasswordApi, deleteAccountApi } from '../services/auth' import { useHandleAuthError } from '../hooks/useHandleAuthError' +import { useAuth } from '../context/AuthContext' export default function Profile() { const user = JSON.parse(localStorage.getItem('user') || '{}') const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase() const handleAuthError = useHandleAuthError() + const { logout } = useAuth() + const navigate = useNavigate() + + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [deleting, setDeleting] = useState(false) + const [deleteError, setDeleteError] = useState('') const [pwForm, setPwForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }) const [showPw, setShowPw] = useState({ current: false, new: false, confirm: false }) diff --git a/frontend/src/pages/SaisirCodeUnique.jsx b/frontend/src/pages/SaisirCodeUnique.jsx index ffc11b1..64da0d9 100644 --- a/frontend/src/pages/SaisirCodeUnique.jsx +++ b/frontend/src/pages/SaisirCodeUnique.jsx @@ -47,7 +47,7 @@ export default function SaisirCodeUnique() { } return ( -
+
diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index 2858198..eb55d8a 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -28,6 +28,28 @@ export async function registerApi({ email, firstname, lastname, password }) { return data } +export async function verifyCodeApi({ email, code }) { + const res = await fetch(`${BASE}/verify-code`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, code }), + }) + const data = await res.json() + if (!res.ok) throw apiError(data.message || 'Code invalide.', res.status) + return data +} + +export async function deleteAccountApi() { + const token = localStorage.getItem('token') + const res = await fetch(`${BASE}/account`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }) + const data = await res.json() + if (!res.ok) throw apiError(data.message || 'Erreur lors de la suppression.', res.status) + return data +} + export async function changePasswordApi({ currentPassword, newPassword }) { const token = localStorage.getItem('token') const res = await fetch(`${BASE}/change-password`, {