@simple-auth-kit/cli 1.4.2 → 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.
Files changed (133) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  3. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  4. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  5. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  6. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  7. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  8. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  9. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  10. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  11. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  12. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  13. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  14. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  15. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  16. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  17. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  18. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  19. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  20. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  23. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  26. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  27. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  28. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  29. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  30. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  31. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  32. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  33. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  34. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  35. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
  36. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  37. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
  38. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  39. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  40. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  41. package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
  42. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  43. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
  44. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  45. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  46. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
  47. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  48. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  49. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  50. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
  53. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  54. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  56. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  57. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  58. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  59. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  62. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  63. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  64. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  65. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  66. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  71. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  79. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  80. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  81. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  82. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  83. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  84. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  85. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  86. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  87. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  88. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  89. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  90. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
  91. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  92. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  93. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  94. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
  95. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  96. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  97. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  98. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  99. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  100. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
  101. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  102. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  115. package/registry.json +6 -4
  116. package/simple-auth-kit.ts +582 -124
  117. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  118. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  119. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  120. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  121. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  122. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  123. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  124. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  125. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  126. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  127. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  128. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  130. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  131. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  132. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -1,14 +1,28 @@
1
- import { ConflictException, Inject, Injectable, NotFoundException } from "@nestjs/common";
1
+ import {
2
+ ConflictException,
3
+ Inject,
4
+ Injectable,
5
+ NotFoundException,
6
+ } from "@nestjs/common";
2
7
  import { resolvePermissions } from "@/lib/auth/core/rbac.js";
3
- import { Prisma, PrismaClient } from "../generated/prisma/client.js";
4
- import { PermissionCache } from "./permission-cache.js";
5
- import { toId, toIdOrNull } from "./id.helper.js";
6
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
7
-
8
- const USER_ROLES_INCLUDE = { roles: { select: { role: { select: { slug: true } } } } } as const;
8
+ import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
9
+ import { PermissionCache } from "../permission-cache.js";
10
+ import { toId, toIdOrNull } from "../id.helper.js";
11
+ import {
12
+ buildPageMeta,
13
+ normalizeLimit,
14
+ normalizePage,
15
+ type Paginated,
16
+ } from "../pagination.js";
17
+
18
+ const USER_ROLES_INCLUDE = {
19
+ roles: { select: { role: { select: { slug: true } } } },
20
+ } as const;
9
21
 
10
22
  /** A `User` row as `findUnique`/`findMany` return it with its roles joined in. */
11
- export type UserRow = Prisma.UserGetPayload<{ include: typeof USER_ROLES_INCLUDE }>;
23
+ export type UserRow = Prisma.UserGetPayload<{
24
+ include: typeof USER_ROLES_INCLUDE;
25
+ }>;
12
26
 
