Checkpoint: Évolution v2 complète : nouveau profil Adhérent FEHAP, connexion locale par email/mot de passe (bcrypt), création manuelle d'utilisateurs par les gestionnaires, affectation d'établissements aux adhérents, page de choix de connexion (/login), refonte de la page Admin avec CRUD complet. 33 tests Vitest passés, zéro erreur TypeScript.
This commit is contained in:
205
server/db.ts
205
server/db.ts
@@ -69,7 +69,7 @@ export async function updateUserCgu(userId: number) {
|
||||
await db.update(users).set({ cguAccepted: true, cguAcceptedAt: new Date() }).where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
export async function updateUserSonumRole(userId: number, sonumRole: "referent" | "gestionnaire") {
|
||||
export async function updateUserSonumRole(userId: number, sonumRole: "referent" | "gestionnaire" | "adherent") {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db.update(users).set({ sonumRole }).where(eq(users.id, userId));
|
||||
@@ -390,3 +390,206 @@ export async function getDemandeById(id: number) {
|
||||
const result = await db.select().from(demandesContact).where(eq(demandesContact.id, id)).limit(1);
|
||||
return result[0] ?? null;
|
||||
}
|
||||
|
||||
// ─── Auth locale ──────────────────────────────────────────────────────────────
|
||||
|
||||
import { localCredentials, userEtablissements } from "../drizzle/schema";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
/** Crée un utilisateur local (sans openId OAuth) avec un mot de passe hashé. */
|
||||
export async function createLocalUser(data: {
|
||||
name: string;
|
||||
email: string;
|
||||
sonumRole: "referent" | "gestionnaire" | "adherent";
|
||||
password: string;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
// Vérifier unicité email
|
||||
const existing = await db.select().from(users).where(eq(users.email, data.email)).limit(1);
|
||||
if (existing.length > 0) throw new Error("EMAIL_EXISTS");
|
||||
|
||||
// openId synthétique pour les comptes locaux
|
||||
const syntheticOpenId = `local_${nanoid(16)}`;
|
||||
const passwordHash = await bcrypt.hash(data.password, 12);
|
||||
|
||||
const insertResult = await db.insert(users).values({
|
||||
openId: syntheticOpenId,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
loginMethod: "local",
|
||||
sonumRole: data.sonumRole,
|
||||
cguAccepted: false,
|
||||
lastSignedIn: new Date(),
|
||||
});
|
||||
|
||||
const userId = Number((insertResult as any)[0]?.insertId ?? 0);
|
||||
if (!userId) throw new Error("Failed to create user");
|
||||
|
||||
await db.insert(localCredentials).values({ userId, passwordHash });
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
/** Authentifie un utilisateur par email + mot de passe. Retourne l'utilisateur ou null. */
|
||||
export async function authenticateLocalUser(email: string, password: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
const result = await db
|
||||
.select({
|
||||
user: users,
|
||||
passwordHash: localCredentials.passwordHash,
|
||||
})
|
||||
.from(users)
|
||||
.innerJoin(localCredentials, eq(localCredentials.userId, users.id))
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (!result.length) return null;
|
||||
|
||||
const { user, passwordHash } = result[0];
|
||||
const valid = await bcrypt.compare(password, passwordHash);
|
||||
if (!valid) return null;
|
||||
|
||||
// Mettre à jour lastSignedIn
|
||||
await db.update(users).set({ lastSignedIn: new Date() }).where(eq(users.id, user.id));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/** Vérifie si un utilisateur possède des credentials locaux. */
|
||||
export async function hasLocalCredentials(userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return false;
|
||||
const result = await db.select().from(localCredentials).where(eq(localCredentials.userId, userId)).limit(1);
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
/** Met à jour le mot de passe d'un utilisateur. */
|
||||
export async function updateLocalPassword(userId: number, newPassword: string) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
const passwordHash = await bcrypt.hash(newPassword, 12);
|
||||
const existing = await db.select().from(localCredentials).where(eq(localCredentials.userId, userId)).limit(1);
|
||||
if (existing.length > 0) {
|
||||
await db.update(localCredentials).set({ passwordHash, updatedAt: new Date() }).where(eq(localCredentials.userId, userId));
|
||||
} else {
|
||||
await db.insert(localCredentials).values({ userId, passwordHash });
|
||||
}
|
||||
}
|
||||
|
||||
/** Met à jour les informations d'un utilisateur. */
|
||||
export async function updateUser(userId: number, data: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
sonumRole?: "referent" | "gestionnaire" | "adherent";
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
/** Supprime un utilisateur et ses credentials locaux. */
|
||||
export async function deleteUser(userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db.delete(localCredentials).where(eq(localCredentials.userId, userId));
|
||||
await db.delete(userEtablissements).where(eq(userEtablissements.userId, userId));
|
||||
await db.delete(users).where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
// ─── Affectations Adhérents ↔ Établissements ─────────────────────────────────
|
||||
|
||||
/** Retourne les établissements affectés à un adhérent. */
|
||||
export async function getEtablissementsByAdherent(userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
return db
|
||||
.select({
|
||||
id: etablissements.id,
|
||||
finess: etablissements.finess,
|
||||
nom: etablissements.nom,
|
||||
region: etablissements.region,
|
||||
departement: etablissements.departement,
|
||||
typeActivite: etablissements.typeActivite,
|
||||
tailleEffectifs: etablissements.tailleEffectifs,
|
||||
referentId: etablissements.referentId,
|
||||
visibilite: etablissements.visibilite,
|
||||
accepteMiseEnRelation: etablissements.accepteMiseEnRelation,
|
||||
})
|
||||
.from(userEtablissements)
|
||||
.innerJoin(etablissements, eq(userEtablissements.etablissementId, etablissements.id))
|
||||
.where(eq(userEtablissements.userId, userId))
|
||||
.orderBy(etablissements.nom);
|
||||
}
|
||||
|
||||
/** Retourne les IDs des établissements affectés à un adhérent. */
|
||||
export async function getAffectationsByUser(userId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
const result = await db
|
||||
.select({ etablissementId: userEtablissements.etablissementId })
|
||||
.from(userEtablissements)
|
||||
.where(eq(userEtablissements.userId, userId));
|
||||
return result.map((r) => r.etablissementId);
|
||||
}
|
||||
|
||||
/** Affecte un établissement à un adhérent (idempotent). */
|
||||
export async function assignEtablissementToUser(userId: number, etablissementId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(userEtablissements)
|
||||
.where(and(eq(userEtablissements.userId, userId), eq(userEtablissements.etablissementId, etablissementId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
await db.insert(userEtablissements).values({ userId, etablissementId });
|
||||
}
|
||||
}
|
||||
|
||||
/** Retire un établissement d'un adhérent. */
|
||||
export async function removeEtablissementFromUser(userId: number, etablissementId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db
|
||||
.delete(userEtablissements)
|
||||
.where(and(eq(userEtablissements.userId, userId), eq(userEtablissements.etablissementId, etablissementId)));
|
||||
}
|
||||
|
||||
/** Remplace toutes les affectations d'un adhérent par une nouvelle liste. */
|
||||
export async function setAffectationsForUser(userId: number, etablissementIds: number[]) {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db.delete(userEtablissements).where(eq(userEtablissements.userId, userId));
|
||||
if (etablissementIds.length > 0) {
|
||||
await db.insert(userEtablissements).values(etablissementIds.map((eid) => ({ userId, etablissementId: eid })));
|
||||
}
|
||||
}
|
||||
|
||||
/** Retourne tous les utilisateurs avec leurs affectations. */
|
||||
export async function getAllUsersWithAffectations() {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const allUsers = await db.select().from(users).orderBy(users.name);
|
||||
const allAffectations = await db
|
||||
.select({
|
||||
userId: userEtablissements.userId,
|
||||
etablissementId: userEtablissements.etablissementId,
|
||||
etablissementNom: etablissements.nom,
|
||||
})
|
||||
.from(userEtablissements)
|
||||
.innerJoin(etablissements, eq(userEtablissements.etablissementId, etablissements.id));
|
||||
|
||||
return allUsers.map((u) => ({
|
||||
...u,
|
||||
etablissements: allAffectations
|
||||
.filter((a) => a.userId === u.id)
|
||||
.map((a) => ({ id: a.etablissementId, nom: a.etablissementNom })),
|
||||
hasLocalCredentials: false, // sera enrichi côté router si besoin
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user