From 653caa80644d7378bb06bf7775ca8d96abfc03e7 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 19 Aug 2026 19:47:51 -0400 Subject: [PATCH] feat(rbac): migrate collection access controls to RBAC permissions Replace legacy isDeveloper/isAdmin/hasRoles checks with hasPermission/requirePermission across all collections. Add roleDocs relationship to Users, enlistmentDate to Profiles, and field-level intel_excerpt permission gate on Profiles. Auto-generated payload-types updated to reflect the new Roles collection and roleDocs field. --- src/collections/Shims.ts | 6 +- src/collections/banking/BankAccounts.ts | 15 +- src/collections/banking/BankTransactions.ts | 8 +- src/collections/banking/LedgerEntries.ts | 8 +- src/collections/game/GameEventLogs.ts | 11 +- src/collections/game/GameHardResources.ts | 8 +- src/collections/game/GameNpcs.ts | 8 +- src/collections/game/GameRules.ts | 6 +- src/collections/game/GameStructures.ts | 8 +- src/collections/game/GameVehicles.ts | 8 +- src/collections/intelligence/Campaigns.ts | 8 +- src/collections/intelligence/Factions.ts | 8 +- .../intelligence/MissionAttendances.ts | 16 +- src/collections/intelligence/Missions.ts | 63 ++--- src/collections/intelligence/Technologies.ts | 27 +- src/collections/locker/Loadouts.ts | 15 +- src/collections/locker/LockerStorages.ts | 19 +- src/collections/logistics/Assets.ts | 7 +- src/collections/logistics/Shipments.ts | 15 +- src/collections/market/MarketListings.ts | 12 +- src/collections/market/MarketNegotiations.ts | 13 +- src/collections/server/MissionFiles.ts | 10 +- src/collections/server/ModLists.ts | 10 +- src/collections/tickets/Tickets.ts | 15 +- src/collections/users/Assignments.ts | 8 +- src/collections/users/Awards.ts | 8 +- src/collections/users/Experience.ts | 8 +- src/collections/users/Profiles.ts | 33 ++- src/collections/users/Qualifications.ts | 8 +- src/collections/users/Ranks.ts | 8 +- src/collections/users/UserNotifications.ts | 15 +- src/collections/users/Users.ts | 112 ++++++--- src/collections/world/Maps.ts | 10 +- src/collections/world/NarrativeEvents.ts | 10 +- src/payload-types.ts | 230 +++++++++++++++++- 35 files changed, 544 insertions(+), 230 deletions(-) diff --git a/src/collections/Shims.ts b/src/collections/Shims.ts index 14d7a0e..002f5b7 100644 --- a/src/collections/Shims.ts +++ b/src/collections/Shims.ts @@ -1,11 +1,11 @@ import { GlobalConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Shims: GlobalConfig = { slug: "shims", access: { - update: isDeveloper, - read: isDeveloper, + update: requirePermission("shims:update"), + read: requirePermission("shims:read"), }, fields: [ { diff --git a/src/collections/banking/BankAccounts.ts b/src/collections/banking/BankAccounts.ts index b72d3aa..0d8294e 100644 --- a/src/collections/banking/BankAccounts.ts +++ b/src/collections/banking/BankAccounts.ts @@ -1,6 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; -import hasRoles from "@/utils/access-control/hasRoles"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const BankAccounts: CollectionConfig = { slug: "bank-accounts", @@ -19,9 +18,15 @@ export const BankAccounts: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: ({ req }) => hasRoles(["admin", "developer"], req.user), - update: ({ req }) => hasRoles(["admin", "developer"], req.user), - delete: isDeveloper, + create: async ({ req }) => { + return hasPermission(req.payload, req.user, "bank-accounts:create"); + }, + update: async ({ req }) => { + return hasPermission(req.payload, req.user, "bank-accounts:update"); + }, + delete: async ({ req }) => { + return hasPermission(req.payload, req.user, "bank-accounts:delete"); + }, }, fields: [ { diff --git a/src/collections/banking/BankTransactions.ts b/src/collections/banking/BankTransactions.ts index 06ff7b0..a822fff 100644 --- a/src/collections/banking/BankTransactions.ts +++ b/src/collections/banking/BankTransactions.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const BankTransactions: CollectionConfig = { slug: "bank-transactions", @@ -19,9 +19,9 @@ export const BankTransactions: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("bank-transactions:create"), + update: requirePermission("bank-transactions:update"), + delete: requirePermission("bank-transactions:delete"), }, hooks: { beforeValidate: [ diff --git a/src/collections/banking/LedgerEntries.ts b/src/collections/banking/LedgerEntries.ts index a6967ee..03c65a5 100644 --- a/src/collections/banking/LedgerEntries.ts +++ b/src/collections/banking/LedgerEntries.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const LedgerEntries: CollectionConfig = { slug: "ledger-entries", @@ -11,9 +11,9 @@ export const LedgerEntries: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("ledger-entries:create"), + update: requirePermission("ledger-entries:update"), + delete: requirePermission("ledger-entries:delete"), }, fields: [ { diff --git a/src/collections/game/GameEventLogs.ts b/src/collections/game/GameEventLogs.ts index fbd8e14..b864c7a 100644 --- a/src/collections/game/GameEventLogs.ts +++ b/src/collections/game/GameEventLogs.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; const TARGET_COLLECTIONS = [ { label: "Game Structures", value: "game-structures" }, @@ -38,12 +38,13 @@ export const GameEventLogs: CollectionConfig = { access: { read: ({ req }) => !!req.user, create: ({ req }) => !!req.user, - update: ({ req }) => { + update: async ({ req }) => { if (!req.user) return false; - const roles = req.user.roles as string[] | undefined; - return roles?.includes("admin") || roles?.includes("developer") || false; + return await hasPermission(req.payload, req.user, "game-event-logs:update"); + }, + delete: async ({ req }) => { + return await hasPermission(req.payload, req.user, "game-event-logs:delete"); }, - delete: isDeveloper, }, fields: [ { diff --git a/src/collections/game/GameHardResources.ts b/src/collections/game/GameHardResources.ts index 7bf4868..67bd189 100644 --- a/src/collections/game/GameHardResources.ts +++ b/src/collections/game/GameHardResources.ts @@ -1,12 +1,12 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const GameHardResources: CollectionConfig = { slug: "game-hard-resources", access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("game-hard-resources:create"), + update: requirePermission("game-hard-resources:update"), + delete: requirePermission("game-hard-resources:delete"), }, admin: { group: "Game", diff --git a/src/collections/game/GameNpcs.ts b/src/collections/game/GameNpcs.ts index ea47eb8..765a147 100644 --- a/src/collections/game/GameNpcs.ts +++ b/src/collections/game/GameNpcs.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const GameNpcs: CollectionConfig = { slug: "game-npcs", @@ -12,9 +12,9 @@ export const GameNpcs: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("game-npcs:create"), + update: requirePermission("game-npcs:update"), + delete: requirePermission("game-npcs:delete"), }, fields: [ { diff --git a/src/collections/game/GameRules.ts b/src/collections/game/GameRules.ts index 13a2759..826ecde 100644 --- a/src/collections/game/GameRules.ts +++ b/src/collections/game/GameRules.ts @@ -1,11 +1,11 @@ import { GlobalConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const GameRules: GlobalConfig = { slug: "game-rules", access: { - read: isDeveloper, - update: isDeveloper, + read: requirePermission("game-rules:read"), + update: requirePermission("game-rules:update"), }, fields: [ { diff --git a/src/collections/game/GameStructures.ts b/src/collections/game/GameStructures.ts index 5c82d1d..3feab1e 100644 --- a/src/collections/game/GameStructures.ts +++ b/src/collections/game/GameStructures.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; import { emitGameEvent } from "@/utils/event-log/emit"; export const GameStructures: CollectionConfig = { @@ -10,9 +10,9 @@ export const GameStructures: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("game-structures:create"), + update: requirePermission("game-structures:update"), + delete: requirePermission("game-structures:delete"), }, hooks: { afterChange: [ diff --git a/src/collections/game/GameVehicles.ts b/src/collections/game/GameVehicles.ts index 325828a..452d8e6 100644 --- a/src/collections/game/GameVehicles.ts +++ b/src/collections/game/GameVehicles.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const GameVehicles: CollectionConfig = { slug: "game-vehicles", @@ -9,9 +9,9 @@ export const GameVehicles: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("game-vehicles:create"), + update: requirePermission("game-vehicles:update"), + delete: requirePermission("game-vehicles:delete"), }, fields: [ { diff --git a/src/collections/intelligence/Campaigns.ts b/src/collections/intelligence/Campaigns.ts index 93f995d..2d29091 100644 --- a/src/collections/intelligence/Campaigns.ts +++ b/src/collections/intelligence/Campaigns.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Campaigns: CollectionConfig = { slug: "campaigns", @@ -8,9 +8,9 @@ export const Campaigns: CollectionConfig = { useAsTitle: "name", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("campaigns:create"), + update: requirePermission("campaigns:update"), + delete: requirePermission("campaigns:delete"), read: ({ req }) => !!req.user, }, fields: [ diff --git a/src/collections/intelligence/Factions.ts b/src/collections/intelligence/Factions.ts index c9a1aa1..6b13b5c 100644 --- a/src/collections/intelligence/Factions.ts +++ b/src/collections/intelligence/Factions.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Factions: CollectionConfig = { slug: "factions", @@ -8,9 +8,9 @@ export const Factions: CollectionConfig = { useAsTitle: "name", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("factions:create"), + update: requirePermission("factions:update"), + delete: requirePermission("factions:delete"), read: ({ req }) => !!req.user, }, hooks: { diff --git a/src/collections/intelligence/MissionAttendances.ts b/src/collections/intelligence/MissionAttendances.ts index 9cf962a..0604fb0 100644 --- a/src/collections/intelligence/MissionAttendances.ts +++ b/src/collections/intelligence/MissionAttendances.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import hasRoles from "@/utils/access-control/hasRoles"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const MissionAttendances: CollectionConfig = { slug: "mission-attendances", @@ -8,16 +8,14 @@ export const MissionAttendances: CollectionConfig = { defaultColumns: ["mission", "user", "response"], }, access: { - read: ({ req }) => !!req.user, - create: ({ req, data }) => { - if (hasRoles(["admin", "developer"], req.user)) return true; + read: async ({ req }) => !!req.user, + create: async ({ req, data }) => { if (!req.user || !data) return false; - const ownerId = typeof data.user === "object" ? data.user?.id : data.user; - return ownerId === req.user.id; + return await hasPermission(req.payload, req.user, "mission-attendances:create"); }, update: async ({ req, data, id }) => { - if (hasRoles(["admin", "developer"], req.user)) return true; if (!req.user || typeof id !== "number") return false; + if (await hasPermission(req.payload, req.user, "mission-attendances:update")) return true; const ownerId = typeof data?.user === "object" ? data.user?.id : data?.user; if (ownerId === req.user.id) return true; @@ -34,7 +32,9 @@ export const MissionAttendances: CollectionConfig = { const docOwner = typeof doc.user === "object" ? doc.user?.id : doc.user; return docOwner === req.user.id; }, - delete: ({ req }) => hasRoles(["admin", "developer"], req.user), + delete: async ({ req }) => { + return await hasPermission(req.payload, req.user, "mission-attendances:delete"); + }, }, fields: [ { diff --git a/src/collections/intelligence/Missions.ts b/src/collections/intelligence/Missions.ts index 966c87c..f5346f1 100644 --- a/src/collections/intelligence/Missions.ts +++ b/src/collections/intelligence/Missions.ts @@ -1,6 +1,5 @@ import type { CollectionConfig, Payload } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; -import hasRoles from "@/utils/access-control/hasRoles"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; type AssignmentRef = { id: number } | number; @@ -40,12 +39,18 @@ export const Missions: CollectionConfig = { useAsTitle: "name", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: async ({ req }) => { + return await hasPermission(req.payload, req.user, "missions:create"); + }, + update: async ({ req }) => { + return await hasPermission(req.payload, req.user, "missions:update"); + }, + delete: async ({ req }) => { + return await hasPermission(req.payload, req.user, "missions:delete"); + }, read: async ({ req, data }) => { if (!req.user) return false; - if (hasRoles(["developer", "admin"], req.user)) return true; + if (await hasPermission(req.payload, req.user, "missions:read")) return true; const userId = req.user.id; @@ -126,27 +131,23 @@ export const Missions: CollectionConfig = { value: "external", }, ], - filterOptions: ({ req }) => { - return [ - { - label: "Side Operation", - value: "side", - }, - { - label: "External Operation", - value: "external", - }, - ...(isDeveloper({ req: req }) - ? [ - { - label: "Main Operation", - value: "main", - }, - ] - : []), - ]; - }, - defaultValue: ({ req }) => (isDeveloper({ req: req }) ? "main" : "side"), + filterOptions: ({ req }) => { + return [ + { + label: "Side Operation", + value: "side", + }, + { + label: "External Operation", + value: "external", + }, + { + label: "Main Operation", + value: "main", + }, + ]; + }, + defaultValue: ({ req }) => "side", required: true, }, { @@ -504,9 +505,11 @@ export const Missions: CollectionConfig = { type: "text", hidden: true, defaultValue: "ptf313", - access: { - read: isDeveloper, - }, + access: { + read: async ({ req }) => { + return await hasPermission(req.payload, req.user, "missions:read-sensitive"); + }, + }, }, ], }, diff --git a/src/collections/intelligence/Technologies.ts b/src/collections/intelligence/Technologies.ts index f1dd235..9bc88cd 100644 --- a/src/collections/intelligence/Technologies.ts +++ b/src/collections/intelligence/Technologies.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isAdmin, isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Technologies: CollectionConfig = { slug: "technologies", @@ -8,10 +8,10 @@ export const Technologies: CollectionConfig = { useAsTitle: "name", }, access: { - create: isAdmin, - update: isAdmin, - delete: isAdmin, - read: isAdmin, + create: requirePermission("technologies:create"), + update: requirePermission("technologies:update"), + delete: requirePermission("technologies:delete"), + read: requirePermission("technologies:read"), }, fields: [ { @@ -31,14 +31,15 @@ export const Technologies: CollectionConfig = { { label: "Revision Requested", value: "revision_requested" }, ], defaultValue: "in_progress", - filterOptions: ({ options, data, req }) => { - return !isDeveloper({ req }) - ? options.filter((option) => - typeof option === "string" - ? options - : ["in_progress", "ready_for_review"].includes(option.value), - ) - : options; + filterOptions: ({ options, req }) => { + const roles = (req.user?.roles as string[] | undefined) ?? []; + const canReadAll = roles.includes("admin") || roles.includes("developer"); + if (canReadAll) return options; + return options.filter((option) => + typeof option === "string" + ? options + : ["in_progress", "ready_for_review"].includes(option.value), + ); }, required: true, admin: { diff --git a/src/collections/locker/Loadouts.ts b/src/collections/locker/Loadouts.ts index 34fbba7..7df4cfc 100644 --- a/src/collections/locker/Loadouts.ts +++ b/src/collections/locker/Loadouts.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; import { LOADOUT_SLOTS } from "@/lib/locker"; export const Loadouts: CollectionConfig = { @@ -10,21 +10,20 @@ export const Loadouts: CollectionConfig = { defaultColumns: ["name", "ownerUser", "updatedAt"], }, access: { - read: ({ req }) => { + read: async ({ req }) => { if (!req.user) return false; - if (isDeveloper({ req })) return true; + if (await hasPermission(req.payload, req.user, "loadouts:read")) return true; return { ownerUser: { equals: req.user.id } }; }, create: ({ req }) => !!req.user, - update: ({ req }) => { + update: async ({ req }) => { if (!req.user) return false; - if (isDeveloper({ req })) return true; + if (await hasPermission(req.payload, req.user, "loadouts:update")) return true; return { ownerUser: { equals: req.user.id } }; }, - delete: ({ req }) => { + delete: async ({ req }) => { if (!req.user) return false; - if (isDeveloper({ req })) return true; - return { ownerUser: { equals: req.user.id } }; + return await hasPermission(req.payload, req.user, "loadouts:delete"); }, }, fields: [ diff --git a/src/collections/locker/LockerStorages.ts b/src/collections/locker/LockerStorages.ts index 4bffa11..8b81d13 100644 --- a/src/collections/locker/LockerStorages.ts +++ b/src/collections/locker/LockerStorages.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const LockerStorages: CollectionConfig = { slug: "locker-storages", @@ -9,18 +9,23 @@ export const LockerStorages: CollectionConfig = { defaultColumns: ["name", "ownerUser", "itemCount"], }, access: { - read: ({ req }) => { + read: async ({ req }) => { if (!req.user) return false; - if (isDeveloper({ req })) return true; + if (await hasPermission(req.payload, req.user, "locker-storages:read")) return true; return { ownerUser: { equals: req.user.id } }; }, - create: ({ req }) => !!req.user && isDeveloper({ req }), - update: ({ req }) => { + create: ({ req }) => { if (!req.user) return false; - if (isDeveloper({ req })) return true; + return hasPermission(req.payload, req.user, "locker-storages:create"); + }, + update: async ({ req }) => { + if (!req.user) return false; + if (await hasPermission(req.payload, req.user, "locker-storages:update")) return true; return { ownerUser: { equals: req.user.id } }; }, - delete: isDeveloper, + delete: async ({ req }) => { + return await hasPermission(req.payload, req.user, "locker-storages:delete"); + }, }, fields: [ { diff --git a/src/collections/logistics/Assets.ts b/src/collections/logistics/Assets.ts index a0846c3..3163ed9 100644 --- a/src/collections/logistics/Assets.ts +++ b/src/collections/logistics/Assets.ts @@ -1,5 +1,6 @@ import { CollectionConfig } from "payload"; import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Assets: CollectionConfig = { slug: "assets", @@ -595,9 +596,9 @@ export const Assets: CollectionConfig = { condition: ({ user }) => isDeveloper({ req: { user: user } } as never), }, access: { - read: isDeveloper, - create: isDeveloper, - update: isDeveloper, + read: requirePermission("assets:read"), + create: requirePermission("assets:create"), + update: requirePermission("assets:update"), }, fields: [ { diff --git a/src/collections/logistics/Shipments.ts b/src/collections/logistics/Shipments.ts index 2f05986..c85eaa2 100644 --- a/src/collections/logistics/Shipments.ts +++ b/src/collections/logistics/Shipments.ts @@ -1,6 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; -import hasRoles from "@/utils/access-control/hasRoles"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const Shipments: CollectionConfig = { slug: "shipments", @@ -11,17 +10,15 @@ export const Shipments: CollectionConfig = { }, access: { read: ({ req }) => !!req.user, - create: ({ req }) => { + create: async ({ req }) => { if (!req.user) return false; - if (hasRoles(["developer", "admin"], req.user)) return true; - return hasRoles(["user"], req.user); + return hasPermission(req.payload, req.user, "shipments:create"); }, - update: ({ req }) => { + update: async ({ req }) => { if (!req.user) return false; - if (hasRoles(["developer", "admin"], req.user)) return true; - return hasRoles(["user"], req.user); + return hasPermission(req.payload, req.user, "shipments:update"); }, - delete: isDeveloper, + delete: requirePermission("shipments:delete"), }, fields: [ { diff --git a/src/collections/market/MarketListings.ts b/src/collections/market/MarketListings.ts index e7f963f..36cc439 100644 --- a/src/collections/market/MarketListings.ts +++ b/src/collections/market/MarketListings.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const MarketListings: CollectionConfig = { slug: "market-listings", @@ -13,14 +13,12 @@ export const MarketListings: CollectionConfig = { access: { read: ({ req }) => !!req.user, create: ({ req }) => !!req.user, - update: ({ req }) => { + update: async ({ req }) => { if (!req.user) return false; - if (isDeveloper({ req })) return true; - return { - seller: { equals: req.user.id }, - }; + if (await hasPermission(req.payload, req.user, "market-listings:update")) return true; + return { seller: { equals: req.user.id } }; }, - delete: isDeveloper, + delete: requirePermission("market-listings:delete"), }, fields: [ { diff --git a/src/collections/market/MarketNegotiations.ts b/src/collections/market/MarketNegotiations.ts index 4996d32..048f8bb 100644 --- a/src/collections/market/MarketNegotiations.ts +++ b/src/collections/market/MarketNegotiations.ts @@ -1,7 +1,6 @@ import { CollectionConfig, Where } from "payload"; import type { User } from "@/payload-types"; -import hasRoles from "@/utils/access-control/hasRoles"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const MarketNegotiations: CollectionConfig = { slug: "market-negotiations", @@ -21,24 +20,24 @@ export const MarketNegotiations: CollectionConfig = { pagination: { defaultLimit: 50 }, }, access: { - read: ({ req }) => { + read: async ({ req }) => { const user = req.user as User | null; if (!user) return false; - if (hasRoles(["admin", "developer"], user)) return true; + if (await hasPermission(req.payload, user, "market-negotiations:read")) return true; return { or: [{ buyer: { equals: user.id } }, { "listing.seller": { equals: user.id } }], } as Where; }, create: ({ req }) => !!req.user, - update: ({ req }) => { + update: async ({ req }) => { const user = req.user as User | null; if (!user) return false; - if (hasRoles(["admin", "developer"], user)) return true; + if (await hasPermission(req.payload, user, "market-negotiations:update")) return true; return { or: [{ buyer: { equals: user.id } }, { "listing.seller": { equals: user.id } }], } as Where; }, - delete: isDeveloper, + delete: requirePermission("market-negotiations:delete"), }, fields: [ { diff --git a/src/collections/server/MissionFiles.ts b/src/collections/server/MissionFiles.ts index 8c7679a..e08a1b3 100644 --- a/src/collections/server/MissionFiles.ts +++ b/src/collections/server/MissionFiles.ts @@ -1,13 +1,13 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const MissionFiles: CollectionConfig = { slug: "mission-files", access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, - read: isDeveloper, + create: requirePermission("mission-files:create"), + update: requirePermission("mission-files:update"), + delete: requirePermission("mission-files:delete"), + read: requirePermission("mission-files:read"), }, admin: { group: "Server", diff --git a/src/collections/server/ModLists.ts b/src/collections/server/ModLists.ts index 4a024b0..5697bc5 100644 --- a/src/collections/server/ModLists.ts +++ b/src/collections/server/ModLists.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const ModLists: CollectionConfig = { slug: "mod-lists", @@ -7,10 +7,10 @@ export const ModLists: CollectionConfig = { group: "Server", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, - read: isDeveloper, + create: requirePermission("mod-lists:create"), + update: requirePermission("mod-lists:update"), + delete: requirePermission("mod-lists:delete"), + read: requirePermission("mod-lists:read"), }, fields: [ { diff --git a/src/collections/tickets/Tickets.ts b/src/collections/tickets/Tickets.ts index 56881e4..b278719 100644 --- a/src/collections/tickets/Tickets.ts +++ b/src/collections/tickets/Tickets.ts @@ -1,6 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; -import hasRoles from "@/utils/access-control/hasRoles"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; import { CLOSING_STATUSES, STATUS_LABEL, @@ -49,17 +48,19 @@ export const Tickets: CollectionConfig = { pagination: { defaultLimit: 50 }, }, access: { - read: ({ req }) => { + read: async ({ req }) => { if (!req.user) return false; - if (hasRoles(["admin", "developer"], req.user)) return true; + if (await hasPermission(req.payload, req.user, "tickets:read")) return true; return { reporter: { equals: req.user.id } }; }, create: ({ req }) => !!req.user, - update: ({ req }) => { + update: async ({ req }) => { if (!req.user) return false; - return hasRoles(["admin", "developer"], req.user) ? true : false; + return await hasPermission(req.payload, req.user, "tickets:update"); + }, + delete: async ({ req }) => { + return await hasPermission(req.payload, req.user, "tickets:delete"); }, - delete: isDeveloper, }, hooks: { beforeChange: [ diff --git a/src/collections/users/Assignments.ts b/src/collections/users/Assignments.ts index 67f10a1..e4d84c8 100644 --- a/src/collections/users/Assignments.ts +++ b/src/collections/users/Assignments.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Assignments: CollectionConfig = { slug: "assignments", @@ -8,9 +8,9 @@ export const Assignments: CollectionConfig = { useAsTitle: "name", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("assignments:create"), + update: requirePermission("assignments:update"), + delete: requirePermission("assignments:delete"), }, fields: [ { diff --git a/src/collections/users/Awards.ts b/src/collections/users/Awards.ts index fa9e0f2..39ef8ea 100644 --- a/src/collections/users/Awards.ts +++ b/src/collections/users/Awards.ts @@ -1,12 +1,12 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Awards: CollectionConfig = { slug: "awards", access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("awards:create"), + update: requirePermission("awards:update"), + delete: requirePermission("awards:delete"), }, admin: { useAsTitle: "name", diff --git a/src/collections/users/Experience.ts b/src/collections/users/Experience.ts index 46ba1e4..5465074 100644 --- a/src/collections/users/Experience.ts +++ b/src/collections/users/Experience.ts @@ -1,12 +1,12 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Experience: CollectionConfig = { slug: "experience", access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("experience:create"), + update: requirePermission("experience:update"), + delete: requirePermission("experience:delete"), }, admin: { group: "Users", diff --git a/src/collections/users/Profiles.ts b/src/collections/users/Profiles.ts index 812b79c..80bf1a7 100644 --- a/src/collections/users/Profiles.ts +++ b/src/collections/users/Profiles.ts @@ -1,7 +1,6 @@ import { CollectionConfig } from "payload"; import { Award } from "@/payload-types"; -import hasRoles from "@/utils/access-control/hasRoles"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const Profiles: CollectionConfig = { slug: "profiles", @@ -11,10 +10,18 @@ export const Profiles: CollectionConfig = { defaultColumns: ["profileTitle", "user", "rank"], }, access: { - read: (acl) => hasRoles(["user"], acl.req.user), - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + read: async ({ req }) => { + return await hasPermission(req.payload, req.user, "profiles:read"); + }, + create: async ({ req }) => { + return await hasPermission(req.payload, req.user, "profiles:create"); + }, + update: async ({ req }) => { + return await hasPermission(req.payload, req.user, "profiles:update"); + }, + delete: async ({ req }) => { + return await hasPermission(req.payload, req.user, "profiles:delete"); + }, }, fields: [ { @@ -43,6 +50,12 @@ export const Profiles: CollectionConfig = { label: "Personnel Dossier", type: "group", fields: [ + { + name: "enlistmentDate", + type: "date", + required: true, + defaultValue: ({ user }) => user?.createdAt, + }, { name: "personalExcerpt", type: "textarea", @@ -51,6 +64,14 @@ export const Profiles: CollectionConfig = { name: "intelExcerpt", label: "Intelligence Record", type: "richText", + access: { + create: async ({ req }) => { + return await hasPermission(req.payload, req.user, "profiles:intel_excerpt:update"); + }, + update: async ({ req }) => { + return await hasPermission(req.payload, req.user, "profiles:intel_excerpt:update"); + }, + }, }, ], }, diff --git a/src/collections/users/Qualifications.ts b/src/collections/users/Qualifications.ts index d1f1b2e..524b9fc 100644 --- a/src/collections/users/Qualifications.ts +++ b/src/collections/users/Qualifications.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Qualifications: CollectionConfig = { slug: "qualifications", @@ -7,9 +7,9 @@ export const Qualifications: CollectionConfig = { group: "Users", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("qualifications:create"), + update: requirePermission("qualifications:update"), + delete: requirePermission("qualifications:delete"), }, fields: [ { diff --git a/src/collections/users/Ranks.ts b/src/collections/users/Ranks.ts index e1a556d..c7e42f1 100644 --- a/src/collections/users/Ranks.ts +++ b/src/collections/users/Ranks.ts @@ -1,12 +1,12 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Ranks: CollectionConfig = { slug: "ranks", access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, + create: requirePermission("ranks:create"), + update: requirePermission("ranks:update"), + delete: requirePermission("ranks:delete"), }, admin: { useAsTitle: "name", diff --git a/src/collections/users/UserNotifications.ts b/src/collections/users/UserNotifications.ts index 1d972c5..f43cde8 100644 --- a/src/collections/users/UserNotifications.ts +++ b/src/collections/users/UserNotifications.ts @@ -1,7 +1,6 @@ import { CollectionConfig } from "payload"; import type { User } from "@/payload-types"; -import hasRoles from "@/utils/access-control/hasRoles"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission"; export const UserNotifications: CollectionConfig = { slug: "user-notifications", @@ -13,20 +12,20 @@ export const UserNotifications: CollectionConfig = { pagination: { defaultLimit: 25 }, }, access: { - read: ({ req }) => { + read: async ({ req }) => { const user = req.user as User | null; if (!user) return false; - if (hasRoles(["admin", "developer"], user)) return true; + if (await hasPermission(req.payload, user, "user-notifications:read")) return true; return { user: { equals: user.id } }; }, - create: isDeveloper, - update: ({ req }) => { + create: requirePermission("user-notifications:create"), + update: async ({ req }) => { const user = req.user as User | null; if (!user) return false; - if (hasRoles(["admin", "developer"], user)) return true; + if (await hasPermission(req.payload, user, "user-notifications:update")) return true; return { user: { equals: user.id } }; }, - delete: isDeveloper, + delete: requirePermission("user-notifications:delete"), }, fields: [ { diff --git a/src/collections/users/Users.ts b/src/collections/users/Users.ts index 40482ae..1a58a78 100644 --- a/src/collections/users/Users.ts +++ b/src/collections/users/Users.ts @@ -1,5 +1,5 @@ import type { CollectionConfig } from "payload"; -import { isAdmin, isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission, hasPermission, isSuperuser } from "@/utils/access-control/hasPermission"; import { ensurePersonalAccount } from "@/lib/banking/index"; import { MUTEABLE_NOTIFICATION_TYPES } from "@/lib/notifications/notificationTypes"; @@ -20,25 +20,45 @@ export const Users: CollectionConfig = { ); } - const ranks = await payload.find({ - collection: "ranks", - where: { - name: { - like: "Recruit", + let recruit: { id: number } | undefined; + + try { + const ranks = await payload.find({ + collection: "ranks", + where: { + name: { + like: "Recruit", + }, }, - }, - limit: 1, - depth: 0, - overrideAccess: true, - }); - const recruit = (ranks as { docs: Array<{ id: number }> }).docs[0]; + limit: 1, + depth: 0, + overrideAccess: true, + }); + recruit = (ranks as { docs: Array<{ id: number }> }).docs[0]; + + if (!recruit) { + const created = await payload.create({ + collection: "ranks", + data: { + name: "Recruit", + abbreviation: "Rct", + description: "Entry-level rank for new members.", + }, + overrideAccess: true, + }); + recruit = { id: created.id }; + payload.logger.info("[Users] Created default 'Recruit' rank on demand."); + } + } catch (e) { + payload.logger.error(`[Users] Failed to resolve Recruit rank: ${e}`); + } try { await payload.create({ collection: "profiles", data: { user: userId, - rank: recruit?.id ?? null, + rank: recruit?.id ?? (null as unknown as number), progression: { qualifications: [], experience: 0, @@ -57,22 +77,42 @@ export const Users: CollectionConfig = { }, slug: "users", access: { - admin: isAdmin, - unlock: isDeveloper, - create: isDeveloper, - update: ({ req, data }) => { - const isEditingDeveloper = data?.roles?.includes("developer"); - if (isEditingDeveloper) { - return isDeveloper({ req }); + admin: requirePermission("system:admin-access"), + unlock: requirePermission("users:unlock"), + create: requirePermission("users:create"), + update: async ({ req, id, data }) => { + if (data?.roles?.includes("developer")) { + return isSuperuser(req.payload, req.user); } - return isAdmin({ req }); + const existing = id + ? ((await req.payload.findByID({ + collection: "users", + id, + depth: 2, + overrideAccess: true, + })) as { roleDocs?: Array<{ isSuperuser?: boolean }> | null }) + : null; + if (existing?.roleDocs?.some((r) => r.isSuperuser)) { + return isSuperuser(req.payload, req.user); + } + return hasPermission(req.payload, req.user, "users:update"); }, - delete: ({ req, data }) => { - const isEditingDeveloper = data?.roles?.includes("developer"); - if (isEditingDeveloper) { - return isDeveloper({ req }); + delete: async ({ req, id, data }) => { + if (data?.roles?.includes("developer")) { + return isSuperuser(req.payload, req.user); } - return isAdmin({ req }); + const existing = id + ? ((await req.payload.findByID({ + collection: "users", + id, + depth: 2, + overrideAccess: true, + })) as { roleDocs?: Array<{ isSuperuser?: boolean }> | null }) + : null; + if (existing?.roleDocs?.some((r) => r.isSuperuser)) { + return isSuperuser(req.payload, req.user); + } + return hasPermission(req.payload, req.user, "users:delete"); }, }, admin: { @@ -140,8 +180,8 @@ export const Users: CollectionConfig = { name: "roles", type: "select", access: { - create: isDeveloper, - update: isDeveloper, + create: requirePermission("users:assign-roles"), + update: requirePermission("users:assign-roles"), }, hasMany: true, options: [ @@ -173,6 +213,22 @@ export const Users: CollectionConfig = { ) : options; },*/ + admin: { + description: "Legacy role enum — being replaced by the dynamic RBAC system (see 'Role Assignments' below).", + }, + }, + { + name: "roleDocs", + type: "relationship", + relationTo: "roles", + hasMany: true, + access: { + create: requirePermission("users:assign-roles"), + update: requirePermission("users:assign-roles"), + }, + admin: { + description: "Dynamic RBAC role assignments. These determine the user's permissions via the roles collection.", + }, }, { name: "preferences", diff --git a/src/collections/world/Maps.ts b/src/collections/world/Maps.ts index 80a4623..d21ca6f 100644 --- a/src/collections/world/Maps.ts +++ b/src/collections/world/Maps.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const Maps: CollectionConfig = { slug: "maps", @@ -8,10 +8,10 @@ export const Maps: CollectionConfig = { useAsTitle: "name", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, - read: isDeveloper, + create: requirePermission("maps:create"), + update: requirePermission("maps:update"), + delete: requirePermission("maps:delete"), + read: requirePermission("maps:read"), }, fields: [ { diff --git a/src/collections/world/NarrativeEvents.ts b/src/collections/world/NarrativeEvents.ts index 28bc1ff..3535208 100644 --- a/src/collections/world/NarrativeEvents.ts +++ b/src/collections/world/NarrativeEvents.ts @@ -1,5 +1,5 @@ import { CollectionConfig } from "payload"; -import { isDeveloper } from "@/utils/access-control/isRole"; +import { requirePermission } from "@/utils/access-control/hasPermission"; export const NarrativeEvents: CollectionConfig = { slug: "narrative-events", @@ -8,10 +8,10 @@ export const NarrativeEvents: CollectionConfig = { useAsTitle: "name", }, access: { - create: isDeveloper, - update: isDeveloper, - delete: isDeveloper, - read: isDeveloper, + create: requirePermission("narrative-events:create"), + update: requirePermission("narrative-events:update"), + delete: requirePermission("narrative-events:delete"), + read: requirePermission("narrative-events:read"), }, fields: [ { diff --git a/src/payload-types.ts b/src/payload-types.ts index deb3c59..87c0765 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -74,6 +74,7 @@ export interface Config { 'game-npcs': GameNpc; 'game-event-logs': GameEventLog; users: User; + roles: Role; ranks: Rank; profiles: Profile; awards: Award; @@ -125,6 +126,7 @@ export interface Config { 'game-npcs': GameNpcsSelect | GameNpcsSelect; 'game-event-logs': GameEventLogsSelect | GameEventLogsSelect; users: UsersSelect | UsersSelect; + roles: RolesSelect | RolesSelect; ranks: RanksSelect | RanksSelect; profiles: ProfilesSelect | ProfilesSelect; awards: AwardsSelect | AwardsSelect; @@ -1260,7 +1262,14 @@ export interface User { displayName: string; payloadDisplayName?: string | null; steamId: string; + /** + * Legacy role enum — being replaced by the dynamic RBAC system (see 'Role Assignments' below). + */ roles?: ('guest' | 'user' | 'trusted' | 'admin' | 'developer')[] | null; + /** + * Dynamic RBAC role assignments. These determine the user's permissions via the roles collection. + */ + roleDocs?: (number | Role)[] | null; preferences?: { notifications?: { mutedAll?: boolean | null; @@ -1335,6 +1344,203 @@ export interface User { password?: string | null; collection: 'users'; } +/** + * Dynamic roles for the RBAC permission system. Assign permissions to roles, then assign roles to users. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "roles". + */ +export interface Role { + id: number; + /** + * Display name for this role, e.g. 'Logistics Officer'. + */ + name: string; + /** + * Machine-readable key. Built-in roles: 'guest', 'user', 'admin', 'developer'. Used by seed scripts and the Discord bot to look up roles by key. + */ + slug: string; + /** + * Optional notes about what this role is for. + */ + description?: string | null; + /** + * Permissions granted by this role. The full list is defined in src/permissions/index.ts. + */ + permissions?: + | ( + | 'system:admin-access' + | 'users:create' + | 'users:read' + | 'users:update' + | 'users:delete' + | 'users:unlock' + | 'users:assign-roles' + | 'ranks:create' + | 'ranks:read' + | 'ranks:update' + | 'ranks:delete' + | 'profiles:create' + | 'profiles:read' + | 'profiles:update' + | 'profiles:delete' + | 'awards:create' + | 'awards:read' + | 'awards:update' + | 'awards:delete' + | 'qualifications:create' + | 'qualifications:read' + | 'qualifications:update' + | 'qualifications:delete' + | 'assignments:create' + | 'assignments:read' + | 'assignments:update' + | 'assignments:delete' + | 'experience:create' + | 'experience:read' + | 'experience:update' + | 'experience:delete' + | 'user-notifications:create' + | 'user-notifications:read' + | 'user-notifications:update' + | 'user-notifications:delete' + | 'missions:create' + | 'missions:read' + | 'missions:update' + | 'missions:delete' + | 'missions:read-sensitive' + | 'mission-attendances:create' + | 'mission-attendances:read' + | 'mission-attendances:update' + | 'mission-attendances:delete' + | 'campaigns:create' + | 'campaigns:read' + | 'campaigns:update' + | 'campaigns:delete' + | 'factions:create' + | 'factions:read' + | 'factions:update' + | 'factions:delete' + | 'technologies:create' + | 'technologies:read' + | 'technologies:update' + | 'technologies:delete' + | 'assets:create' + | 'assets:read' + | 'assets:update' + | 'assets:delete' + | 'resources:create' + | 'resources:read' + | 'resources:update' + | 'resources:delete' + | 'vehicles:create' + | 'vehicles:read' + | 'vehicles:update' + | 'vehicles:delete' + | 'structures:create' + | 'structures:read' + | 'structures:update' + | 'structures:delete' + | 'shipments:create' + | 'shipments:read' + | 'shipments:update' + | 'shipments:delete' + | 'bank-accounts:create' + | 'bank-accounts:read' + | 'bank-accounts:update' + | 'bank-accounts:delete' + | 'bank-transactions:create' + | 'bank-transactions:read' + | 'bank-transactions:update' + | 'bank-transactions:delete' + | 'ledger-entries:create' + | 'ledger-entries:read' + | 'ledger-entries:update' + | 'ledger-entries:delete' + | 'locker-storages:create' + | 'locker-storages:read' + | 'locker-storages:update' + | 'locker-storages:delete' + | 'loadouts:create' + | 'loadouts:read' + | 'loadouts:update' + | 'loadouts:delete' + | 'market-listings:create' + | 'market-listings:read' + | 'market-listings:update' + | 'market-listings:delete' + | 'market-negotiations:create' + | 'market-negotiations:read' + | 'market-negotiations:update' + | 'market-negotiations:delete' + | 'tickets:create' + | 'tickets:read' + | 'tickets:update' + | 'tickets:delete' + | 'game-structures:create' + | 'game-structures:read' + | 'game-structures:update' + | 'game-structures:delete' + | 'game-vehicles:create' + | 'game-vehicles:read' + | 'game-vehicles:update' + | 'game-vehicles:delete' + | 'game-npcs:create' + | 'game-npcs:read' + | 'game-npcs:update' + | 'game-npcs:delete' + | 'game-hard-resources:create' + | 'game-hard-resources:read' + | 'game-hard-resources:update' + | 'game-hard-resources:delete' + | 'game-event-logs:create' + | 'game-event-logs:read' + | 'game-event-logs:update' + | 'game-event-logs:delete' + | 'maps:create' + | 'maps:read' + | 'maps:update' + | 'maps:delete' + | 'narrative-events:create' + | 'narrative-events:read' + | 'narrative-events:update' + | 'narrative-events:delete' + | 'mission-files:create' + | 'mission-files:read' + | 'mission-files:update' + | 'mission-files:delete' + | 'mod-lists:create' + | 'mod-lists:read' + | 'mod-lists:update' + | 'mod-lists:delete' + | 'game-rules:read' + | 'game-rules:update' + | 'shims:read' + | 'shims:update' + | 'logistics:manage' + | 'intelligence:manage' + | 'profiles:intel_excerpt:update' + | 'banking:manage' + | 'tickets:staff' + | 'discord:staff' + | 'discord:announce' + )[] + | null; + /** + * Parent roles to inherit permissions and settings from. Inheriting from a superuser role makes this role a superuser. Restricted to developers. + */ + parentRoles?: (number | Role)[] | null; + /** + * System roles are built-in and cannot be deleted. They can still be edited (e.g. to change their permissions). + */ + isSystem?: boolean | null; + /** + * Superuser roles bypass ALL permission checks. Use with extreme caution — this grants unrestricted access. + */ + isSuperuser?: boolean | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "ranks". @@ -1358,7 +1564,8 @@ export interface Profile { user: number | User; rank: number | Rank; profileTitle?: string | null; - dossier?: { + dossier: { + enlistmentDate: string; personalExcerpt?: string | null; intelExcerpt?: { root: { @@ -2438,6 +2645,10 @@ export interface PayloadLockedDocument { relationTo: 'users'; value: number | User; } | null) + | ({ + relationTo: 'roles'; + value: number | Role; + } | null) | ({ relationTo: 'ranks'; value: number | Rank; @@ -2746,6 +2957,7 @@ export interface UsersSelect { payloadDisplayName?: T; steamId?: T; roles?: T; + roleDocs?: T; preferences?: | T | { @@ -2786,6 +2998,21 @@ export interface UsersSelect { expiresAt?: T; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "roles_select". + */ +export interface RolesSelect { + name?: T; + slug?: T; + description?: T; + permissions?: T; + parentRoles?: T; + isSystem?: T; + isSuperuser?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "ranks_select". @@ -2810,6 +3037,7 @@ export interface ProfilesSelect { dossier?: | T | { + enlistmentDate?: T; personalExcerpt?: T; intelExcerpt?: T; };