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