565 lines
23 KiB
TypeScript
565 lines
23 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod";
|
|
import {
|
|
assignEtablissementToUser,
|
|
authenticateLocalUser,
|
|
createDemandeContact,
|
|
createBlocFonctionnel,
|
|
updateBlocFonctionnel,
|
|
deleteBlocFonctionnel,
|
|
createEditeur,
|
|
updateEditeur,
|
|
deleteEditeur,
|
|
createLocalUser,
|
|
createSolution,
|
|
updateSolution,
|
|
deleteSolution,
|
|
getStatistiques,
|
|
deleteLogicielEtablissement,
|
|
deleteUser,
|
|
getAllDemandes,
|
|
getAllEtablissements,
|
|
getAllUsersWithAffectations,
|
|
getAffectationsByUser,
|
|
getBlocsFonctionnels,
|
|
getConsultationCount,
|
|
getConsultationsList,
|
|
getDemandeById,
|
|
getDemandesByDemandeur,
|
|
getDemandesRecuesParEtablissement,
|
|
getEditeurs,
|
|
getEtablissementById,
|
|
getEtablissementsByAdherent,
|
|
getEtablissementsByReferent,
|
|
getLogicielsByEtablissement,
|
|
getMesSolutionsGroupees,
|
|
getToutesLesSolutionsGroupees,
|
|
getSolutions,
|
|
recordConsultation,
|
|
removeEtablissementFromUser,
|
|
repondreDemandeContact,
|
|
setAffectationsForUser,
|
|
updateLocalPassword,
|
|
updateUser,
|
|
updateUserCgu,
|
|
updateUserSonumRole,
|
|
upsertLogicielEtablissement,
|
|
upsertUser,
|
|
} from "./db";
|
|
import { COOKIE_NAME } from "@shared/const";
|
|
import { getSessionCookieOptions } from "./_core/cookies";
|
|
import { systemRouter } from "./_core/systemRouter";
|
|
import { protectedProcedure, publicProcedure, router } from "./_core/trpc";
|
|
import { notifyOwner } from "./_core/notification";
|
|
import { getDb } from "./db";
|
|
import { etablissements } from "../drizzle/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { sdk } from "./_core/sdk";
|
|
|
|
// ─── Middleware gestionnaire SONUM ────────────────────────────────────────────
|
|
|
|
const gestionnaireProcedure = protectedProcedure.use(({ ctx, next }) => {
|
|
if (ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") {
|
|
throw new TRPCError({ code: "FORBIDDEN", message: "Accès réservé aux gestionnaires SONUM" });
|
|
}
|
|
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({
|
|
system: systemRouter,
|
|
|
|
// ─── Auth ──────────────────────────────────────────────────────────────────
|
|
auth: router({
|
|
me: publicProcedure.query((opts) => opts.ctx.user),
|
|
|
|
logout: publicProcedure.mutation(({ ctx }) => {
|
|
const cookieOptions = getSessionCookieOptions(ctx.req);
|
|
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
|
|
return { success: true } as const;
|
|
}),
|
|
|
|
/**
|
|
* Connexion locale par email + mot de passe.
|
|
* Crée un cookie de session identique à celui de l'OAuth.
|
|
*/
|
|
loginLocal: publicProcedure
|
|
.input(z.object({
|
|
// 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: "Identifiant ou mot de passe incorrect" });
|
|
}
|
|
|
|
// Créer un token de session avec l'openId de l'utilisateur local
|
|
const sessionToken = await sdk.createSessionToken(user.openId!, {
|
|
name: user.name ?? "",
|
|
});
|
|
|
|
const cookieOptions = getSessionCookieOptions(ctx.req);
|
|
ctx.res.cookie(COOKIE_NAME, sessionToken, {
|
|
...cookieOptions,
|
|
maxAge: 1000 * 60 * 60 * 24 * 365, // 1 an
|
|
});
|
|
|
|
return { success: true, user };
|
|
}),
|
|
}),
|
|
|
|
// ─── CGU ───────────────────────────────────────────────────────────────────
|
|
cgu: router({
|
|
accept: protectedProcedure.mutation(async ({ ctx }) => {
|
|
await updateUserCgu(ctx.user.id);
|
|
return { success: true };
|
|
}),
|
|
status: protectedProcedure.query(({ ctx }) => ({
|
|
// La CGU doit être acceptée à chaque session (pas seulement la première fois)
|
|
// On retourne toujours les données réelles mais le frontend gère la session
|
|
accepted: ctx.user.cguAccepted,
|
|
acceptedAt: ctx.user.cguAcceptedAt,
|
|
userId: ctx.user.id,
|
|
})),
|
|
}),
|
|
|
|
// ─── Référentiel ───────────────────────────────────────────────────────────
|
|
referentiel: router({
|
|
editeurs: publicProcedure.query(() => getEditeurs()),
|
|
blocsFonctionnels: publicProcedure.query(() => getBlocsFonctionnels()),
|
|
solutions: publicProcedure
|
|
.input(z.object({ search: z.string().optional() }))
|
|
.query(({ input }) => getSolutions(input.search)),
|
|
|
|
createEditeur: protectedProcedure
|
|
.input(z.object({ nom: z.string().min(1) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
|
|
return createEditeur(input.nom, isGestionnaire);
|
|
}),
|
|
|
|
createBlocFonctionnel: protectedProcedure
|
|
.input(z.object({ nom: z.string().min(1) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
|
|
return createBlocFonctionnel(input.nom, isGestionnaire);
|
|
}),
|
|
updateBlocFonctionnel: gestionnaireProcedure
|
|
.input(z.object({ id: z.number().int(), nom: z.string().min(1) }))
|
|
.mutation(({ input }) => updateBlocFonctionnel(input.id, input.nom)),
|
|
deleteBlocFonctionnel: gestionnaireProcedure
|
|
.input(z.object({ id: z.number().int() }))
|
|
.mutation(({ input }) => deleteBlocFonctionnel(input.id)),
|
|
updateEditeur: gestionnaireProcedure
|
|
.input(z.object({ id: z.number().int(), nom: z.string().min(1) }))
|
|
.mutation(({ input }) => updateEditeur(input.id, input.nom)),
|
|
deleteEditeur: gestionnaireProcedure
|
|
.input(z.object({ id: z.number().int() }))
|
|
.mutation(({ input }) => deleteEditeur(input.id)),
|
|
statistiques: gestionnaireProcedure.query(() => getStatistiques()),
|
|
|
|
createSolution: protectedProcedure
|
|
.input(z.object({
|
|
nom: z.string().min(1),
|
|
editeurId: z.number().int(),
|
|
blocFonctionnelId: z.number().int().optional().nullable(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const isGestionnaire = ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
|
|
return createSolution(input.nom, input.editeurId, input.blocFonctionnelId, isGestionnaire);
|
|
}),
|
|
updateSolution: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number().int(),
|
|
nom: z.string().min(1),
|
|
editeurId: z.number().int(),
|
|
blocFonctionnelId: z.number().int().optional().nullable(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") throw new TRPCError({ code: "FORBIDDEN" });
|
|
return updateSolution(input.id, input.nom, input.editeurId, input.blocFonctionnelId ?? null);
|
|
}),
|
|
deleteSolution: protectedProcedure
|
|
.input(z.object({ id: z.number().int() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") throw new TRPCError({ code: "FORBIDDEN" });
|
|
return deleteSolution(input.id);
|
|
}),
|
|
}),
|
|
|
|
// ─── Établissements ────────────────────────────────────────────────────────
|
|
etablissements: router({
|
|
/**
|
|
* Retourne les établissements selon le rôle :
|
|
* - référent : ses établissements
|
|
* - adhérent : ses établissements affectés
|
|
* - gestionnaire : tous
|
|
*/
|
|
mesEtablissements: protectedProcedure.query(({ ctx }) => {
|
|
if (ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin") {
|
|
return getAllEtablissements();
|
|
}
|
|
if (ctx.user.sonumRole === "adherent") {
|
|
return getEtablissementsByAdherent(ctx.user.id);
|
|
}
|
|
return getEtablissementsByReferent(ctx.user.id);
|
|
}),
|
|
|
|
all: gestionnaireProcedure.query(() => getAllEtablissements()),
|
|
|
|
byId: protectedProcedure
|
|
.input(z.object({ id: z.number().int() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const etab = await getEtablissementById(input.id);
|
|
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
|
|
// Adhérent : vérifier qu'il a accès à cet établissement
|
|
if (ctx.user.sonumRole === "adherent") {
|
|
const affectations = await getAffectationsByUser(ctx.user.id);
|
|
if (!affectations.includes(input.id)) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
}
|
|
if (etab.visibilite === "gestionnaires" && ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") {
|
|
if (etab.referentId !== ctx.user.id) {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
}
|
|
return etab;
|
|
}),
|
|
|
|
search: protectedProcedure
|
|
.input(z.object({
|
|
solutionId: z.number().int().optional(),
|
|
editeurId: z.number().int().optional(),
|
|
blocFonctionnelId: z.number().int().optional(),
|
|
region: z.string().optional(),
|
|
typeActivite: z.string().optional(),
|
|
tailleEffectifs: z.string().optional(),
|
|
etatDeploiement: z.string().optional(),
|
|
}))
|
|
.query(({ input, ctx }) => {
|
|
return import("./db").then(({ searchEtablissements }) =>
|
|
searchEtablissements({
|
|
...input,
|
|
userId: ctx.user.id,
|
|
sonumRole: ctx.user.sonumRole,
|
|
})
|
|
);
|
|
}),
|
|
|
|
create: gestionnaireProcedure
|
|
.input(z.object({
|
|
finess: z.string().optional(),
|
|
nom: z.string().min(1),
|
|
region: z.string().optional(),
|
|
departement: z.string().optional(),
|
|
typeActivite: z.string().optional(),
|
|
tailleEffectifs: z.string().optional(),
|
|
referentId: z.number().int().optional(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const db = await getDb();
|
|
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
const result = await db.insert(etablissements).values(input);
|
|
return result[0];
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number().int(),
|
|
visibilite: z.enum(["tous", "gestionnaires"]).optional(),
|
|
accepteMiseEnRelation: z.boolean().optional(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const etab = await getEtablissementById(input.id);
|
|
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
|
|
if (etab.referentId !== ctx.user.id && ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
const db = await getDb();
|
|
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
|
const { id, ...updateData } = input;
|
|
await db.update(etablissements).set(updateData).where(eq(etablissements.id, id));
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
// ─── Logiciels ─────────────────────────────────────────────────────────────
|
|
logiciels: router({
|
|
byEtablissement: protectedProcedure
|
|
.input(z.object({ etablissementId: z.number().int() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const etab = await getEtablissementById(input.etablissementId);
|
|
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
|
|
// Adhérent : vérifier affectation
|
|
if (ctx.user.sonumRole === "adherent") {
|
|
const affectations = await getAffectationsByUser(ctx.user.id);
|
|
if (!affectations.includes(input.etablissementId)) throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
if (etab.visibilite === "gestionnaires" && ctx.user.sonumRole !== "gestionnaire" && ctx.user.role !== "admin") {
|
|
if (etab.referentId !== ctx.user.id) throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
return getLogicielsByEtablissement(input.etablissementId);
|
|
}),
|
|
|
|
upsert: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number().int().optional(),
|
|
etablissementId: z.number().int(),
|
|
solutionId: z.number().int(),
|
|
etatDeploiement: z.enum(["demarrage", "en_cours", "operationnel", "en_remplacement"]),
|
|
modeHebergement: z.enum(["hds", "on_premise", "hybride"]).optional().nullable(),
|
|
modeFacturation: z.enum(["saas", "achat_maintenance", "location"]).optional().nullable(),
|
|
interoperabilite: z.enum(["non", "oui_interface", "oui_eai"]).optional().nullable(),
|
|
versionMajeure: z.string().optional().nullable(),
|
|
commentaire: z.string().optional().nullable(),
|
|
contactNom: z.string().optional().nullable(),
|
|
contactFonction: z.string().optional().nullable(),
|
|
contactEmail: z.string().optional().nullable(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
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") {
|
|
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") {
|
|
throw new TRPCError({ code: "FORBIDDEN" });
|
|
}
|
|
await deleteLogicielEtablissement(input.id);
|
|
return { success: true };
|
|
}),
|
|
mesSolutions: protectedProcedure
|
|
.query(({ ctx }) =>
|
|
getMesSolutionsGroupees(ctx.user.id, ctx.user.sonumRole ?? "referent")
|
|
),
|
|
toutesLesSolutions: protectedProcedure
|
|
.query(() => getToutesLesSolutionsGroupees()),
|
|
}),
|
|
|
|
// ─── Traçabilité ───────────────────────────────────────────────────────────
|
|
tracabilite: router({
|
|
enregistrerConsultation: protectedProcedure
|
|
.input(z.object({ etablissementId: z.number().int() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
await recordConsultation(input.etablissementId, ctx.user.id, ctx.user.name ?? "Inconnu");
|
|
return { success: true };
|
|
}),
|
|
|
|
compteur: protectedProcedure
|
|
.input(z.object({ etablissementId: z.number().int() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const etab = await getEtablissementById(input.etablissementId);
|
|
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
|
|
const canSee = etab.referentId === ctx.user.id || ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
|
|
if (!canSee) throw new TRPCError({ code: "FORBIDDEN" });
|
|
return { count: await getConsultationCount(input.etablissementId) };
|
|
}),
|
|
|
|
liste: protectedProcedure
|
|
.input(z.object({ etablissementId: z.number().int() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const etab = await getEtablissementById(input.etablissementId);
|
|
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
|
|
const canSee = etab.referentId === ctx.user.id || ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
|
|
if (!canSee) throw new TRPCError({ code: "FORBIDDEN" });
|
|
return getConsultationsList(input.etablissementId);
|
|
}),
|
|
}),
|
|
|
|
// ─── Demandes de Contact ───────────────────────────────────────────────────
|
|
contact: router({
|
|
envoyer: writeProcedure
|
|
.input(z.object({
|
|
etablissementCibleId: z.number().int(),
|
|
message: z.string().min(1),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const etab = await getEtablissementById(input.etablissementCibleId);
|
|
if (!etab) throw new TRPCError({ code: "NOT_FOUND" });
|
|
|
|
await createDemandeContact({
|
|
etablissementCibleId: input.etablissementCibleId,
|
|
demandeurId: ctx.user.id,
|
|
demandeurNom: ctx.user.name ?? "Inconnu",
|
|
demandeurEmail: ctx.user.email ?? "",
|
|
message: input.message,
|
|
});
|
|
|
|
await notifyOwner({
|
|
title: `Nouvelle demande de contact — ${etab.nom}`,
|
|
content: `${ctx.user.name} souhaite contacter le référent de ${etab.nom}.\n\nMessage : ${input.message}`,
|
|
});
|
|
|
|
return { success: true };
|
|
}),
|
|
|
|
mesDemandes: protectedProcedure.query(({ ctx }) =>
|
|
getDemandesByDemandeur(ctx.user.id)
|
|
),
|
|
|
|
demandesRecues: protectedProcedure.query(({ ctx }) =>
|
|
getDemandesRecuesParEtablissement(ctx.user.id)
|
|
),
|
|
|
|
toutesLesDemandes: gestionnaireProcedure.query(() => getAllDemandes()),
|
|
|
|
repondre: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number().int(),
|
|
reponse: z.string().min(1),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const demande = await getDemandeById(input.id);
|
|
if (!demande) throw new TRPCError({ code: "NOT_FOUND" });
|
|
const etab = await getEtablissementById(demande.etablissementCibleId);
|
|
const canReply = (etab?.referentId === ctx.user.id) || ctx.user.sonumRole === "gestionnaire" || ctx.user.role === "admin";
|
|
if (!canReply) throw new TRPCError({ code: "FORBIDDEN" });
|
|
await repondreDemandeContact(input.id, input.reponse, ctx.user.id);
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
// ─── Administration ────────────────────────────────────────────────────────
|
|
admin: router({
|
|
/** Liste tous les utilisateurs avec leurs établissements affectés */
|
|
users: gestionnaireProcedure.query(() => getAllUsersWithAffectations()),
|
|
|
|
/** Crée un utilisateur manuellement avec un mot de passe local */
|
|
createUser: gestionnaireProcedure
|
|
.input(z.object({
|
|
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 }) => {
|
|
try {
|
|
const userId = await createLocalUser(input);
|
|
return { success: true, userId };
|
|
} catch (err: any) {
|
|
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 });
|
|
}
|
|
}),
|
|
|
|
/** Met à jour les informations d'un utilisateur */
|
|
updateUser: gestionnaireProcedure
|
|
.input(z.object({
|
|
userId: z.number().int(),
|
|
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;
|
|
await updateUser(userId, data);
|
|
return { success: true };
|
|
}),
|
|
|
|
/** Réinitialise le mot de passe d'un utilisateur local */
|
|
resetPassword: gestionnaireProcedure
|
|
.input(z.object({
|
|
userId: z.number().int(),
|
|
newPassword: z.string().min(8),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await updateLocalPassword(input.userId, input.newPassword);
|
|
return { success: true };
|
|
}),
|
|
|
|
/** Supprime un utilisateur */
|
|
deleteUser: gestionnaireProcedure
|
|
.input(z.object({ userId: z.number().int() }))
|
|
.mutation(async ({ input }) => {
|
|
await deleteUser(input.userId);
|
|
return { success: true };
|
|
}),
|
|
|
|
/** Ancienne procédure de mise à jour du rôle (rétrocompatibilité) */
|
|
updateRole: gestionnaireProcedure
|
|
.input(z.object({
|
|
userId: z.number().int(),
|
|
sonumRole: z.enum(["referent", "gestionnaire", "adherent"]),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await updateUserSonumRole(input.userId, input.sonumRole);
|
|
return { success: true };
|
|
}),
|
|
|
|
/** Retourne les établissements affectés à un utilisateur */
|
|
affectations: gestionnaireProcedure
|
|
.input(z.object({ userId: z.number().int() }))
|
|
.query(({ input }) => getAffectationsByUser(input.userId)),
|
|
|
|
/** Remplace toutes les affectations d'un adhérent */
|
|
setAffectations: gestionnaireProcedure
|
|
.input(z.object({
|
|
userId: z.number().int(),
|
|
etablissementIds: z.array(z.number().int()),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await setAffectationsForUser(input.userId, input.etablissementIds);
|
|
return { success: true };
|
|
}),
|
|
|
|
/** Ajoute un établissement à un utilisateur */
|
|
assignEtablissement: gestionnaireProcedure
|
|
.input(z.object({
|
|
userId: z.number().int(),
|
|
etablissementId: z.number().int(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await assignEtablissementToUser(input.userId, input.etablissementId);
|
|
return { success: true };
|
|
}),
|
|
|
|
/** Retire un établissement d'un utilisateur */
|
|
removeEtablissement: gestionnaireProcedure
|
|
.input(z.object({
|
|
userId: z.number().int(),
|
|
etablissementId: z.number().int(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await removeEtablissementFromUser(input.userId, input.etablissementId);
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
});
|
|
|
|
export type AppRouter = typeof appRouter;
|