@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.
- package/lib/copy.ts +1 -1
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +42 -11
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ConflictException,
|
|
3
|
+
Inject,
|
|
4
|
+
Injectable,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
} from "@nestjs/common";
|
|
7
|
+
import {
|
|
8
|
+
and,
|
|
9
|
+
asc,
|
|
10
|
+
count,
|
|
11
|
+
desc,
|
|
12
|
+
eq,
|
|
13
|
+
ilike,
|
|
14
|
+
inArray,
|
|
15
|
+
type SQL,
|
|
16
|
+
} from "drizzle-orm";
|
|
3
17
|
import { resolvePermissions } from "@/lib/auth/core/rbac.js";
|
|
4
|
-
import { DRIZZLE_DB, type Database } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
19
|
+
import {
|
|
20
|
+
buildPageMeta,
|
|
21
|
+
normalizeLimit,
|
|
22
|
+
normalizePage,
|
|
23
|
+
type Paginated,
|
|
24
|
+
} from "../pagination.js";
|
|
25
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
26
|
+
import {
|
|
27
|
+
permissionRole,
|
|
28
|
+
permissionUser,
|
|
29
|
+
permissions,
|
|
30
|
+
roleUser,
|
|
31
|
+
roles,
|
|
32
|
+
users,
|
|
33
|
+
} from "../schema.js";
|
|
34
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
9
35
|
|
|
10
36
|
export interface UserSummary {
|
|
11
37
|
id: string;
|
|
@@ -148,9 +174,15 @@ interface RoleRow {
|
|
|
148
174
|
isActive: boolean;
|
|
149
175
|
}
|
|
150
176
|
|
|
151
|
-
const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({
|
|
177
|
+
const asPermissionSummary = (row: PermissionRow): PermissionSummary => ({
|
|
178
|
+
...row,
|
|
179
|
+
id: row.id.toString(),
|
|
180
|
+
});
|
|
152
181
|
|
|
153
|
-
const asRoleSummary = (row: RoleRow): RoleSummary => ({
|
|
182
|
+
const asRoleSummary = (row: RoleRow): RoleSummary => ({
|
|
183
|
+
...row,
|
|
184
|
+
id: row.id.toString(),
|
|
185
|
+
});
|
|
154
186
|
|
|
155
187
|
@Injectable()
|
|
156
188
|
export class RbacRepository {
|
|
@@ -177,7 +209,9 @@ export class RbacRepository {
|
|
|
177
209
|
* makes deactivating either one enforcement-changing without unpicking any grant; `isDeleted`
|
|
178
210
|
* on the user themselves takes the whole answer to empty.
|
|
179
211
|
*/
|
|
180
|
-
async resolveAuthzContext(
|
|
212
|
+
async resolveAuthzContext(
|
|
213
|
+
userId: string,
|
|
214
|
+
): Promise<{ roles: string[]; permissions: string[] }> {
|
|
181
215
|
const userIdBig = toId(userId);
|
|
182
216
|
const [user] = await this.db
|
|
183
217
|
.select({ id: users.id })
|
|
@@ -192,21 +226,44 @@ export class RbacRepository {
|
|
|
192
226
|
this.db
|
|
193
227
|
.select({ roleSlug: roles.slug, permissionSlug: permissions.slug })
|
|
194
228
|
.from(roleUser)
|
|
195
|
-
.innerJoin(
|
|
229
|
+
.innerJoin(
|
|
230
|
+
roles,
|
|
231
|
+
and(
|
|
232
|
+
eq(roles.id, roleUser.roleId),
|
|
233
|
+
eq(roles.isActive, true),
|
|
234
|
+
eq(roles.isDeleted, false),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
196
237
|
.leftJoin(permissionRole, eq(permissionRole.roleId, roles.id))
|
|
197
|
-
.leftJoin(
|
|
238
|
+
.leftJoin(
|
|
239
|
+
permissions,
|
|
240
|
+
and(
|
|
241
|
+
eq(permissions.id, permissionRole.permissionId),
|
|
242
|
+
eq(permissions.isActive, true),
|
|
243
|
+
eq(permissions.isDeleted, false),
|
|
244
|
+
),
|
|
245
|
+
)
|
|
198
246
|
.where(eq(roleUser.userId, userIdBig)),
|
|
199
247
|
this.db
|
|
200
248
|
.select({ slug: permissions.slug })
|
|
201
249
|
.from(permissionUser)
|
|
202
|
-
.innerJoin(
|
|
250
|
+
.innerJoin(
|
|
251
|
+
permissions,
|
|
252
|
+
and(
|
|
253
|
+
eq(permissions.id, permissionUser.permissionId),
|
|
254
|
+
eq(permissions.isActive, true),
|
|
255
|
+
eq(permissions.isDeleted, false),
|
|
256
|
+
),
|
|
257
|
+
)
|
|
203
258
|
.where(eq(permissionUser.userId, userIdBig)),
|
|
204
259
|
]);
|
|
205
260
|
|
|
206
261
|
return {
|
|
207
262
|
roles: [...new Set(roleRows.map((row) => row.roleSlug))].sort(),
|
|
208
263
|
permissions: resolvePermissions(
|
|
209
|
-
roleRows
|
|
264
|
+
roleRows
|
|
265
|
+
.map((row) => row.permissionSlug)
|
|
266
|
+
.filter((slug): slug is string => slug !== null),
|
|
210
267
|
directRows.map((row) => row.slug),
|
|
211
268
|
),
|
|
212
269
|
};
|
|
@@ -222,7 +279,8 @@ export class RbacRepository {
|
|
|
222
279
|
const limit = normalizeLimit(filter.limit);
|
|
223
280
|
|
|
224
281
|
const conditions: SQL[] = [eq(users.isDeleted, false)];
|
|
225
|
-
if (filter.search)
|
|
282
|
+
if (filter.search)
|
|
283
|
+
conditions.push(ilike(users.email, `%${filter.search}%`));
|
|
226
284
|
const where = and(...conditions)!;
|
|
227
285
|
|
|
228
286
|
const rows = await this.db
|
|
@@ -232,7 +290,10 @@ export class RbacRepository {
|
|
|
232
290
|
.orderBy(desc(users.createdAt), desc(users.id))
|
|
233
291
|
.limit(limit)
|
|
234
292
|
.offset((page - 1) * limit);
|
|
235
|
-
const [{ value: total }] = await this.db
|
|
293
|
+
const [{ value: total }] = await this.db
|
|
294
|
+
.select({ value: count() })
|
|
295
|
+
.from(users)
|
|
296
|
+
.where(where);
|
|
236
297
|
|
|
237
298
|
// One read for the page's role assignments rather than one per user.
|
|
238
299
|
const assignments = rows.length
|
|
@@ -254,7 +315,10 @@ export class RbacRepository {
|
|
|
254
315
|
}
|
|
255
316
|
|
|
256
317
|
return {
|
|
257
|
-
items: rows.map((row) => ({
|
|
318
|
+
items: rows.map((row) => ({
|
|
319
|
+
...row,
|
|
320
|
+
roles: (rolesByUser.get(row.id.toString()) ?? []).sort(),
|
|
321
|
+
})),
|
|
258
322
|
meta: buildPageMeta(page, limit, total),
|
|
259
323
|
};
|
|
260
324
|
}
|
|
@@ -269,9 +333,16 @@ export class RbacRepository {
|
|
|
269
333
|
.limit(1);
|
|
270
334
|
if (!row) throw new NotFoundException(`user "${userId}" not found`);
|
|
271
335
|
|
|
272
|
-
const assignments = await this.db
|
|
336
|
+
const assignments = await this.db
|
|
337
|
+
.select({ slug: roles.slug })
|
|
338
|
+
.from(roleUser)
|
|
339
|
+
.innerJoin(roles, eq(roles.id, roleUser.roleId))
|
|
340
|
+
.where(eq(roleUser.userId, userIdBig));
|
|
273
341
|
|
|
274
|
-
return toUserSummary({
|
|
342
|
+
return toUserSummary({
|
|
343
|
+
...row,
|
|
344
|
+
roles: assignments.map((a) => a.slug).sort(),
|
|
345
|
+
});
|
|
275
346
|
}
|
|
276
347
|
|
|
277
348
|
/**
|
|
@@ -295,7 +366,11 @@ export class RbacRepository {
|
|
|
295
366
|
},
|
|
296
367
|
actorUserId: string | null,
|
|
297
368
|
): Promise<UserSummary> {
|
|
298
|
-
const [existing] = await this.db
|
|
369
|
+
const [existing] = await this.db
|
|
370
|
+
.select({ id: users.id })
|
|
371
|
+
.from(users)
|
|
372
|
+
.where(eq(users.email, input.email))
|
|
373
|
+
.limit(1);
|
|
299
374
|
if (existing) throw new ConflictException("email already registered");
|
|
300
375
|
|
|
301
376
|
const [user] = await this.db
|
|
@@ -320,11 +395,19 @@ export class RbacRepository {
|
|
|
320
395
|
const grantedRoles = await this.db
|
|
321
396
|
.select({ id: roles.id })
|
|
322
397
|
.from(roles)
|
|
323
|
-
.where(
|
|
398
|
+
.where(
|
|
399
|
+
and(
|
|
400
|
+
inArray(roles.slug, input.roles),
|
|
401
|
+
eq(roles.isActive, true),
|
|
402
|
+
eq(roles.isDeleted, false),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
324
405
|
if (grantedRoles.length > 0) {
|
|
325
406
|
await this.db
|
|
326
407
|
.insert(roleUser)
|
|
327
|
-
.values(
|
|
408
|
+
.values(
|
|
409
|
+
grantedRoles.map((role) => ({ userId: user.id, roleId: role.id })),
|
|
410
|
+
)
|
|
328
411
|
.onConflictDoNothing({ target: [roleUser.userId, roleUser.roleId] });
|
|
329
412
|
await this.cache.invalidateSubject(user.id.toString());
|
|
330
413
|
}
|
|
@@ -363,7 +446,9 @@ export class RbacRepository {
|
|
|
363
446
|
if (!existing) throw new NotFoundException(`user "${userId}" not found`);
|
|
364
447
|
|
|
365
448
|
// Only the fields the caller sent are written; an absent one leaves its column alone.
|
|
366
|
-
const changed = Object.fromEntries(
|
|
449
|
+
const changed = Object.fromEntries(
|
|
450
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
451
|
+
);
|
|
367
452
|
await this.db
|
|
368
453
|
.update(users)
|
|
369
454
|
.set({ ...changed, updatedBy: toIdOrNull(actorUserId) })
|
|
@@ -376,7 +461,11 @@ export class RbacRepository {
|
|
|
376
461
|
* pattern — the row survives for audit purposes, it just stops appearing in listings and can no
|
|
377
462
|
* longer authenticate (mirrors how `block` already disables login without removing the row).
|
|
378
463
|
*/
|
|
379
|
-
async deleteUser(
|
|
464
|
+
async deleteUser(
|
|
465
|
+
userId: string,
|
|
466
|
+
actorUserId: string | null,
|
|
467
|
+
reason?: string,
|
|
468
|
+
): Promise<void> {
|
|
380
469
|
const userIdBig = toId(userId);
|
|
381
470
|
const [existing] = await this.db
|
|
382
471
|
.select({ id: users.id })
|
|
@@ -387,7 +476,12 @@ export class RbacRepository {
|
|
|
387
476
|
|
|
388
477
|
await this.db
|
|
389
478
|
.update(users)
|
|
390
|
-
.set({
|
|
479
|
+
.set({
|
|
480
|
+
isDeleted: true,
|
|
481
|
+
deletedAt: new Date(),
|
|
482
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
483
|
+
deletedReason: reason ?? null,
|
|
484
|
+
})
|
|
391
485
|
.where(eq(users.id, userIdBig));
|
|
392
486
|
await this.cache.invalidateSubject(userId);
|
|
393
487
|
}
|
|
@@ -400,7 +494,12 @@ export class RbacRepository {
|
|
|
400
494
|
.select(PERMISSION_COLUMNS)
|
|
401
495
|
.from(permissions)
|
|
402
496
|
.where(eq(permissions.isDeleted, false))
|
|
403
|
-
.orderBy(
|
|
497
|
+
.orderBy(
|
|
498
|
+
asc(permissions.groupOrder),
|
|
499
|
+
asc(permissions.group),
|
|
500
|
+
asc(permissions.order),
|
|
501
|
+
asc(permissions.slug),
|
|
502
|
+
);
|
|
404
503
|
return rows.map(asPermissionSummary);
|
|
405
504
|
}
|
|
406
505
|
|
|
@@ -413,7 +512,10 @@ export class RbacRepository {
|
|
|
413
512
|
* Upserted on `slug`, which stays the stable identifier grants and revocations use, so renaming
|
|
414
513
|
* `name`/`displayName` never breaks a grant.
|
|
415
514
|
*/
|
|
416
|
-
async upsertPermission(
|
|
515
|
+
async upsertPermission(
|
|
516
|
+
input: PermissionInput,
|
|
517
|
+
actorUserId: string | null,
|
|
518
|
+
): Promise<PermissionSummary> {
|
|
417
519
|
const actorId = toIdOrNull(actorUserId);
|
|
418
520
|
// Only the fields the caller sent are written; an absent one leaves its column alone, so a
|
|
419
521
|
// partial edit cannot blank out metadata it did not mention.
|
|
@@ -444,7 +546,9 @@ export class RbacRepository {
|
|
|
444
546
|
// returns none — so the no-op case rewrites the slug with itself.
|
|
445
547
|
.onConflictDoUpdate({
|
|
446
548
|
target: permissions.slug,
|
|
447
|
-
set: Object.keys(changed).length
|
|
549
|
+
set: Object.keys(changed).length
|
|
550
|
+
? { ...changed, updatedBy: actorId }
|
|
551
|
+
: { slug: input.slug, updatedBy: actorId },
|
|
448
552
|
})
|
|
449
553
|
.returning(PERMISSION_COLUMNS);
|
|
450
554
|
|
|
@@ -454,12 +558,23 @@ export class RbacRepository {
|
|
|
454
558
|
}
|
|
455
559
|
|
|
456
560
|
async listRoles(): Promise<RoleSummary[]> {
|
|
457
|
-
const rows = await this.db
|
|
561
|
+
const rows = await this.db
|
|
562
|
+
.select(ROLE_COLUMNS)
|
|
563
|
+
.from(roles)
|
|
564
|
+
.where(eq(roles.isDeleted, false))
|
|
565
|
+
.orderBy(asc(roles.order), asc(roles.slug));
|
|
458
566
|
return rows.map(asRoleSummary);
|
|
459
567
|
}
|
|
460
568
|
|
|
461
569
|
async createRole(
|
|
462
|
-
input: {
|
|
570
|
+
input: {
|
|
571
|
+
slug: string;
|
|
572
|
+
name?: string;
|
|
573
|
+
displayName?: string;
|
|
574
|
+
description?: string | null;
|
|
575
|
+
isDefault?: boolean;
|
|
576
|
+
isActive?: boolean;
|
|
577
|
+
},
|
|
463
578
|
actorUserId: string | null,
|
|
464
579
|
): Promise<RoleSummary> {
|
|
465
580
|
const actorId = toIdOrNull(actorUserId);
|
|
@@ -481,7 +596,13 @@ export class RbacRepository {
|
|
|
481
596
|
|
|
482
597
|
async updateRole(
|
|
483
598
|
roleId: string,
|
|
484
|
-
input: {
|
|
599
|
+
input: {
|
|
600
|
+
name?: string;
|
|
601
|
+
displayName?: string;
|
|
602
|
+
description?: string | null;
|
|
603
|
+
isDefault?: boolean;
|
|
604
|
+
isActive?: boolean;
|
|
605
|
+
},
|
|
485
606
|
actorUserId: string | null,
|
|
486
607
|
): Promise<RoleSummary> {
|
|
487
608
|
const roleIdBig = toId(roleId);
|
|
@@ -492,7 +613,9 @@ export class RbacRepository {
|
|
|
492
613
|
.limit(1);
|
|
493
614
|
if (!existing) throw new NotFoundException("role not found");
|
|
494
615
|
|
|
495
|
-
const changed = Object.fromEntries(
|
|
616
|
+
const changed = Object.fromEntries(
|
|
617
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
618
|
+
);
|
|
496
619
|
const [role] = await this.db
|
|
497
620
|
.update(roles)
|
|
498
621
|
.set({ ...changed, updatedBy: toIdOrNull(actorUserId) })
|
|
@@ -508,7 +631,11 @@ export class RbacRepository {
|
|
|
508
631
|
* deleted role simply stops being returned by `listRoles`/`resolveAuthzContext`, rather than
|
|
509
632
|
* cascading a delete that would silently strip a user's other roles' foreign keys.
|
|
510
633
|
*/
|
|
511
|
-
async deleteRole(
|
|
634
|
+
async deleteRole(
|
|
635
|
+
roleId: string,
|
|
636
|
+
actorUserId: string | null,
|
|
637
|
+
reason?: string,
|
|
638
|
+
): Promise<void> {
|
|
512
639
|
const roleIdBig = toId(roleId);
|
|
513
640
|
const [existing] = await this.db
|
|
514
641
|
.select({ id: roles.id })
|
|
@@ -519,7 +646,12 @@ export class RbacRepository {
|
|
|
519
646
|
|
|
520
647
|
await this.db
|
|
521
648
|
.update(roles)
|
|
522
|
-
.set({
|
|
649
|
+
.set({
|
|
650
|
+
isDeleted: true,
|
|
651
|
+
deletedAt: new Date(),
|
|
652
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
653
|
+
deletedReason: reason ?? null,
|
|
654
|
+
})
|
|
523
655
|
.where(eq(roles.id, roleIdBig));
|
|
524
656
|
await this.cache.invalidatePolicy();
|
|
525
657
|
}
|
|
@@ -530,7 +662,11 @@ export class RbacRepository {
|
|
|
530
662
|
* closed-catalog property the routes always had — `@CheckAbility` only accepts slugs from
|
|
531
663
|
* `rbac.defaults.ts`, so an invented slug cannot gate anything shipped by this library.
|
|
532
664
|
*/
|
|
533
|
-
async attachPermissionToRole(
|
|
665
|
+
async attachPermissionToRole(
|
|
666
|
+
roleId: string,
|
|
667
|
+
permissionSlug: string,
|
|
668
|
+
actorUserId: string | null,
|
|
669
|
+
): Promise<void> {
|
|
534
670
|
const roleIdBig = toId(roleId);
|
|
535
671
|
const [role] = await this.db
|
|
536
672
|
.select({ id: roles.id })
|
|
@@ -543,27 +679,46 @@ export class RbacRepository {
|
|
|
543
679
|
await this.db
|
|
544
680
|
.insert(permissionRole)
|
|
545
681
|
.values({ permissionId: permission.id, roleId: roleIdBig })
|
|
546
|
-
.onConflictDoNothing({
|
|
682
|
+
.onConflictDoNothing({
|
|
683
|
+
target: [permissionRole.permissionId, permissionRole.roleId],
|
|
684
|
+
});
|
|
547
685
|
await this.cache.invalidatePolicy(); // everyone holding this role is affected
|
|
548
686
|
}
|
|
549
687
|
|
|
550
688
|
async assignRoleToUser(userId: string, roleSlug: string): Promise<void> {
|
|
551
689
|
const userIdBig = toId(userId);
|
|
552
|
-
const [role] = await this.db
|
|
690
|
+
const [role] = await this.db
|
|
691
|
+
.select({ id: roles.id })
|
|
692
|
+
.from(roles)
|
|
693
|
+
.where(eq(roles.slug, roleSlug))
|
|
694
|
+
.limit(1);
|
|
553
695
|
if (!role) throw new NotFoundException(`role "${roleSlug}" not found`);
|
|
554
696
|
|
|
555
|
-
const [user] = await this.db
|
|
697
|
+
const [user] = await this.db
|
|
698
|
+
.select({ id: users.id })
|
|
699
|
+
.from(users)
|
|
700
|
+
.where(eq(users.id, userIdBig))
|
|
701
|
+
.limit(1);
|
|
556
702
|
if (!user) throw new NotFoundException(`user "${userId}" not found`);
|
|
557
703
|
|
|
558
|
-
await this.db
|
|
704
|
+
await this.db
|
|
705
|
+
.insert(roleUser)
|
|
706
|
+
.values({ userId: userIdBig, roleId: role.id })
|
|
707
|
+
.onConflictDoNothing({ target: [roleUser.userId, roleUser.roleId] });
|
|
559
708
|
await this.cache.invalidateSubject(userId);
|
|
560
709
|
}
|
|
561
710
|
|
|
562
711
|
async revokeRoleFromUser(userId: string, roleSlug: string): Promise<void> {
|
|
563
712
|
const userIdBig = toId(userId);
|
|
564
|
-
const [role] = await this.db
|
|
713
|
+
const [role] = await this.db
|
|
714
|
+
.select({ id: roles.id })
|
|
715
|
+
.from(roles)
|
|
716
|
+
.where(eq(roles.slug, roleSlug))
|
|
717
|
+
.limit(1);
|
|
565
718
|
if (!role) return;
|
|
566
|
-
await this.db
|
|
719
|
+
await this.db
|
|
720
|
+
.delete(roleUser)
|
|
721
|
+
.where(and(eq(roleUser.userId, userIdBig), eq(roleUser.roleId, role.id)));
|
|
567
722
|
await this.cache.invalidateSubject(userId);
|
|
568
723
|
}
|
|
569
724
|
|
|
@@ -579,7 +734,13 @@ export class RbacRepository {
|
|
|
579
734
|
const defaults = await this.db
|
|
580
735
|
.select({ id: roles.id })
|
|
581
736
|
.from(roles)
|
|
582
|
-
.where(
|
|
737
|
+
.where(
|
|
738
|
+
and(
|
|
739
|
+
eq(roles.isDefault, true),
|
|
740
|
+
eq(roles.isActive, true),
|
|
741
|
+
eq(roles.isDeleted, false),
|
|
742
|
+
),
|
|
743
|
+
);
|
|
583
744
|
if (!defaults.length) return;
|
|
584
745
|
await this.db
|
|
585
746
|
.insert(roleUser)
|
|
@@ -589,25 +750,48 @@ export class RbacRepository {
|
|
|
589
750
|
}
|
|
590
751
|
|
|
591
752
|
async hasAnyRole(userId: bigint): Promise<boolean> {
|
|
592
|
-
const [row] = await this.db
|
|
753
|
+
const [row] = await this.db
|
|
754
|
+
.select({ value: count() })
|
|
755
|
+
.from(roleUser)
|
|
756
|
+
.where(eq(roleUser.userId, userId));
|
|
593
757
|
return (row?.value ?? 0) > 0;
|
|
594
758
|
}
|
|
595
759
|
|
|
596
|
-
async grantPermissionToUser(
|
|
760
|
+
async grantPermissionToUser(
|
|
761
|
+
userId: string,
|
|
762
|
+
permissionSlug: string,
|
|
763
|
+
actorUserId: string | null,
|
|
764
|
+
): Promise<void> {
|
|
597
765
|
const userIdBig = toId(userId);
|
|
598
766
|
const permission = await this.ensurePermission(permissionSlug, actorUserId);
|
|
599
767
|
await this.db
|
|
600
768
|
.insert(permissionUser)
|
|
601
769
|
.values({ userId: userIdBig, permissionId: permission.id })
|
|
602
|
-
.onConflictDoNothing({
|
|
770
|
+
.onConflictDoNothing({
|
|
771
|
+
target: [permissionUser.userId, permissionUser.permissionId],
|
|
772
|
+
});
|
|
603
773
|
await this.cache.invalidateSubject(userId);
|
|
604
774
|
}
|
|
605
775
|
|
|
606
|
-
async revokePermissionFromUser(
|
|
776
|
+
async revokePermissionFromUser(
|
|
777
|
+
userId: string,
|
|
778
|
+
permissionSlug: string,
|
|
779
|
+
): Promise<void> {
|
|
607
780
|
const userIdBig = toId(userId);
|
|
608
|
-
const [permission] = await this.db
|
|
781
|
+
const [permission] = await this.db
|
|
782
|
+
.select({ id: permissions.id })
|
|
783
|
+
.from(permissions)
|
|
784
|
+
.where(eq(permissions.slug, permissionSlug))
|
|
785
|
+
.limit(1);
|
|
609
786
|
if (!permission) return;
|
|
610
|
-
await this.db
|
|
787
|
+
await this.db
|
|
788
|
+
.delete(permissionUser)
|
|
789
|
+
.where(
|
|
790
|
+
and(
|
|
791
|
+
eq(permissionUser.userId, userIdBig),
|
|
792
|
+
eq(permissionUser.permissionId, permission.id),
|
|
793
|
+
),
|
|
794
|
+
);
|
|
611
795
|
await this.cache.invalidateSubject(userId);
|
|
612
796
|
}
|
|
613
797
|
|
|
@@ -620,13 +804,27 @@ export class RbacRepository {
|
|
|
620
804
|
* Creates the row for a slug granted before anyone defined it; leaves an existing definition
|
|
621
805
|
* alone. Returns bigint directly: both callers feed the id straight into another database call.
|
|
622
806
|
*/
|
|
623
|
-
private async ensurePermission(
|
|
624
|
-
|
|
807
|
+
private async ensurePermission(
|
|
808
|
+
slug: string,
|
|
809
|
+
actorUserId: string | null,
|
|
810
|
+
): Promise<{ id: bigint }> {
|
|
811
|
+
const [existing] = await this.db
|
|
812
|
+
.select({ id: permissions.id })
|
|
813
|
+
.from(permissions)
|
|
814
|
+
.where(eq(permissions.slug, slug))
|
|
815
|
+
.limit(1);
|
|
625
816
|
if (existing) return existing;
|
|
626
817
|
const actorId = toIdOrNull(actorUserId);
|
|
627
818
|
const [created] = await this.db
|
|
628
819
|
.insert(permissions)
|
|
629
|
-
.values({
|
|
820
|
+
.values({
|
|
821
|
+
slug,
|
|
822
|
+
name: slug,
|
|
823
|
+
displayName: slug,
|
|
824
|
+
group: "Custom",
|
|
825
|
+
createdBy: actorId,
|
|
826
|
+
updatedBy: actorId,
|
|
827
|
+
})
|
|
630
828
|
.returning({ id: permissions.id });
|
|
631
829
|
return created;
|
|
632
830
|
}
|