@simple-auth-kit/cli 1.4.3 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/lib/copy.ts +1 -1
  2. package/package.json +1 -1
  3. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  4. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  5. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  6. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  7. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  8. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  9. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  10. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  11. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  12. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  13. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  14. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  15. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  16. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  17. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  18. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  19. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  20. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  23. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  26. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  27. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  28. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  29. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  30. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  31. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  32. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  33. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  34. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  35. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  36. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
  37. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  38. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
  39. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  40. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  41. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  42. package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
  43. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  44. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
  45. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  46. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  47. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
  48. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  49. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  50. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  53. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
  54. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  56. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  57. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  58. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  59. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  62. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  63. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  64. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  65. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  66. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  71. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  79. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  80. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  81. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  82. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  83. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  84. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  85. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  86. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  87. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  88. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  89. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  90. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  91. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
  92. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  93. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  94. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  95. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
  96. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  97. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  98. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  99. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  100. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  101. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
  102. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
  115. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  116. package/registry.json +4 -4
  117. package/simple-auth-kit.ts +42 -11
  118. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  119. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  120. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  121. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  122. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  123. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  124. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  125. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  126. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  127. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  128. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  129. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  130. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  131. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  132. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  134. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -1,15 +1,25 @@
1
1
  import { Inject, Injectable, NotFoundException } from "@nestjs/common";
2
2
  import { resolvePermissions } from "@/lib/auth/core/rbac.js";
3
- import { Prisma, PrismaClient } from "../generated/prisma/client.js";
4
- import type { AuthzContext } from "./authz.guard.js";
5
- import { PermissionCache } from "./permission-cache.js";
6
- import { toId, toIdOrNull } from "./id.helper.js";
7
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
8
-
9
- const MEMBER_INCLUDE = { user: true, roles: { select: { role: { select: { slug: true } } } } } as const;
3
+ import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
4
+ import type { AuthzContext } from "../guards/authz.guard.js";
5
+ import { PermissionCache } from "../permission-cache.js";
6
+ import { toId, toIdOrNull } from "../id.helper.js";
7
+ import {
8
+ buildPageMeta,
9
+ normalizeLimit,
10
+ normalizePage,
11
+ type Paginated,
12
+ } from "../pagination.js";
13
+
14
+ const MEMBER_INCLUDE = {
15
+ user: true,
16
+ roles: { select: { role: { select: { slug: true } } } },
17
+ } as const;
10
18
 
11
19
  /** A `WorkspaceMember` row as `findUnique`/`findMany` return it, joined with its user and roles. */
12
- export type MemberRow = Prisma.WorkspaceMemberGetPayload<{ include: typeof MEMBER_INCLUDE }>;
20
+ export type MemberRow = Prisma.WorkspaceMemberGetPayload<{
21
+ include: typeof MEMBER_INCLUDE;
22
+ }>;
13
23
 
14
24
  /** Shapes a raw row into the wire DTO. Exported so `listMembers`'s collection can be shaped by the caller. */
15
25
  /** Every column `User`/`WorkspaceMember` has, except the two secrets (`passwordHash`, `twoFactorSecret`) — see the note on `MemberSummary`. */
@@ -107,7 +117,9 @@ export interface RoleSummary {
107
117
  permissions: string[];
108
118
  }
109
119
 
110
- const ROLE_PERMISSIONS_INCLUDE = { permissions: { select: { permission: { select: { slug: true } } } } } as const;
120
+ const ROLE_PERMISSIONS_INCLUDE = {
121
+ permissions: { select: { permission: { select: { slug: true } } } },
122
+ } as const;
111
123
 
