queue-med/server/_core/trpc.ts

93 lines
2.7 KiB
TypeScript

import { initTRPC, TRPCError } from "@trpc/server";
import type { TrpcContext } from "./context.js";
import { isSubscriptionActive } from "../db.js";
import { childLogger } from "./logger.js";
const trpcLog = childLogger("trpc");
const t = initTRPC.context<TrpcContext>().create({
errorFormatter({ shape }) {
return shape;
},
});
export const router = t.router;
export const middleware = t.middleware;
// Timing middleware: logs every procedure call with its duration. Errors are
// re-thrown so the existing error handlers in the express adapter still fire.
const timing = t.middleware(async ({ path, type, next, ctx }) => {
const start = Date.now();
try {
const result = await next();
const durationMs = Date.now() - start;
if (result.ok) {
trpcLog.debug({ path, type, durationMs, userId: ctx.user?.id ?? null }, "procedure ok");
} else {
trpcLog.warn(
{ path, type, durationMs, userId: ctx.user?.id ?? null, code: result.error.code },
"procedure error"
);
}
return result;
} catch (err) {
const durationMs = Date.now() - start;
trpcLog.error({ err, path, type, durationMs, userId: ctx.user?.id ?? null }, "procedure threw");
throw err;
}
});
export const publicProcedure = t.procedure.use(timing);
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Authentication required" });
}
if (ctx.user.disabled) {
throw new TRPCError({ code: "FORBIDDEN", message: "Compte désactivé" });
}
return next({
ctx: {
...ctx,
user: ctx.user,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
const isAdmin = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Authentication required" });
}
if (ctx.user.disabled) {
throw new TRPCError({ code: "FORBIDDEN", message: "Compte désactivé" });
}
if (ctx.user.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN", message: "Accès réservé aux administrateurs" });
}
return next({
ctx: {
...ctx,
user: ctx.user,
},
});
});
export const adminProcedure = t.procedure.use(isAdmin);
const requireActiveSubscription = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Authentication required" });
}
const active = await isSubscriptionActive(ctx.user.id);
if (!active) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Subscription expired or inactive",
});
}
return next({ ctx: { ...ctx, user: ctx.user } });
});
export const subscriptionProcedure = t.procedure.use(requireActiveSubscription);