@simple-auth-kit/cli 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/copy.ts +1 -1
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +42 -11
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -42,40 +42,65 @@ export interface PermissionSeed {
|
|
|
42
42
|
* evaluate.
|
|
43
43
|
*/
|
|
44
44
|
export const PERMISSION_CATALOG = {
|
|
45
|
-
"users:read": {
|
|
46
|
-
|
|
45
|
+
"users:read": {
|
|
46
|
+
displayName: "List users",
|
|
47
|
+
description: "GET /auth/admin/users, GET /auth/admin/users/:userId",
|
|
48
|
+
group: "Users",
|
|
49
|
+
order: 1,
|
|
50
|
+
},
|
|
51
|
+
"users:block": {
|
|
52
|
+
displayName: "Block and unblock users",
|
|
53
|
+
description: "POST /auth/admin/users/:userId/block, .../unblock",
|
|
54
|
+
group: "Users",
|
|
55
|
+
order: 2,
|
|
56
|
+
},
|
|
47
57
|
"users:manage": {
|
|
48
58
|
displayName: "Edit and delete users",
|
|
49
|
-
description:
|
|
59
|
+
description:
|
|
60
|
+
"Edit a user's profile or delete their account — PATCH /auth/admin/users/:userId, DELETE /auth/admin/users/:userId",
|
|
50
61
|
group: "Users",
|
|
51
62
|
order: 3,
|
|
52
63
|
},
|
|
53
64
|
"roles:manage": {
|
|
54
65
|
displayName: "Define roles",
|
|
55
|
-
description:
|
|
66
|
+
description:
|
|
67
|
+
"Create, edit, or delete roles and say what they carry — POST/PATCH/DELETE /auth/admin/roles(/:roleId), POST /auth/admin/roles/:roleId/permissions",
|
|
56
68
|
group: "Roles",
|
|
57
69
|
order: 1,
|
|
58
70
|
},
|
|
59
71
|
"roles:assign": {
|
|
60
72
|
displayName: "Assign roles",
|
|
61
|
-
description:
|
|
73
|
+
description:
|
|
74
|
+
"Assign and revoke a user's roles — POST /auth/admin/users/:userId/roles, .../roles/:roleSlug/revoke",
|
|
62
75
|
group: "Roles",
|
|
63
76
|
order: 2,
|
|
64
77
|
},
|
|
65
|
-
"permissions:read": {
|
|
78
|
+
"permissions:read": {
|
|
79
|
+
displayName: "Read the permission catalog",
|
|
80
|
+
description: "GET /auth/admin/permissions",
|
|
81
|
+
group: "Permissions",
|
|
82
|
+
order: 1,
|
|
83
|
+
},
|
|
66
84
|
"permissions:define": {
|
|
67
85
|
displayName: "Define permissions",
|
|
68
|
-
description:
|
|
86
|
+
description:
|
|
87
|
+
"Create a permission, rename it, or deactivate it — POST /auth/admin/permissions. The authority that defines all the others.",
|
|
69
88
|
group: "Permissions",
|
|
70
89
|
order: 2,
|
|
71
90
|
},
|
|
72
91
|
"permissions:grant": {
|
|
73
92
|
displayName: "Grant permissions directly",
|
|
74
|
-
description:
|
|
93
|
+
description:
|
|
94
|
+
"Grant and revoke a permission straight to a user, bypassing roles — POST /auth/admin/users/:userId/permissions, .../permissions/:slug/revoke",
|
|
75
95
|
group: "Permissions",
|
|
76
96
|
order: 3,
|
|
77
97
|
},
|
|
78
|
-
"audit-log:read": {
|
|
98
|
+
"audit-log:read": {
|
|
99
|
+
displayName: "Read the audit log",
|
|
100
|
+
description: "GET /auth/admin/audit-log",
|
|
101
|
+
group: "Audit",
|
|
102
|
+
order: 1,
|
|
103
|
+
},
|
|
79
104
|
} as const satisfies Record<string, PermissionSeed>;
|
|
80
105
|
|
|
81
106
|
/**
|
|
@@ -87,10 +112,14 @@ export const PERMISSION_CATALOG = {
|
|
|
87
112
|
*/
|
|
88
113
|
export type PermissionSlug = keyof typeof PERMISSION_CATALOG;
|
|
89
114
|
|
|
90
|
-
export const PERMISSION_SLUGS = Object.keys(
|
|
115
|
+
export const PERMISSION_SLUGS = Object.keys(
|
|
116
|
+
PERMISSION_CATALOG,
|
|
117
|
+
) as PermissionSlug[];
|
|
91
118
|
|
|
92
119
|
/** Groups, in the order an admin console should render them. Derived from the catalog so it cannot drift from it. */
|
|
93
|
-
export const PERMISSION_GROUP_ORDER: string[] = [
|
|
120
|
+
export const PERMISSION_GROUP_ORDER: string[] = [
|
|
121
|
+
...new Set(PERMISSION_SLUGS.map((slug) => PERMISSION_CATALOG[slug].group)),
|
|
122
|
+
];
|
|
94
123
|
|
|
95
124
|
/** The display metadata a seeded role carries, plus what it starts out granting. */
|
|
96
125
|
export interface RoleSeed {
|
|
@@ -125,7 +154,8 @@ export const DEFAULT_ROLES: readonly RoleSeed[] = [
|
|
|
125
154
|
{
|
|
126
155
|
slug: "superadmin",
|
|
127
156
|
displayName: "Super Admin",
|
|
128
|
-
description:
|
|
157
|
+
description:
|
|
158
|
+
"Carries every permission in the catalog — same authority as admin, held by the seeded super_admin account (see SEED_SUPERADMIN_* in seed.ts).",
|
|
129
159
|
isDefault: false,
|
|
130
160
|
order: 1,
|
|
131
161
|
permissions: PERMISSION_SLUGS,
|
|
@@ -147,7 +177,10 @@ export const SEED_ADMIN_ROLES: string[] = ["admin", "member"];
|
|
|
147
177
|
export const SEED_SUPERADMIN_ROLES: string[] = ["superadmin", "member"];
|
|
148
178
|
|
|
149
179
|
/** Anything that can write the catalog tables: the PrismaClient, or a `$transaction` client. */
|
|
150
|
-
export type RbacWriter = Pick<
|
|
180
|
+
export type RbacWriter = Pick<
|
|
181
|
+
PrismaClient,
|
|
182
|
+
"permission" | "role" | "permissionRole"
|
|
183
|
+
>;
|
|
151
184
|
|
|
152
185
|
/**
|
|
153
186
|
* Writes the starting catalog and roles above into `db`, which may be a transaction client — so a
|
|
@@ -166,12 +199,17 @@ export async function provisionDefaultRoles(db: RbacWriter): Promise<void> {
|
|
|
166
199
|
displayName: PERMISSION_CATALOG[slug].displayName,
|
|
167
200
|
description: PERMISSION_CATALOG[slug].description,
|
|
168
201
|
group: PERMISSION_CATALOG[slug].group,
|
|
169
|
-
groupOrder: PERMISSION_GROUP_ORDER.indexOf(
|
|
202
|
+
groupOrder: PERMISSION_GROUP_ORDER.indexOf(
|
|
203
|
+
PERMISSION_CATALOG[slug].group,
|
|
204
|
+
),
|
|
170
205
|
order: PERMISSION_CATALOG[slug].order,
|
|
171
206
|
})),
|
|
172
207
|
skipDuplicates: true,
|
|
173
208
|
});
|
|
174
|
-
const permissions = await db.permission.findMany({
|
|
209
|
+
const permissions = await db.permission.findMany({
|
|
210
|
+
where: { slug: { in: PERMISSION_SLUGS } },
|
|
211
|
+
select: { id: true, slug: true },
|
|
212
|
+
});
|
|
175
213
|
const permissionId = new Map(permissions.map((p) => [p.slug, p.id]));
|
|
176
214
|
|
|
177
215
|
await db.role.createMany({
|
|
@@ -185,11 +223,21 @@ export async function provisionDefaultRoles(db: RbacWriter): Promise<void> {
|
|
|
185
223
|
})),
|
|
186
224
|
skipDuplicates: true,
|
|
187
225
|
});
|
|
188
|
-
const roles = await db.role.findMany({
|
|
226
|
+
const roles = await db.role.findMany({
|
|
227
|
+
where: { slug: { in: DEFAULT_ROLES.map((r) => r.slug) } },
|
|
228
|
+
select: { id: true, slug: true },
|
|
229
|
+
});
|
|
189
230
|
const seedBySlug = new Map(DEFAULT_ROLES.map((role) => [role.slug, role]));
|
|
190
231
|
|
|
191
232
|
const rolePermissions = roles.flatMap((role) =>
|
|
192
|
-
(seedBySlug.get(role.slug)?.permissions ?? []).map((slug) => ({
|
|
233
|
+
(seedBySlug.get(role.slug)?.permissions ?? []).map((slug) => ({
|
|
234
|
+
roleId: role.id,
|
|
235
|
+
permissionId: permissionId.get(slug)!,
|
|
236
|
+
})),
|
|
193
237
|
);
|
|
194
|
-
if (rolePermissions.length)
|
|
238
|
+
if (rolePermissions.length)
|
|
239
|
+
await db.permissionRole.createMany({
|
|
240
|
+
data: rolePermissions,
|
|
241
|
+
skipDuplicates: true,
|
|
242
|
+
});
|
|
195
243
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { AuditEvent } from "@/lib/auth/core/types.js";
|
|
2
|
-
import { Prisma, PrismaClient } from "
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
|
|
3
|
+
import {
|
|
4
|
+
buildPageMeta,
|
|
5
|
+
normalizeLimit,
|
|
6
|
+
normalizePage,
|
|
7
|
+
type Paginated,
|
|
8
|
+
} from "../pagination.js";
|
|
9
|
+
import { toIdOrNull, toIdOrUndefined } from "../id.helper.js";
|
|
5
10
|
|
|
6
11
|
export interface AuditLogEntry {
|
|
7
12
|
id: string;
|
|
@@ -43,7 +48,10 @@ export function toAuditLogEntry(row: AuditLogRow): AuditLogEntry {
|
|
|
43
48
|
/** "role_assigned" -> "Role assigned". Derived rather than passed in, so core never has to carry display text. */
|
|
44
49
|
export function humanizeAction(action: string): string {
|
|
45
50
|
const words = action.split("_");
|
|
46
|
-
return [
|
|
51
|
+
return [
|
|
52
|
+
words[0].charAt(0).toUpperCase() + words[0].slice(1),
|
|
53
|
+
...words.slice(1),
|
|
54
|
+
].join(" ");
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
// Plain class, no DI container — constructed directly with a PrismaClient in
|
|
@@ -51,7 +59,10 @@ export function humanizeAction(action: string): string {
|
|
|
51
59
|
export class AuditLogRepository {
|
|
52
60
|
constructor(private readonly prisma: PrismaClient) {}
|
|
53
61
|
|
|
54
|
-
async append(
|
|
62
|
+
async append(
|
|
63
|
+
event: AuditEvent,
|
|
64
|
+
opts: { remarks?: string } = {},
|
|
65
|
+
): Promise<void> {
|
|
55
66
|
await this.prisma.auditLog.create({
|
|
56
67
|
data: {
|
|
57
68
|
action: event.type,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { resolvePermissions } from "@/lib/auth/core/rbac.js";
|
|
2
|
-
import { Prisma, PrismaClient } from "
|
|
3
|
-
import { HttpError } from "
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
|
|
3
|
+
import { HttpError } from "../http-error.js";
|
|
4
|
+
import {
|
|
5
|
+
buildPageMeta,
|
|
6
|
+
normalizeLimit,
|
|
7
|
+
normalizePage,
|
|
8
|
+
type Paginated,
|
|
9
|
+
} from "../pagination.js";
|
|
10
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
11
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
7
12
|
|
|
8
13
|
export interface UserSummary {
|
|
9
14
|
id: string;
|
|
@@ -34,10 +39,14 @@ export interface UserListFilter {
|
|
|
34
39
|
|
|
35
40
|
export type UserListResult = Paginated<UserSummary>;
|
|
36
41
|
|
|
37
|
-
const USER_ROLES_INCLUDE = {
|
|
42
|
+
const USER_ROLES_INCLUDE = {
|
|
43
|
+
roles: { select: { role: { select: { slug: true } } } },
|
|
44
|
+
} as const;
|
|
38
45
|
|
|
39
46
|
/** A `User` row as `findUnique`/`findMany` return it with its roles joined in. */
|
|
40
|
-
export type UserRow = Prisma.UserGetPayload<{
|
|
47
|
+
export type UserRow = Prisma.UserGetPayload<{
|
|
48
|
+
include: typeof USER_ROLES_INCLUDE;
|
|
49
|
+
}>;
|
|
41
50
|
|
|
42
51
|
/**
|
|
43
52
|
* Shapes a raw row into the wire DTO — bigint id to string, `Date` to ISO, the roles relation
|
|
@@ -112,7 +121,14 @@ const PERMISSION_SELECT = {
|
|
|
112
121
|
isActive: true,
|
|
113
122
|
} as const;
|
|
114
123
|
|
|
115
|
-
const ROLE_SELECT = {
|
|
124
|
+
const ROLE_SELECT = {
|
|
125
|
+
id: true,
|
|
126
|
+
slug: true,
|
|
127
|
+
name: true,
|
|
128
|
+
displayName: true,
|
|
129
|
+
isDefault: true,
|
|
130
|
+
isActive: true,
|
|
131
|
+
} as const;
|
|
116
132
|
|
|
117
133
|
export class RbacRepository {
|
|
118
134
|
constructor(
|
|
@@ -141,7 +157,9 @@ export class RbacRepository {
|
|
|
141
157
|
* `isActive: false` on a role or a permission takes it out of the answer here, which is what
|
|
142
158
|
* makes deactivating either one enforcement-changing without unpicking any grant.
|
|
143
159
|
*/
|
|
144
|
-
async resolveAuthzContext(
|
|
160
|
+
async resolveAuthzContext(
|
|
161
|
+
userId: string,
|
|
162
|
+
): Promise<{ roles: string[]; permissions: string[] }> {
|
|
145
163
|
const user = await this.prisma.user.findUnique({
|
|
146
164
|
where: { id: toId(userId), isDeleted: false },
|
|
147
165
|
select: {
|
|
@@ -151,17 +169,25 @@ export class RbacRepository {
|
|
|
151
169
|
role: {
|
|
152
170
|
select: {
|
|
153
171
|
slug: true,
|
|
154
|
-
permissions: {
|
|
172
|
+
permissions: {
|
|
173
|
+
where: { permission: { isActive: true, isDeleted: false } },
|
|
174
|
+
select: { permission: { select: { slug: true } } },
|
|
175
|
+
},
|
|
155
176
|
},
|
|
156
177
|
},
|
|
157
178
|
},
|
|
158
179
|
},
|
|
159
|
-
permissions: {
|
|
180
|
+
permissions: {
|
|
181
|
+
where: { permission: { isActive: true, isDeleted: false } },
|
|
182
|
+
select: { permission: { select: { slug: true } } },
|
|
183
|
+
},
|
|
160
184
|
},
|
|
161
185
|
});
|
|
162
186
|
if (!user) return { roles: [], permissions: [] };
|
|
163
187
|
|
|
164
|
-
const rolePermissions = user.roles.flatMap((roleUser) =>
|
|
188
|
+
const rolePermissions = user.roles.flatMap((roleUser) =>
|
|
189
|
+
roleUser.role.permissions.map((pr) => pr.permission.slug),
|
|
190
|
+
);
|
|
165
191
|
const directPermissions = user.permissions.map((pu) => pu.permission.slug);
|
|
166
192
|
|
|
167
193
|
return {
|
|
@@ -180,7 +206,9 @@ export class RbacRepository {
|
|
|
180
206
|
const limit = normalizeLimit(filter.limit);
|
|
181
207
|
const where = {
|
|
182
208
|
isDeleted: false,
|
|
183
|
-
email: filter.search
|
|
209
|
+
email: filter.search
|
|
210
|
+
? { contains: filter.search, mode: "insensitive" as const }
|
|
211
|
+
: undefined,
|
|
184
212
|
};
|
|
185
213
|
|
|
186
214
|
const [rows, total] = await this.prisma.$transaction([
|
|
@@ -223,7 +251,9 @@ export class RbacRepository {
|
|
|
223
251
|
},
|
|
224
252
|
actorUserId: string | null,
|
|
225
253
|
): Promise<UserSummary> {
|
|
226
|
-
const existing = await this.prisma.user.findUnique({
|
|
254
|
+
const existing = await this.prisma.user.findUnique({
|
|
255
|
+
where: { email: input.email },
|
|
256
|
+
});
|
|
227
257
|
if (existing) throw new HttpError(409, "email already registered");
|
|
228
258
|
|
|
229
259
|
const user = await this.prisma.user.create({
|
|
@@ -240,9 +270,15 @@ export class RbacRepository {
|
|
|
240
270
|
});
|
|
241
271
|
|
|
242
272
|
if (input.roles && input.roles.length > 0) {
|
|
243
|
-
const roles = await this.prisma.role.findMany({
|
|
273
|
+
const roles = await this.prisma.role.findMany({
|
|
274
|
+
where: { slug: { in: input.roles }, isActive: true, isDeleted: false },
|
|
275
|
+
select: { id: true },
|
|
276
|
+
});
|
|
244
277
|
if (roles.length > 0) {
|
|
245
|
-
await this.prisma.roleUser.createMany({
|
|
278
|
+
await this.prisma.roleUser.createMany({
|
|
279
|
+
data: roles.map((role) => ({ userId: user.id, roleId: role.id })),
|
|
280
|
+
skipDuplicates: true,
|
|
281
|
+
});
|
|
246
282
|
await this.cache.invalidateSubject(user.id.toString());
|
|
247
283
|
}
|
|
248
284
|
} else {
|
|
@@ -256,37 +292,70 @@ export class RbacRepository {
|
|
|
256
292
|
// uniqueness/re-verification concerns a general "edit profile" screen shouldn't have to handle.
|
|
257
293
|
async updateUser(
|
|
258
294
|
userId: string,
|
|
259
|
-
input: {
|
|
295
|
+
input: {
|
|
296
|
+
firstName?: string | null;
|
|
297
|
+
lastName?: string | null;
|
|
298
|
+
displayName?: string | null;
|
|
299
|
+
phone?: string | null;
|
|
300
|
+
username?: string | null;
|
|
301
|
+
photo?: string | null;
|
|
302
|
+
},
|
|
260
303
|
actorUserId: string | null,
|
|
261
304
|
): Promise<UserSummary> {
|
|
262
305
|
const userIdBig = toId(userId);
|
|
263
|
-
const existing = await this.prisma.user.findUnique({
|
|
306
|
+
const existing = await this.prisma.user.findUnique({
|
|
307
|
+
where: { id: userIdBig, isDeleted: false },
|
|
308
|
+
select: { id: true },
|
|
309
|
+
});
|
|
264
310
|
if (!existing) throw new HttpError(404, `user "${userId}" not found`);
|
|
265
|
-
await this.prisma.user.update({
|
|
311
|
+
await this.prisma.user.update({
|
|
312
|
+
where: { id: userIdBig },
|
|
313
|
+
data: { ...input, updatedBy: toIdOrNull(actorUserId) },
|
|
314
|
+
});
|
|
266
315
|
return this.getUser(userId);
|
|
267
316
|
}
|
|
268
317
|
|
|
269
318
|
// Soft-delete, matching every other table's `isDeleted`/`deletedAt`/`deletedBy`/`deletedReason`
|
|
270
319
|
// pattern — the row survives for audit purposes, it just stops appearing in listings and can
|
|
271
320
|
// no longer authenticate (mirrors how `block` already disables login without removing the row).
|
|
272
|
-
async deleteUser(
|
|
321
|
+
async deleteUser(
|
|
322
|
+
userId: string,
|
|
323
|
+
actorUserId: string | null,
|
|
324
|
+
reason?: string,
|
|
325
|
+
): Promise<void> {
|
|
273
326
|
const userIdBig = toId(userId);
|
|
274
|
-
const existing = await this.prisma.user.findUnique({
|
|
327
|
+
const existing = await this.prisma.user.findUnique({
|
|
328
|
+
where: { id: userIdBig, isDeleted: false },
|
|
329
|
+
select: { id: true },
|
|
330
|
+
});
|
|
275
331
|
if (!existing) throw new HttpError(404, `user "${userId}" not found`);
|
|
276
332
|
await this.prisma.user.update({
|
|
277
333
|
where: { id: userIdBig },
|
|
278
|
-
data: {
|
|
334
|
+
data: {
|
|
335
|
+
isDeleted: true,
|
|
336
|
+
deletedAt: new Date(),
|
|
337
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
338
|
+
deletedReason: reason ?? null,
|
|
339
|
+
},
|
|
279
340
|
});
|
|
280
341
|
await this.cache.invalidateSubject(userId);
|
|
281
342
|
}
|
|
282
343
|
|
|
283
344
|
async updateRole(
|
|
284
345
|
roleId: string,
|
|
285
|
-
input: {
|
|
346
|
+
input: {
|
|
347
|
+
name?: string;
|
|
348
|
+
displayName?: string;
|
|
349
|
+
description?: string | null;
|
|
350
|
+
isActive?: boolean;
|
|
351
|
+
},
|
|
286
352
|
actorUserId: string | null,
|
|
287
353
|
): Promise<RoleSummary> {
|
|
288
354
|
const roleIdBig = toId(roleId);
|
|
289
|
-
const existing = await this.prisma.role.findUnique({
|
|
355
|
+
const existing = await this.prisma.role.findUnique({
|
|
356
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
357
|
+
select: { id: true },
|
|
358
|
+
});
|
|
290
359
|
if (!existing) throw new HttpError(404, "role not found");
|
|
291
360
|
const role = await this.prisma.role.update({
|
|
292
361
|
where: { id: roleIdBig },
|
|
@@ -301,13 +370,25 @@ export class RbacRepository {
|
|
|
301
370
|
// Soft-delete. The `role_user`/`permission_role` rows pointing at it are left in place — a
|
|
302
371
|
// deleted role simply stops being returned by `listRoles`/`resolveAuthzContext`, rather than
|
|
303
372
|
// cascading a delete that would silently strip a user's other roles' foreign keys.
|
|
304
|
-
async deleteRole(
|
|
373
|
+
async deleteRole(
|
|
374
|
+
roleId: string,
|
|
375
|
+
actorUserId: string | null,
|
|
376
|
+
reason?: string,
|
|
377
|
+
): Promise<void> {
|
|
305
378
|
const roleIdBig = toId(roleId);
|
|
306
|
-
const existing = await this.prisma.role.findUnique({
|
|
379
|
+
const existing = await this.prisma.role.findUnique({
|
|
380
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
381
|
+
select: { id: true },
|
|
382
|
+
});
|
|
307
383
|
if (!existing) throw new HttpError(404, "role not found");
|
|
308
384
|
await this.prisma.role.update({
|
|
309
385
|
where: { id: roleIdBig },
|
|
310
|
-
data: {
|
|
386
|
+
data: {
|
|
387
|
+
isDeleted: true,
|
|
388
|
+
deletedAt: new Date(),
|
|
389
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
390
|
+
deletedReason: reason ?? null,
|
|
391
|
+
},
|
|
311
392
|
});
|
|
312
393
|
await this.cache.invalidatePolicy();
|
|
313
394
|
}
|
|
@@ -319,7 +400,12 @@ export class RbacRepository {
|
|
|
319
400
|
const rows = await this.prisma.permission.findMany({
|
|
320
401
|
where: { isDeleted: false },
|
|
321
402
|
select: PERMISSION_SELECT,
|
|
322
|
-
orderBy: [
|
|
403
|
+
orderBy: [
|
|
404
|
+
{ groupOrder: "asc" },
|
|
405
|
+
{ group: "asc" },
|
|
406
|
+
{ order: "asc" },
|
|
407
|
+
{ slug: "asc" },
|
|
408
|
+
],
|
|
323
409
|
});
|
|
324
410
|
return rows.map((row) => ({ ...row, id: row.id.toString() }));
|
|
325
411
|
}
|
|
@@ -333,7 +419,10 @@ export class RbacRepository {
|
|
|
333
419
|
* Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
|
|
334
420
|
* `name`/`displayName` never breaks a grant.
|
|
335
421
|
*/
|
|
336
|
-
async upsertPermission(
|
|
422
|
+
async upsertPermission(
|
|
423
|
+
input: PermissionInput,
|
|
424
|
+
actorUserId: string | null,
|
|
425
|
+
): Promise<PermissionSummary> {
|
|
337
426
|
const shared = {
|
|
338
427
|
name: input.name,
|
|
339
428
|
displayName: input.displayName,
|
|
@@ -365,12 +454,21 @@ export class RbacRepository {
|
|
|
365
454
|
}
|
|
366
455
|
|
|
367
456
|
async listRoles(): Promise<RoleSummary[]> {
|
|
368
|
-
const rows = await this.prisma.role.findMany({
|
|
457
|
+
const rows = await this.prisma.role.findMany({
|
|
458
|
+
where: { isDeleted: false },
|
|
459
|
+
select: ROLE_SELECT,
|
|
460
|
+
orderBy: [{ order: "asc" }, { slug: "asc" }],
|
|
461
|
+
});
|
|
369
462
|
return rows.map((row) => ({ ...row, id: row.id.toString() }));
|
|
370
463
|
}
|
|
371
464
|
|
|
372
465
|
async createRole(
|
|
373
|
-
input: {
|
|
466
|
+
input: {
|
|
467
|
+
slug: string;
|
|
468
|
+
name?: string;
|
|
469
|
+
displayName?: string;
|
|
470
|
+
description?: string | null;
|
|
471
|
+
},
|
|
374
472
|
actorUserId: string | null,
|
|
375
473
|
): Promise<RoleSummary> {
|
|
376
474
|
const role = await this.prisma.role.create({
|
|
@@ -393,14 +491,23 @@ export class RbacRepository {
|
|
|
393
491
|
* closed-catalog property the routes always had — `@CheckAbility` only accepts slugs from
|
|
394
492
|
* `rbac.defaults.ts`, so an invented slug cannot gate anything shipped by this library.
|
|
395
493
|
*/
|
|
396
|
-
async attachPermissionToRole(
|
|
494
|
+
async attachPermissionToRole(
|
|
495
|
+
roleId: string,
|
|
496
|
+
permissionSlug: string,
|
|
497
|
+
actorUserId: string | null,
|
|
498
|
+
): Promise<void> {
|
|
397
499
|
const roleIdBig = toId(roleId);
|
|
398
|
-
const role = await this.prisma.role.findUnique({
|
|
500
|
+
const role = await this.prisma.role.findUnique({
|
|
501
|
+
where: { id: roleIdBig, isDeleted: false },
|
|
502
|
+
select: { id: true },
|
|
503
|
+
});
|
|
399
504
|
if (!role) throw new HttpError(404, "role not found");
|
|
400
505
|
|
|
401
506
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
402
507
|
await this.prisma.permissionRole.upsert({
|
|
403
|
-
where: {
|
|
508
|
+
where: {
|
|
509
|
+
permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig },
|
|
510
|
+
},
|
|
404
511
|
create: { permissionId: permission.id, roleId: roleIdBig },
|
|
405
512
|
update: {},
|
|
406
513
|
});
|
|
@@ -409,10 +516,16 @@ export class RbacRepository {
|
|
|
409
516
|
|
|
410
517
|
async assignRoleToUser(userId: string, roleSlug: string): Promise<void> {
|
|
411
518
|
const userIdBig = toId(userId);
|
|
412
|
-
const role = await this.prisma.role.findUnique({
|
|
519
|
+
const role = await this.prisma.role.findUnique({
|
|
520
|
+
where: { slug: roleSlug },
|
|
521
|
+
select: { id: true },
|
|
522
|
+
});
|
|
413
523
|
if (!role) throw new HttpError(404, `role "${roleSlug}" not found`);
|
|
414
524
|
|
|
415
|
-
await this.prisma.user.findUniqueOrThrow({
|
|
525
|
+
await this.prisma.user.findUniqueOrThrow({
|
|
526
|
+
where: { id: userIdBig },
|
|
527
|
+
select: { id: true },
|
|
528
|
+
});
|
|
416
529
|
await this.prisma.roleUser.upsert({
|
|
417
530
|
where: { userId_roleId: { userId: userIdBig, roleId: role.id } },
|
|
418
531
|
create: { userId: userIdBig, roleId: role.id },
|
|
@@ -422,9 +535,14 @@ export class RbacRepository {
|
|
|
422
535
|
}
|
|
423
536
|
|
|
424
537
|
async revokeRoleFromUser(userId: string, roleSlug: string): Promise<void> {
|
|
425
|
-
const role = await this.prisma.role.findUnique({
|
|
538
|
+
const role = await this.prisma.role.findUnique({
|
|
539
|
+
where: { slug: roleSlug },
|
|
540
|
+
select: { id: true },
|
|
541
|
+
});
|
|
426
542
|
if (!role) return;
|
|
427
|
-
await this.prisma.roleUser.deleteMany({
|
|
543
|
+
await this.prisma.roleUser.deleteMany({
|
|
544
|
+
where: { userId: toId(userId), roleId: role.id },
|
|
545
|
+
});
|
|
428
546
|
await this.cache.invalidateSubject(userId);
|
|
429
547
|
}
|
|
430
548
|
|
|
@@ -437,9 +555,15 @@ export class RbacRepository {
|
|
|
437
555
|
* Prisma lookup, so there's no string to convert from.
|
|
438
556
|
*/
|
|
439
557
|
async assignDefaultRoles(userId: bigint): Promise<void> {
|
|
440
|
-
const defaults = await this.prisma.role.findMany({
|
|
558
|
+
const defaults = await this.prisma.role.findMany({
|
|
559
|
+
where: { isDefault: true, isActive: true, isDeleted: false },
|
|
560
|
+
select: { id: true },
|
|
561
|
+
});
|
|
441
562
|
if (!defaults.length) return;
|
|
442
|
-
await this.prisma.roleUser.createMany({
|
|
563
|
+
await this.prisma.roleUser.createMany({
|
|
564
|
+
data: defaults.map((role) => ({ userId, roleId: role.id })),
|
|
565
|
+
skipDuplicates: true,
|
|
566
|
+
});
|
|
443
567
|
await this.cache.invalidateSubject(userId.toString());
|
|
444
568
|
}
|
|
445
569
|
|
|
@@ -447,21 +571,35 @@ export class RbacRepository {
|
|
|
447
571
|
return (await this.prisma.roleUser.count({ where: { userId } })) > 0;
|
|
448
572
|
}
|
|
449
573
|
|
|
450
|
-
async grantPermissionToUser(
|
|
574
|
+
async grantPermissionToUser(
|
|
575
|
+
userId: string,
|
|
576
|
+
permissionSlug: string,
|
|
577
|
+
actorUserId: string | null,
|
|
578
|
+
): Promise<void> {
|
|
451
579
|
const userIdBig = toId(userId);
|
|
452
580
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
453
581
|
await this.prisma.permissionUser.upsert({
|
|
454
|
-
where: {
|
|
582
|
+
where: {
|
|
583
|
+
userId_permissionId: { userId: userIdBig, permissionId: permission.id },
|
|
584
|
+
},
|
|
455
585
|
create: { userId: userIdBig, permissionId: permission.id },
|
|
456
586
|
update: {},
|
|
457
587
|
});
|
|
458
588
|
await this.cache.invalidateSubject(userId);
|
|
459
589
|
}
|
|
460
590
|
|
|
461
|
-
async revokePermissionFromUser(
|
|
462
|
-
|
|
591
|
+
async revokePermissionFromUser(
|
|
592
|
+
userId: string,
|
|
593
|
+
permissionSlug: string,
|
|
594
|
+
): Promise<void> {
|
|
595
|
+
const permission = await this.prisma.permission.findUnique({
|
|
596
|
+
where: { slug: permissionSlug },
|
|
597
|
+
select: { id: true },
|
|
598
|
+
});
|
|
463
599
|
if (!permission) return;
|
|
464
|
-
await this.prisma.permissionUser.deleteMany({
|
|
600
|
+
await this.prisma.permissionUser.deleteMany({
|
|
601
|
+
where: { userId: toId(userId), permissionId: permission.id },
|
|
602
|
+
});
|
|
465
603
|
await this.cache.invalidateSubject(userId);
|
|
466
604
|
}
|
|
467
605
|
|
|
@@ -471,12 +609,25 @@ export class RbacRepository {
|
|
|
471
609
|
}
|
|
472
610
|
|
|
473
611
|
/** Creates the row for a slug granted before anyone defined it; leaves an existing definition alone. */
|
|
474
|
-
private async ensurePermission(
|
|
475
|
-
|
|
612
|
+
private async ensurePermission(
|
|
613
|
+
slug: string,
|
|
614
|
+
actorUserId: string | null,
|
|
615
|
+
): Promise<{ id: bigint }> {
|
|
616
|
+
const existing = await this.prisma.permission.findUnique({
|
|
617
|
+
where: { slug },
|
|
618
|
+
select: { id: true },
|
|
619
|
+
});
|
|
476
620
|
return (
|
|
477
621
|
existing ??
|
|
478
622
|
(await this.prisma.permission.create({
|
|
479
|
-
data: {
|
|
623
|
+
data: {
|
|
624
|
+
slug,
|
|
625
|
+
name: slug,
|
|
626
|
+
displayName: slug,
|
|
627
|
+
group: "Custom",
|
|
628
|
+
createdBy: toIdOrNull(actorUserId),
|
|
629
|
+
updatedBy: toIdOrNull(actorUserId),
|
|
630
|
+
},
|
|
480
631
|
select: { id: true },
|
|
481
632
|
}))
|
|
482
633
|
);
|