feat: v8 - skill itinova-user-management (3 profils admin/standard/readonly, logo FEHAP, login/email)

This commit is contained in:
Manus Deploy
2026-04-21 06:51:07 -04:00
parent 65e345459c
commit a8b1784e28
14 changed files with 1356 additions and 219 deletions

View File

@@ -65,6 +65,14 @@ const gestionnaireProcedure = protectedProcedure.use(({ ctx, next }) => {
return next({ ctx });
});
/** Bloque les mutations pour les utilisateurs en lecture seule (role === 'readonly') */
const writeProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.user.role === "readonly") {
throw new TRPCError({ code: "FORBIDDEN", message: "Votre compte est en lecture seule. Contactez un gestionnaire SONUM pour obtenir les droits de modification." });
}
return next({ ctx });
});
// ─── Router principal ─────────────────────────────────────────────────────────
export const appRouter = router({
@@ -86,13 +94,14 @@ export const appRouter = router({
*/
loginLocal: publicProcedure
.input(z.object({
email: z.string().email(),
// Accepte email ou login court
email: z.string().min(1),
password: z.string().min(1),
}))
.mutation(async ({ input, ctx }) => {
const user = await authenticateLocalUser(input.email, input.password);
if (!user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Email ou mot de passe incorrect" });
throw new TRPCError({ code: "UNAUTHORIZED", message: "Identifiant ou mot de passe incorrect" });
}
// Créer un token de session avec l'openId de l'utilisateur local
@@ -325,12 +334,14 @@ export const appRouter = router({
if (etab.referentId !== ctx.user.id && ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN" });
}
if (ctx.user.role === "readonly") throw new TRPCError({ code: "FORBIDDEN", message: "Compte en lecture seule" });
return upsertLogicielEtablissement({ ...input, saisiePar: ctx.user.id });
}),
delete: protectedProcedure
.input(z.object({ id: z.number().int(), etablissementId: z.number().int() }))
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "readonly") throw new TRPCError({ code: "FORBIDDEN", message: "Compte en lecture seule" });
const etab = await getEtablissementById(input.etablissementId);
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
if (etab.referentId !== ctx.user.id && ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") {
@@ -379,7 +390,7 @@ export const appRouter = router({
// ─── Demandes de Contact ───────────────────────────────────────────────────
contact: router({
envoyer: protectedProcedure
envoyer: writeProcedure
.input(z.object({
etablissementCibleId: z.number().int(),
message: z.string().min(1),
@@ -438,9 +449,13 @@ export const appRouter = router({
/** Crée un utilisateur manuellement avec un mot de passe local */
createUser: gestionnaireProcedure
.input(z.object({
name: z.string().min(1),
firstName: z.string().min(1),
lastName: z.string().min(1),
login: z.string().min(2).optional(),
email: z.string().email(),
sonumRole: z.enum(["referent", "gestionnaire", "adherent"]),
role: z.enum(["admin", "standard", "readonly"]).default("standard"),
isActive: z.boolean().default(true),
password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"),
}))
.mutation(async ({ input }) => {
@@ -451,6 +466,9 @@ export const appRouter = router({
if (err.message === "EMAIL_EXISTS") {
throw new TRPCError({ code: "CONFLICT", message: "Un utilisateur avec cet email existe déjà" });
}
if (err.message === "LOGIN_EXISTS") {
throw new TRPCError({ code: "CONFLICT", message: "Un utilisateur avec ce login existe déjà" });
}
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: err.message });
}
}),
@@ -459,9 +477,13 @@ export const appRouter = router({
updateUser: gestionnaireProcedure
.input(z.object({
userId: z.number().int(),
name: z.string().min(1).optional(),
firstName: z.string().min(1).optional(),
lastName: z.string().min(1).optional(),
login: z.string().min(2).optional(),
email: z.string().email().optional(),
sonumRole: z.enum(["referent", "gestionnaire", "adherent"]).optional(),
role: z.enum(["admin", "standard", "readonly"]).optional(),
isActive: z.boolean().optional(),
}))
.mutation(async ({ input }) => {
const { userId, ...data } = input;