@simple-auth-kit/cli 1.4.3 → 1.5.0
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/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 +57 -22
- 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 +67 -19
- 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 +78 -20
- 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 +81 -26
- 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 +73 -21
- 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 +109 -29
- 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 +59 -28
- 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 +98 -27
- 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 +78 -20
- 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 +55 -27
- 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 +104 -29
- 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 +109 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +29 -2
- 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
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
and,
|
|
4
|
+
asc,
|
|
5
|
+
count,
|
|
6
|
+
desc,
|
|
7
|
+
eq,
|
|
8
|
+
ilike,
|
|
9
|
+
inArray,
|
|
10
|
+
type SQL,
|
|
11
|
+
} from "drizzle-orm";
|
|
3
12
|
import { resolvePermissions } from "@/lib/auth/core/rbac.js";
|
|
4
|
-
import type { AuthzContext } from "
|
|
5
|
-
import { DRIZZLE_DB, type Database } from "
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
import type { AuthzContext } from "../guards/authz.guard.js";
|
|
14
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
15
|
+
import {
|
|
16
|
+
buildPageMeta,
|
|
17
|
+
normalizeLimit,
|
|
18
|
+
normalizePage,
|
|
19
|
+
type Paginated,
|
|
20
|
+
} from "../pagination.js";
|
|
21
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
22
|
+
import {
|
|
23
|
+
permissionMember,
|
|
24
|
+
permissionRole,
|
|
25
|
+
permissions,
|
|
26
|
+
roleMember,
|
|
27
|
+
roles,
|
|
28
|
+
users,
|
|
29
|
+
workspaceMembers,
|
|
30
|
+
} from "../schema.js";
|
|
31
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
10
32
|
|
|
11
33
|
export interface MemberSummary {
|
|
12
34
|
memberId: string;
|
|
@@ -196,9 +218,15 @@ interface RoleRow {
|
|
|
196
218
|
isActive: boolean;
|
|
197
219
|
}
|
|
198
220
|
|
|
199
|
-
const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({
|
|
221
|
+
const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({
|
|
222
|
+
...row,
|
|
223
|
+
id: row.id.toString(),
|
|
224
|
+
});
|
|
200
225
|
|
|
201
|
-
const asRoleSummary = (row: RoleRow): RoleSummary => ({
|
|
226
|
+
const asRoleSummary = (row: RoleRow): RoleSummary => ({
|
|
227
|
+
...row,
|
|
228
|
+
id: row.id.toString(),
|
|
229
|
+
});
|
|
202
230
|
|
|
203
231
|
/**
|
|
204
232
|
* The cache subject in this variant: authorization belongs to a membership, so the key has to be
|
|
@@ -209,7 +237,8 @@ const asRoleSummary = (row: RoleRow): RoleSummary => ({ ...row, id: row.id.toStr
|
|
|
209
237
|
* between the two one-directional — the guard needs this repository at runtime, so the repository
|
|
210
238
|
* must not need the guard.
|
|
211
239
|
*/
|
|
212
|
-
export const memberCacheKey = (userId: string, workspaceId: string) =>
|
|
240
|
+
export const memberCacheKey = (userId: string, workspaceId: string) =>
|
|
241
|
+
`${userId}:${workspaceId}`;
|
|
213
242
|
|
|
214
243
|
@Injectable()
|
|
215
244
|
export class RbacRepository {
|
|
@@ -234,11 +263,18 @@ export class RbacRepository {
|
|
|
234
263
|
// hand from its own database lookup, so there's nothing to parse here.
|
|
235
264
|
private async invalidateMemberById(memberId: bigint): Promise<void> {
|
|
236
265
|
const [member] = await this.db
|
|
237
|
-
.select({
|
|
266
|
+
.select({
|
|
267
|
+
userId: workspaceMembers.userId,
|
|
268
|
+
workspaceId: workspaceMembers.workspaceId,
|
|
269
|
+
})
|
|
238
270
|
.from(workspaceMembers)
|
|
239
271
|
.where(eq(workspaceMembers.id, memberId))
|
|
240
272
|
.limit(1);
|
|
241
|
-
if (member)
|
|
273
|
+
if (member)
|
|
274
|
+
await this.invalidateMember(
|
|
275
|
+
member.userId.toString(),
|
|
276
|
+
member.workspaceId.toString(),
|
|
277
|
+
);
|
|
242
278
|
}
|
|
243
279
|
|
|
244
280
|
/**
|
|
@@ -256,7 +292,10 @@ export class RbacRepository {
|
|
|
256
292
|
* `isActive: false` on a role or a permission takes it out of the answer, which is what makes
|
|
257
293
|
* deactivating either one enforcement-changing without unpicking any grant.
|
|
258
294
|
*/
|
|
259
|
-
async resolveAuthzContext(
|
|
295
|
+
async resolveAuthzContext(
|
|
296
|
+
userId: string,
|
|
297
|
+
workspaceId: string,
|
|
298
|
+
): Promise<AuthzContext | null> {
|
|
260
299
|
// workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
|
|
261
300
|
// isn't a valid bigint is exactly as "not a member" as one that parses but names no
|
|
262
301
|
// membership, so it must not throw and 500 the request.
|
|
@@ -272,8 +311,16 @@ export class RbacRepository {
|
|
|
272
311
|
const [member] = await this.db
|
|
273
312
|
.select({ id: workspaceMembers.id })
|
|
274
313
|
.from(workspaceMembers)
|
|
275
|
-
.innerJoin(
|
|
276
|
-
|
|
314
|
+
.innerJoin(
|
|
315
|
+
users,
|
|
316
|
+
and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)),
|
|
317
|
+
)
|
|
318
|
+
.where(
|
|
319
|
+
and(
|
|
320
|
+
eq(workspaceMembers.userId, userIdBig),
|
|
321
|
+
eq(workspaceMembers.workspaceId, workspaceIdBig),
|
|
322
|
+
),
|
|
323
|
+
)
|
|
277
324
|
.limit(1);
|
|
278
325
|
if (!member) return null;
|
|
279
326
|
|
|
@@ -283,14 +330,35 @@ export class RbacRepository {
|
|
|
283
330
|
this.db
|
|
284
331
|
.select({ roleSlug: roles.slug, permissionSlug: permissions.slug })
|
|
285
332
|
.from(roleMember)
|
|
286
|
-
.innerJoin(
|
|
333
|
+
.innerJoin(
|
|
334
|
+
roles,
|
|
335
|
+
and(
|
|
336
|
+
eq(roles.id, roleMember.roleId),
|
|
337
|
+
eq(roles.isActive, true),
|
|
338
|
+
eq(roles.isDeleted, false),
|
|
339
|
+
),
|
|
340
|
+
)
|
|
287
341
|
.leftJoin(permissionRole, eq(permissionRole.roleId, roles.id))
|
|
288
|
-
.leftJoin(
|
|
342
|
+
.leftJoin(
|
|
343
|
+
permissions,
|
|
344
|
+
and(
|
|
345
|
+
eq(permissions.id, permissionRole.permissionId),
|
|
346
|
+
eq(permissions.isActive, true),
|
|
347
|
+
eq(permissions.isDeleted, false),
|
|
348
|
+
),
|
|
349
|
+
)
|
|
289
350
|
.where(eq(roleMember.memberId, member.id)),
|
|
290
351
|
this.db
|
|
291
352
|
.select({ slug: permissions.slug })
|
|
292
353
|
.from(permissionMember)
|
|
293
|
-
.innerJoin(
|
|
354
|
+
.innerJoin(
|
|
355
|
+
permissions,
|
|
356
|
+
and(
|
|
357
|
+
eq(permissions.id, permissionMember.permissionId),
|
|
358
|
+
eq(permissions.isActive, true),
|
|
359
|
+
eq(permissions.isDeleted, false),
|
|
360
|
+
),
|
|
361
|
+
)
|
|
294
362
|
.where(eq(permissionMember.memberId, member.id)),
|
|
295
363
|
]);
|
|
296
364
|
|
|
@@ -299,7 +367,9 @@ export class RbacRepository {
|
|
|
299
367
|
memberId: member.id.toString(),
|
|
300
368
|
roles: [...new Set(roleRows.map((row) => row.roleSlug))].sort(),
|
|
301
369
|
permissions: resolvePermissions(
|
|
302
|
-
roleRows
|
|
370
|
+
roleRows
|
|
371
|
+
.map((row) => row.permissionSlug)
|
|
372
|
+
.filter((slug): slug is string => slug !== null),
|
|
303
373
|
directRows.map((row) => row.slug),
|
|
304
374
|
),
|
|
305
375
|
};
|
|
@@ -311,13 +381,22 @@ export class RbacRepository {
|
|
|
311
381
|
* caller feeds the id straight into another database call, and the one external caller
|
|
312
382
|
* (AuthService) discards the result.
|
|
313
383
|
*/
|
|
314
|
-
async requireMember(
|
|
384
|
+
async requireMember(
|
|
385
|
+
workspaceId: string,
|
|
386
|
+
userId: string,
|
|
387
|
+
): Promise<{ id: bigint }> {
|
|
315
388
|
const [member] = await this.db
|
|
316
389
|
.select({ id: workspaceMembers.id })
|
|
317
390
|
.from(workspaceMembers)
|
|
318
|
-
.where(
|
|
391
|
+
.where(
|
|
392
|
+
and(
|
|
393
|
+
eq(workspaceMembers.userId, toId(userId)),
|
|
394
|
+
eq(workspaceMembers.workspaceId, toId(workspaceId)),
|
|
395
|
+
),
|
|
396
|
+
)
|
|
319
397
|
.limit(1);
|
|
320
|
-
if (!member)
|
|
398
|
+
if (!member)
|
|
399
|
+
throw new NotFoundException("user is not a member of this workspace");
|
|
321
400
|
return member;
|
|
322
401
|
}
|
|
323
402
|
|
|
@@ -325,13 +404,20 @@ export class RbacRepository {
|
|
|
325
404
|
* Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
|
|
326
405
|
* shaping the collection is the caller's job, see `toMemberSummary`.
|
|
327
406
|
*/
|
|
328
|
-
async listMembers(
|
|
407
|
+
async listMembers(
|
|
408
|
+
workspaceId: string,
|
|
409
|
+
filter: MemberListFilter = {},
|
|
410
|
+
): Promise<Paginated<MemberRow>> {
|
|
329
411
|
const page = normalizePage(filter.page);
|
|
330
412
|
const limit = normalizeLimit(filter.limit);
|
|
331
413
|
const workspaceIdBig = toId(workspaceId);
|
|
332
414
|
|
|
333
|
-
const conditions: SQL[] = [
|
|
334
|
-
|
|
415
|
+
const conditions: SQL[] = [
|
|
416
|
+
eq(workspaceMembers.workspaceId, workspaceIdBig),
|
|
417
|
+
eq(users.isDeleted, false),
|
|
418
|
+
];
|
|
419
|
+
if (filter.search)
|
|
420
|
+
conditions.push(ilike(users.email, `%${filter.search}%`));
|
|
335
421
|
const where = and(...conditions)!;
|
|
336
422
|
|
|
337
423
|
const rows = await this.db
|
|
@@ -368,7 +454,10 @@ export class RbacRepository {
|
|
|
368
454
|
}
|
|
369
455
|
|
|
370
456
|
return {
|
|
371
|
-
items: rows.map((row) => ({
|
|
457
|
+
items: rows.map((row) => ({
|
|
458
|
+
...row,
|
|
459
|
+
roles: (rolesByMember.get(row.memberId.toString()) ?? []).sort(),
|
|
460
|
+
})),
|
|
372
461
|
meta: buildPageMeta(page, limit, total),
|
|
373
462
|
};
|
|
374
463
|
}
|
|
@@ -385,14 +474,24 @@ export class RbacRepository {
|
|
|
385
474
|
const [row] = await this.db
|
|
386
475
|
.select(MEMBER_SELECT)
|
|
387
476
|
.from(workspaceMembers)
|
|
388
|
-
.innerJoin(
|
|
477
|
+
.innerJoin(
|
|
478
|
+
users,
|
|
479
|
+
and(eq(users.id, workspaceMembers.userId), eq(users.isDeleted, false)),
|
|
480
|
+
)
|
|
389
481
|
.where(eq(workspaceMembers.id, member.id))
|
|
390
482
|
.limit(1);
|
|
391
483
|
if (!row) throw new NotFoundException(`user "${userId}" not found`);
|
|
392
484
|
|
|
393
|
-
const assignments = await this.db
|
|
485
|
+
const assignments = await this.db
|
|
486
|
+
.select({ slug: roles.slug })
|
|
487
|
+
.from(roleMember)
|
|
488
|
+
.innerJoin(roles, eq(roles.id, roleMember.roleId))
|
|
489
|
+
.where(eq(roleMember.memberId, row.memberId));
|
|
394
490
|
|
|
395
|
-
return toMemberSummary({
|
|
491
|
+
return toMemberSummary({
|
|
492
|
+
...row,
|
|
493
|
+
roles: assignments.map((a) => a.slug).sort(),
|
|
494
|
+
});
|
|
396
495
|
}
|
|
397
496
|
|
|
398
497
|
/** Profile fields only — email is the login identifier and stays out of this endpoint. */
|
|
@@ -413,7 +512,9 @@ export class RbacRepository {
|
|
|
413
512
|
actorUserId: string | null,
|
|
414
513
|
): Promise<MemberSummary> {
|
|
415
514
|
await this.requireMember(workspaceId, userId);
|
|
416
|
-
const changed = Object.fromEntries(
|
|
515
|
+
const changed = Object.fromEntries(
|
|
516
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
517
|
+
);
|
|
417
518
|
await this.db
|
|
418
519
|
.update(users)
|
|
419
520
|
.set({ ...changed, updatedBy: toIdOrNull(actorUserId) })
|
|
@@ -427,11 +528,21 @@ export class RbacRepository {
|
|
|
427
528
|
* member's account — this disables the account across every workspace it belongs to, not just
|
|
428
529
|
* this one, which is why it's gated the same way `block` is.
|
|
429
530
|
*/
|
|
430
|
-
async deleteMember(
|
|
531
|
+
async deleteMember(
|
|
532
|
+
workspaceId: string,
|
|
533
|
+
userId: string,
|
|
534
|
+
actorUserId: string | null,
|
|
535
|
+
reason?: string,
|
|
536
|
+
): Promise<void> {
|
|
431
537
|
await this.requireMember(workspaceId, userId);
|
|
432
538
|
await this.db
|
|
433
539
|
.update(users)
|
|
434
|
-
.set({
|
|
540
|
+
.set({
|
|
541
|
+
isDeleted: true,
|
|
542
|
+
deletedAt: new Date(),
|
|
543
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
544
|
+
deletedReason: reason ?? null,
|
|
545
|
+
})
|
|
435
546
|
.where(eq(users.id, toId(userId)));
|
|
436
547
|
await this.invalidateMember(userId, workspaceId);
|
|
437
548
|
}
|
|
@@ -448,7 +559,12 @@ export class RbacRepository {
|
|
|
448
559
|
.select(PERMISSION_COLUMNS)
|
|
449
560
|
.from(permissions)
|
|
450
561
|
.where(eq(permissions.isDeleted, false))
|
|
451
|
-
.orderBy(
|
|
562
|
+
.orderBy(
|
|
563
|
+
asc(permissions.groupOrder),
|
|
564
|
+
asc(permissions.group),
|
|
565
|
+
asc(permissions.order),
|
|
566
|
+
asc(permissions.slug),
|
|
567
|
+
);
|
|
452
568
|
return rows.map(asPermissionSummary);
|
|
453
569
|
}
|
|
454
570
|
|
|
@@ -461,7 +577,10 @@ export class RbacRepository {
|
|
|
461
577
|
* Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
|
|
462
578
|
* `name`/`displayName` never breaks a grant.
|
|
463
579
|
*/
|
|
464
|
-
async upsertPermission(
|
|
580
|
+
async upsertPermission(
|
|
581
|
+
input: PermissionInput,
|
|
582
|
+
actorUserId: string | null,
|
|
583
|
+
): Promise<PermissionSummary> {
|
|
465
584
|
const actorId = toIdOrNull(actorUserId);
|
|
466
585
|
// Only the fields the caller sent are written; an absent one leaves its column alone, so a
|
|
467
586
|
// partial edit cannot blank out metadata it did not mention.
|
|
@@ -492,7 +611,9 @@ export class RbacRepository {
|
|
|
492
611
|
// returns none — so the no-op case rewrites the slug with itself.
|
|
493
612
|
.onConflictDoUpdate({
|
|
494
613
|
target: permissions.slug,
|
|
495
|
-
set: Object.keys(changed).length
|
|
614
|
+
set: Object.keys(changed).length
|
|
615
|
+
? { ...changed, updatedBy: actorId }
|
|
616
|
+
: { slug: input.slug, updatedBy: actorId },
|
|
496
617
|
})
|
|
497
618
|
.returning(PERMISSION_COLUMNS);
|
|
498
619
|
|
|
@@ -505,14 +626,26 @@ export class RbacRepository {
|
|
|
505
626
|
const rows = await this.db
|
|
506
627
|
.select(ROLE_COLUMNS)
|
|
507
628
|
.from(roles)
|
|
508
|
-
.where(
|
|
629
|
+
.where(
|
|
630
|
+
and(
|
|
631
|
+
eq(roles.workspaceId, toId(workspaceId)),
|
|
632
|
+
eq(roles.isDeleted, false),
|
|
633
|
+
),
|
|
634
|
+
)
|
|
509
635
|
.orderBy(asc(roles.order), asc(roles.slug));
|
|
510
636
|
return rows.map(asRoleSummary);
|
|
511
637
|
}
|
|
512
638
|
|
|
513
639
|
async createRole(
|
|
514
640
|
workspaceId: string,
|
|
515
|
-
input: {
|
|
641
|
+
input: {
|
|
642
|
+
slug: string;
|
|
643
|
+
name?: string;
|
|
644
|
+
displayName?: string;
|
|
645
|
+
description?: string | null;
|
|
646
|
+
isDefault?: boolean;
|
|
647
|
+
isActive?: boolean;
|
|
648
|
+
},
|
|
516
649
|
actorUserId: string | null,
|
|
517
650
|
): Promise<RoleSummary> {
|
|
518
651
|
const actorId = toIdOrNull(actorUserId);
|
|
@@ -536,7 +669,13 @@ export class RbacRepository {
|
|
|
536
669
|
async updateRole(
|
|
537
670
|
workspaceId: string,
|
|
538
671
|
roleId: string,
|
|
539
|
-
input: {
|
|
672
|
+
input: {
|
|
673
|
+
name?: string;
|
|
674
|
+
displayName?: string;
|
|
675
|
+
description?: string | null;
|
|
676
|
+
isDefault?: boolean;
|
|
677
|
+
isActive?: boolean;
|
|
678
|
+
},
|
|
540
679
|
actorUserId: string | null,
|
|
541
680
|
): Promise<RoleSummary> {
|
|
542
681
|
const workspaceIdBig = toId(workspaceId);
|
|
@@ -546,9 +685,12 @@ export class RbacRepository {
|
|
|
546
685
|
.from(roles)
|
|
547
686
|
.where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
|
|
548
687
|
.limit(1);
|
|
549
|
-
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
688
|
+
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
689
|
+
throw new NotFoundException("role not found in this workspace");
|
|
550
690
|
|
|
551
|
-
const changed = Object.fromEntries(
|
|
691
|
+
const changed = Object.fromEntries(
|
|
692
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
693
|
+
);
|
|
552
694
|
const [role] = await this.db
|
|
553
695
|
.update(roles)
|
|
554
696
|
.set({ ...changed, updatedBy: toIdOrNull(actorUserId) })
|
|
@@ -562,7 +704,12 @@ export class RbacRepository {
|
|
|
562
704
|
* Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the role
|
|
563
705
|
* simply stops being resolved by `resolveAuthzContext`/`listRoles`.
|
|
564
706
|
*/
|
|
565
|
-
async deleteRole(
|
|
707
|
+
async deleteRole(
|
|
708
|
+
workspaceId: string,
|
|
709
|
+
roleId: string,
|
|
710
|
+
actorUserId: string | null,
|
|
711
|
+
reason?: string,
|
|
712
|
+
): Promise<void> {
|
|
566
713
|
const workspaceIdBig = toId(workspaceId);
|
|
567
714
|
const roleIdBig = toId(roleId);
|
|
568
715
|
const [existing] = await this.db
|
|
@@ -570,11 +717,17 @@ export class RbacRepository {
|
|
|
570
717
|
.from(roles)
|
|
571
718
|
.where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
|
|
572
719
|
.limit(1);
|
|
573
|
-
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
720
|
+
if (!existing || existing.workspaceId !== workspaceIdBig)
|
|
721
|
+
throw new NotFoundException("role not found in this workspace");
|
|
574
722
|
|
|
575
723
|
await this.db
|
|
576
724
|
.update(roles)
|
|
577
|
-
.set({
|
|
725
|
+
.set({
|
|
726
|
+
isDeleted: true,
|
|
727
|
+
deletedAt: new Date(),
|
|
728
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
729
|
+
deletedReason: reason ?? null,
|
|
730
|
+
})
|
|
578
731
|
.where(eq(roles.id, roleIdBig));
|
|
579
732
|
await this.cache.invalidatePolicy();
|
|
580
733
|
}
|
|
@@ -585,7 +738,12 @@ export class RbacRepository {
|
|
|
585
738
|
* be granted, and it opens nothing until a route names it — and no route shipped by this
|
|
586
739
|
* library can name it, because `@CheckAbility` only accepts slugs from `rbac.defaults.ts`.
|
|
587
740
|
*/
|
|
588
|
-
async attachPermissionToRole(
|
|
741
|
+
async attachPermissionToRole(
|
|
742
|
+
workspaceId: string,
|
|
743
|
+
roleId: string,
|
|
744
|
+
permissionSlug: string,
|
|
745
|
+
actorUserId: string | null,
|
|
746
|
+
): Promise<void> {
|
|
589
747
|
const workspaceIdBig = toId(workspaceId);
|
|
590
748
|
const roleIdBig = toId(roleId);
|
|
591
749
|
const [role] = await this.db
|
|
@@ -593,57 +751,94 @@ export class RbacRepository {
|
|
|
593
751
|
.from(roles)
|
|
594
752
|
.where(and(eq(roles.id, roleIdBig), eq(roles.isDeleted, false)))
|
|
595
753
|
.limit(1);
|
|
596
|
-
if (!role || role.workspaceId !== workspaceIdBig)
|
|
754
|
+
if (!role || role.workspaceId !== workspaceIdBig)
|
|
755
|
+
throw new NotFoundException("role not found in this workspace");
|
|
597
756
|
|
|
598
757
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
599
758
|
await this.db
|
|
600
759
|
.insert(permissionRole)
|
|
601
760
|
.values({ permissionId: permission.id, roleId: roleIdBig })
|
|
602
|
-
.onConflictDoNothing({
|
|
761
|
+
.onConflictDoNothing({
|
|
762
|
+
target: [permissionRole.permissionId, permissionRole.roleId],
|
|
763
|
+
});
|
|
603
764
|
await this.cache.invalidatePolicy(); // every membership holding this role is affected
|
|
604
765
|
}
|
|
605
766
|
|
|
606
|
-
async assignRoleToMember(
|
|
767
|
+
async assignRoleToMember(
|
|
768
|
+
workspaceId: string,
|
|
769
|
+
userId: string,
|
|
770
|
+
roleSlug: string,
|
|
771
|
+
): Promise<void> {
|
|
607
772
|
const role = await this.requireRole(workspaceId, roleSlug);
|
|
608
773
|
const member = await this.requireMember(workspaceId, userId);
|
|
609
774
|
await this.db
|
|
610
775
|
.insert(roleMember)
|
|
611
776
|
.values({ memberId: member.id, roleId: role.id })
|
|
612
|
-
.onConflictDoNothing({
|
|
777
|
+
.onConflictDoNothing({
|
|
778
|
+
target: [roleMember.memberId, roleMember.roleId],
|
|
779
|
+
});
|
|
613
780
|
await this.invalidateMember(userId, workspaceId);
|
|
614
781
|
}
|
|
615
782
|
|
|
616
|
-
async revokeRoleFromMember(
|
|
783
|
+
async revokeRoleFromMember(
|
|
784
|
+
workspaceId: string,
|
|
785
|
+
userId: string,
|
|
786
|
+
roleSlug: string,
|
|
787
|
+
): Promise<void> {
|
|
617
788
|
const member = await this.requireMember(workspaceId, userId);
|
|
618
789
|
const [role] = await this.db
|
|
619
790
|
.select({ id: roles.id })
|
|
620
791
|
.from(roles)
|
|
621
|
-
.where(
|
|
792
|
+
.where(
|
|
793
|
+
and(eq(roles.workspaceId, toId(workspaceId)), eq(roles.slug, roleSlug)),
|
|
794
|
+
)
|
|
622
795
|
.limit(1);
|
|
623
796
|
if (!role) return;
|
|
624
|
-
await this.db
|
|
797
|
+
await this.db
|
|
798
|
+
.delete(roleMember)
|
|
799
|
+
.where(
|
|
800
|
+
and(eq(roleMember.memberId, member.id), eq(roleMember.roleId, role.id)),
|
|
801
|
+
);
|
|
625
802
|
await this.invalidateMember(userId, workspaceId);
|
|
626
803
|
}
|
|
627
804
|
|
|
628
805
|
/** Replaces the whole set — the "set a member's roles" operation, as opposed to assign/revoke one at a time. */
|
|
629
|
-
async setMemberRoles(
|
|
806
|
+
async setMemberRoles(
|
|
807
|
+
workspaceId: string,
|
|
808
|
+
memberId: string,
|
|
809
|
+
roleSlugs: string[],
|
|
810
|
+
): Promise<{ memberId: string; roles: string[] }> {
|
|
630
811
|
const workspaceIdBig = toId(workspaceId);
|
|
631
812
|
const memberIdBig = toId(memberId);
|
|
632
813
|
const [member] = await this.db
|
|
633
|
-
.select({
|
|
814
|
+
.select({
|
|
815
|
+
id: workspaceMembers.id,
|
|
816
|
+
workspaceId: workspaceMembers.workspaceId,
|
|
817
|
+
})
|
|
634
818
|
.from(workspaceMembers)
|
|
635
819
|
.where(eq(workspaceMembers.id, memberIdBig))
|
|
636
820
|
.limit(1);
|
|
637
|
-
if (!member || member.workspaceId !== workspaceIdBig)
|
|
821
|
+
if (!member || member.workspaceId !== workspaceIdBig)
|
|
822
|
+
throw new NotFoundException("member not found in this workspace");
|
|
638
823
|
|
|
639
824
|
const found = roleSlugs.length
|
|
640
825
|
? await this.db
|
|
641
826
|
.select({ id: roles.id, slug: roles.slug })
|
|
642
827
|
.from(roles)
|
|
643
|
-
.where(
|
|
828
|
+
.where(
|
|
829
|
+
and(
|
|
830
|
+
eq(roles.workspaceId, workspaceIdBig),
|
|
831
|
+
inArray(roles.slug, roleSlugs),
|
|
832
|
+
),
|
|
833
|
+
)
|
|
644
834
|
: [];
|
|
645
|
-
const unknown = roleSlugs.filter(
|
|
646
|
-
|
|
835
|
+
const unknown = roleSlugs.filter(
|
|
836
|
+
(slug) => !found.some((role) => role.slug === slug),
|
|
837
|
+
);
|
|
838
|
+
if (unknown.length)
|
|
839
|
+
throw new NotFoundException(
|
|
840
|
+
`role(s) not defined in this workspace: ${unknown.join(", ")}`,
|
|
841
|
+
);
|
|
647
842
|
|
|
648
843
|
// Replace, in one transaction: a member must never be briefly role-less to a concurrent request.
|
|
649
844
|
await this.db.transaction(async (tx) => {
|
|
@@ -651,8 +846,12 @@ export class RbacRepository {
|
|
|
651
846
|
if (found.length) {
|
|
652
847
|
await tx
|
|
653
848
|
.insert(roleMember)
|
|
654
|
-
.values(
|
|
655
|
-
|
|
849
|
+
.values(
|
|
850
|
+
found.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
|
|
851
|
+
)
|
|
852
|
+
.onConflictDoNothing({
|
|
853
|
+
target: [roleMember.memberId, roleMember.roleId],
|
|
854
|
+
});
|
|
656
855
|
}
|
|
657
856
|
});
|
|
658
857
|
await this.invalidateMemberById(memberIdBig);
|
|
@@ -668,37 +867,76 @@ export class RbacRepository {
|
|
|
668
867
|
const rows = await this.db
|
|
669
868
|
.select({ id: roles.id })
|
|
670
869
|
.from(roles)
|
|
671
|
-
.where(
|
|
870
|
+
.where(
|
|
871
|
+
and(
|
|
872
|
+
eq(roles.workspaceId, toId(workspaceId)),
|
|
873
|
+
eq(roles.isDefault, true),
|
|
874
|
+
eq(roles.isActive, true),
|
|
875
|
+
eq(roles.isDeleted, false),
|
|
876
|
+
),
|
|
877
|
+
);
|
|
672
878
|
return rows.map((role) => role.id);
|
|
673
879
|
}
|
|
674
880
|
|
|
675
|
-
async grantPermissionToMember(
|
|
881
|
+
async grantPermissionToMember(
|
|
882
|
+
workspaceId: string,
|
|
883
|
+
userId: string,
|
|
884
|
+
permissionSlug: string,
|
|
885
|
+
actorUserId: string | null,
|
|
886
|
+
): Promise<void> {
|
|
676
887
|
const member = await this.requireMember(workspaceId, userId);
|
|
677
888
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
678
889
|
await this.db
|
|
679
890
|
.insert(permissionMember)
|
|
680
891
|
.values({ memberId: member.id, permissionId: permission.id })
|
|
681
|
-
.onConflictDoNothing({
|
|
892
|
+
.onConflictDoNothing({
|
|
893
|
+
target: [permissionMember.memberId, permissionMember.permissionId],
|
|
894
|
+
});
|
|
682
895
|
await this.invalidateMember(userId, workspaceId);
|
|
683
896
|
}
|
|
684
897
|
|
|
685
|
-
async revokePermissionFromMember(
|
|
898
|
+
async revokePermissionFromMember(
|
|
899
|
+
workspaceId: string,
|
|
900
|
+
userId: string,
|
|
901
|
+
permissionSlug: string,
|
|
902
|
+
): Promise<void> {
|
|
686
903
|
const member = await this.requireMember(workspaceId, userId);
|
|
687
|
-
const [permission] = await this.db
|
|
904
|
+
const [permission] = await this.db
|
|
905
|
+
.select({ id: permissions.id })
|
|
906
|
+
.from(permissions)
|
|
907
|
+
.where(eq(permissions.slug, permissionSlug))
|
|
908
|
+
.limit(1);
|
|
688
909
|
if (!permission) return;
|
|
689
|
-
await this.db
|
|
910
|
+
await this.db
|
|
911
|
+
.delete(permissionMember)
|
|
912
|
+
.where(
|
|
913
|
+
and(
|
|
914
|
+
eq(permissionMember.memberId, member.id),
|
|
915
|
+
eq(permissionMember.permissionId, permission.id),
|
|
916
|
+
),
|
|
917
|
+
);
|
|
690
918
|
await this.invalidateMember(userId, workspaceId);
|
|
691
919
|
}
|
|
692
920
|
|
|
693
921
|
// Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
|
|
694
922
|
// another database call.
|
|
695
|
-
private async requireRole(
|
|
923
|
+
private async requireRole(
|
|
924
|
+
workspaceId: string,
|
|
925
|
+
slug: string,
|
|
926
|
+
): Promise<{ id: bigint }> {
|
|
696
927
|
const [role] = await this.db
|
|
697
928
|
.select({ id: roles.id })
|
|
698
929
|
.from(roles)
|
|
699
|
-
.where(
|
|
930
|
+
.where(
|
|
931
|
+
and(
|
|
932
|
+
eq(roles.workspaceId, toId(workspaceId)),
|
|
933
|
+
eq(roles.slug, slug),
|
|
934
|
+
eq(roles.isDeleted, false),
|
|
935
|
+
),
|
|
936
|
+
)
|
|
700
937
|
.limit(1);
|
|
701
|
-
if (!role)
|
|
938
|
+
if (!role)
|
|
939
|
+
throw new NotFoundException(`role "${slug}" not found in this workspace`);
|
|
702
940
|
return role;
|
|
703
941
|
}
|
|
704
942
|
|
|
@@ -706,13 +944,27 @@ export class RbacRepository {
|
|
|
706
944
|
* Creates the row for a slug granted before anyone defined it; leaves an existing definition
|
|
707
945
|
* alone. Returns bigint directly: both callers feed the id straight into another database call.
|
|
708
946
|
*/
|
|
709
|
-
private async ensurePermission(
|
|
710
|
-
|
|
947
|
+
private async ensurePermission(
|
|
948
|
+
slug: string,
|
|
949
|
+
actorUserId: string | null,
|
|
950
|
+
): Promise<{ id: bigint }> {
|
|
951
|
+
const [existing] = await this.db
|
|
952
|
+
.select({ id: permissions.id })
|
|
953
|
+
.from(permissions)
|
|
954
|
+
.where(eq(permissions.slug, slug))
|
|
955
|
+
.limit(1);
|
|
711
956
|
if (existing) return existing;
|
|
712
957
|
const actorId = toIdOrNull(actorUserId);
|
|
713
958
|
const [created] = await this.db
|
|
714
959
|
.insert(permissions)
|
|
715
|
-
.values({
|
|
960
|
+
.values({
|
|
961
|
+
slug,
|
|
962
|
+
name: slug,
|
|
963
|
+
displayName: slug,
|
|
964
|
+
group: "Custom",
|
|
965
|
+
createdBy: actorId,
|
|
966
|
+
updatedBy: actorId,
|
|
967
|
+
})
|
|
716
968
|
.returning({ id: permissions.id });
|
|
717
969
|
return created;
|
|
718
970
|
}
|