Ver Fonte

RHL-041 refactor(auth): enhance role-based access control with user management capabilities

codeUWE há 1 dia atrás
pai
commit
cf67fc4e21
3 ficheiros alterados com 114 adições e 10 exclusões
  1. 44 5
      lib/auth/permissions.js
  2. 68 4
      lib/auth/permissions.test.js
  3. 2 1
      models/user.js

+ 44 - 5
lib/auth/permissions.js

@@ -1,8 +1,10 @@
+import { forbidden } from "@/lib/api/errors";
+
 /**
  * Role-Based Access Control (RBAC) helpers.
  *
  * @typedef {Object} Session
- * @property {"branch"|"admin"|"dev"|string} role
+ * @property {"branch"|"admin"|"superadmin"|"dev"|string} role
  * @property {string=} branchId
  */
 
@@ -12,7 +14,7 @@
  * Rules:
  * - No session => not allowed (caller should return 401)
  * - role "branch" => allowed only for session.branchId === branchId
- * - role "admin" / "dev" => allowed for any branch
+ * - role "admin" / "superadmin" / "dev" => allowed for any branch
  *
  * @param {Session|null} session
  * @param {string} branchId
@@ -26,7 +28,11 @@ export function canAccessBranch(session, branchId) {
 		return session.branchId === branchId;
 	}
 
-	if (session.role === "admin" || session.role === "dev") {
+	if (
+		session.role === "admin" ||
+		session.role === "superadmin" ||
+		session.role === "dev"
+	) {
 		return true;
 	}
 
@@ -38,7 +44,7 @@ export function canAccessBranch(session, branchId) {
  *
  * - No session => []
  * - role "branch" => [session.branchId] if present in branchIds, else []
- * - role "admin" / "dev" => all branchIds
+ * - role "admin" / "superadmin" / "dev" => all branchIds
  *
  * @param {Session|null} session
  * @param {string[]} branchIds
@@ -54,9 +60,42 @@ export function filterBranchesForSession(session, branchIds) {
 		return branchIds.includes(own) ? [own] : [];
 	}
 
-	if (session.role === "admin" || session.role === "dev") {
+	if (
+		session.role === "admin" ||
+		session.role === "superadmin" ||
+		session.role === "dev"
+	) {
 		return branchIds;
 	}
 
 	return [];
 }
+
+/**
+ * Returns true if the given session can manage users (RHL-012 capability).
+ *
+ * Rules:
+ * - dev / superadmin => true
+ * - admin / branch / unknown => false
+ *
+ * @param {Session|null} session
+ * @returns {boolean}
+ */
+export function canManageUsers(session) {
+	if (!session) return false;
+	return session.role === "dev" || session.role === "superadmin";
+}
+
+/**
+ * Guard helper for user-management endpoints (to be used in RHL-012).
+ *
+ * Throws a standardized 403 error when the session lacks user-management permission.
+ *
+ * @param {Session|null} session
+ * @returns {void}
+ */
+export function requireUserManagement(session) {
+	if (!canManageUsers(session)) {
+		throw forbidden("AUTH_FORBIDDEN_USER_MANAGEMENT", "Forbidden");
+	}
+}

+ 68 - 4
lib/auth/permissions.test.js

@@ -1,7 +1,12 @@
 /* @vitest-environment node */
 
 import { describe, it, expect } from "vitest";
-import { canAccessBranch, filterBranchesForSession } from "./permissions.js";
+import {
+	canAccessBranch,
+	filterBranchesForSession,
+	canManageUsers,
+	requireUserManagement,
+} from "./permissions.js";
 
 describe("lib/auth/permissions", () => {
 	describe("canAccessBranch", () => {
@@ -27,6 +32,12 @@ describe("lib/auth/permissions", () => {
 			expect(canAccessBranch(session, "NL99")).toBe(true);
 		});
 
+		it("allows superadmin role for any branch", () => {
+			const session = { role: "superadmin" };
+			expect(canAccessBranch(session, "NL01")).toBe(true);
+			expect(canAccessBranch(session, "NL99")).toBe(true);
+		});
+
 		it("allows dev role for any branch", () => {
 			const session = { role: "dev" };
 			expect(canAccessBranch(session, "NL01")).toBe(true);
@@ -57,13 +68,16 @@ describe("lib/auth/permissions", () => {
 			expect(filterBranchesForSession(session, ["NL02", "NL03"])).toEqual([]);
 		});
 
-		it("for admin/dev: returns the full list", () => {
+		it("for admin/superadmin/dev: returns the full list", () => {
 			const branches = ["NL01", "NL02", "NL03"];
 			expect(filterBranchesForSession({ role: "admin" }, branches)).toEqual(
-				branches
+				branches,
 			);
+			expect(
+				filterBranchesForSession({ role: "superadmin" }, branches),
+			).toEqual(branches);
 			expect(filterBranchesForSession({ role: "dev" }, branches)).toEqual(
-				branches
+				branches,
 			);
 		});
 
@@ -74,4 +88,54 @@ describe("lib/auth/permissions", () => {
 			expect(branches).toEqual(copy);
 		});
 	});
+
+	describe("canManageUsers", () => {
+		it("returns true for dev and superadmin", () => {
+			expect(canManageUsers({ role: "dev" })).toBe(true);
+			expect(canManageUsers({ role: "superadmin" })).toBe(true);
+		});
+
+		it("returns false for admin and branch", () => {
+			expect(canManageUsers({ role: "admin" })).toBe(false);
+			expect(canManageUsers({ role: "branch", branchId: "NL01" })).toBe(false);
+		});
+
+		it("returns false for unknown roles or missing session", () => {
+			expect(canManageUsers({ role: "user" })).toBe(false);
+			expect(canManageUsers(null)).toBe(false);
+		});
+	});
+
+	describe("requireUserManagement", () => {
+		it("does not throw for dev and superadmin", () => {
+			expect(() => requireUserManagement({ role: "dev" })).not.toThrow();
+			expect(() => requireUserManagement({ role: "superadmin" })).not.toThrow();
+		});
+
+		it("throws 403 AUTH_FORBIDDEN_USER_MANAGEMENT for admin", () => {
+			try {
+				requireUserManagement({ role: "admin" });
+				throw new Error("Expected requireUserManagement to throw");
+			} catch (err) {
+				expect(err).toMatchObject({
+					status: 403,
+					code: "AUTH_FORBIDDEN_USER_MANAGEMENT",
+					message: "Forbidden",
+				});
+			}
+		});
+
+		it("throws 403 AUTH_FORBIDDEN_USER_MANAGEMENT for branch", () => {
+			try {
+				requireUserManagement({ role: "branch", branchId: "NL01" });
+				throw new Error("Expected requireUserManagement to throw");
+			} catch (err) {
+				expect(err).toMatchObject({
+					status: 403,
+					code: "AUTH_FORBIDDEN_USER_MANAGEMENT",
+					message: "Forbidden",
+				});
+			}
+		});
+	});
 });

+ 2 - 1
models/user.js

@@ -5,6 +5,7 @@ const { Schema, models, model } = mongoose;
 export const USER_ROLES = Object.freeze({
 	BRANCH: "branch",
 	ADMIN: "admin",
+	SUPERADMIN: "superadmin",
 	DEV: "dev",
 });
 
@@ -80,7 +81,7 @@ const userSchema = new Schema(
 				return ret;
 			},
 		},
-	}
+	},
 );
 
 // Avoid model overwrite issues in Next.js dev / hot reload