feat: RBAC Rollen & Rechte (Phase 1 - Datenmodell, Roles-Verwaltung, Guard-Infrastruktur)
Replaces the fixed 5-value Role enum with a database-driven Role/Permission model: admins can create custom roles with a precise per-resource/action permission set (83-key code-owned catalog), instead of being locked to ADMIN/BEEKEEPER/STAFF/READONLY/API_CLIENT. - New Role/Permission/RolePermission tables; legacy Role enum renamed to LegacyRole and kept as User.legacyRole so all existing @Roles() call sites keep working unchanged during the migration. - New RolesController/RolesService (CRUD, permission-set editing, deactivate/reactivate) with lockout-prevention invariants: system roles (ADMIN) can't be edited or deactivated, and a role still assigned to users can't be deactivated. - New PermissionResolverService (30s-cached, DB-fresh per user) and PermissionsGuard + @RequirePermission(), registered as an additional APP_GUARD alongside the legacy RolesGuard so both systems run side-by-side until controllers are migrated in Phase 2. - PermissionCatalogSyncService seeds any new catalog entries into the DB on boot, so future features only touch the TS catalog. - New GET /auth/me returns the caller's role and effective permissions. - Migration hand-written (destructive column rename) with grants derived from a fresh grep of every existing @Roles() call site, verified against 5 controllers read in full for STAFF's exact write boundary. - test/roles.e2e-spec.ts covers the new endpoints and invariants end to end; test/authorization-coverage.e2e-spec.ts added but skipped until Phase 2 gives every controller a @RequirePermission. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
d31aa79f50
commit
139ea6fdd7
84 files changed
+1819
-103
No files matched your search
@@ -0,0 +1,244 @@
|
||||
-- RBAC migration: replaces the fixed User.role enum with a database-driven
|
||||
-- Role/Permission/RolePermission model. See docs/lastenheft.md RBAC notes
|
||||
-- for the phased rollout this belongs to (Phase 1 of 5).
|
||||
--
|
||||
-- Grants below reproduce today's effective access exactly, derived from a
|
||||
-- fresh grep of every @Roles(...) call site in src/**/*.controller.ts at
|
||||
-- the time this migration was written. Do not hand-tune without re-deriving
|
||||
-- from the code.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. Preserve the old enum + column under a new name so every existing
|
||||
-- @Roles(Role.ADMIN, ...) call site keeps compiling untouched during the
|
||||
-- migration (Phase 1-2 alias `LegacyRole as Role` in TS).
|
||||
ALTER TYPE "Role" RENAME TO "LegacyRole";
|
||||
ALTER TABLE "User" RENAME COLUMN "role" TO "legacyRole";
|
||||
|
||||
-- 2. Drop the old index on the renamed column (replaced by an index on the
|
||||
-- new roleId column in step 10).
|
||||
DROP INDEX "User_role_idx";
|
||||
|
||||
-- 3. New RBAC tables.
|
||||
CREATE TYPE "RoleStatus" AS ENUM ('ACTIVE', 'INACTIVE');
|
||||
|
||||
CREATE TABLE "Role" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"isSystem" BOOLEAN NOT NULL DEFAULT false,
|
||||
"status" "RoleStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deactivatedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "Role_key_key" ON "Role"("key");
|
||||
CREATE INDEX "Role_status_idx" ON "Role"("status");
|
||||
|
||||
CREATE TABLE "Permission" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"resource" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"labelDe" TEXT NOT NULL,
|
||||
"groupDe" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Permission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "Permission_key_key" ON "Permission"("key");
|
||||
CREATE UNIQUE INDEX "Permission_resource_action_key" ON "Permission"("resource", "action");
|
||||
CREATE INDEX "Permission_resource_idx" ON "Permission"("resource");
|
||||
|
||||
CREATE TABLE "RolePermission" (
|
||||
"roleId" TEXT NOT NULL,
|
||||
"permissionId" TEXT NOT NULL,
|
||||
"grantedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "RolePermission_pkey" PRIMARY KEY ("roleId", "permissionId")
|
||||
);
|
||||
|
||||
CREATE INDEX "RolePermission_permissionId_idx" ON "RolePermission"("permissionId");
|
||||
|
||||
ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_roleId_fkey"
|
||||
FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_permissionId_fkey"
|
||||
FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- 4. Seed the fixed permission catalog (generated from
|
||||
-- src/auth/permissions/permission-catalog.ts — the code-owned source of
|
||||
-- truth this table mirrors).
|
||||
INSERT INTO "Permission" (id, key, resource, action, "labelDe", "groupDe", "sortOrder")
|
||||
SELECT gen_random_uuid()::text, v.key, v.resource, v.action, v."labelDe", v."groupDe", v."sortOrder"
|
||||
FROM (VALUES
|
||||
('apiaries:read', 'apiaries', 'read', 'Ansehen', 'Bienenstände', 100),
|
||||
('apiaries:create', 'apiaries', 'create', 'Anlegen', 'Bienenstände', 101),
|
||||
('apiaries:update', 'apiaries', 'update', 'Bearbeiten', 'Bienenstände', 102),
|
||||
('apiaries:deactivate', 'apiaries', 'deactivate', 'Deaktivieren', 'Bienenstände', 103),
|
||||
('colonies:read', 'colonies', 'read', 'Ansehen', 'Völker', 200),
|
||||
('colonies:create', 'colonies', 'create', 'Anlegen', 'Völker', 201),
|
||||
('colonies:update', 'colonies', 'update', 'Bearbeiten', 'Völker', 202),
|
||||
('colonies:deactivate', 'colonies', 'deactivate', 'Deaktivieren', 'Völker', 203),
|
||||
('colonies:manage-lifecycle', 'colonies', 'manage-lifecycle', 'Umsetzen/Teilen/Vereinigen/Auflösen', 'Völker', 204),
|
||||
('queens:read', 'queens', 'read', 'Ansehen', 'Königinnen', 300),
|
||||
('queens:create', 'queens', 'create', 'Anlegen', 'Königinnen', 301),
|
||||
('queens:update', 'queens', 'update', 'Bearbeiten', 'Königinnen', 302),
|
||||
('queens:assign', 'queens', 'assign', 'Volk zuweisen', 'Königinnen', 303),
|
||||
('queens:evaluate', 'queens', 'evaluate', 'Bewerten', 'Königinnen', 304),
|
||||
('inspections:read', 'inspections', 'read', 'Ansehen', 'Durchsichten', 400),
|
||||
('inspections:create', 'inspections', 'create', 'Anlegen', 'Durchsichten', 401),
|
||||
('inspections:update', 'inspections', 'update', 'Bearbeiten', 'Durchsichten', 402),
|
||||
('tasks:read', 'tasks', 'read', 'Ansehen', 'Aufgaben', 500),
|
||||
('tasks:create', 'tasks', 'create', 'Anlegen', 'Aufgaben', 501),
|
||||
('tasks:update', 'tasks', 'update', 'Bearbeiten', 'Aufgaben', 502),
|
||||
('tasks:complete', 'tasks', 'complete', 'Erledigen', 'Aufgaben', 503),
|
||||
('task-templates:read', 'task-templates', 'read', 'Ansehen', 'Wiederkehrende Aufgaben', 600),
|
||||
('task-templates:create', 'task-templates', 'create', 'Anlegen', 'Wiederkehrende Aufgaben', 601),
|
||||
('task-templates:update', 'task-templates', 'update', 'Bearbeiten', 'Wiederkehrende Aufgaben', 602),
|
||||
('task-templates:deactivate', 'task-templates', 'deactivate', 'Deaktivieren', 'Wiederkehrende Aufgaben', 603),
|
||||
('varroa:read', 'varroa', 'read', 'Ansehen', 'Varroa', 700),
|
||||
('varroa:create', 'varroa', 'create', 'Anlegen', 'Varroa', 701),
|
||||
('varroa:update', 'varroa', 'update', 'Bearbeiten', 'Varroa', 702),
|
||||
('swarm-control:read', 'swarm-control', 'read', 'Ansehen', 'Schwarmkontrolle', 800),
|
||||
('swarm-control:create', 'swarm-control', 'create', 'Anlegen', 'Schwarmkontrolle', 801),
|
||||
('swarm-control:update', 'swarm-control', 'update', 'Bearbeiten', 'Schwarmkontrolle', 802),
|
||||
('treatments:read', 'treatments', 'read', 'Ansehen', 'Behandlungen', 900),
|
||||
('treatments:create', 'treatments', 'create', 'Anlegen', 'Behandlungen', 901),
|
||||
('treatments:update', 'treatments', 'update', 'Bearbeiten', 'Behandlungen', 902),
|
||||
('feedings:read', 'feedings', 'read', 'Ansehen', 'Fütterungen', 1000),
|
||||
('feedings:create', 'feedings', 'create', 'Anlegen', 'Fütterungen', 1001),
|
||||
('feedings:update', 'feedings', 'update', 'Bearbeiten', 'Fütterungen', 1002),
|
||||
('harvests:read', 'harvests', 'read', 'Ansehen', 'Ernten', 1100),
|
||||
('harvests:create', 'harvests', 'create', 'Anlegen', 'Ernten', 1101),
|
||||
('harvests:update', 'harvests', 'update', 'Bearbeiten', 'Ernten', 1102),
|
||||
('extractions:read', 'extractions', 'read', 'Ansehen', 'Schleuderungen', 1200),
|
||||
('extractions:create', 'extractions', 'create', 'Anlegen', 'Schleuderungen', 1201),
|
||||
('extractions:update', 'extractions', 'update', 'Bearbeiten', 'Schleuderungen', 1202),
|
||||
('honey-batches:read', 'honey-batches', 'read', 'Ansehen', 'Honigchargen', 1300),
|
||||
('honey-batches:create', 'honey-batches', 'create', 'Anlegen', 'Honigchargen', 1301),
|
||||
('honey-batches:relocate', 'honey-batches', 'relocate', 'Umlagern', 'Honigchargen', 1302),
|
||||
('honey-batches:measure', 'honey-batches', 'measure', 'Messwerte erfassen', 'Honigchargen', 1303),
|
||||
('storage:read', 'storage', 'read', 'Ansehen', 'Lager', 1400),
|
||||
('storage:create', 'storage', 'create', 'Anlegen', 'Lager', 1401),
|
||||
('storage:move', 'storage', 'move', 'Bewegung buchen', 'Lager', 1402),
|
||||
('products:read', 'products', 'read', 'Ansehen', 'Produkte', 1500),
|
||||
('products:create', 'products', 'create', 'Anlegen', 'Produkte', 1501),
|
||||
('products:update', 'products', 'update', 'Bearbeiten', 'Produkte', 1502),
|
||||
('bottling:read', 'bottling', 'read', 'Ansehen', 'Abfüllungen', 1600),
|
||||
('bottling:create', 'bottling', 'create', 'Anlegen', 'Abfüllungen', 1601),
|
||||
('bottling:update', 'bottling', 'update', 'Bearbeiten', 'Abfüllungen', 1602),
|
||||
('equipment:read', 'equipment', 'read', 'Ansehen', 'Inventar', 1700),
|
||||
('equipment:create', 'equipment', 'create', 'Anlegen', 'Inventar', 1701),
|
||||
('equipment:update', 'equipment', 'update', 'Bearbeiten', 'Inventar', 1702),
|
||||
('documents:read', 'documents', 'read', 'Ansehen', 'Dokumente', 1800),
|
||||
('documents:upload', 'documents', 'upload', 'Hochladen', 'Dokumente', 1801),
|
||||
('documents:delete', 'documents', 'delete', 'Löschen', 'Dokumente', 1802),
|
||||
('webhooks:read', 'webhooks', 'read', 'Ansehen', 'Webhooks', 1900),
|
||||
('webhooks:create', 'webhooks', 'create', 'Anlegen', 'Webhooks', 1901),
|
||||
('webhooks:delete', 'webhooks', 'delete', 'Löschen', 'Webhooks', 1902),
|
||||
('automation:read', 'automation', 'read', 'Ansehen', 'Automatisierungen', 2000),
|
||||
('automation:update', 'automation', 'update', 'Bearbeiten', 'Automatisierungen', 2001),
|
||||
('import:execute', 'import', 'execute', 'Ausführen', 'Import', 2100),
|
||||
('export:read', 'export', 'read', 'Ansehen', 'Export', 2200),
|
||||
('users:read', 'users', 'read', 'Ansehen', 'Benutzer', 2300),
|
||||
('users:create', 'users', 'create', 'Anlegen', 'Benutzer', 2301),
|
||||
('users:update', 'users', 'update', 'Bearbeiten', 'Benutzer', 2302),
|
||||
('users:deactivate', 'users', 'deactivate', 'Deaktivieren', 'Benutzer', 2303),
|
||||
('roles:read', 'roles', 'read', 'Ansehen', 'Rollen & Rechte', 2400),
|
||||
('roles:create', 'roles', 'create', 'Anlegen', 'Rollen & Rechte', 2401),
|
||||
('roles:update', 'roles', 'update', 'Bearbeiten', 'Rollen & Rechte', 2402),
|
||||
('roles:deactivate', 'roles', 'deactivate', 'Deaktivieren', 'Rollen & Rechte', 2403),
|
||||
('audit:read', 'audit', 'read', 'Ansehen', 'Audit-Log', 2500),
|
||||
('calendar:read', 'calendar', 'read', 'Ansehen', 'Kalender', 2600),
|
||||
('dashboard:read', 'dashboard', 'read', 'Ansehen', 'Dashboard', 2700),
|
||||
('statistics:read', 'statistics', 'read', 'Ansehen', 'Statistik', 2800),
|
||||
('search:read', 'search', 'read', 'Ansehen', 'Suche', 2900),
|
||||
('qr:read', 'qr', 'read', 'Ansehen', 'QR-Codes', 3000)
|
||||
) AS v(key, resource, action, "labelDe", "groupDe", "sortOrder");
|
||||
|
||||
-- 5. Seed the 5 roles that replace the legacy enum values, one-for-one.
|
||||
INSERT INTO "Role" (id, key, name, description, "isSystem", status, "createdAt", "updatedAt")
|
||||
VALUES
|
||||
(gen_random_uuid()::text, 'ADMIN', 'Administrator', 'Voller Zugriff auf alle Bereiche, inkl. Benutzer- und Rechteverwaltung.', true, 'ACTIVE', now(), now()),
|
||||
(gen_random_uuid()::text, 'BEEKEEPER', 'Imker', 'Voller fachlicher Zugriff, ohne Benutzer-, Rollen- und Systemverwaltung.', false, 'ACTIVE', now(), now()),
|
||||
(gen_random_uuid()::text, 'STAFF', 'Mitarbeiter', 'Ansicht aller Bereiche, Erfassung der laufenden Tagesarbeit.', false, 'ACTIVE', now(), now()),
|
||||
(gen_random_uuid()::text, 'READONLY', 'Nur Lesen', 'Lesezugriff auf alle Bereiche, keine Änderungen.', false, 'ACTIVE', now(), now()),
|
||||
(gen_random_uuid()::text, 'API_CLIENT', 'API-Client', 'Lesezugriff für externe Integrationen.', false, 'ACTIVE', now(), now());
|
||||
|
||||
-- 6. Grants, derived from a fresh grep of every @Roles(...) call site.
|
||||
|
||||
-- ADMIN: everything.
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE r.key = 'ADMIN';
|
||||
|
||||
-- BEEKEEPER: everything except the administrative/system resources that
|
||||
-- were ADMIN-only via class-level @Roles(Role.ADMIN).
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE r.key = 'BEEKEEPER'
|
||||
AND p.resource NOT IN ('users', 'roles', 'audit', 'webhooks', 'automation', 'import');
|
||||
|
||||
-- STAFF: read access to every non-administrative resource ...
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE r.key = 'STAFF'
|
||||
AND p.action = 'read'
|
||||
AND p.resource NOT IN ('task-templates', 'users', 'roles', 'audit', 'webhooks', 'automation', 'import');
|
||||
|
||||
-- ... plus exactly the write actions that carried Role.STAFF today.
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE r.key = 'STAFF'
|
||||
AND p.key IN (
|
||||
'inspections:create', 'inspections:update',
|
||||
'tasks:create', 'tasks:update', 'tasks:complete',
|
||||
'feedings:create', 'feedings:update',
|
||||
'harvests:create', 'harvests:update',
|
||||
'honey-batches:relocate',
|
||||
'documents:upload', 'documents:delete'
|
||||
);
|
||||
|
||||
-- READONLY and API_CLIENT: the same read-only set as STAFF (neither role
|
||||
-- appears in any @Roles(...) call site today, so both currently mean
|
||||
-- "read everything" — kept identical here, adjustable later via the UI).
|
||||
INSERT INTO "RolePermission" ("roleId", "permissionId", "grantedAt")
|
||||
SELECT r.id, p.id, now()
|
||||
FROM "Role" r, "Permission" p
|
||||
WHERE r.key IN ('READONLY', 'API_CLIENT')
|
||||
AND p.action = 'read'
|
||||
AND p.resource NOT IN ('task-templates', 'users', 'roles', 'audit', 'webhooks', 'automation', 'import');
|
||||
|
||||
-- 7. New nullable roleId column on User, backfilled below.
|
||||
ALTER TABLE "User" ADD COLUMN "roleId" TEXT;
|
||||
|
||||
-- 8. Backfill from the legacy enum value.
|
||||
UPDATE "User" u
|
||||
SET "roleId" = r.id
|
||||
FROM "Role" r
|
||||
WHERE r.key = u."legacyRole"::text;
|
||||
|
||||
-- 9. Verification gate: refuse to proceed if any user was left unresolved.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM "User" WHERE "roleId" IS NULL) THEN
|
||||
RAISE EXCEPTION 'RBAC backfill incomplete: at least one User row has no resolvable roleId';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 10. Lock it down.
|
||||
ALTER TABLE "User" ALTER COLUMN "roleId" SET NOT NULL;
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey"
|
||||
FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
CREATE INDEX "User_roleId_idx" ON "User"("roleId");
|
||||
|
||||
COMMIT;
|
||||
@@ -16,7 +16,10 @@ datasource db {
|
||||
// Benutzer / Auth / Audit (Abschnitt 3, 14, 15, 16)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum Role {
|
||||
// Superseded by the Role/Permission/RolePermission models below. Kept only
|
||||
// as a transitional column (User.legacyRole) until the RBAC rollout's
|
||||
// Phase 3 drops it — see docs/lastenheft.md RBAC migration notes.
|
||||
enum LegacyRole {
|
||||
ADMIN
|
||||
BEEKEEPER
|
||||
STAFF
|
||||
@@ -24,15 +27,65 @@ enum Role {
|
||||
API_CLIENT
|
||||
}
|
||||
|
||||
enum RoleStatus {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
name String
|
||||
description String?
|
||||
isSystem Boolean @default(false)
|
||||
status RoleStatus @default(ACTIVE)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deactivatedAt DateTime?
|
||||
|
||||
permissions RolePermission[]
|
||||
users User[]
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
resource String
|
||||
action String
|
||||
labelDe String
|
||||
groupDe String
|
||||
sortOrder Int
|
||||
|
||||
roles RolePermission[]
|
||||
|
||||
@@unique([resource, action])
|
||||
@@index([resource])
|
||||
}
|
||||
|
||||
model RolePermission {
|
||||
roleId String
|
||||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
permissionId String
|
||||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||
grantedAt DateTime @default(now())
|
||||
|
||||
@@id([roleId, permissionId])
|
||||
@@index([permissionId])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
name String
|
||||
role Role @default(STAFF)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
legacyRole LegacyRole @default(STAFF)
|
||||
roleId String
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deactivatedAt DateTime?
|
||||
|
||||
refreshTokens RefreshToken[]
|
||||
@@ -59,7 +112,7 @@ model User {
|
||||
documents Document[]
|
||||
honeyBatchRelocations HoneyBatchRelocation[]
|
||||
|
||||
@@index([role])
|
||||
@@index([roleId])
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
|
||||
@@ -22,8 +22,17 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
|
||||
const adminRole = await prisma.role.findUniqueOrThrow({
|
||||
where: { key: 'ADMIN' },
|
||||
});
|
||||
await prisma.user.create({
|
||||
data: { email, passwordHash, name: 'Administrator', role: 'ADMIN' },
|
||||
data: {
|
||||
email,
|
||||
passwordHash,
|
||||
name: 'Administrator',
|
||||
legacyRole: 'ADMIN',
|
||||
roleId: adminRole.id,
|
||||
},
|
||||
});
|
||||
console.log(`Seed: created admin user ${email}.`);
|
||||
await prisma.$disconnect();
|
||||
|
||||
Reference in new issue
Block a user