112
124
  const PERMISSION_SELECT = {
113
125
  id: true,
@@ -121,11 +133,19 @@ const PERMISSION_SELECT = {
121
133
  isActive: true,
122
134
  } as const;
123
135
 
124
- const ROLE_SELECT = { id: true, slug: true, name: true, displayName: true, isDefault: true, isActive: true } as const;
136
+ const ROLE_SELECT = {
137
+ id: true,
138
+ slug: true,
139
+ name: true,
140
+ displayName: true,
141
+ isDefault: true,
142
+ isActive: true,
143
+ } as const;
125
144
 
126
145
  // The cache subject in this variant: authorization belongs to a membership, so the key is the
127
146
  // `[userId, workspaceId]` pair — keeps invalidation in one workspace from flushing the others.
128
- export const memberCacheKey = (userId: string, workspaceId: string) => `${userId}:${workspaceId}`;
147
+ export const memberCacheKey = (userId: string, workspaceId: string) =>
148
+ `${userId}:${workspaceId}`;
129
149
 
130
150
  @Injectable()
131
151
  export class RbacRepository {
@@ -147,8 +167,15 @@ export class RbacRepository {
147
167
  // Takes bigint directly: its one caller (setMemberRoles) already has the membership's id in
148
168
  // hand from its own Prisma lookup, so there's nothing to parse here.
149
169
  private async invalidateMemberById(memberId: bigint): Promise<void> {
150
- const member = await this.prisma.workspaceMember.findUnique({ where: { id: memberId }, select: { userId: true, workspaceId: true } });
151
- if (member) await this.invalidateMember(member.userId.toString(), member.workspaceId.toString());
170
+ const member = await this.prisma.workspaceMember.findUnique({
171
+ where: { id: memberId },
172
+ select: { userId: true, workspaceId: true },
173
+ });
174
+ if (member)
175
+ await this.invalidateMember(
176
+ member.userId.toString(),
177
+ member.workspaceId.toString(),
178
+ );
152
179
  }
153
180
 
154
181
  // The hot path: every workspace-scoped request runs this once. Anchored on the
@@ -156,7 +183,10 @@ export class RbacRepository {
156
183
  // member → role_member → roles → permission_role → permissions, and member →
157
184
  // permission_member → permissions. Workspace-scoped by construction: the same login in
158
185
  // another workspace resolves a different membership.
159
- async resolveAuthzContext(userId: string, workspaceId: string): Promise<AuthzContext | null> {
186
+ async resolveAuthzContext(
187
+ userId: string,
188
+ workspaceId: string,
189
+ ): Promise<AuthzContext | null> {
160
190
  // workspaceId arrives straight from the untrusted `X-Workspace-Id` header — a value that
161
191
  // isn't a valid bigint is exactly as "not a member" as one that parses but names no
162
192
  // membership, so it must not throw and 500 the request.
@@ -170,7 +200,10 @@ export class RbacRepository {
170
200
  }
171
201
 
172
202
  const member = await this.prisma.workspaceMember.findUnique({
173
- where: { userId_workspaceId: { userId: userIdBig, workspaceId: workspaceIdBig }, user: { isDeleted: false } },
203
+ where: {
204
+ userId_workspaceId: { userId: userIdBig, workspaceId: workspaceIdBig },
205
+ user: { isDeleted: false },
206
+ },
174
207
  select: {
175
208
  id: true,
176
209
  roles: {
@@ -179,18 +212,28 @@ export class RbacRepository {
179
212
  role: {
180
213
  select: {
181
214
  slug: true,
182
- permissions: { where: { permission: { isActive: true, isDeleted: false } }, select: { permission: { select: { slug: true } } } },
215
+ permissions: {
216
+ where: { permission: { isActive: true, isDeleted: false } },
217
+ select: { permission: { select: { slug: true } } },
218
+ },
183
219
  },
184
220
  },
185
221
  },
186
222
  },
187
- permissions: { where: { permission: { isActive: true, isDeleted: false } }, select: { permission: { select: { slug: true } } } },
223
+ permissions: {
224
+ where: { permission: { isActive: true, isDeleted: false } },
225
+ select: { permission: { select: { slug: true } } },
226
+ },
188
227
  },
189
228
  });
190
229
  if (!member) return null;
191
230
 
192
- const rolePermissions = member.roles.flatMap((roleMember) => roleMember.role.permissions.map((pr) => pr.permission.slug));
193
- const directPermissions = member.permissions.map((pm) => pm.permission.slug);
231
+ const rolePermissions = member.roles.flatMap((roleMember) =>
232
+ roleMember.role.permissions.map((pr) => pr.permission.slug),
233
+ );
234
+ const directPermissions = member.permissions.map(
235
+ (pm) => pm.permission.slug,
236
+ );
194
237
 
195
238
  return {
196
239
  workspaceId,
@@ -203,12 +246,21 @@ export class RbacRepository {
203
246
  // Never crosses a workspace boundary: a caller can only name a target inside their own scope.
204
247
  // Returns bigint directly: every internal caller feeds the id straight into another Prisma
205
248
  // call, and the one external caller (auth.service.ts) discards the result.
206
- async requireMember(workspaceId: string, userId: string): Promise<{ id: bigint }> {
249
+ async requireMember(
250
+ workspaceId: string,
251
+ userId: string,
252
+ ): Promise<{ id: bigint }> {
207
253
  const member = await this.prisma.workspaceMember.findUnique({
208
- where: { userId_workspaceId: { userId: toId(userId), workspaceId: toId(workspaceId) } },
254
+ where: {
255
+ userId_workspaceId: {
256
+ userId: toId(userId),
257
+ workspaceId: toId(workspaceId),
258
+ },
259
+ },
209
260
  select: { id: true },
210
261
  });
211
- if (!member) throw new NotFoundException("user is not a member of this workspace");
262
+ if (!member)
263
+ throw new NotFoundException("user is not a member of this workspace");
212
264
  return member;
213
265
  }
214
266
 
@@ -216,12 +268,20 @@ export class RbacRepository {
216
268
  * Newest-first, page-paginated; `search` matches an email substring. Returns raw rows —
217
269
  * shaping the collection is the caller's job, see `toMemberSummary`.
218
270
  */
219
- async listMembers(workspaceId: string, filter: MemberListFilter = {}): Promise<Paginated<MemberRow>> {
271
+ async listMembers(
272
+ workspaceId: string,
273
+ filter: MemberListFilter = {},
274
+ ): Promise<Paginated<MemberRow>> {
220
275
  const page = normalizePage(filter.page);
221
276
  const limit = normalizeLimit(filter.limit);
222
277
  const where = {
223
278
  workspaceId: toId(workspaceId),
224
- user: { isDeleted: false, email: filter.search ? { contains: filter.search, mode: "insensitive" as const } : undefined },
279
+ user: {
280
+ isDeleted: false,
281
+ email: filter.search
282
+ ? { contains: filter.search, mode: "insensitive" as const }
283
+ : undefined,
284
+ },
225
285
  };
226
286
 
227
287
  const [rows, total] = await this.prisma.$transaction([
@@ -256,11 +316,21 @@ export class RbacRepository {
256
316
  async updateMember(
257
317
  workspaceId: string,
258
318
  userId: string,
259
- input: { firstName?: string | null; lastName?: string | null; displayName?: string | null; phone?: string | null; username?: string | null; photo?: string | null },
319
+ input: {
320
+ firstName?: string | null;
321
+ lastName?: string | null;
322
+ displayName?: string | null;
323
+ phone?: string | null;
324
+ username?: string | null;
325
+ photo?: string | null;
326
+ },
260
327
  actorUserId: string | null,
261
328
  ): Promise<MemberSummary> {
262
329
  await this.requireMember(workspaceId, userId);
263
- await this.prisma.user.update({ where: { id: toId(userId) }, data: { ...input, updatedBy: toIdOrNull(actorUserId) } });
330
+ await this.prisma.user.update({
331
+ where: { id: toId(userId) },
332
+ data: { ...input, updatedBy: toIdOrNull(actorUserId) },
333
+ });
264
334
  return this.getMember(workspaceId, userId);
265
335
  }
266
336
 
@@ -268,11 +338,21 @@ export class RbacRepository {
268
338
  // pattern, and `block`'s existing precedent of a workspace admin taking a global action on a
269
339
  // member's account — this disables the account across every workspace it belongs to, not just
270
340
  // this one, which is why it's gated the same way `block` is.
271
- async deleteMember(workspaceId: string, userId: string, actorUserId: string | null, reason?: string): Promise<void> {
341
+ async deleteMember(
342
+ workspaceId: string,
343
+ userId: string,
344
+ actorUserId: string | null,
345
+ reason?: string,
346
+ ): Promise<void> {
272
347
  await this.requireMember(workspaceId, userId);
273
348
  await this.prisma.user.update({
274
349
  where: { id: toId(userId) },
275
- data: { isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null },
350
+ data: {
351
+ isDeleted: true,
352
+ deletedAt: new Date(),
353
+ deletedBy: toIdOrNull(actorUserId),
354
+ deletedReason: reason ?? null,
355
+ },
276
356
  });
277
357
  await this.invalidateMember(userId, workspaceId);
278
358
  }
@@ -280,32 +360,59 @@ export class RbacRepository {
280
360
  async updateRole(
281
361
  workspaceId: string,
282
362
  roleId: string,
283
- input: { name?: string; displayName?: string; description?: string | null; isActive?: boolean },
363
+ input: {
364
+ name?: string;
365
+ displayName?: string;
366
+ description?: string | null;
367
+ isActive?: boolean;
368
+ },
284
369
  actorUserId: string | null,
285
370
  ): Promise<RoleSummary> {
286
371
  const workspaceIdBig = toId(workspaceId);
287
372
  const roleIdBig = toId(roleId);
288
- const existing = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
289
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
373
+ const existing = await this.prisma.role.findUnique({
374
+ where: { id: roleIdBig, isDeleted: false },
375
+ select: { id: true, workspaceId: true },
376
+ });
377
+ if (!existing || existing.workspaceId !== workspaceIdBig)
378
+ throw new NotFoundException("role not found in this workspace");
290
379
  const role = await this.prisma.role.update({
291
380
  where: { id: roleIdBig },
292
381
  data: { ...input, updatedBy: toIdOrNull(actorUserId) },
293
382
  select: { ...ROLE_SELECT, ...ROLE_PERMISSIONS_INCLUDE },
294
383
  });
295
384
  await this.cache.invalidatePolicy();
296
- return { ...role, id: role.id.toString(), permissions: role.permissions.map((p) => p.permission.slug).sort() };
385
+ return {
386
+ ...role,
387
+ id: role.id.toString(),
388
+ permissions: role.permissions.map((p) => p.permission.slug).sort(),
389
+ };
297
390
  }
298
391
 
299
392
  // Soft-delete. `role_member`/`permission_role` rows pointing at it are left in place — the
300
393
  // role simply stops being resolved by `resolveAuthzContext`/`listRoles`.
301
- async deleteRole(workspaceId: string, roleId: string, actorUserId: string | null, reason?: string): Promise<void> {
394
+ async deleteRole(
395
+ workspaceId: string,
396
+ roleId: string,
397
+ actorUserId: string | null,
398
+ reason?: string,
399
+ ): Promise<void> {
302
400
  const workspaceIdBig = toId(workspaceId);
303
401
  const roleIdBig = toId(roleId);
304
- const existing = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
305
- if (!existing || existing.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
402
+ const existing = await this.prisma.role.findUnique({
403
+ where: { id: roleIdBig, isDeleted: false },
404
+ select: { id: true, workspaceId: true },
405
+ });
406
+ if (!existing || existing.workspaceId !== workspaceIdBig)
407
+ throw new NotFoundException("role not found in this workspace");
306
408
  await this.prisma.role.update({
307
409
  where: { id: roleIdBig },
308
- data: { isDeleted: true, deletedAt: new Date(), deletedBy: toIdOrNull(actorUserId), deletedReason: reason ?? null },
410
+ data: {
411
+ isDeleted: true,
412
+ deletedAt: new Date(),
413
+ deletedBy: toIdOrNull(actorUserId),
414
+ deletedReason: reason ?? null,
415
+ },
309
416
  });
310
417
  await this.cache.invalidatePolicy();
311
418
  }
@@ -318,14 +425,22 @@ export class RbacRepository {
318
425
  const rows = await this.prisma.permission.findMany({
319
426
  where: { isDeleted: false, isActive: activeOnly ? true : undefined },
320
427
  select: PERMISSION_SELECT,
321
- orderBy: [{ groupOrder: "asc" }, { group: "asc" }, { order: "asc" }, { slug: "asc" }],
428
+ orderBy: [
429
+ { groupOrder: "asc" },
430
+ { group: "asc" },
431
+ { order: "asc" },
432
+ { slug: "asc" },
433
+ ],
322
434
  });
323
435
  return rows.map((row) => ({ ...row, id: row.id.toString() }));
324
436
  }
325
437
 
326
438
  // Upserted on `slug`, the stable identifier grants/revocations use, so renaming the display
327
439
  // name never breaks a grant. Effective in every workspace on the next request.
328
- async upsertPermission(input: PermissionInput, actorUserId: string | null): Promise<PermissionSummary> {
440
+ async upsertPermission(
441
+ input: PermissionInput,
442
+ actorUserId: string | null,
443
+ ): Promise<PermissionSummary> {
329
444
  const shared = {
330
445
  name: input.name,
331
446
  displayName: input.displayName,
@@ -355,18 +470,34 @@ export class RbacRepository {
355
470
  }
356
471
 
357
472
  /** `activeOnly` is what a role picker (Add member) passes — the admin management list itself wants everything, active or not. */
358
- async listRoles(workspaceId: string, activeOnly?: boolean): Promise<RoleSummary[]> {
473
+ async listRoles(
474
+ workspaceId: string,
475
+ activeOnly?: boolean,
476
+ ): Promise<RoleSummary[]> {
359
477
  const rows = await this.prisma.role.findMany({
360
- where: { workspaceId: toId(workspaceId), isDeleted: false, isActive: activeOnly ? true : undefined },
478
+ where: {
479
+ workspaceId: toId(workspaceId),
480
+ isDeleted: false,
481
+ isActive: activeOnly ? true : undefined,
482
+ },
361
483
  select: { ...ROLE_SELECT, ...ROLE_PERMISSIONS_INCLUDE },
362
484
  orderBy: [{ order: "asc" }, { slug: "asc" }],
363
485
  });
364
- return rows.map((row) => ({ ...row, id: row.id.toString(), permissions: row.permissions.map((p) => p.permission.slug).sort() }));
486
+ return rows.map((row) => ({
487
+ ...row,
488
+ id: row.id.toString(),
489
+ permissions: row.permissions.map((p) => p.permission.slug).sort(),
490
+ }));
365
491
  }
366
492
 
367
493
  async createRole(
368
494
  workspaceId: string,
369
- input: { slug: string; name?: string; displayName?: string; description?: string | null },
495
+ input: {
496
+ slug: string;
497
+ name?: string;
498
+ displayName?: string;
499
+ description?: string | null;
500
+ },
370
501
  actorUserId: string | null,
371
502
  ): Promise<RoleSummary> {
372
503
  const role = await this.prisma.role.create({
@@ -385,15 +516,26 @@ export class RbacRepository {
385
516
  }
386
517
 
387
518
  // Scoped by workspace, so an admin can't reach a role id belonging to a workspace they aren't in.
388
- async attachPermissionToRole(workspaceId: string, roleId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
519
+ async attachPermissionToRole(
520
+ workspaceId: string,
521
+ roleId: string,
522
+ permissionSlug: string,
523
+ actorUserId: string | null,
524
+ ): Promise<void> {
389
525
  const workspaceIdBig = toId(workspaceId);
390
526
  const roleIdBig = toId(roleId);
391
- const role = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
392
- if (!role || role.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
527
+ const role = await this.prisma.role.findUnique({
528
+ where: { id: roleIdBig, isDeleted: false },
529
+ select: { id: true, workspaceId: true },
530
+ });
531
+ if (!role || role.workspaceId !== workspaceIdBig)
532
+ throw new NotFoundException("role not found in this workspace");
393
533
 
394
534
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
395
535
  await this.prisma.permissionRole.upsert({
396
- where: { permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig } },
536
+ where: {
537
+ permissionId_roleId: { permissionId: permission.id, roleId: roleIdBig },
538
+ },
397
539
  create: { permissionId: permission.id, roleId: roleIdBig },
398
540
  update: {},
399
541
  });
@@ -401,20 +543,37 @@ export class RbacRepository {
401
543
  }
402
544
 
403
545
  // Scoped by workspace, so an admin can't reach a role id belonging to a workspace they aren't in.
404
- async detachPermissionFromRole(workspaceId: string, roleId: string, permissionSlug: string): Promise<void> {
546
+ async detachPermissionFromRole(
547
+ workspaceId: string,
548
+ roleId: string,
549
+ permissionSlug: string,
550
+ ): Promise<void> {
405
551
  const workspaceIdBig = toId(workspaceId);
406
552
  const roleIdBig = toId(roleId);
407
- const role = await this.prisma.role.findUnique({ where: { id: roleIdBig, isDeleted: false }, select: { id: true, workspaceId: true } });
408
- if (!role || role.workspaceId !== workspaceIdBig) throw new NotFoundException("role not found in this workspace");
553
+ const role = await this.prisma.role.findUnique({
554
+ where: { id: roleIdBig, isDeleted: false },
555
+ select: { id: true, workspaceId: true },
556
+ });
557
+ if (!role || role.workspaceId !== workspaceIdBig)
558
+ throw new NotFoundException("role not found in this workspace");
409
559
 
410
- const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug }, select: { id: true } });
560
+ const permission = await this.prisma.permission.findUnique({
561
+ where: { slug: permissionSlug },
562
+ select: { id: true },
563
+ });
411
564
  if (!permission) throw new NotFoundException("permission not found");
412
565
 
413
- await this.prisma.permissionRole.deleteMany({ where: { roleId: roleIdBig, permissionId: permission.id } });
566
+ await this.prisma.permissionRole.deleteMany({
567
+ where: { roleId: roleIdBig, permissionId: permission.id },
568
+ });
414
569
  await this.cache.invalidatePolicy();
415
570
  }
416
571
 
417
- async assignRoleToMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
572
+ async assignRoleToMember(
573
+ workspaceId: string,
574
+ userId: string,
575
+ roleSlug: string,
576
+ ): Promise<void> {
418
577
  const role = await this.requireRole(workspaceId, roleSlug);
419
578
  const member = await this.requireMember(workspaceId, userId);
420
579
  await this.prisma.roleMember.upsert({
@@ -425,32 +584,63 @@ export class RbacRepository {
425
584
  await this.invalidateMember(userId, workspaceId);
426
585
  }
427
586
 
428
- async revokeRoleFromMember(workspaceId: string, userId: string, roleSlug: string): Promise<void> {
587
+ async revokeRoleFromMember(
588
+ workspaceId: string,
589
+ userId: string,
590
+ roleSlug: string,
591
+ ): Promise<void> {
429
592
  const member = await this.requireMember(workspaceId, userId);
430
- const role = await this.prisma.role.findUnique({ where: { workspaceId_slug: { workspaceId: toId(workspaceId), slug: roleSlug } }, select: { id: true } });
593
+ const role = await this.prisma.role.findUnique({
594
+ where: {
595
+ workspaceId_slug: { workspaceId: toId(workspaceId), slug: roleSlug },
596
+ },
597
+ select: { id: true },
598
+ });
431
599
  if (!role) return;
432
- await this.prisma.roleMember.deleteMany({ where: { memberId: member.id, roleId: role.id } });
600
+ await this.prisma.roleMember.deleteMany({
601
+ where: { memberId: member.id, roleId: role.id },
602
+ });
433
603
  await this.invalidateMember(userId, workspaceId);
434
604
  }
435
605
 
436
606
  // Replaces the whole set, as opposed to assign/revoke one at a time.
437
- async setMemberRoles(workspaceId: string, memberId: string, roleSlugs: string[]): Promise<{ memberId: string; roles: string[] }> {
607
+ async setMemberRoles(
608
+ workspaceId: string,
609
+ memberId: string,
610
+ roleSlugs: string[],
611
+ ): Promise<{ memberId: string; roles: string[] }> {
438
612
  const workspaceIdBig = toId(workspaceId);
439
613
  const memberIdBig = toId(memberId);
440
- const member = await this.prisma.workspaceMember.findUnique({ where: { id: memberIdBig }, select: { id: true, workspaceId: true } });
441
- if (!member || member.workspaceId !== workspaceIdBig) throw new NotFoundException("member not found in this workspace");
614
+ const member = await this.prisma.workspaceMember.findUnique({
615
+ where: { id: memberIdBig },
616
+ select: { id: true, workspaceId: true },
617
+ });
618
+ if (!member || member.workspaceId !== workspaceIdBig)
619
+ throw new NotFoundException("member not found in this workspace");
442
620
 
443
621
  const roles = await this.prisma.role.findMany({
444
- where: { workspaceId: workspaceIdBig, slug: { in: roleSlugs }, isDeleted: false },
622
+ where: {
623
+ workspaceId: workspaceIdBig,
624
+ slug: { in: roleSlugs },
625
+ isDeleted: false,
626
+ },
445
627
  select: { id: true, slug: true },
446
628
  });
447
- const unknown = roleSlugs.filter((slug) => !roles.some((role) => role.slug === slug));
448
- if (unknown.length) throw new NotFoundException(`role(s) not defined in this workspace: ${unknown.join(", ")}`);
629
+ const unknown = roleSlugs.filter(
630
+ (slug) => !roles.some((role) => role.slug === slug),
631
+ );
632
+ if (unknown.length)
633
+ throw new NotFoundException(
634
+ `role(s) not defined in this workspace: ${unknown.join(", ")}`,
635
+ );
449
636
 
450
637
  // One transaction: a member must never be briefly role-less to a concurrent request.
451
638
  await this.prisma.$transaction([
452
639
  this.prisma.roleMember.deleteMany({ where: { memberId: memberIdBig } }),
453
- this.prisma.roleMember.createMany({ data: roles.map((role) => ({ memberId: memberIdBig, roleId: role.id })), skipDuplicates: true }),
640
+ this.prisma.roleMember.createMany({
641
+ data: roles.map((role) => ({ memberId: memberIdBig, roleId: role.id })),
642
+ skipDuplicates: true,
643
+ }),
454
644
  ]);
455
645
  await this.invalidateMemberById(memberIdBig);
456
646
  return { memberId, roles: roles.map((role) => role.slug).sort() };
@@ -459,45 +649,95 @@ export class RbacRepository {
459
649
  // The roles a brand-new membership starts with — the rows flagged `isDefault` in this workspace.
460
650
  // Unused by any caller today; kept bigint-native rather than round-tripped through string.
461
651
  async defaultRoleIdsFor(workspaceId: string): Promise<bigint[]> {
462
- const roles = await this.prisma.role.findMany({ where: { workspaceId: toId(workspaceId), isDefault: true, isActive: true, isDeleted: false }, select: { id: true } });
652
+ const roles = await this.prisma.role.findMany({
653
+ where: {
654
+ workspaceId: toId(workspaceId),
655
+ isDefault: true,
656
+ isActive: true,
657
+ isDeleted: false,
658
+ },
659
+ select: { id: true },
660
+ });
463
661
  return roles.map((role) => role.id);
464
662
  }
465
663
 
466
- async grantPermissionToMember(workspaceId: string, userId: string, permissionSlug: string, actorUserId: string | null): Promise<void> {
664
+ async grantPermissionToMember(
665
+ workspaceId: string,
666
+ userId: string,
667
+ permissionSlug: string,
668
+ actorUserId: string | null,
669
+ ): Promise<void> {
467
670
  const member = await this.requireMember(workspaceId, userId);
468
671
  const permission = await this.ensurePermission(permissionSlug, actorUserId);
469
672
  await this.prisma.permissionMember.upsert({
470
- where: { memberId_permissionId: { memberId: member.id, permissionId: permission.id } },
673
+ where: {
674
+ memberId_permissionId: {
675
+ memberId: member.id,
676
+ permissionId: permission.id,
677
+ },
678
+ },
471
679
  create: { memberId: member.id, permissionId: permission.id },
472
680
  update: {},
473
681
  });
474
682
  await this.invalidateMember(userId, workspaceId);
475
683
  }
476
684
 
477
- async revokePermissionFromMember(workspaceId: string, userId: string, permissionSlug: string): Promise<void> {
685
+ async revokePermissionFromMember(
686
+ workspaceId: string,
687
+ userId: string,
688
+ permissionSlug: string,
689
+ ): Promise<void> {
478
690
  const member = await this.requireMember(workspaceId, userId);
479
- const permission = await this.prisma.permission.findUnique({ where: { slug: permissionSlug }, select: { id: true } });
691
+ const permission = await this.prisma.permission.findUnique({
692
+ where: { slug: permissionSlug },
693
+ select: { id: true },
694
+ });
480
695
  if (!permission) return;
481
- await this.prisma.permissionMember.deleteMany({ where: { memberId: member.id, permissionId: permission.id } });
696
+ await this.prisma.permissionMember.deleteMany({
697
+ where: { memberId: member.id, permissionId: permission.id },
698
+ });
482
699
  await this.invalidateMember(userId, workspaceId);
483
700
  }
484
701
 
485
702
  // Returns bigint directly: its one caller (assignRoleToMember) feeds the id straight into
486
703
  // another Prisma call.
487
- private async requireRole(workspaceId: string, slug: string): Promise<{ id: bigint }> {
488
- const role = await this.prisma.role.findUnique({ where: { workspaceId_slug: { workspaceId: toId(workspaceId), slug }, isDeleted: false }, select: { id: true } });
489
- if (!role) throw new NotFoundException(`role "${slug}" not found in this workspace`);
704
+ private async requireRole(
705
+ workspaceId: string,
706
+ slug: string,
707
+ ): Promise<{ id: bigint }> {
708
+ const role = await this.prisma.role.findUnique({
709
+ where: {
710
+ workspaceId_slug: { workspaceId: toId(workspaceId), slug },
711
+ isDeleted: false,
712
+ },
713
+ select: { id: true },
714
+ });
715
+ if (!role)
716
+ throw new NotFoundException(`role "${slug}" not found in this workspace`);
490
717
  return role;
491
718
  }
492
719
 
493
720
  // Creates the row for a slug granted before anyone defined it; leaves an existing definition alone.
494
721
  // Returns bigint directly: both callers feed the id straight into another Prisma call.
495
- private async ensurePermission(slug: string, actorUserId: string | null): Promise<{ id: bigint }> {
496
- const existing = await this.prisma.permission.findUnique({ where: { slug }, select: { id: true } });
722
+ private async ensurePermission(
723
+ slug: string,
724
+ actorUserId: string | null,
725
+ ): Promise<{ id: bigint }> {
726
+ const existing = await this.prisma.permission.findUnique({
727
+ where: { slug },
728
+ select: { id: true },
729
+ });
497
730
  return (
498
731
  existing ??
499
732
  (await this.prisma.permission.create({
500
- data: { slug, name: slug, displayName: slug, group: "Custom", createdBy: toIdOrNull(actorUserId), updatedBy: toIdOrNull(actorUserId) },
733
+ data: {
734
+ slug,
735
+ name: slug,
736
+ displayName: slug,
737
+ group: "Custom",
738
+ createdBy: toIdOrNull(actorUserId),
739
+ updatedBy: toIdOrNull(actorUserId),
740
+ },
501
741
  select: { id: true },
502
742
  }))
503
743
  );