13
27
  /**
14
28
  * Shapes a raw row into the wire DTO — bigint id to string, `Date` to ISO, the roles relation
@@ -116,7 +130,9 @@ export interface RoleSummary {
116
130
  permissions: string[];
117
131
  }
118
132
 
119
- const ROLE_PERMISSIONS_INCLUDE = { permissions: { select: { permission: { select: { slug: true } } } } } as const;
133
+ const ROLE_PERMISSIONS_INCLUDE = {
134
+ permissions: { select: { permission: { select: { slug: true } } } },
135
+ } as const;
120
136
 
121
137
  const PERMISSION_SELECT = {
122
138
  id: true,
@@ -130,7 +146,14 @@ const PERMISSION_SELECT = {
130
146
  isActive: true,
131
147
  } as const;
132
148
 
133
- const ROLE_SELECT = { id: true, slug: true, name: true, displayName: true, isDefault: true, isActive: true } as const;
149
+ const ROLE_SELECT = {
150
+ id: true,
151
+ slug: true,
152
+ name: true,
153
+ displayName: true,
154
+ isDefault: true,
155
+ isActive: true,
156
+ } as const;
134
157
 
135
158
  @Injectable()
136
159
  export class RbacRepository {
@@ -146,7 +169,9 @@ export class RbacRepository {
146
169
  // The hot path: every authenticated request runs this once. Walks user → role_user → roles →
147
170
  // permission_role → permissions, and user → permission_user → permissions — constant cost in
148
171
  // the number of users, since every hop is an indexed read.
149
- async resolveAuthzContext(userId: string): Promise<{ roles: string[]; permissions: string[] }> {
172
+ async resolveAuthzContext(
173
+ userId: string,
174
+ ): Promise<{ roles: string[]; permissions: string[] }> {
150
175
  const user = await this.prisma.user.findUnique({
151
176
  where: { id: toId(userId), isDeleted: false },
152
177
  select: {
@@ -156,17 +181,25 @@ export class RbacRepository {
156
181
  role: {
157
182
  select: {
158
183
  slug: true,
159
- permissions: { where: { permission: { isActive: true, isDeleted: false } }, select: { permission: { select: { slug: true } } } },
184
+ permissions: {
185
+ where: { permission: { isActive: true, isDeleted: false } },
186
+ select: { permission: { select: { slug: true } } },
187
+ },
160
188
  },
161
189
  },
162
190
  },
163
191
  },
164
- permissions: { where: { permission: { isActive: true, isDeleted: false } }, select: { permission: { select: { slug: true } } } },
192
+ permissions: {
193
+ where: { permission: { isActive: true, isDeleted: false } },
194
+ select: { permission: { select: { slug: true } } },
195
+ },
165
196
  },
166
197
  });
167
198
  if (!user) return { roles: [], permissions: [] };
168
199
 
169
- const rolePermissions = user.roles.flatMap((roleUser) => roleUser.role.permissions.map((pr) => pr.permission.slug));
200
+ const rolePermissions = user.roles.flatMap((roleUser) =>
201
+ roleUser.role.permissions.map((pr) => pr.permission.slug),
202
+ );
170
203
  const directPermissions = user.permissions.map((pu) => pu.permission.slug);
171
204
 
172
205
  return {
@@ -185,7 +218,9 @@ export class RbacRepository {
185
218
  const limit = normalizeLimit(filter.limit);
186
219
  const where = {
187
220
  isDeleted: false,
188
- email: filter.search ? { contains: filter.search, mode: "insensitive" as const } : undefined,
221
+ email: filter.search
222
+ ? { contains: filter.search, mode: "insensitive" as const }
223
+ : undefined,
189
224
  };
190
225
 
191
226
  const [rows, total] = await this.prisma.$transaction([
@@ -233,7 +268,9 @@ export class RbacRepository {
233
268
  },
234
269
  actorUserId: string | null,
235
270
  ): Promise<UserSummary> {
236
- const existing = await this.prisma.user.findUnique({ where: { email: input.email } });
271
+ const existing = await this.prisma.user.findUnique({
272
+ where: { email: input.email },
273
+ });
237
274
  if (existing) throw new ConflictException("email already registered");
238
275
 
239
276
  const user = await this.prisma.user.create({
@@ -255,9 +292,15 @@ export class RbacRepository {
255
292
  });
256
293
 
257
294
  if (input.roles && input.roles.length > 0) {
258
- const roles = await this.prisma.role.findMany({ where: { slug: { in: input.roles }, isActive: true, isDeleted: false }, select: { id: true } });
295
+ const roles = await this.prisma.role.findMany({
296
+ where: { slug: { in: input.roles }, isActive: true, isDeleted: false },
297
+ select: { id: true },
298
+ });
259
299
  if (roles.length > 0) {
260
- await this.prisma.roleUser.createMany({ data: roles.map((role) => ({ userId: user.id, roleId: role.id })), skipDuplicates: true });
300
+ await this.prisma.roleUser.createMany({
301
+ data: roles.map((role) => ({ userId: user.id, roleId: role.id })),
302
+ skipDuplicates: true,
303
+ });
261
304
  await this.cache.invalidateSubject(user.id.toString());
262
305
  }
263
306
  } else {
@@ -285,15 +328,26 @@ export class RbacRepository {
285
328
  actorUserId: string | null,
286
329
  ): Promise<UserSummary> {
287
330
  const userIdBig = toId(userId);
288
- const existing = await this.prisma.user.findUnique({ where: { id: userIdBig, isDeleted: false }, select: { id: true } });
331
+ const existing = await this.prisma.user.findUnique({
332
+ where: { id: userIdBig, isDeleted: false },
333
+ select: { id: true },
334
+ });
289
335
  if (!existing) throw new NotFoundException(`user "${userId}" not found`);
290
336
  await this.prisma.user.update({
291
337
  where: { id: userIdBig },
292
338
  data: {
293
339
  ...input,
294
340
  // Date columns need Date values; `undefined` leaves them alone, like every other field here.
295
- dob: input.dob === undefined ? undefined : input.dob === null ? null : new Date(input.dob),
296
- joinedDate: input.joinedDate === undefined ? undefined : new Date(input.joinedDate),
341
+ dob:
342
+ input.dob === undefined
343
+ ? undefined
344
+ : input.dob === null
345
+ ? null
346
+ : new Date(input.dob),
347
+ joinedDate:
348
+ input.joinedDate === undefined
349
+ ? undefined
350
+ : new Date(input.joinedDate),
297
351
  updatedBy: toIdOrNull(actorUserId),
298
352
  },
299
353
  });
@@ -303,24 +357,45 @@ export class RbacRepository {
303
357
  // Soft-delete, matching every other table's `isDeleted`/`deletedAt`/`deletedBy`/`deletedReason`
304
358
  // pattern — the row survives for audit purposes, it just stops appearing in listings and can
305
359
  // no longer authenticate (mirrors how `block` already disables login without removing the row).
306
- async deleteUser(userId: string, actorUserId: string | null, reason?: string): Promise<void> {
360
+ async deleteUser(
361
+ userId: string,
362
+ actorUserId: string | null,
363
+ reason?: string,
364
+ ): Promise<void> {
307
365
  const userIdBig = toId(userId);
308
- const existing = await this.prisma.user.findUnique({ where: { id: userIdBig, isDeleted: false }, select: { id: true } });
366
+ const existing = await this.prisma.user.findUnique({
367
+ where: { id: userIdBig, isDeleted: false },
368
+ select: { id: true },
369
+ });
309
370
  if (!existing) throw new NotFoundException(`user "${userId}" not found`);
310
371
  await this.prisma.user.update({
311
372
  where: { id: userIdBig },
312
- data: { isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null },
373
+ data: {
374
+ isDeleted: true,
375
+ deletedAt: new Date(),
376
+ deletedBy: toIdOrNull(actorUserId),
377
+ deletedReason: reason ?? null,
378
+ },
313
379
  });
314
380
  await this.cache.invalidateSubject(userId);
315
381
  }
316
382
 
317
383
  async updateRole(
318
384
  roleId: string,
319
- input: { name?: string; displayName?: string; description?: string | null; isDefault?: boolean; isActive?: boolean },
385
+ input: {
386
+ name?: string;
387
+ displayName?: string;
388
+ description?: string | null;
389
+ isDefault?: boolean;
390
+ isActive?: boolean;
391
+ },
320
392
  actorUserId: string | null,
321
393
  ): Promise<RoleSummary> {
322
394
  const roleIdBig = toId(roleId);
323
- const existing = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true } });
395
+ const existing = await this.prisma.role.findUnique({
396
+ where: { id: roleIdBig, isDeleted: false },
397
+ select: { id: true },
398
+ });
324
399
  if (!existing) throw new NotFoundException("role not found");
325
400
  const role = await this.prisma.role.update({
326
401
  where: { id: roleIdBig },
@@ -329,19 +404,35 @@ export class RbacRepository {
329
404
  });
330
405
  // Renaming/deactivating changes what every holder resolves to.
331
406
  await this.cache.invalidatePolicy();
332
- return { ...role, id: role.id.toString(), permissions: role.permissions.map((p) => p.permission.slug).sort() };
407
+ return {
408
+ ...role,
409
+ id: role.id.toString(),
410
+ permissions: role.permissions.map((p) => p.permission.slug).sort(),
411
+ };
333
412
  }
334
413
 
335
414
  // Soft-delete. The `role_user`/`permission_role` rows pointing at it are left in place — a
336
415
  // deleted role simply stops being returned by `listRoles`/`resolveAuthzContext`, rather than
337
416
  // cascading a delete that would silently strip a user's other roles' foreign keys.
338
- async deleteRole(roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
417
+ async deleteRole(
418
+ roleId: string,
419
+ actorUserId: string | null,
420
+ reason?: string,
421
+ ): Promise<void> {
339
422
  const roleIdBig = toId(roleId);
340
- const existing = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true } });
423
+ const existing = await this.prisma.role.findUnique({
424
+ where: { id: roleIdBig, isDeleted: false },
425
+ select: { id: true },
426
+ });
341
427
  if (!existing) throw new NotFoundException("role not found");
342
428
  await this.prisma.role.update({
343
429
  where: { id: roleIdBig },
344
- data: { isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null },
430
+ data: {
431
+ isDeleted: true,
432
+ deletedAt: new Date(),
433
+ deletedBy: toIdOrNull(actorUserId),
434
+ deletedReason: reason ?? null,
435
+ },
345
436
  });
346
437
  await this.cache.invalidatePolicy();
347
438
  }
@@ -351,14 +442,22 @@ export class RbacRepository {
351
442
  const rows = await this.prisma.permission.findMany({
352
443
  where: { isDeleted: false, isActive: activeOnly ? true : undefined },
353
444
  select: PERMISSION_SELECT,
354
- orderBy: [{ groupOrder: "asc" }, { group: "asc" }, { order: "asc" }, { slug: "asc" }],
445
+ orderBy: [
446
+ { groupOrder: "asc" },
447
+ { group: "asc" },
448
+ { order: "asc" },
449
+ { slug: "asc" },
450
+ ],
355
451
  });
356
452
  return rows.map((row) => ({ ...row, id: row.id.toString() }));
357
453
  }
358
454
 
359
455
  // Upserted on `slug`, the stable identifier grants/revocations use, so renaming the display
360
456
  // name never breaks a grant.
361
- async upsertPermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
457
+ async upsertPermission(
458
+ input: PermissionInput,
459
+ actorUserId: string | null,
460
+ ): Promise<PermissionSummary> {
362
461
  const shared = {
363
462
  name: input.name,
364
463
  displayName: input.displayName,
@@ -394,11 +493,22 @@ export class RbacRepository {
394
493
  select: { ...ROLE_SELECT, ...ROLE_PERMISSIONS_INCLUDE },
395
494
  orderBy: [{ order: "asc" }, { slug: "asc" }],
396
495
  });
397
- return rows.map((row) => ({ ...row, id: row.id.toString(), permissions: row.permissions.map((p) => p.permission.slug).sort() }));
496
+ return rows.map((row) => ({
497
+ ...row,
498
+ id: row.id.toString(),
499
+ permissions: row.permissions.map((p) => p.permission.slug).sort(),
500
+ }));
398
501
  }
399
502
 
400
503
  async createRole(
401
- input: { slug: string; name?: string; displayName?: string; description?: string | null; isDefault?: boolean; isActive?: boolean },
504
+ input: {
505
+ slug: string;
506
+ name?: string;
507
+ displayName?: string;
508
+ description?: string | null;
509
+ isDefault?: boolean;
510
+ isActive?: boolean;
511
+ },
402
512
  actorUserId: string | null,
403
513
  ): Promise<RoleSummary> {
404
514
  const role = await this.prisma.role.create({
@@ -417,38 +527,64 @@ export class RbacRepository {
417
527
  return { ...role, id: role.id.toString(), permissions: [] };
418
528
  }
419
529
 
420
- async attachPermissionToRole(roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
530
+ async attachPermissionToRole(
531
+ roleId: string,
532
+ permissionSlug: string,
533
+ actorUserId: string | null,
534
+ ): Promise<void> {
421
535
  const roleIdBig = toId(roleId);
422
- const role = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true } });
536
+ const role = await this.prisma.role.findUnique({
537
+ where: { id: roleIdBig, isDeleted: false },
538
+ select: { id: true },
539
+ });
423
540
  if (!role) throw new NotFoundException("role not found");
424
541
 
425
542
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
426
543
  await this.prisma.permissionRole.upsert({
427
- where: { permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig } },
544
+ where: {
545
+ permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig },
546
+ },
428
547
  create: { permissionId: permission.id, roleId: roleIdBig },
429
548
  update: {},
430
549
  });
431
550
  await this.cache.invalidatePolicy(); // everyone holding this role is affected
432
551
  }
433
552
 
434
- async detachPermissionFromRole(roleId: string, permissionSlug: string): Promise<void> {
553
+ async detachPermissionFromRole(
554
+ roleId: string,
555
+ permissionSlug: string,
556
+ ): Promise<void> {
435
557
  const roleIdBig = toId(roleId);
436
- const role = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true } });
558
+ const role = await this.prisma.role.findUnique({
559
+ where: { id: roleIdBig, isDeleted: false },
560
+ select: { id: true },
561
+ });
437
562
  if (!role) throw new NotFoundException("role not found");
438
563
 
439
- const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug }, select: { id: true } });
564
+ const permission = await this.prisma.permission.findUnique({
565
+ where: { slug: permissionSlug },
566
+ select: { id: true },
567
+ });
440
568
  if (!permission) throw new NotFoundException("permission not found");
441
569
 
442
- await this.prisma.permissionRole.deleteMany({ where: { roleId: roleIdBig, permissionId: permission.id } });
570
+ await this.prisma.permissionRole.deleteMany({
571
+ where: { roleId: roleIdBig, permissionId: permission.id },
572
+ });
443
573
  await this.cache.invalidatePolicy(); // everyone holding this role is affected
444
574
  }
445
575
 
446
576
  async assignRoleToUser(userId: string, roleSlug: string): Promise<void> {
447
577
  const userIdBig = toId(userId);
448
- const role = await this.prisma.role.findUnique({ where: { slug: roleSlug, isDeleted: false }, select: { id: true } });
578
+ const role = await this.prisma.role.findUnique({
579
+ where: { slug: roleSlug, isDeleted: false },
580
+ select: { id: true },
581
+ });
449
582
  if (!role) throw new NotFoundException(`role "${roleSlug}" not found`);
450
583
 
451
- await this.prisma.user.findUniqueOrThrow({ where: { id: userIdBig }, select: { id: true } });
584
+ await this.prisma.user.findUniqueOrThrow({
585
+ where: { id: userIdBig },
586
+ select: { id: true },
587
+ });
452
588
  await this.prisma.roleUser.upsert({
453
589
  where: { userId_roleId: { userId: userIdBig, roleId: role.id } },
454
590
  create: { userId: userIdBig, roleId: role.id },
@@ -458,9 +594,14 @@ export class RbacRepository {
458
594
  }
459
595
 
460
596
  async revokeRoleFromUser(userId: string, roleSlug: string): Promise<void> {
461
- const role = await this.prisma.role.findUnique({ where: { slug: roleSlug }, select: { id: true } });
597
+ const role = await this.prisma.role.findUnique({
598
+ where: { slug: roleSlug },
599
+ select: { id: true },
600
+ });
462
601
  if (!role) return;
463
- await this.prisma.roleUser.deleteMany({ where: { userId: toId(userId), roleId: role.id } });
602
+ await this.prisma.roleUser.deleteMany({
603
+ where: { userId: toId(userId), roleId: role.id },
604
+ });
464
605
  await this.cache.invalidateSubject(userId);
465
606
  }
466
607
 
@@ -468,9 +609,15 @@ export class RbacRepository {
468
609
  // Takes the bigint directly: every caller already has the user row in hand (just created or
469
610
  // just fetched), so there's no string to convert from.
470
611
  async assignDefaultRoles(userId: bigint): Promise<void> {
471
- const defaults = await this.prisma.role.findMany({ where: { isDefault: true, isActive: true, isDeleted: false }, select: { id: true } });
612
+ const defaults = await this.prisma.role.findMany({
613
+ where: { isDefault: true, isActive: true, isDeleted: false },
614
+ select: { id: true },
615
+ });
472
616
  if (!defaults.length) return;
473
- await this.prisma.roleUser.createMany({ data: defaults.map((role) => ({ userId, roleId: role.id })), skipDuplicates: true });
617
+ await this.prisma.roleUser.createMany({
618
+ data: defaults.map((role) => ({ userId, roleId: role.id })),
619
+ skipDuplicates: true,
620
+ });
474
621
  await this.cache.invalidateSubject(userId.toString());
475
622
  }
476
623
 
@@ -478,21 +625,35 @@ export class RbacRepository {
478
625
  return (await this.prisma.roleUser.count({ where: { userId } })) > 0;
479
626
  }
480
627
 
481
- async grantPermissionToUser(userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
628
+ async grantPermissionToUser(
629
+ userId: string,
630
+ permissionSlug: string,
631
+ actorUserId: string | null,
632
+ ): Promise<void> {
482
633
  const userIdBig = toId(userId);
483
634
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
484
635
  await this.prisma.permissionUser.upsert({
485
- where: { userId_permissionId: { userId: userIdBig, permissionId: permission.id } },
636
+ where: {
637
+ userId_permissionId: { userId: userIdBig, permissionId: permission.id },
638
+ },
486
639
  create: { userId: userIdBig, permissionId: permission.id },
487
640
  update: {},
488
641
  });
489
642
  await this.cache.invalidateSubject(userId);
490
643
  }
491
644
 
492
- async revokePermissionFromUser(userId: string, permissionSlug: string): Promise<void> {
493
- const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug }, select: { id: true } });
645
+ async revokePermissionFromUser(
646
+ userId: string,
647
+ permissionSlug: string,
648
+ ): Promise<void> {
649
+ const permission = await this.prisma.permission.findUnique({
650
+ where: { slug: permissionSlug },
651
+ select: { id: true },
652
+ });
494
653
  if (!permission) return;
495
- await this.prisma.permissionUser.deleteMany({ where: { userId: toId(userId), permissionId: permission.id } });
654
+ await this.prisma.permissionUser.deleteMany({
655
+ where: { userId: toId(userId), permissionId: permission.id },
656
+ });
496
657
  await this.cache.invalidateSubject(userId);
497
658
  }
498
659
 
@@ -501,12 +662,25 @@ export class RbacRepository {
501
662
  }
502
663
 
503
664
  // Creates the row for a slug granted before anyone defined it; leaves an existing definition alone.
504
- private async ensurePermission(slug: string, actorUserId: string | null): Promise<{ id: bigint }> {
505
- const existing = await this.prisma.permission.findUnique({ where: { slug }, select: { id: true } });
665
+ private async ensurePermission(
666
+ slug: string,
667
+ actorUserId: string | null,
668
+ ): Promise<{ id: bigint }> {
669
+ const existing = await this.prisma.permission.findUnique({
670
+ where: { slug },
671
+ select: { id: true },
672
+ });
506
673
  return (
507
674
  existing ??
508
675
  (await this.prisma.permission.create({
509
- data: { slug, name: slug, displayName: slug, group: "Custom", createdBy: toIdOrNull(actorUserId), updatedBy: toIdOrNull(actorUserId) },
676
+ data: {
677
+ slug,
678
+ name: slug,
679
+ displayName: slug,
680
+ group: "Custom",
681
+ createdBy: toIdOrNull(actorUserId),
682
+ updatedBy: toIdOrNull(actorUserId),
683
+ },
510
684
  select: { id: true },
511
685
  }))
512
686
  );
@@ -5,44 +5,82 @@
5
5
  // existing admin keeps the password they already have.
6
6
  import { PrismaPg } from "@prisma/adapter-pg";
7
7
  import { hashPassword } from "@/lib/auth/core/crypto.js";
8
- import { PrismaClient } from "../generated/prisma/client.js";
9
- import { DEFAULT_ROLES, PERMISSION_SLUGS, provisionDefaultRoles, SEED_ADMIN_ROLES, SEED_SUPERADMIN_ROLES } from "./rbac.defaults.js";
8
+ import { PrismaClient } from "./../generated/prisma/client.js";
9
+ import {
10
+ DEFAULT_ROLES,
11
+ PERMISSION_SLUGS,
12
+ provisionDefaultRoles,
13
+ SEED_ADMIN_ROLES,
14
+ SEED_SUPERADMIN_ROLES,
15
+ } from "./rbac.defaults.js";
10
16
 
11
17
  function requireEnv(name: string): string {
12
18
  const value = process.env[name];
13
- if (!value) throw new Error(`${name} is not set — the seeder cannot reach the database without it`);
19
+ if (!value)
20
+ throw new Error(
21
+ `${name} is not set — the seeder cannot reach the database without it`,
22
+ );
14
23
  return value;
15
24
  }
16
25
 
17
26
  async function seedRbacDefaults(prisma: PrismaClient): Promise<void> {
18
27
  await provisionDefaultRoles(prisma);
19
28
  console.log(`permissions: ${PERMISSION_SLUGS.length} slug(s) in the catalog`);
20
- for (const role of DEFAULT_ROLES) console.log(`role "${role.slug}": ${role.permissions.length} permission(s)${role.isDefault ? " (signup default)" : ""}`);
29
+ for (const role of DEFAULT_ROLES)
30
+ console.log(
31
+ `role "${role.slug}": ${role.permissions.length} permission(s)${role.isDefault ? " (signup default)" : ""}`,
32
+ );
21
33
 
22
34
  // Slugs in the database but not this build's catalog aren't an error — a deployment can
23
35
  // define its own — but a typo would otherwise silently grant nothing, so surface them.
24
- const rows = await prisma.permission.findMany({ select: { slug: true, isActive: true } });
25
- const unknown = rows.filter((row) => !(PERMISSION_SLUGS as string[]).includes(row.slug));
26
- if (unknown.length) console.log(`permissions: ${unknown.length} slug(s) outside this build's catalog (no route names them): ${unknown.map((r) => r.slug).join(", ")}`);
36
+ const rows = await prisma.permission.findMany({
37
+ select: { slug: true, isActive: true },
38
+ });
39
+ const unknown = rows.filter(
40
+ (row) => !(PERMISSION_SLUGS as string[]).includes(row.slug),
41
+ );
42
+ if (unknown.length)
43
+ console.log(
44
+ `permissions: ${unknown.length} slug(s) outside this build's catalog (no route names them): ${unknown.map((r) => r.slug).join(", ")}`,
45
+ );
27
46
  const inactive = rows.filter((row) => !row.isActive);
28
- if (inactive.length) console.log(`permissions: ${inactive.length} deactivated, granting nothing: ${inactive.map((r) => r.slug).join(", ")}`);
47
+ if (inactive.length)
48
+ console.log(
49
+ `permissions: ${inactive.length} deactivated, granting nothing: ${inactive.map((r) => r.slug).join(", ")}`,
50
+ );
29
51
  }
30
52
 
31
53
  async function seedAdminUser(prisma: PrismaClient): Promise<void> {
32
54
  const email = process.env["SEED_ADMIN_EMAIL"];
33
55
  const password = process.env["SEED_ADMIN_PASSWORD"];
34
56
  if (!email || !password) {
35
- console.log("admin: skipped — set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD to create one (there is no default password)");
57
+ console.log(
58
+ "admin: skipped — set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD to create one (there is no default password)",
59
+ );
36
60
  return;
37
61
  }
38
62
 
39
63
  // Rewriting the password here would silently reset one the user may have already changed.
40
64
  const existing = await prisma.user.findUnique({ where: { email } });
41
- const user = existing ?? (await prisma.user.create({ data: { email, passwordHash: await hashPassword(password) } }));
42
- console.log(existing ? `admin: ${email} already exists — password left unchanged` : `admin: created ${email}`);
65
+ const user =
66
+ existing ??
67
+ (await prisma.user.create({
68
+ data: { email, passwordHash: await hashPassword(password) },
69
+ }));
70
+ console.log(
71
+ existing
72
+ ? `admin: ${email} already exists — password left unchanged`
73
+ : `admin: created ${email}`,
74
+ );
43
75
 
44
- const roles = await prisma.role.findMany({ where: { slug: { in: SEED_ADMIN_ROLES } }, select: { id: true, slug: true } });
45
- await prisma.roleUser.createMany({ data: roles.map((role) => ({ userId: user.id, roleId: role.id })), skipDuplicates: true });
76
+ const roles = await prisma.role.findMany({
77
+ where: { slug: { in: SEED_ADMIN_ROLES } },
78
+ select: { id: true, slug: true },
79
+ });
80
+ await prisma.roleUser.createMany({
81
+ data: roles.map((role) => ({ userId: user.id, roleId: role.id })),
82
+ skipDuplicates: true,
83
+ });
46
84
  console.log(`admin: holds roles ${roles.map((r) => r.slug).join(", ")}`);
47
85
  }
48
86
 
@@ -50,19 +88,37 @@ async function seedSuperAdminUser(prisma: PrismaClient): Promise<void> {
50
88
  const email = process.env["SEED_SUPERADMIN_EMAIL"];
51
89
  const password = process.env["SEED_SUPERADMIN_PASSWORD"];
52
90
  if (!email || !password) {
53
- console.log("super admin: skipped — set SEED_SUPERADMIN_EMAIL and SEED_SUPERADMIN_PASSWORD to create one (there is no default password)");
91
+ console.log(
92
+ "super admin: skipped — set SEED_SUPERADMIN_EMAIL and SEED_SUPERADMIN_PASSWORD to create one (there is no default password)",
93
+ );
54
94
  return;
55
95
  }
56
96
  const username = process.env["SEED_SUPERADMIN_USERNAME"] || undefined;
57
97
 
58
98
  // Rewriting the password here would silently reset one the user may have already changed.
59
99
  const existing = await prisma.user.findUnique({ where: { email } });
60
- const user = existing ?? (await prisma.user.create({ data: { email, username, passwordHash: await hashPassword(password) } }));
61
- console.log(existing ? `super admin: ${email} already exists — password left unchanged` : `super admin: created ${email}${username ? ` (username ${username})` : ""}`);
100
+ const user =
101
+ existing ??
102
+ (await prisma.user.create({
103
+ data: { email, username, passwordHash: await hashPassword(password) },
104
+ }));
105
+ console.log(
106
+ existing
107
+ ? `super admin: ${email} already exists — password left unchanged`
108
+ : `super admin: created ${email}${username ? ` (username ${username})` : ""}`,
109
+ );
62
110
 
63
- const roles = await prisma.role.findMany({ where: { slug: { in: SEED_SUPERADMIN_ROLES } }, select: { id: true, slug: true } });
64
- await prisma.roleUser.createMany({ data: roles.map((role) => ({ userId: user.id, roleId: role.id })), skipDuplicates: true });
65
- console.log(`super admin: holds roles ${roles.map((r) => r.slug).join(", ")}`);
111
+ const roles = await prisma.role.findMany({
112
+ where: { slug: { in: SEED_SUPERADMIN_ROLES } },
113
+ select: { id: true, slug: true },
114
+ });
115
+ await prisma.roleUser.createMany({
116
+ data: roles.map((role) => ({ userId: user.id, roleId: role.id })),
117
+ skipDuplicates: true,
118
+ });
119
+ console.log(
120
+ `super admin: holds roles ${roles.map((r) => r.slug).join(", ")}`,
121
+ );
66
122
  }
67
123
 
68
124
  async function main(): Promise<void> {
@@ -72,7 +128,9 @@ async function main(): Promise<void> {
72
128
  // No .env file — the environment is expected to carry the variables itself.
73
129
  }
74
130
 
75
- const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: requireEnv("DATABASE_URL") }) });
131
+ const prisma = new PrismaClient({
132
+ adapter: new PrismaPg({ connectionString: requireEnv("DATABASE_URL") }),
133
+ });
76
134
  try {
77
135
  await seedRbacDefaults(prisma);
78
136
  await seedAdminUser(prisma);