From fb74e6b0c2f85f863bf2312ef11ff2142fc55399 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 19 Aug 2026 19:48:31 -0400 Subject: [PATCH] chore(seed): update seed scripts for RBAC roles Assign the developer role document to the sysadmin user on seed. Add seedRoles script to create default RBAC roles. --- src/tools/seed/seedRoles.ts | 226 ++++++++++++++++++++++++++++++++++++ src/tools/seed/seedUsers.ts | 10 ++ 2 files changed, 236 insertions(+) create mode 100644 src/tools/seed/seedRoles.ts diff --git a/src/tools/seed/seedRoles.ts b/src/tools/seed/seedRoles.ts new file mode 100644 index 0000000..e65139b --- /dev/null +++ b/src/tools/seed/seedRoles.ts @@ -0,0 +1,226 @@ +import { getPayload } from "payload"; +import config from "@payload-config"; +import type { Permission } from "@/permissions"; + +const USER_PERMISSIONS: Permission[] = [ + "users:read", + "ranks:read", + "profiles:read", + "awards:read", + "qualifications:read", + "assignments:read", + "experience:read", + "user-notifications:read", + "missions:read", + "mission-attendances:read", + "campaigns:read", + "factions:read", + "technologies:read", + "assets:read", + "resources:read", + "vehicles:read", + "structures:read", + "shipments:read", + "bank-accounts:read", + "bank-transactions:read", + "ledger-entries:read", + "locker-storages:read", + "loadouts:read", + "market-listings:read", + "market-negotiations:read", + "tickets:read", + "game-structures:read", + "game-vehicles:read", + "game-npcs:read", + "game-hard-resources:read", + "game-event-logs:read", + "maps:read", + "narrative-events:read", + "mission-files:read", + "mod-lists:read", + "game-rules:read", + "shims:read", + "shipments:create", + "shipments:update", + "structures:update", + "structures:delete", +]; + +const ADMIN_PERMISSIONS: Permission[] = [ + ...USER_PERMISSIONS, + "system:admin-access", + "users:read", + "users:update", + "users:delete", + "technologies:create", + "technologies:read", + "technologies:update", + "technologies:delete", + "tickets:read", + "tickets:update", + "tickets:staff", + "bank-accounts:create", + "bank-accounts:update", + "user-notifications:read", + "user-notifications:update", + "mission-attendances:create", + "mission-attendances:read", + "mission-attendances:update", + "mission-attendances:delete", + "missions:read", + "market-negotiations:read", + "market-negotiations:update", + "logistics:manage", + "banking:manage", + "discord:staff", + "discord:announce", + "structures:create", +]; + +interface BuiltinRole { + slug: string; + name: string; + description: string; + permissions: Permission[]; + isSystem: boolean; + isSuperuser: boolean; +} + +const BUILTIN_ROLES: BuiltinRole[] = [ + { + slug: "guest", + name: "Guest", + description: "Default role for unauthenticated or basic users. No permissions.", + permissions: [], + isSystem: true, + isSuperuser: false, + }, + { + slug: "user", + name: "User", + description: "Standard authenticated user. Can manage shipments, structures, and read profiles.", + permissions: [...USER_PERMISSIONS], + isSystem: true, + isSuperuser: false, + }, + { + slug: "admin", + name: "Admin", + description: "Administrator. Can manage users, tickets, banking, logistics, and most collections.", + permissions: [...ADMIN_PERMISSIONS], + isSystem: true, + isSuperuser: false, + }, + { + slug: "developer", + name: "Developer", + description: "Superuser. Bypasses all permission checks. Full access to everything.", + permissions: [], + isSystem: true, + isSuperuser: true, + }, +]; + +const ENUM_TO_SLUG: Record = { + guest: "guest", + user: "user", + trusted: "user", + admin: "admin", + developer: "developer", +}; + +export const seedRoles = async () => { + const payload = await getPayload({ config }); + + payload.logger.info("Seeding built-in RBAC roles..."); + + const roleIdMap = new Map(); + + for (const role of BUILTIN_ROLES) { + const existing = await payload.find({ + collection: "roles", + where: { slug: { equals: role.slug } }, + limit: 1, + overrideAccess: true, + }); + + if (existing.docs.length > 0) { + const doc = existing.docs[0] as { id: number }; + roleIdMap.set(role.slug, doc.id); + payload.logger.info(` Role "${role.slug}" already exists (id=${doc.id}), skipping.`); + continue; + } + + const created = await payload.create({ + collection: "roles", + data: { + name: role.name, + slug: role.slug, + description: role.description, + permissions: role.permissions, + isSystem: role.isSystem, + isSuperuser: role.isSuperuser, + }, + overrideAccess: true, + }); + + roleIdMap.set(role.slug, created.id); + payload.logger.info(` Created role "${role.slug}" (id=${created.id}).`); + } + + payload.logger.info("Migrating existing users from roles enum to roleDocs..."); + + const users = await payload.find({ + collection: "users", + limit: 0, + depth: 0, + overrideAccess: true, + select: { roles: true, roleDocs: true }, + }); + + let migrated = 0; + let skipped = 0; + + for (const user of users.docs) { + const u = user as { id: number; roles?: string[] | null; roleDocs?: unknown[] | null }; + + if (u.roleDocs && Array.isArray(u.roleDocs) && u.roleDocs.length > 0) { + skipped++; + continue; + } + + const enumRoles = u.roles ?? []; + if (enumRoles.length === 0) { + skipped++; + continue; + } + + const roleDocIds: number[] = []; + const seenSlugs = new Set(); + + for (const enumRole of enumRoles) { + const slug = ENUM_TO_SLUG[enumRole]; + if (!slug || seenSlugs.has(slug)) continue; + seenSlugs.add(slug); + const roleId = roleIdMap.get(slug); + if (roleId) roleDocIds.push(roleId); + } + + if (roleDocIds.length === 0) { + skipped++; + continue; + } + + await payload.update({ + collection: "users", + id: user.id, + data: { roleDocs: roleDocIds }, + overrideAccess: true, + }); + migrated++; + } + + payload.logger.info(`Migration complete: ${migrated} users migrated, ${skipped} users skipped.`); +}; + +await seedRoles(); diff --git a/src/tools/seed/seedUsers.ts b/src/tools/seed/seedUsers.ts index 92bb4ee..59b6c5e 100644 --- a/src/tools/seed/seedUsers.ts +++ b/src/tools/seed/seedUsers.ts @@ -14,6 +14,15 @@ export const seedUsers = async () => { }, }); + const devRole = await payload.find({ + collection: "roles", + where: { slug: { equals: "developer" } }, + limit: 1, + depth: 0, + overrideAccess: true, + }); + const devRoleId = devRole.docs[0]?.id; + payload.logger.info(`Creating initial sysadmin/developer user...`); try { const result = await payload.create({ @@ -25,6 +34,7 @@ export const seedUsers = async () => { discordUsername: "Z8MB1E", steamId: "76561198091303179", roles: ["developer"], + roleDocs: devRoleId ? [devRoleId] : [], }, });