chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
import { prisma } from "@planarchy/db";
import { resolvePermissions, PermissionKey, SystemRole } from "@planarchy/shared";
import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
// Minimal Session type to avoid next-auth peer-dep in this package
interface Session {
user?: { email?: string | null; name?: string | null; image?: string | null } | null;
expires: string;
}
// ─── Context ──────────────────────────────────────────────────────────────────
export interface TRPCContext {
session: Session | null;
db: typeof prisma;
dbUser: { id: string; systemRole: string; permissionOverrides: unknown } | null;
}
export function createTRPCContext(opts: {
session: Session | null;
dbUser?: { id: string; systemRole: string; permissionOverrides: unknown } | null;
}): TRPCContext {
return {
session: opts.session,
db: prisma,
dbUser: opts.dbUser ?? null,
};
}
// ─── tRPC Init ───────────────────────────────────────────────────────────────
const t = initTRPC.context<TRPCContext>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// ─── Procedures ──────────────────────────────────────────────────────────────
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
/**
* Public procedure — no authentication required.
*/
export const publicProcedure = t.procedure;
/**
* Protected procedure — requires any authenticated session.
*/
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Authentication required" });
}
return next({
ctx: {
...ctx,
session: ctx.session,
user: ctx.session.user,
},
});
});
/**
* Manager procedure — requires MANAGER or ADMIN role.
*/
export const managerProcedure = protectedProcedure.use(({ ctx, next }) => {
const user = ctx.dbUser;
if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });
const allowedRoles: string[] = [SystemRole.ADMIN, SystemRole.MANAGER];
if (!allowedRoles.includes(user.systemRole)) {
throw new TRPCError({ code: "FORBIDDEN", message: "Manager or Admin role required" });
}
const permissions = resolvePermissions(
user.systemRole as SystemRole,
user.permissionOverrides as import("@planarchy/shared").PermissionOverrides | null
);
return next({ ctx: { ...ctx, user, permissions } });
});
/**
* Controller procedure — requires CONTROLLER, MANAGER, or ADMIN role.
* Grants read-only access to financial and export data.
*/
export const controllerProcedure = protectedProcedure.use(({ ctx, next }) => {
const user = ctx.dbUser;
if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });
const allowed: SystemRole[] = [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER];
if (!allowed.includes(user.systemRole as SystemRole)) {
throw new TRPCError({ code: "FORBIDDEN", message: "Controller access required" });
}
const permissions = resolvePermissions(
user.systemRole as SystemRole,
user.permissionOverrides as import("@planarchy/shared").PermissionOverrides | null
);
return next({ ctx: { ...ctx, user, permissions } });
});
/**
* Admin procedure — requires ADMIN role only.
*/
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
const user = ctx.dbUser;
if (!user || user.systemRole !== SystemRole.ADMIN) {
throw new TRPCError({ code: "FORBIDDEN", message: "Admin role required" });
}
const permissions = resolvePermissions(SystemRole.ADMIN, null);
return next({ ctx: { ...ctx, user, permissions } });
});
/**
* requirePermission — throws FORBIDDEN if the ctx lacks the given permission.
*/
export function requirePermission(
ctx: { permissions: Set<PermissionKey> },
key: PermissionKey
): void {
if (!ctx.permissions.has(key)) {
throw new TRPCError({ code: "FORBIDDEN", message: `Permission required: ${key}` });
}
}