chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -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}` });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user