From 128ce36df2676a13a6ecefa9e108dbdaf5fb4e73 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Sun, 7 Jun 2026 19:27:55 +0300 Subject: [PATCH] backend: add appointment/prescription/task RBAC resources Extend the clinic access-control statements and role grants with appointment/prescription/task resources (mirrored in the frontend client AC), and widen the requirePermission type to accept any defined resource. Co-Authored-By: Claude Opus 4.8 --- backend/src/lib/access.ts | 26 +++++++++++++++++++++----- backend/src/middleware/auth.ts | 10 +++++++--- frontend/lib/access.ts | 15 +++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/backend/src/lib/access.ts b/backend/src/lib/access.ts index d506115..d8aa50a 100644 --- a/backend/src/lib/access.ts +++ b/backend/src/lib/access.ts @@ -7,11 +7,15 @@ import { } from "better-auth/plugins/organization/access"; // RBAC for clinics (organizations). We extend Better Auth's default -// organization statements (organization / member / invitation / team) with a -// `patient` resource so roles can be granted fine-grained access to records. +// organization statements (organization / member / invitation / team) with +// clinical resources (`patient`, `appointment`, `prescription`, `task`) so roles +// can be granted fine-grained access to records. export const statements = { ...defaultStatements, patient: ["read", "write", "delete"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], } as const; export const ac = createAccessControl(statements); @@ -21,26 +25,38 @@ export const ac = createAccessControl(statements); // and add a read-only `viewer`. In the UI these read as Owner / Admin / // Clinician (member) / Viewer. // -// owner / admin: run the clinic AND have full access to patient records. +// owner / admin: run the clinic AND have full access to clinical records. export const owner = ac.newRole({ ...ownerAc.statements, patient: ["read", "write", "delete"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], }); export const admin = ac.newRole({ ...adminAc.statements, patient: ["read", "write", "delete"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], }); -// member (clinician): a regular member who can read and edit patient records. +// member (clinician): a regular member who can read and edit clinical records. export const member = ac.newRole({ ...memberAc.statements, patient: ["read", "write"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], }); -// viewer: read-only access to patient records. +// viewer: read-only access to clinical records. export const viewer = ac.newRole({ patient: ["read"], + appointment: ["read"], + prescription: ["read"], + task: ["read"], }); export const roles = { owner, admin, member, viewer }; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 6844f62..49b1a7d 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -5,7 +5,7 @@ import type { NextFunction, Request, Response } from "express"; import { auth } from "../auth.js"; import { db } from "../db/index.js"; import { member } from "../db/schema/auth.js"; -import { roles } from "../lib/access.js"; +import { roles, type statements } from "../lib/access.js"; import { HttpError } from "../lib/http-error.js"; // Validates the Better Auth session cookie and attaches the user + session. @@ -61,8 +61,12 @@ export async function requireOrg( } } -type PatientAction = "read" | "write" | "delete"; -type PermissionRequest = { patient?: PatientAction[] }; +// A permission request maps clinic resources (patient / appointment / … defined +// in src/lib/access.ts) to the actions required on them. Mirrors the shape Better +// Auth's `role.authorize` accepts. +type PermissionRequest = Partial<{ + [R in keyof typeof statements]: ((typeof statements)[R][number])[]; +}>; // Gates a route on a clinic permission, evaluated against the caller's role(s) // using the shared access-control definitions. Must run after requireOrg. diff --git a/frontend/lib/access.ts b/frontend/lib/access.ts index cbd3e8d..b03c489 100644 --- a/frontend/lib/access.ts +++ b/frontend/lib/access.ts @@ -11,6 +11,9 @@ import { export const statements = { ...defaultStatements, patient: ["read", "write", "delete"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], } as const; export const ac = createAccessControl(statements); @@ -18,20 +21,32 @@ export const ac = createAccessControl(statements); export const owner = ac.newRole({ ...ownerAc.statements, patient: ["read", "write", "delete"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], }); export const admin = ac.newRole({ ...adminAc.statements, patient: ["read", "write", "delete"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], }); export const member = ac.newRole({ ...memberAc.statements, patient: ["read", "write"], + appointment: ["read", "write", "delete"], + prescription: ["read", "write", "delete"], + task: ["read", "write", "delete"], }); export const viewer = ac.newRole({ patient: ["read"], + appointment: ["read"], + prescription: ["read"], + task: ["read"], }); export const roles = { owner, admin, member, viewer };