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