From 839aa5d045d3c26c5e216580f2e7afc53cd64b37 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 27 Jun 2026 09:42:49 +0200 Subject: [PATCH] Backfill: creer les comptes des adherents existants - Action createMissingMemberAccounts : cree un compte de connexion (mdp temporaire a changer) pour chaque adherent ayant un e-mail mais pas encore de compte. Ignore ceux sans e-mail ou e-mail deja pris. - Banniere sur /backend/adherents : compte des fiches sans compte + bouton "Creer les comptes manquants" (pas besoin de CLI serveur) Co-Authored-By: Claude Opus 4.8 --- src/app/backend/actions.ts | 43 ++++++++++++++++++++++++++++++ src/app/backend/adherents/page.tsx | 40 ++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/app/backend/actions.ts b/src/app/backend/actions.ts index 585ce64..dc1327d 100644 --- a/src/app/backend/actions.ts +++ b/src/app/backend/actions.ts @@ -156,6 +156,49 @@ export async function addMember(formData: FormData) { revalidatePath("/"); } +// Crée des comptes de connexion pour les adhérents existants qui n'en ont pas +// encore (ceux ajoutés avant l'arrivée des comptes adhérent). Chaque compte +// reçoit un mot de passe temporaire à changer à la première connexion. +export async function createMissingMemberAccounts() { + const { role } = await requireRole(); + if (!can(role, "manageMembers")) throw new Error("Accès refusé"); + + const allMembers = await db + .select({ id: members.id, name: members.name, email: members.email }) + .from(members); + const allUsers = await db + .select({ email: users.email, memberId: users.memberId }) + .from(users); + + const takenEmails = new Set(allUsers.map((u) => u.email.toLowerCase())); + const linkedMemberIds = new Set(allUsers.map((u) => u.memberId).filter((x): x is number => x != null)); + + let created = 0; + for (const m of allMembers) { + if (linkedMemberIds.has(m.id)) continue; + const email = (m.email ?? "").trim().toLowerCase(); + if (!email || takenEmails.has(email)) continue; // pas d'e-mail ou déjà pris : on saute + + const tempPassword = generateTempPassword(); + await db.insert(users).values({ + name: m.name, + email, + role: "member", + memberId: m.id, + passwordHash: await bcrypt.hash(tempPassword, 10), + tempPassword, + mustChangePassword: true, + }); + takenEmails.add(email); + created++; + } + + if (created > 0) { + await logActivity(`${created} compte(s) adhérent créé(s) pour les fiches existantes`, "#2C6FB3"); + } + revalidatePath("/backend/adherents"); +} + // Réinitialise le mot de passe d'un adhérent : nouveau mot de passe temporaire // visible par l'admin jusqu'à la prochaine connexion de l'adhérent. export async function resetMemberPassword(formData: FormData) { diff --git a/src/app/backend/adherents/page.tsx b/src/app/backend/adherents/page.tsx index bd36910..df7589b 100644 --- a/src/app/backend/adherents/page.tsx +++ b/src/app/backend/adherents/page.tsx @@ -3,8 +3,9 @@ import { redirect } from "next/navigation"; import { asc, eq, ilike } from "drizzle-orm"; import { auth } from "@/auth"; import { db } from "@/db"; -import { categories, members } from "@/db/schema"; +import { categories, members, users } from "@/db/schema"; import { can } from "@/lib/rbac"; +import { createMissingMemberAccounts } from "../actions"; import { AddMemberPanel } from "./AddMemberPanel"; export const dynamic = "force-dynamic"; @@ -45,10 +46,47 @@ export default async function AdherentsPage({ .where(search ? ilike(members.name, `%${search}%`) : undefined) .orderBy(asc(members.name)); + // Adhérents sans compte de connexion (créés avant l'arrivée des comptes). + const accountRows = await db + .select({ email: users.email, memberId: users.memberId }) + .from(users); + const linkedMemberIds = new Set(accountRows.map((u) => u.memberId).filter((x): x is number => x != null)); + const takenEmails = new Set(accountRows.map((u) => u.email.toLowerCase())); + const allForCount = await db.select({ id: members.id, email: members.email }).from(members); + const missingAccounts = allForCount.filter((m) => { + if (linkedMemberIds.has(m.id)) return false; + const e = (m.email ?? "").trim().toLowerCase(); + return !!e && !takenEmails.has(e); + }).length; + + async function handleBackfill() { + "use server"; + await createMissingMemberAccounts(); + redirect("/backend/adherents"); + } + const COLS = "1.6fr 1fr 1.2fr 0.9fr 0.8fr"; return (
+ {missingAccounts > 0 && ( +
+
+ {missingAccounts} adhérent(s) avec un e-mail n'ont pas encore de compte de connexion. +
+ +
+ )} +