feat(defaults/withRules): add permission rule wrapper

This commit is contained in:
2026-04-22 15:10:29 +02:00
parent f71128a7f3
commit ad64fd0a99
+90
View File
@@ -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<AuthenticatedState>,
) => Promise<boolean> | boolean;
async function hasPermission(
uid: string,
permission: string,
): Promise<boolean> {
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<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "student_read"),
student_write: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "student_write"),
note_read: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "note_read"),
note_write: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "note_write"),
module_read: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "module_read"),
module_write: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "module_write"),
user_read: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "user_read"),
user_write: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "user_write"),
role_write: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
hasPermission(ctx.state.session.uid, "role_write"),
// Contextual rules — student accessing their own data
own_student: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
parseNumEtud(ctx.state.session.uid) === Number(ctx.params.numEtud),
own_note: (_req: Request, ctx: FreshContext<AuthenticatedState>) =>
parseNumEtud(ctx.state.session.uid) === Number(ctx.params.numEtud),
};
export type RuleName = keyof typeof rules;
type HandlerFn = (
req: Request,
ctx: FreshContext<AuthenticatedState>,
) => Promise<Response>;
/**
* 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<null, AuthenticatedState> = {
* 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);
};
};
}