From 01cf4dd5a15a5d81370bcf60726e8c0d7b42690c Mon Sep 17 00:00:00 2001 From: streaper2 Date: Mon, 11 May 2026 22:55:30 +0200 Subject: [PATCH] gestion supp user admin --- .env | 5 +- admin/src/app/actions/user.ts | 92 +++++++++ admin/src/app/api/cron/cleanup-users/route.ts | 51 +++++ admin/src/app/users/page.tsx | 18 +- admin/src/components/ClientActions.tsx | 5 + admin/src/components/DeleteAccountButton.tsx | 178 ++++++++++++++++++ admin/src/components/EntrepreneurActions.tsx | 5 + app/actions/auth.ts | 52 +++++ app/api/auth/login/route.ts | 8 + app/auth/reactivate/ReactivateForm.tsx | 85 +++++++++ app/auth/reactivate/page.tsx | 67 +++++++ doc/admin/gestion_utilisateurs.md | 38 ++++ doc/admin/guide_maintenance.md | 35 ++++ doc/admin/specs_fonctionnelles.md | 44 +++++ doc/front/specs_fonctionnelles.md | 40 ++++ next-env.d.ts | 2 +- prisma/schema.prisma | 11 +- 17 files changed, 728 insertions(+), 8 deletions(-) create mode 100644 admin/src/app/api/cron/cleanup-users/route.ts create mode 100644 admin/src/components/DeleteAccountButton.tsx create mode 100644 app/actions/auth.ts create mode 100644 app/auth/reactivate/ReactivateForm.tsx create mode 100644 app/auth/reactivate/page.tsx create mode 100644 doc/admin/gestion_utilisateurs.md create mode 100644 doc/admin/guide_maintenance.md create mode 100644 doc/admin/specs_fonctionnelles.md create mode 100644 doc/front/specs_fonctionnelles.md diff --git a/.env b/.env index 0946db5..73ab351 100644 --- a/.env +++ b/.env @@ -1,6 +1,9 @@ # Connexion locale via le Docker Compose fourni : #DATABASE_URL="postgresql://admin:adminpassword@localhost:5432/afrohub_dev?schema=public" -DATABASE_URL="postgresql://admin:adminpassword@192.168.1.25:5432/afrohub_dev?schema=public" +DATABASE_URL="postgresql://admin:adminpassword@192.168.1.25:5432/afrohub_dev?connect_timeout=300" + + +#SHADOW_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/shadow_db?schema=public" # Exemple avec Supabase ou Neon (si base distante) # DATABASE_URL="postgresql://postgres:[MOT_DE_PASSE]@db.[ID_PROJET].supabase.co:5432/postgres" diff --git a/admin/src/app/actions/user.ts b/admin/src/app/actions/user.ts index dd2acbd..7ee5dd5 100644 --- a/admin/src/app/actions/user.ts +++ b/admin/src/app/actions/user.ts @@ -41,3 +41,95 @@ export async function verifyUserEmail(userId: string) { return { success: false, error: "Erreur lors de la vérification de l'email" }; } } +export async function scheduleUserDeletion(userId: string, reason: string, delayDays: number) { + try { + const user = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!user) return { success: false, error: "Utilisateur non trouvé" }; + + const settings = await prisma.siteSetting.findUnique({ + where: { id: "singleton" } + }); + + const deletionDate = new Date(); + deletionDate.setDate(deletionDate.getDate() + delayDays); + + if (delayDays === 0) { + // Immediate deletion + await prisma.user.delete({ + where: { id: userId } + }); + return { success: true, message: "Supprimé immédiatement" }; + } + + await prisma.user.update({ + where: { id: userId }, + data: { + deletedAt: new Date(), + deletionScheduledAt: deletionDate, + deletionReason: reason + } + }); + + // Send notification email + if (settings?.deleteAccountTemplate) { + const { sendEmail } = await import("@/../../lib/mail"); + const html = settings.deleteAccountTemplate + .replace(/{name}/g, user.name) + .replace(/{siteName}/g, settings.siteName) + .replace(/{deletionDate}/g, delayDays === 0 ? "immédiatement" : deletionDate.toLocaleDateString("fr-FR")) + .replace(/{reason}/g, reason) + .replace(/{reactivateUrl}/g, `${process.env.NEXT_PUBLIC_APP_URL}/auth/reactivate?id=${user.id}`); + + await sendEmail({ + to: user.email, + subject: settings.deleteAccountSubject, + html + }); + } + + return { success: true }; + } catch (error) { + console.error("Failed to schedule user deletion:", error); + return { success: false, error: "Erreur lors de la programmation de la suppression" }; + } +} + +export async function reactivateUser(userId: string, reason: string) { + try { + const user = await prisma.user.update({ + where: { id: userId }, + data: { + deletedAt: null, + deletionScheduledAt: null, + reactivationReason: reason + } + }); + + const settings = await prisma.siteSetting.findUnique({ + where: { id: "singleton" } + }); + + // Send confirmation email + if (settings?.reactivateAccountTemplate) { + const { sendEmail } = await import("@/../../lib/mail"); + const html = settings.reactivateAccountTemplate + .replace(/{name}/g, user.name) + .replace(/{siteName}/g, settings.siteName) + .replace(/{reason}/g, reason); + + await sendEmail({ + to: user.email, + subject: settings.reactivateAccountSubject, + html + }); + } + + return { success: true }; + } catch (error) { + console.error("Failed to reactivate user:", error); + return { success: false, error: "Erreur lors de la réactivation de l'utilisateur" }; + } +} diff --git a/admin/src/app/api/cron/cleanup-users/route.ts b/admin/src/app/api/cron/cleanup-users/route.ts new file mode 100644 index 0000000..244b875 --- /dev/null +++ b/admin/src/app/api/cron/cleanup-users/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +/** + * Route pour le nettoyage automatique des comptes + * À appeler via une tâche cron (ex: chaque nuit à minuit) + * GET /api/cron/cleanup-users?key=VOTRE_CLE_SECRET + */ +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const key = searchParams.get("key"); + + if (key !== process.env.CRON_SECRET) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + try { + const now = new Date(); + + // 1. Trouver les utilisateurs dont le délai est passé + const usersToDelete = await prisma.user.findMany({ + where: { + deletionScheduledAt: { + lte: now + } + } + }); + + if (usersToDelete.length === 0) { + return NextResponse.json({ message: "Aucun compte à supprimer" }); + } + + // 2. Suppression définitive + // Prisma supprimera les données liées si onDelete: Cascade est configuré dans le schéma + const deleteResult = await prisma.user.deleteMany({ + where: { + id: { + in: usersToDelete.map(u => u.id) + } + } + }); + + return NextResponse.json({ + message: `${deleteResult.count} compte(s) supprimé(s) définitivement`, + deletedIds: usersToDelete.map(u => u.id) + }); + } catch (error) { + console.error("Cron Cleanup Error:", error); + return NextResponse.json({ error: "Erreur lors du nettoyage" }, { status: 500 }); + } +} diff --git a/admin/src/app/users/page.tsx b/admin/src/app/users/page.tsx index 5830950..8035e9a 100644 --- a/admin/src/app/users/page.tsx +++ b/admin/src/app/users/page.tsx @@ -6,7 +6,7 @@ import EntrepreneurActions from '@/components/EntrepreneurActions'; import PlanSelector from '@/components/PlanSelector'; import ClientActions from "@/components/ClientActions"; import ManualVerifyButton from "@/components/ManualVerifyButton"; -import { ShieldAlert, ShieldX, ShieldCheck, Users, Briefcase, User, Mail, Phone, MapPin, MailCheck, MailX } from 'lucide-react'; +import { Trash2, ShieldAlert, ShieldX, ShieldCheck, Users, Briefcase, User, Mail, Phone, MapPin, MailCheck, MailX } from 'lucide-react'; import Link from 'next/link'; async function getBusinesses() { @@ -109,7 +109,7 @@ export default async function UsersPage({ ) : ( businesses.map((business: any) => ( - +
@@ -148,7 +148,11 @@ export default async function UsersPage({
- {business.owner?.isSuspended ? ( + {business.owner?.deletedAt ? ( + + Suppression en cours + + ) : business.owner?.isSuspended ? ( Compte Suspendu @@ -218,7 +222,7 @@ export default async function UsersPage({ ) : ( clients.map((client: any) => ( - +
@@ -270,7 +274,11 @@ export default async function UsersPage({
- {client.isSuspended ? ( + {client.deletedAt ? ( + + Suppression en cours + + ) : client.isSuspended ? ( <> Compte Suspendu diff --git a/admin/src/components/ClientActions.tsx b/admin/src/components/ClientActions.tsx index f7d5791..be8ae78 100644 --- a/admin/src/components/ClientActions.tsx +++ b/admin/src/components/ClientActions.tsx @@ -5,6 +5,7 @@ import { ShieldAlert, ShieldCheck } from 'lucide-react'; import { suspendUser, unsuspendUser } from '@/app/actions/suspension'; import { toast } from 'react-hot-toast'; import SuspensionModal from './SuspensionModal'; +import DeleteAccountButton from './DeleteAccountButton'; interface ClientActionsProps { userId: string; @@ -62,6 +63,10 @@ export default function ClientActions({ userId, userName, isSuspended }: ClientA title="Suspendre le compte" subtitle={`Voulez-vous suspendre le compte de ${userName} ? L'utilisateur sera bloqué et recevra votre message.`} /> + +
+ + ); } diff --git a/admin/src/components/DeleteAccountButton.tsx b/admin/src/components/DeleteAccountButton.tsx new file mode 100644 index 0000000..06328af --- /dev/null +++ b/admin/src/components/DeleteAccountButton.tsx @@ -0,0 +1,178 @@ +"use client"; + +import React, { useState } from 'react'; +import { UserX, Loader2 } from 'lucide-react'; +import { scheduleUserDeletion } from '@/app/actions/user'; +import { toast } from 'react-hot-toast'; + +interface DeleteAccountButtonProps { + userId: string; + userName: string; +} + +export default function DeleteAccountButton({ userId, userName }: DeleteAccountButtonProps) { + const [isOpen, setIsOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [reason, setReason] = useState("Non-respect des conditions générales d'utilisation"); + const [customReason, setCustomReason] = useState(""); + const [delay, setDelay] = useState(30); + + const reasons = [ + "Non-respect des conditions générales d'utilisation", + "Inactivité prolongée", + "Comportement inapproprié / Signalements", + "Demande directe de l'utilisateur", + "Violation des règles de publication", + "Autre" + ]; + + const delays = [ + { label: "Immédiat", value: 0 }, + { label: "7 Jours", value: 7 }, + { label: "15 Jours", value: 15 }, + { label: "30 Jours", value: 30 } + ]; + + const handleDelete = async () => { + const finalReason = reason === "Autre" ? customReason : reason; + + if (reason === "Autre" && !customReason.trim()) { + toast.error("Veuillez préciser la raison"); + return; + } + + setIsDeleting(true); + try { + const result = await scheduleUserDeletion(userId, finalReason, delay); + if (result.success) { + toast.success(`La suppression du compte de ${userName} a été programmée.`); + setIsOpen(false); + } else { + toast.error(result.error || "Échec de la programmation de la suppression"); + } + } catch (error) { + toast.error("Une erreur est survenue"); + } finally { + setIsDeleting(false); + } + }; + + return ( + <> + + + {isOpen && ( +
+
+
+
+ +
+
+

Supprimer le compte

+

Configuration de la suppression pour {userName}

+
+
+ +
+ {/* Raison selector */} +
+ + +
+ + {/* Custom Reason Field */} + {reason === "Autre" && ( +
+ +