import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { COOKIE_NAME } from "@shared/const"; import { getSessionCookieOptions } from "./_core/cookies"; import { systemRouter } from "./_core/systemRouter"; import { publicProcedure, protectedProcedure, router } from "./_core/trpc"; import { getVeilleItems, getVeilleDistinctValues, getAapItems, getAapDistinctValues, getAllSettings, setSettings, getImportLogs, getImportStats, getLocalUsers, createLocalUser, updateLocalUser, deleteLocalUser, } from "./db"; import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer"; import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth"; // ─── Middleware admin ───────────────────────────────────────────────────────── const adminProcedure = protectedProcedure.use(({ ctx, next }) => { if (ctx.user.role !== "admin") { throw new TRPCError({ code: "FORBIDDEN", message: "Accès réservé aux administrateurs" }); } 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 localLogin: publicProcedure .input(z.object({ email: z.string().email(), password: z.string().min(1) })) .mutation(async ({ input, ctx }) => { const result = await loginLocalUser(input.email, input.password); // Stocker le token dans un cookie const cookieOptions = getSessionCookieOptions(ctx.req); ctx.res.cookie("veille_local_auth", result.token, { ...cookieOptions, maxAge: 7 * 24 * 60 * 60 * 1000, }); return { success: true, user: result.user }; }), localLogout: publicProcedure.mutation(({ ctx }) => { const cookieOptions = getSessionCookieOptions(ctx.req); ctx.res.clearCookie("veille_local_auth", { ...cookieOptions, maxAge: -1 }); return { success: true }; }), }), // ─── Veille ───────────────────────────────────────────────────────────────── veille: router({ list: publicProcedure .input( z.object({ typeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).optional(), categorie: z.string().optional(), niveau: z.string().optional(), territoire: z.string().optional(), search: z.string().optional(), dateFrom: z.date().optional(), dateTo: z.date().optional(), page: z.number().int().positive().default(1), pageSize: z.number().int().positive().max(200).default(50), }) ) .query(async ({ input }) => { return getVeilleItems(input); }), filters: publicProcedure.query(async () => { return getVeilleDistinctValues(); }), }), // ─── AAP ──────────────────────────────────────────────────────────────────── aap: router({ list: publicProcedure .input( z.object({ categorie: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).optional(), region: z.string().optional(), departement: z.string().optional(), search: z.string().optional(), dateFrom: z.date().optional(), dateTo: z.date().optional(), clotureFrom: z.date().optional(), clotureTo: z.date().optional(), page: z.number().int().positive().default(1), pageSize: z.number().int().positive().max(200).default(50), }) ) .query(async ({ input }) => { return getAapItems(input); }), filters: publicProcedure.query(async () => { return getAapDistinctValues(); }), }), // ─── Import ───────────────────────────────────────────────────────────────── import: router({ run: adminProcedure .input(z.object({ type: z.enum(["veille", "aap", "all"]).default("all") })) .mutation(async ({ input }) => { const config = await getImportConfig(); if (input.type === "all") return runFullImport(); if (input.type === "veille") return { veille: await importVeille(config) }; return { aap: await importAAP(config) }; }), logs: adminProcedure .input(z.object({ page: z.number().int().positive().default(1), pageSize: z.number().int().positive().max(100).default(20) })) .query(async ({ input }) => { const allLogs = await getImportLogs(500); const start = (input.page - 1) * input.pageSize; const logs = allLogs.slice(start, start + input.pageSize); const stats = await getImportStats(); return { logs, total: allLogs.length, stats }; }), stats: publicProcedure.query(async () => { return getImportStats(); }), }), // ─── Paramètres ───────────────────────────────────────────────────────────── settings: router({ get: adminProcedure.query(async () => { const all = await getAllSettings(); // Masquer les mots de passe const safe = { ...all }; if (safe.ftp_password) safe.ftp_password = "••••••••"; if (safe.onedrive_token) safe.onedrive_token = "••••••••"; if (safe.sharepoint_token) safe.sharepoint_token = "••••••••"; return safe; }), save: adminProcedure .input( z.object({ source_type: z.enum(["local", "onedrive", "ftp", "sharepoint"]), veille_file_path: z.string().optional(), aap_file_path: z.string().optional(), ftp_host: z.string().optional(), ftp_port: z.string().optional(), ftp_user: z.string().optional(), ftp_password: z.string().optional(), ftp_secure: z.string().optional(), onedrive_token: z.string().optional(), sharepoint_site_url: z.string().optional(), sharepoint_token: z.string().optional(), auth_mode: z.enum(["local", "free"]).optional(), import_time: z.string().optional(), }) ) .mutation(async ({ input }) => { const toSave: Record = {}; for (const [k, v] of Object.entries(input)) { if (v !== undefined && v !== "••••••••") toSave[k] = v; } await setSettings(toSave); return { success: true }; }), }), // ─── Utilisateurs locaux ───────────────────────────────────────────────────── users: router({ list: adminProcedure.query(async () => { return getLocalUsers(); }), create: adminProcedure .input( z.object({ name: z.string().min(2).max(255), email: z.string().email(), password: z.string().min(8), role: z.enum(["admin", "user", "readonly"]).default("user"), }) ) .mutation(async ({ input }) => { const passwordHash = await hashPassword(input.password); await createLocalUser({ name: input.name, email: input.email.toLowerCase(), passwordHash, role: input.role, isActive: true, }); return { success: true }; }), update: adminProcedure .input( z.object({ id: z.number().int().positive(), name: z.string().min(2).max(255).optional(), email: z.string().email().optional(), password: z.string().min(8).optional(), role: z.enum(["admin", "user", "readonly"]).optional(), isActive: z.boolean().optional(), }) ) .mutation(async ({ input }) => { const { id, password, ...rest } = input; const data: Record = { ...rest }; if (password) data.passwordHash = await hashPassword(password); await updateLocalUser(id, data as Parameters[1]); return { success: true }; }), delete: adminProcedure .input(z.object({ id: z.number().int().positive() })) .mutation(async ({ input }) => { await deleteLocalUser(input.id); return { success: true }; }), ensureAdmin: publicProcedure.mutation(async () => { await ensureAdminExists(); return { success: true }; }), }), }); export type AppRouter = typeof appRouter;