From ad64fd0a9997e87f1faf563925391b4755de0eac Mon Sep 17 00:00:00 2001 From: Djalim Simaila Date: Wed, 22 Apr 2026 15:10:29 +0200 Subject: [PATCH] feat(defaults/withRules): add permission rule wrapper --- defaults/withRules.ts | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 defaults/withRules.ts diff --git a/defaults/withRules.ts b/defaults/withRules.ts new file mode 100644 index 0000000..e249785 --- /dev/null +++ b/defaults/withRules.ts @@ -0,0 +1,90 @@ +import { FreshContext } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { rolePermissions, users } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +type RuleFn = ( + req: Request, + ctx: FreshContext, +) => Promise | boolean; + +async function hasPermission( + uid: string, + permission: string, +): Promise { + const [user] = await db.select().from(users).where(eq(users.id, uid)); + if (!user || user.idRole === null) return false; + + const [rp] = await db.select().from(rolePermissions).where( + and( + eq(rolePermissions.idRole, user.idRole!), + eq(rolePermissions.idPermission, permission), + ), + ); + return !!rp; +} + +function parseNumEtud(uid: string): number { + return parseInt(uid.slice(1)); +} + +const rules = { + student_read: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "student_read"), + student_write: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "student_write"), + note_read: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "note_read"), + note_write: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "note_write"), + module_read: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "module_read"), + module_write: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "module_write"), + user_read: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "user_read"), + user_write: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "user_write"), + role_write: (_req: Request, ctx: FreshContext) => + hasPermission(ctx.state.session.uid, "role_write"), + + // Contextual rules — student accessing their own data + own_student: (_req: Request, ctx: FreshContext) => + parseNumEtud(ctx.state.session.uid) === Number(ctx.params.numEtud), + own_note: (_req: Request, ctx: FreshContext) => + parseNumEtud(ctx.state.session.uid) === Number(ctx.params.numEtud), +}; + +export type RuleName = keyof typeof rules; + +type HandlerFn = ( + req: Request, + ctx: FreshContext, +) => Promise; + +/** + * Wraps a route handler with permission checks. + * Access is granted if ANY of the provided rules passes (OR logic). + * Returns 403 if none pass. + * + * @example + * export const handler: Handlers = { + * GET: withRules(["note_read", "own_note"])(async (req, ctx) => { + * // ... + * }), + * }; + */ +export function withRules(ruleNames: RuleName[]) { + return (handler: HandlerFn): HandlerFn => { + return async (req, ctx) => { + const results = await Promise.all( + ruleNames.map((name) => rules[name](req, ctx)), + ); + if (!results.some(Boolean)) { + return new Response(null, { status: 403 }); + } + return handler(req, ctx); + }; + }; +}