Files
veille-reglementaire/server/routers.ts

313 lines
12 KiB
TypeScript

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,
createIdea,
getAllIdeas,
getIdeasByUser,
repondreIdea,
updateIdeaStatut,
} 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().min(1), 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<string, string> = {};
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),
username: z.string().min(2).max(128).optional(),
email: z.string().email().optional(),
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,
username: input.username ?? null,
email: input.email ? input.email.toLowerCase() : null,
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(),
username: z.string().min(2).max(128).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<string, unknown> = { ...rest };
if (password) data.passwordHash = await hashPassword(password);
await updateLocalUser(id, data as Parameters<typeof updateLocalUser>[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 };
}),
}),
// ─── Boîte à idées ───────────────────────────────────────────────────────────
ideas: router({
// Créer une nouvelle idée / question
create: protectedProcedure
.input(
z.object({
titre: z.string().min(3).max(512),
message: z.string().min(10),
})
)
.mutation(async ({ input, ctx }) => {
await createIdea({
userId: ctx.user.id,
userName: ctx.user.name ?? ctx.user.email ?? "Utilisateur",
titre: input.titre,
message: input.message,
});
return { success: true };
}),
// Lister toutes les idées (admin) ou les siennes (user)
list: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "admin") {
return getAllIdeas();
}
return getIdeasByUser(ctx.user.id);
}),
// Répondre à une idée (admin uniquement)
repondre: adminProcedure
.input(
z.object({
id: z.number().int().positive(),
reponseAdmin: z.string().min(1),
statut: z.enum(["ouvert", "en_cours", "resolu", "ferme"]),
})
)
.mutation(async ({ input, ctx }) => {
await repondreIdea(
input.id,
input.reponseAdmin,
ctx.user.name ?? ctx.user.email ?? "Admin",
input.statut
);
return { success: true };
}),
// Changer le statut (admin uniquement)
updateStatut: adminProcedure
.input(
z.object({
id: z.number().int().positive(),
statut: z.enum(["ouvert", "en_cours", "resolu", "ferme"]),
})
)
.mutation(async ({ input }) => {
await updateIdeaStatut(input.id, input.statut);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;