Task #649: Public Relations & Protocol module (bookings, external meetings, gifts)
Separate additive module, fully independent from executive-meetings. All tables prefixed protocol_, routes under /protocol, Arabic-first RTL UI. This session: addressed code-review rejection by switching protocol authorization from hardcoded role-name checks to scoped permission checks. - auth.ts: requireProtocolAccess now guards on the "protocol.access" permission (same permission that gates the home-screen tile via app_permissions), so backend read access aligns with tile visibility. - protocol.ts: replaced role-name helpers with makeRequirePerm + PROTOCOL_PERMS map (protocol.access / request / mutate / approve / rooms.manage / audit.read). Booking auto-approve and /protocol/me capabilities now derive from permissions. - seed.ts: seeds all 6 scoped permissions and maps them to admin + the 5 protocol roles (access:6, request:5, mutate:4, approve:3, audit:3, rooms:2 role grants). Verified: typecheck clean (tx-os, api-server protocol/auth, scripts), seed applied, DB mappings confirmed, /api/protocol/me returns 401 unauth. Architect review PASS. Deviations: none from spec. Frontend already consumed capability flags from /me, so no frontend changes were needed for the permission switch.
This commit is contained in:
+101
-30
@@ -20,7 +20,7 @@ import {
|
||||
executiveMeetingNotificationsTable,
|
||||
protocolRoomsTable,
|
||||
} from "@workspace/db";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { eq, inArray, sql } from "drizzle-orm";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
async function main() {
|
||||
@@ -814,46 +814,117 @@ async function main() {
|
||||
await db.insert(rolesTable).values(protocolRoles).onConflictDoNothing();
|
||||
console.log("Protocol roles created");
|
||||
|
||||
// Gate the Protocol home-screen icon to admin + the 5 protocol roles.
|
||||
await db
|
||||
.insert(permissionsTable)
|
||||
.values({
|
||||
// Scoped protocol permissions. `protocol.access` also gates the home-screen
|
||||
// tile (via app_permissions); the rest guard individual route capabilities.
|
||||
// Route middleware authorizes by these permissions, never by role name, so
|
||||
// an admin can delegate any capability by granting the permission.
|
||||
const protocolPermissions = [
|
||||
{
|
||||
name: "protocol.access",
|
||||
descriptionAr: "الوصول إلى وحدة العلاقات العامة والمراسم",
|
||||
descriptionEn: "Access the Public Relations & Protocol module",
|
||||
})
|
||||
},
|
||||
{
|
||||
name: "protocol.request",
|
||||
descriptionAr: "تقديم طلبات الحجز وإصدار الدروع",
|
||||
descriptionEn: "Submit protocol booking and gift-issue requests",
|
||||
},
|
||||
{
|
||||
name: "protocol.mutate",
|
||||
descriptionAr: "إنشاء وتعديل سجلات العلاقات العامة والمراسم",
|
||||
descriptionEn: "Create and edit protocol records",
|
||||
},
|
||||
{
|
||||
name: "protocol.approve",
|
||||
descriptionAr: "اعتماد أو رفض الحجوزات وإصدارات الدروع",
|
||||
descriptionEn: "Approve or reject protocol bookings and gift issues",
|
||||
},
|
||||
{
|
||||
name: "protocol.rooms.manage",
|
||||
descriptionAr: "إدارة سجل القاعات",
|
||||
descriptionEn: "Manage the protocol rooms registry",
|
||||
},
|
||||
{
|
||||
name: "protocol.audit.read",
|
||||
descriptionAr: "عرض سجل تدقيق العلاقات العامة والمراسم",
|
||||
descriptionEn: "View the protocol audit log",
|
||||
},
|
||||
];
|
||||
await db
|
||||
.insert(permissionsTable)
|
||||
.values(protocolPermissions)
|
||||
.onConflictDoNothing();
|
||||
|
||||
const [protocolAccessPerm] = await db
|
||||
.select()
|
||||
.from(permissionsTable)
|
||||
.where(eq(permissionsTable.name, "protocol.access"));
|
||||
|
||||
if (protocolAccessPerm) {
|
||||
const protocolRoleNames = [
|
||||
// Map each protocol permission to the roles that should hold it. Capabilities
|
||||
// are additive: super admin holds everything, viewer only reads.
|
||||
const protocolRolePermMap: Record<string, string[]> = {
|
||||
"protocol.access": [
|
||||
"admin",
|
||||
"protocol_super_admin",
|
||||
"protocol_pr_manager",
|
||||
"protocol_officer",
|
||||
"protocol_requester",
|
||||
"protocol_viewer",
|
||||
];
|
||||
const allRolesForProtocol = await db.select().from(rolesTable);
|
||||
const protocolRoleRows = allRolesForProtocol.filter((r) =>
|
||||
protocolRoleNames.includes(r.name),
|
||||
);
|
||||
if (protocolRoleRows.length > 0) {
|
||||
await db
|
||||
.insert(rolePermissionsTable)
|
||||
.values(
|
||||
protocolRoleRows.map((r) => ({
|
||||
roleId: r.id,
|
||||
permissionId: protocolAccessPerm.id,
|
||||
})),
|
||||
)
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
],
|
||||
"protocol.request": [
|
||||
"admin",
|
||||
"protocol_super_admin",
|
||||
"protocol_pr_manager",
|
||||
"protocol_officer",
|
||||
"protocol_requester",
|
||||
],
|
||||
"protocol.mutate": [
|
||||
"admin",
|
||||
"protocol_super_admin",
|
||||
"protocol_pr_manager",
|
||||
"protocol_officer",
|
||||
],
|
||||
"protocol.approve": [
|
||||
"admin",
|
||||
"protocol_super_admin",
|
||||
"protocol_pr_manager",
|
||||
],
|
||||
"protocol.rooms.manage": ["admin", "protocol_super_admin"],
|
||||
"protocol.audit.read": [
|
||||
"admin",
|
||||
"protocol_super_admin",
|
||||
"protocol_pr_manager",
|
||||
],
|
||||
};
|
||||
|
||||
const allRolesForProtocol = await db.select().from(rolesTable);
|
||||
const roleIdByName = new Map(allRolesForProtocol.map((r) => [r.name, r.id]));
|
||||
const allProtocolPerms = await db
|
||||
.select()
|
||||
.from(permissionsTable)
|
||||
.where(
|
||||
inArray(
|
||||
permissionsTable.name,
|
||||
protocolPermissions.map((p) => p.name),
|
||||
),
|
||||
);
|
||||
const permIdByName = new Map(allProtocolPerms.map((p) => [p.name, p.id]));
|
||||
|
||||
const rolePermValues: { roleId: number; permissionId: number }[] = [];
|
||||
for (const [permName, roleNames] of Object.entries(protocolRolePermMap)) {
|
||||
const permId = permIdByName.get(permName);
|
||||
if (permId === undefined) continue;
|
||||
for (const roleName of roleNames) {
|
||||
const roleId = roleIdByName.get(roleName);
|
||||
if (roleId === undefined) continue;
|
||||
rolePermValues.push({ roleId, permissionId: permId });
|
||||
}
|
||||
}
|
||||
if (rolePermValues.length > 0) {
|
||||
await db
|
||||
.insert(rolePermissionsTable)
|
||||
.values(rolePermValues)
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
console.log("Protocol scoped permissions mapped to roles");
|
||||
|
||||
const protocolAccessPermId = permIdByName.get("protocol.access");
|
||||
if (protocolAccessPermId !== undefined) {
|
||||
const [protocolApp] = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
@@ -861,7 +932,7 @@ async function main() {
|
||||
if (protocolApp) {
|
||||
await db
|
||||
.insert(appPermissionsTable)
|
||||
.values({ appId: protocolApp.id, permissionId: protocolAccessPerm.id })
|
||||
.values({ appId: protocolApp.id, permissionId: protocolAccessPermId })
|
||||
.onConflictDoNothing();
|
||||
console.log(
|
||||
"App permissions set: protocol app restricted to protocol.access permission",
|
||||
|
||||
Reference in New Issue
Block a user