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:
Manus
2026-04-16 04:02:22 -04:00
parent 522d2e3c87
commit a58296774f
17 changed files with 2321 additions and 146 deletions

View File

@@ -0,0 +1,20 @@
CREATE TABLE `local_credentials` (
`id` int AUTO_INCREMENT NOT NULL,
`userId` int NOT NULL,
`passwordHash` varchar(255) NOT NULL,
`createdAt` timestamp NOT NULL DEFAULT (now()),
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT `local_credentials_id` PRIMARY KEY(`id`),
CONSTRAINT `local_credentials_userId_unique` UNIQUE(`userId`)
);
--> statement-breakpoint
CREATE TABLE `user_etablissements` (
`id` int AUTO_INCREMENT NOT NULL,
`userId` int NOT NULL,
`etablissementId` int NOT NULL,
`createdAt` timestamp NOT NULL DEFAULT (now()),
CONSTRAINT `user_etablissements_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `users` MODIFY COLUMN `openId` varchar(64);--> statement-breakpoint
ALTER TABLE `users` MODIFY COLUMN `sonumRole` enum('referent','gestionnaire','adherent') NOT NULL DEFAULT 'referent';

View File

@@ -0,0 +1,785 @@
{
"version": "5",
"dialect": "mysql",
"id": "71420563-53eb-41c0-b873-b65ca13fb3fb",
"prevId": "0242a5e6-a3ef-4093-887e-4d05cea98433",
"tables": {
"blocs_fonctionnels": {
"name": "blocs_fonctionnels",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"estValide": {
"name": "estValide",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"blocs_fonctionnels_id": {
"name": "blocs_fonctionnels_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"consultations": {
"name": "consultations",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"etablissementId": {
"name": "etablissementId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"consultePar": {
"name": "consultePar",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"consulteParNom": {
"name": "consulteParNom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"consultations_id": {
"name": "consultations_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"demandes_contact": {
"name": "demandes_contact",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"etablissementCibleId": {
"name": "etablissementCibleId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"demandeurId": {
"name": "demandeurId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"demandeurNom": {
"name": "demandeurNom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"demandeurEmail": {
"name": "demandeurEmail",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"statut": {
"name": "statut",
"type": "enum('en_attente','repondu','ferme')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'en_attente'"
},
"reponse": {
"name": "reponse",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reponsePar": {
"name": "reponsePar",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reponduAt": {
"name": "reponduAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"demandes_contact_id": {
"name": "demandes_contact_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"editeurs": {
"name": "editeurs",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"estValide": {
"name": "estValide",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"editeurs_id": {
"name": "editeurs_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"etablissements": {
"name": "etablissements",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"finess": {
"name": "finess",
"type": "varchar(20)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"region": {
"name": "region",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"departement": {
"name": "departement",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"typeActivite": {
"name": "typeActivite",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"tailleEffectifs": {
"name": "tailleEffectifs",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"referentId": {
"name": "referentId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"visibilite": {
"name": "visibilite",
"type": "enum('tous','gestionnaires')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'tous'"
},
"accepteMiseEnRelation": {
"name": "accepteMiseEnRelation",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"etablissements_id": {
"name": "etablissements_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"local_credentials": {
"name": "local_credentials",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"passwordHash": {
"name": "passwordHash",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"local_credentials_id": {
"name": "local_credentials_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"local_credentials_userId_unique": {
"name": "local_credentials_userId_unique",
"columns": [
"userId"
]
}
},
"checkConstraint": {}
},
"logiciels_etablissements": {
"name": "logiciels_etablissements",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"etablissementId": {
"name": "etablissementId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"solutionId": {
"name": "solutionId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"etatDeploiement": {
"name": "etatDeploiement",
"type": "enum('demarrage','en_cours','operationnel','en_remplacement')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"modeHebergement": {
"name": "modeHebergement",
"type": "enum('hds','on_premise','hybride')",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"modeFacturation": {
"name": "modeFacturation",
"type": "enum('saas','achat_maintenance','location')",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"interoperabilite": {
"name": "interoperabilite",
"type": "enum('non','oui_interface','oui_eai')",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"versionMajeure": {
"name": "versionMajeure",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"commentaire": {
"name": "commentaire",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"contactNom": {
"name": "contactNom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"contactFonction": {
"name": "contactFonction",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"contactEmail": {
"name": "contactEmail",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"saisiePar": {
"name": "saisiePar",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"logiciels_etablissements_id": {
"name": "logiciels_etablissements_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"solutions": {
"name": "solutions",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"editeurId": {
"name": "editeurId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"blocFonctionnelId": {
"name": "blocFonctionnelId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"estValide": {
"name": "estValide",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"solutions_id": {
"name": "solutions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"user_etablissements": {
"name": "user_etablissements",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"etablissementId": {
"name": "etablissementId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"user_etablissements_id": {
"name": "user_etablissements_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"sonumRole": {
"name": "sonumRole",
"type": "enum('referent','gestionnaire','adherent')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'referent'"
},
"cguAccepted": {
"name": "cguAccepted",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"cguAcceptedAt": {
"name": "cguAcceptedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@@ -15,6 +15,13 @@
"when": 1776268633192,
"tag": "0001_wide_frightful_four",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1776325170672,
"tag": "0002_fast_luckman",
"breakpoints": true
}
]
}

View File

@@ -12,13 +12,19 @@ import {
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
openId: varchar("openId", { length: 64 }).notNull().unique(),
// openId peut être null pour les comptes créés manuellement (connexion locale uniquement)
openId: varchar("openId", { length: 64 }).unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
// Profil SONUM : referent = référent numérique, gestionnaire = gestionnaire SONUM
sonumRole: mysqlEnum("sonumRole", ["referent", "gestionnaire"]).default("referent").notNull(),
// Profil SONUM :
// referent = référent numérique (saisit les logiciels de ses établissements)
// gestionnaire = gestionnaire SONUM (accès admin complet)
// adherent = adhérent FEHAP (consultation des fiches de ses établissements affectés)
sonumRole: mysqlEnum("sonumRole", ["referent", "gestionnaire", "adherent"])
.default("referent")
.notNull(),
// CGU acceptée
cguAccepted: boolean("cguAccepted").default(false).notNull(),
cguAcceptedAt: timestamp("cguAcceptedAt"),
@@ -30,12 +36,39 @@ export const users = mysqlTable("users", {
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
// ─── Authentification locale ──────────────────────────────────────────────────
// Stocke le hash bcrypt du mot de passe pour les comptes créés manuellement.
// Un utilisateur OAuth peut aussi avoir un mot de passe local (double connexion possible).
export const localCredentials = mysqlTable("local_credentials", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull().unique(), // FK → users.id
passwordHash: varchar("passwordHash", { length: 255 }).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type LocalCredential = typeof localCredentials.$inferSelect;
// ─── Affectation Adhérents ↔ Établissements ───────────────────────────────────
// Permet au gestionnaire SONUM d'affecter des établissements à un adhérent FEHAP.
// Un adhérent ne voit que les établissements qui lui sont affectés.
export const userEtablissements = mysqlTable("user_etablissements", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(), // FK → users.id (adhérent)
etablissementId: int("etablissementId").notNull(), // FK → etablissements.id
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type UserEtablissement = typeof userEtablissements.$inferSelect;
// ─── Référentiel : Éditeurs ───────────────────────────────────────────────────
export const editeurs = mysqlTable("editeurs", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
estValide: boolean("estValide").default(true).notNull(), // false = ajouté par un référent, en attente de validation FEHAP
estValide: boolean("estValide").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
@@ -75,11 +108,8 @@ export const etablissements = mysqlTable("etablissements", {
departement: varchar("departement", { length: 100 }),
typeActivite: varchar("typeActivite", { length: 100 }),
tailleEffectifs: varchar("tailleEffectifs", { length: 50 }),
// Référent numérique responsable
referentId: int("referentId"),
// Visibilité : "tous" = visible par tous les référents, "gestionnaires" = visible uniquement par gestionnaires SONUM
visibilite: mysqlEnum("visibilite", ["tous", "gestionnaires"]).default("tous").notNull(),
// Acceptation mise en relation
accepteMiseEnRelation: boolean("accepteMiseEnRelation").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
@@ -99,24 +129,15 @@ export const logicielsEtablissements = mysqlTable("logiciels_etablissements", {
"operationnel",
"en_remplacement",
]).notNull(),
modeHebergement: mysqlEnum("modeHebergement", [
"hds",
"on_premise",
"hybride",
]),
modeFacturation: mysqlEnum("modeFacturation", [
"saas",
"achat_maintenance",
"location",
]),
modeHebergement: mysqlEnum("modeHebergement", ["hds", "on_premise", "hybride"]),
modeFacturation: mysqlEnum("modeFacturation", ["saas", "achat_maintenance", "location"]),
interoperabilite: mysqlEnum("interoperabilite", ["non", "oui_interface", "oui_eai"]),
versionMajeure: varchar("versionMajeure", { length: 50 }),
commentaire: text("commentaire"),
// Contact référent pour ce logiciel
contactNom: varchar("contactNom", { length: 255 }),
contactFonction: varchar("contactFonction", { length: 255 }),
contactEmail: varchar("contactEmail", { length: 320 }),
saisiePar: int("saisiePar"), // userId
saisiePar: int("saisiePar"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
@@ -128,7 +149,7 @@ export type LogicielEtablissement = typeof logicielsEtablissements.$inferSelect;
export const consultations = mysqlTable("consultations", {
id: int("id").autoincrement().primaryKey(),
etablissementId: int("etablissementId").notNull(),
consultePar: int("consultePar").notNull(), // userId
consultePar: int("consultePar").notNull(),
consultéParNom: varchar("consulteParNom", { length: 255 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
@@ -139,15 +160,11 @@ export type Consultation = typeof consultations.$inferSelect;
export const demandesContact = mysqlTable("demandes_contact", {
id: int("id").autoincrement().primaryKey(),
// Établissement cible
etablissementCibleId: int("etablissementCibleId").notNull(),
// Demandeur
demandeurId: int("demandeurId").notNull(),
demandeurNom: varchar("demandeurNom", { length: 255 }),
demandeurEmail: varchar("demandeurEmail", { length: 320 }),
// Message
message: text("message").notNull(),
// Statut
statut: mysqlEnum("statut", ["en_attente", "repondu", "ferme"]).default("en_attente").notNull(),
reponse: text("reponse"),
reponsePar: int("reponsePar"),