mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-23 15:18:04 +02:00
Comptes adherents, boite de reception admin, edition profil
Comptes de connexion adherent - addMember cree desormais un compte users (role member, lie a la fiche) avec un mot de passe temporaire genere - Le mdp temporaire s'affiche sur la fiche admin (/backend/adherents/[id]) jusqu'a la 1ere connexion ; bouton "Reinitialiser le mot de passe" - 1ere connexion : changement de mot de passe force (must_change_password) via /backend/changer-mot-de-passe, redirection geree dans authorized() - users += must_change_password, temp_password (migration 0003) Boite de reception admin (consultation) - Page /backend/demandes : demandes d'adhesion + messages de contact avec statuts (approuvee/rejetee, lu/archive) - Lien sidebar + badge (nouveaux), carte tableau de bord cliquable Espace adherent - L'adherent peut editer sa propre fiche (updateOwnProfile) : infos, description, horaires, tags, image d'entete + logo - Statut/categorie/mise a l'honneur restent reserves au staff Promotions : moderation deja en place, inchangee. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
b4d94f2115
commit
119ad4665a
16 files changed
+1146
-25
No files matched your search
+149
-2
@@ -8,7 +8,9 @@ import { db } from "@/db";
|
||||
import {
|
||||
activityLog,
|
||||
categories,
|
||||
contactMessages,
|
||||
members,
|
||||
membershipRequests,
|
||||
promotions,
|
||||
users,
|
||||
} from "@/db/schema";
|
||||
@@ -40,6 +42,16 @@ async function logActivity(message: string, dot = "#2C6FB3") {
|
||||
await db.insert(activityLog).values({ message, dot });
|
||||
}
|
||||
|
||||
// Mot de passe temporaire lisible (sans caractères ambigus).
|
||||
function generateTempPassword(): string {
|
||||
const alphabet = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||
let out = "";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
out += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function requireRole(): Promise<{ role: AppRole; memberId: number | null; name: string }> {
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Non authentifié");
|
||||
@@ -108,12 +120,35 @@ export async function addMember(formData: FormData) {
|
||||
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
if (!name) return;
|
||||
const email = String(formData.get("email") ?? "").trim();
|
||||
const email = String(formData.get("email") ?? "").trim().toLowerCase();
|
||||
if (!email) throw new Error("L'e-mail est requis pour créer le compte de l'adhérent.");
|
||||
const categoryId = formData.get("categoryId") ? Number(formData.get("categoryId")) : null;
|
||||
const city = String(formData.get("city") ?? "").trim() || null;
|
||||
const status = (String(formData.get("status") ?? "pending") as "active" | "pending");
|
||||
|
||||
await db.insert(members).values({ name, email, categoryId, city, status });
|
||||
// Un seul compte par e-mail.
|
||||
const existing = await db.select({ id: users.id }).from(users).where(eq(users.email, email));
|
||||
if (existing.length > 0) {
|
||||
throw new Error("Un compte existe déjà avec cet e-mail.");
|
||||
}
|
||||
|
||||
const [newMember] = await db
|
||||
.insert(members)
|
||||
.values({ name, email, categoryId, city, status })
|
||||
.returning({ id: members.id });
|
||||
|
||||
// Compte de connexion adhérent avec mot de passe temporaire à changer.
|
||||
const tempPassword = generateTempPassword();
|
||||
await db.insert(users).values({
|
||||
name,
|
||||
email,
|
||||
role: "member",
|
||||
memberId: newMember.id,
|
||||
passwordHash: await bcrypt.hash(tempPassword, 10),
|
||||
tempPassword,
|
||||
mustChangePassword: true,
|
||||
});
|
||||
|
||||
await logActivity(`Nouvel adhérent ajouté : <strong>${name}</strong>`, "#2C6FB3");
|
||||
|
||||
revalidatePath("/backend/adherents");
|
||||
@@ -121,6 +156,31 @@ export async function addMember(formData: FormData) {
|
||||
revalidatePath("/");
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const { role } = await requireRole();
|
||||
if (!can(role, "manageMembers")) throw new Error("Accès refusé");
|
||||
|
||||
const memberId = Number(formData.get("memberId"));
|
||||
if (!memberId) return;
|
||||
|
||||
const [u] = await db.select().from(users).where(eq(users.memberId, memberId));
|
||||
if (!u) throw new Error("Aucun compte de connexion lié à cet adhérent.");
|
||||
|
||||
const tempPassword = generateTempPassword();
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
passwordHash: await bcrypt.hash(tempPassword, 10),
|
||||
tempPassword,
|
||||
mustChangePassword: true,
|
||||
})
|
||||
.where(eq(users.id, u.id));
|
||||
|
||||
revalidatePath(`/backend/adherents/${memberId}`);
|
||||
}
|
||||
|
||||
export async function updateMember(formData: FormData) {
|
||||
const { role } = await requireRole();
|
||||
if (!can(role, "manageMembers")) throw new Error("Accès refusé");
|
||||
@@ -267,6 +327,93 @@ export async function deleteCategory(formData: FormData) {
|
||||
revalidatePath("/annuaire");
|
||||
}
|
||||
|
||||
// ---- Self password change (forced at first login) ----
|
||||
export async function changeOwnPassword(formData: FormData) {
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Non authentifié");
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
const password = String(formData.get("password") ?? "");
|
||||
const confirm = String(formData.get("confirm") ?? "");
|
||||
if (password.length < 8) throw new Error("Le mot de passe doit faire au moins 8 caractères.");
|
||||
if (password !== confirm) throw new Error("Les deux mots de passe ne correspondent pas.");
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
passwordHash: await bcrypt.hash(password, 10),
|
||||
tempPassword: null,
|
||||
mustChangePassword: false,
|
||||
})
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
// Force une nouvelle session (jeton sans le drapeau « mot de passe à changer »).
|
||||
await signOut({ redirectTo: "/login" });
|
||||
}
|
||||
|
||||
// ---- Member: edit own profile ----
|
||||
export async function updateOwnProfile(formData: FormData) {
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Non authentifié");
|
||||
const memberId = session.user.memberId;
|
||||
if (!memberId) throw new Error("Aucune fiche adhérent liée à votre compte.");
|
||||
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
if (!name) return;
|
||||
|
||||
await db
|
||||
.update(members)
|
||||
.set({
|
||||
name,
|
||||
description: String(formData.get("description") ?? "").trim() || null,
|
||||
address: String(formData.get("address") ?? "").trim() || null,
|
||||
postalCode: String(formData.get("postalCode") ?? "").trim() || null,
|
||||
city: String(formData.get("city") ?? "").trim() || null,
|
||||
phone: String(formData.get("phone") ?? "").trim() || null,
|
||||
website: String(formData.get("website") ?? "").trim() || null,
|
||||
hours: String(formData.get("hours") ?? "").trim() || null,
|
||||
tags: String(formData.get("tags") ?? "").trim() || null,
|
||||
coverUrl: String(formData.get("coverUrl") ?? "").trim() || null,
|
||||
logoUrl: String(formData.get("logoUrl") ?? "").trim() || null,
|
||||
})
|
||||
// Sécurité : on ne touche que SA propre fiche, jamais statut/catégorie/mise à l'honneur.
|
||||
.where(eq(members.id, memberId));
|
||||
|
||||
revalidatePath("/backend/espace");
|
||||
revalidatePath(`/adherents/${memberId}`);
|
||||
revalidatePath("/annuaire");
|
||||
revalidatePath("/");
|
||||
}
|
||||
|
||||
// ---- Inbox: membership requests + contact messages ----
|
||||
export async function setRequestStatus(formData: FormData) {
|
||||
const { role } = await requireRole();
|
||||
if (!can(role, "manageMembers")) throw new Error("Accès refusé");
|
||||
const id = Number(formData.get("id"));
|
||||
const status = String(formData.get("status") ?? "");
|
||||
if (!id || !["new", "approved", "rejected"].includes(status)) return;
|
||||
await db
|
||||
.update(membershipRequests)
|
||||
.set({ status: status as "new" | "approved" | "rejected" })
|
||||
.where(eq(membershipRequests.id, id));
|
||||
revalidatePath("/backend/demandes");
|
||||
revalidatePath("/backend");
|
||||
}
|
||||
|
||||
export async function setContactStatus(formData: FormData) {
|
||||
const { role } = await requireRole();
|
||||
if (!can(role, "manageMembers")) throw new Error("Accès refusé");
|
||||
const id = Number(formData.get("id"));
|
||||
const status = String(formData.get("status") ?? "");
|
||||
if (!id || !["new", "read", "archived"].includes(status)) return;
|
||||
await db
|
||||
.update(contactMessages)
|
||||
.set({ status: status as "new" | "read" | "archived" })
|
||||
.where(eq(contactMessages.id, id));
|
||||
revalidatePath("/backend/demandes");
|
||||
revalidatePath("/backend");
|
||||
}
|
||||
|
||||
// ---- Sign out ----
|
||||
export async function doSignOut() {
|
||||
await signOut({ redirectTo: "/" });
|
||||
|
||||
Reference in new issue
Block a user