@simple-auth-kit/cli 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +6 -4
- package/simple-auth-kit.ts +582 -124
- 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,15 +1,25 @@
|
|
|
1
1
|
import { Inject } from "@nestjs/common";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
OnGatewayConnection,
|
|
4
|
+
WebSocketGateway,
|
|
5
|
+
WebSocketServer,
|
|
6
|
+
} from "@nestjs/websockets";
|
|
3
7
|
import type { Server, Socket } from "socket.io";
|
|
4
8
|
import { verifyAccessToken } from "@/lib/auth/core/token-service.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
import {
|
|
10
|
+
AuditLogEntry,
|
|
11
|
+
AuditLogRepository,
|
|
12
|
+
} from "../repositories/audit-log.repository.js";
|
|
13
|
+
import { KeyProviderService } from "../key-provider.js";
|
|
14
|
+
import { SessionRepository } from "../repositories/session.repository.js";
|
|
8
15
|
|
|
9
16
|
// socket.io clients disagree on where a bearer token travels: `auth.token` (the socket.io-native
|
|
10
17
|
// slot), an Authorization header, or a `token` query param. Accept all three, first present wins.
|
|
11
18
|
function extractToken(client: Socket): string | undefined {
|
|
12
|
-
const raw =
|
|
19
|
+
const raw =
|
|
20
|
+
client.handshake.auth?.["token"] ??
|
|
21
|
+
client.handshake.headers.authorization ??
|
|
22
|
+
client.handshake.query?.["token"];
|
|
13
23
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
14
24
|
if (typeof value !== "string" || value.length === 0) return undefined;
|
|
15
25
|
return value.startsWith("Bearer ") ? value.slice("Bearer ".length) : value;
|
|
@@ -38,7 +48,13 @@ export class AuditLogGateway implements OnGatewayConnection {
|
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
40
50
|
try {
|
|
41
|
-
await verifyAccessToken(
|
|
51
|
+
await verifyAccessToken(
|
|
52
|
+
{
|
|
53
|
+
secret: this.keys.secret,
|
|
54
|
+
isDenylisted: (jti) => this.sessions.isDenylisted(jti),
|
|
55
|
+
},
|
|
56
|
+
token,
|
|
57
|
+
);
|
|
42
58
|
} catch {
|
|
43
59
|
client.disconnect(true);
|
|
44
60
|
}
|
package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts}
RENAMED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Inject,
|
|
5
|
+
Injectable,
|
|
6
|
+
} from "@nestjs/common";
|
|
7
|
+
import { defineAbilitiesFor } from "../ability.js";
|
|
8
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
9
|
+
import { RbacRepository } from "../repositories/rbac.repository.js";
|
|
5
10
|
|
|
6
11
|
// Roles and permissions are global to this deployment — one set per user, resolved from the
|
|
7
12
|
// database on the request that uses them. The access token carries identity only.
|
|
@@ -26,7 +31,9 @@ export class AuthzGuard implements CanActivate {
|
|
|
26
31
|
const req = context.switchToHttp().getRequest();
|
|
27
32
|
if (req.auth) {
|
|
28
33
|
const userId = req.auth.sub as string;
|
|
29
|
-
req.authz = (await this.cache.resolve<AuthzContext>(userId, () =>
|
|
34
|
+
req.authz = (await this.cache.resolve<AuthzContext>(userId, () =>
|
|
35
|
+
this.rbac.resolveAuthzContext(userId),
|
|
36
|
+
)) ?? { roles: [], permissions: [] };
|
|
30
37
|
req.ability = defineAbilitiesFor(req.authz.permissions);
|
|
31
38
|
}
|
|
32
39
|
return true;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// `provisionDefaultRoles` writes these rows once at seed time; from then on the database is
|
|
5
5
|
// authoritative and nothing in the request path reads this file. Callers: `src/seed.ts` and
|
|
6
6
|
// `variants/base/test/variant-hooks.ts`.
|
|
7
|
-
import type { PrismaClient } from "
|
|
7
|
+
import type { PrismaClient } from "./../generated/prisma/client.js";
|
|
8
8
|
|
|
9
9
|
export interface PermissionSeed {
|
|
10
10
|
displayName: string;
|
|
@@ -17,8 +17,18 @@ export interface PermissionSeed {
|
|
|
17
17
|
// same noun are separate slugs; block and unblock share one since they're a single capability
|
|
18
18
|
// used in two directions.
|
|
19
19
|
export const PERMISSION_CATALOG = {
|
|
20
|
-
"users:read": {
|
|
21
|
-
|
|
20
|
+
"users:read": {
|
|
21
|
+
displayName: "List users",
|
|
22
|
+
description: "GET /auth/admin/users, GET /auth/admin/users/:userId",
|
|
23
|
+
group: "Users",
|
|
24
|
+
order: 1,
|
|
25
|
+
},
|
|
26
|
+
"users:block": {
|
|
27
|
+
displayName: "Block and unblock users",
|
|
28
|
+
description: "POST /auth/admin/users/:userId/block, .../unblock",
|
|
29
|
+
group: "Users",
|
|
30
|
+
order: 2,
|
|
31
|
+
},
|
|
22
32
|
"users:manage": {
|
|
23
33
|
displayName: "Create, edit, and delete users",
|
|
24
34
|
description:
|
|
@@ -28,69 +38,107 @@ export const PERMISSION_CATALOG = {
|
|
|
28
38
|
},
|
|
29
39
|
"roles:manage": {
|
|
30
40
|
displayName: "Define roles",
|
|
31
|
-
description:
|
|
41
|
+
description:
|
|
42
|
+
"Create, edit, or delete roles and say what they carry — POST/PATCH/DELETE /auth/admin/roles(/:roleId), POST /auth/admin/roles/:roleId/permissions",
|
|
32
43
|
group: "Roles",
|
|
33
44
|
order: 1,
|
|
34
45
|
},
|
|
35
46
|
"roles:assign": {
|
|
36
47
|
displayName: "Assign roles",
|
|
37
|
-
description:
|
|
48
|
+
description:
|
|
49
|
+
"Assign and revoke a user's roles — POST /auth/admin/users/:userId/roles, .../roles/:roleSlug/revoke",
|
|
38
50
|
group: "Roles",
|
|
39
51
|
order: 2,
|
|
40
52
|
},
|
|
41
|
-
"permissions:read": {
|
|
53
|
+
"permissions:read": {
|
|
54
|
+
displayName: "Read the permission catalog",
|
|
55
|
+
description: "GET /auth/admin/permissions",
|
|
56
|
+
group: "Permissions",
|
|
57
|
+
order: 1,
|
|
58
|
+
},
|
|
42
59
|
"permissions:define": {
|
|
43
60
|
displayName: "Define permissions",
|
|
44
|
-
description:
|
|
61
|
+
description:
|
|
62
|
+
"Create a permission, rename it, or deactivate it — POST /auth/admin/permissions. The authority that defines all the others.",
|
|
45
63
|
group: "Permissions",
|
|
46
64
|
order: 2,
|
|
47
65
|
},
|
|
48
66
|
"permissions:grant": {
|
|
49
67
|
displayName: "Grant permissions directly",
|
|
50
|
-
description:
|
|
68
|
+
description:
|
|
69
|
+
"Grant and revoke a permission straight to a user, bypassing roles — POST /auth/admin/users/:userId/permissions, .../permissions/:slug/revoke",
|
|
51
70
|
group: "Permissions",
|
|
52
71
|
order: 3,
|
|
53
72
|
},
|
|
54
|
-
"audit-log:read": {
|
|
73
|
+
"audit-log:read": {
|
|
74
|
+
displayName: "Read the audit log",
|
|
75
|
+
description: "GET /auth/admin/audit-log",
|
|
76
|
+
group: "Audit",
|
|
77
|
+
order: 1,
|
|
78
|
+
},
|
|
55
79
|
|
|
56
|
-
"countries:read": {
|
|
80
|
+
"countries:read": {
|
|
81
|
+
displayName: "List countries",
|
|
82
|
+
description:
|
|
83
|
+
"GET /auth/admin/countries, GET /auth/admin/countries/:countryId",
|
|
84
|
+
group: "Countries",
|
|
85
|
+
order: 1,
|
|
86
|
+
},
|
|
57
87
|
"countries:manage": {
|
|
58
88
|
displayName: "Create, edit, and delete countries",
|
|
59
|
-
description:
|
|
89
|
+
description:
|
|
90
|
+
"POST /auth/admin/countries, PATCH /auth/admin/countries/:countryId, DELETE /auth/admin/countries/:countryId",
|
|
60
91
|
group: "Countries",
|
|
61
92
|
order: 2,
|
|
62
93
|
},
|
|
63
94
|
"countries:status": {
|
|
64
95
|
displayName: "Activate and deactivate countries",
|
|
65
|
-
description:
|
|
96
|
+
description:
|
|
97
|
+
"POST /auth/admin/countries/:countryId/activate, .../deactivate",
|
|
66
98
|
group: "Countries",
|
|
67
99
|
order: 3,
|
|
68
100
|
},
|
|
69
101
|
|
|
70
|
-
"languages:read": {
|
|
102
|
+
"languages:read": {
|
|
103
|
+
displayName: "List languages",
|
|
104
|
+
description:
|
|
105
|
+
"GET /auth/admin/languages, GET /auth/admin/languages/:languageId",
|
|
106
|
+
group: "Languages",
|
|
107
|
+
order: 1,
|
|
108
|
+
},
|
|
71
109
|
"languages:manage": {
|
|
72
110
|
displayName: "Create, edit, and delete languages",
|
|
73
|
-
description:
|
|
111
|
+
description:
|
|
112
|
+
"POST /auth/admin/languages, PATCH /auth/admin/languages/:languageId, DELETE /auth/admin/languages/:languageId",
|
|
74
113
|
group: "Languages",
|
|
75
114
|
order: 2,
|
|
76
115
|
},
|
|
77
116
|
"languages:status": {
|
|
78
117
|
displayName: "Activate and deactivate languages",
|
|
79
|
-
description:
|
|
118
|
+
description:
|
|
119
|
+
"POST /auth/admin/languages/:languageId/activate, .../deactivate",
|
|
80
120
|
group: "Languages",
|
|
81
121
|
order: 3,
|
|
82
122
|
},
|
|
83
123
|
|
|
84
|
-
"customers:read": {
|
|
124
|
+
"customers:read": {
|
|
125
|
+
displayName: "List customers",
|
|
126
|
+
description:
|
|
127
|
+
"GET /auth/admin/customers, GET /auth/admin/customers/:customerId",
|
|
128
|
+
group: "Customers",
|
|
129
|
+
order: 1,
|
|
130
|
+
},
|
|
85
131
|
"customers:manage": {
|
|
86
132
|
displayName: "Create, edit, and delete customers",
|
|
87
|
-
description:
|
|
133
|
+
description:
|
|
134
|
+
"POST /auth/admin/customers, PATCH /auth/admin/customers/:customerId, DELETE /auth/admin/customers/:customerId",
|
|
88
135
|
group: "Customers",
|
|
89
136
|
order: 2,
|
|
90
137
|
},
|
|
91
138
|
"customers:status": {
|
|
92
139
|
displayName: "Activate and deactivate customers",
|
|
93
|
-
description:
|
|
140
|
+
description:
|
|
141
|
+
"POST /auth/admin/customers/:customerId/activate, .../deactivate",
|
|
94
142
|
group: "Customers",
|
|
95
143
|
order: 3,
|
|
96
144
|
},
|
|
@@ -101,10 +149,14 @@ export const PERMISSION_CATALOG = {
|
|
|
101
149
|
// type at compile time.
|
|
102
150
|
export type PermissionSlug = keyof typeof PERMISSION_CATALOG;
|
|
103
151
|
|
|
104
|
-
export const PERMISSION_SLUGS = Object.keys(
|
|
152
|
+
export const PERMISSION_SLUGS = Object.keys(
|
|
153
|
+
PERMISSION_CATALOG,
|
|
154
|
+
) as PermissionSlug[];
|
|
105
155
|
|
|
106
156
|
/** Groups, in render order for an admin console. Derived from the catalog so it can't drift. */
|
|
107
|
-
export const PERMISSION_GROUP_ORDER: string[] = [
|
|
157
|
+
export const PERMISSION_GROUP_ORDER: string[] = [
|
|
158
|
+
...new Set(PERMISSION_SLUGS.map((slug) => PERMISSION_CATALOG[slug].group)),
|
|
159
|
+
];
|
|
108
160
|
|
|
109
161
|
export interface RoleSeed {
|
|
110
162
|
slug: string;
|
|
@@ -131,7 +183,8 @@ export const DEFAULT_ROLES: readonly RoleSeed[] = [
|
|
|
131
183
|
{
|
|
132
184
|
slug: "superadmin",
|
|
133
185
|
displayName: "Super Admin",
|
|
134
|
-
description:
|
|
186
|
+
description:
|
|
187
|
+
"Carries every permission in the catalog — same authority as admin, held by the seeded super_admin account (see SEED_SUPERADMIN_* in seed.ts).",
|
|
135
188
|
isDefault: false,
|
|
136
189
|
order: 1,
|
|
137
190
|
permissions: PERMISSION_SLUGS,
|
|
@@ -152,7 +205,10 @@ export const SEED_ADMIN_ROLES: string[] = ["admin", "member"];
|
|
|
152
205
|
/** Role slugs given to the seeded super_admin. `member` is included so they're also an ordinary user. */
|
|
153
206
|
export const SEED_SUPERADMIN_ROLES: string[] = ["superadmin", "member"];
|
|
154
207
|
|
|
155
|
-
export type RbacWriter = Pick<
|
|
208
|
+
export type RbacWriter = Pick<
|
|
209
|
+
PrismaClient,
|
|
210
|
+
"permission" | "role" | "permissionRole"
|
|
211
|
+
>;
|
|
156
212
|
|
|
157
213
|
// Idempotent and safe to run concurrently: every insert is `skipDuplicates` on the natural
|
|
158
214
|
// unique key, and nothing is ever deleted or updated.
|
|
@@ -164,12 +220,17 @@ export async function provisionDefaultRoles(db: RbacWriter): Promise<void> {
|
|
|
164
220
|
displayName: PERMISSION_CATALOG[slug].displayName,
|
|
165
221
|
description: PERMISSION_CATALOG[slug].description,
|
|
166
222
|
group: PERMISSION_CATALOG[slug].group,
|
|
167
|
-
groupOrder: PERMISSION_GROUP_ORDER.indexOf(
|
|
223
|
+
groupOrder: PERMISSION_GROUP_ORDER.indexOf(
|
|
224
|
+
PERMISSION_CATALOG[slug].group,
|
|
225
|
+
),
|
|
168
226
|
order: PERMISSION_CATALOG[slug].order,
|
|
169
227
|
})),
|
|
170
228
|
skipDuplicates: true,
|
|
171
229
|
});
|
|
172
|
-
const permissions = await db.permission.findMany({
|
|
230
|
+
const permissions = await db.permission.findMany({
|
|
231
|
+
where: { slug: { in: PERMISSION_SLUGS } },
|
|
232
|
+
select: { id: true, slug: true },
|
|
233
|
+
});
|
|
173
234
|
const permissionId = new Map(permissions.map((p) => [p.slug, p.id]));
|
|
174
235
|
|
|
175
236
|
await db.role.createMany({
|
|
@@ -183,11 +244,21 @@ export async function provisionDefaultRoles(db: RbacWriter): Promise<void> {
|
|
|
183
244
|
})),
|
|
184
245
|
skipDuplicates: true,
|
|
185
246
|
});
|
|
186
|
-
const roles = await db.role.findMany({
|
|
247
|
+
const roles = await db.role.findMany({
|
|
248
|
+
where: { slug: { in: DEFAULT_ROLES.map((r) => r.slug) } },
|
|
249
|
+
select: { id: true, slug: true },
|
|
250
|
+
});
|
|
187
251
|
const seedBySlug = new Map(DEFAULT_ROLES.map((role) => [role.slug, role]));
|
|
188
252
|
|
|
189
253
|
const rolePermissions = roles.flatMap((role) =>
|
|
190
|
-
(seedBySlug.get(role.slug)?.permissions ?? []).map((slug) => ({
|
|
254
|
+
(seedBySlug.get(role.slug)?.permissions ?? []).map((slug) => ({
|
|
255
|
+
roleId: role.id,
|
|
256
|
+
permissionId: permissionId.get(slug)!,
|
|
257
|
+
})),
|
|
191
258
|
);
|
|
192
|
-
if (rolePermissions.length)
|
|
259
|
+
if (rolePermissions.length)
|
|
260
|
+
await db.permissionRole.createMany({
|
|
261
|
+
data: rolePermissions,
|
|
262
|
+
skipDuplicates: true,
|
|
263
|
+
});
|
|
193
264
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Inject, Injectable } from "@nestjs/common";
|
|
2
2
|
import type { AuditEvent } from "@/lib/auth/core/types.js";
|
|
3
|
-
import { Prisma, PrismaClient } from "
|
|
4
|
-
import { toIdOrNull, toIdOrUndefined } from "
|
|
5
|
-
import {
|
|
3
|
+
import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
|
|
4
|
+
import { toIdOrNull, toIdOrUndefined } from "../id.helper.js";
|
|
5
|
+
import {
|
|
6
|
+
buildPageMeta,
|
|
7
|
+
normalizeLimit,
|
|
8
|
+
normalizePage,
|
|
9
|
+
type Paginated,
|
|
10
|
+
} from "../pagination.js";
|
|
6
11
|
|
|
7
12
|
export interface AuditLogEntry {
|
|
8
13
|
id: string;
|
|
@@ -27,7 +32,10 @@ export interface AuditLogListFilter {
|
|
|
27
32
|
// "role_assigned" -> "Role assigned". Derived rather than passed in, so core never carries display text.
|
|
28
33
|
export function humanizeAction(action: string): string {
|
|
29
34
|
const words = action.split("_");
|
|
30
|
-
return [
|
|
35
|
+
return [
|
|
36
|
+
words[0].charAt(0).toUpperCase() + words[0].slice(1),
|
|
37
|
+
...words.slice(1),
|
|
38
|
+
].join(" ");
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/** An `AuditLog` row as `findMany` returns it. */
|
|
@@ -63,7 +71,10 @@ export class AuditLogRepository {
|
|
|
63
71
|
this.listeners.add(listener);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
async append(
|
|
74
|
+
async append(
|
|
75
|
+
event: AuditEvent,
|
|
76
|
+
opts: { remarks?: string } = {},
|
|
77
|
+
): Promise<void> {
|
|
67
78
|
const row = await this.prisma.auditLog.create({
|
|
68
79
|
data: {
|
|
69
80
|
action: event.type,
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
ConflictException,
|
|
3
|
+
Inject,
|
|
4
|
+
Injectable,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
} from "@nestjs/common";
|
|
7
|
+
import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
|
|
8
|
+
import { toId, toIdOrNull } from "../id.helper.js";
|
|
9
|
+
import {
|
|
10
|
+
buildPageMeta,
|
|
11
|
+
normalizeLimit,
|
|
12
|
+
normalizePage,
|
|
13
|
+
type Paginated,
|
|
14
|
+
} from "../pagination.js";
|
|
5
15
|
|
|
6
16
|
/** A `Country` row as `findUnique`/`findMany` return it. */
|
|
7
17
|
export type CountryRow = Prisma.CountryGetPayload<object>;
|
|
@@ -90,60 +100,123 @@ export class CountryRepository {
|
|
|
90
100
|
};
|
|
91
101
|
|
|
92
102
|
const [rows, total] = await this.prisma.$transaction([
|
|
93
|
-
this.prisma.country.findMany({
|
|
103
|
+
this.prisma.country.findMany({
|
|
104
|
+
where,
|
|
105
|
+
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
|
106
|
+
take: limit,
|
|
107
|
+
skip: (page - 1) * limit,
|
|
108
|
+
}),
|
|
94
109
|
this.prisma.country.count({ where }),
|
|
95
110
|
]);
|
|
96
111
|
|
|
97
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
items: rows.map(toCountrySummary),
|
|
114
|
+
meta: buildPageMeta(page, limit, total),
|
|
115
|
+
};
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
async get(countryId: string): Promise<CountrySummary> {
|
|
101
|
-
const row = await this.prisma.country.findUnique({
|
|
119
|
+
const row = await this.prisma.country.findUnique({
|
|
120
|
+
where: { id: toId(countryId), isDeleted: false },
|
|
121
|
+
});
|
|
102
122
|
if (!row) throw new NotFoundException(`country "${countryId}" not found`);
|
|
103
123
|
return toCountrySummary(row);
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
async create(
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
async create(
|
|
127
|
+
input: CountryInput,
|
|
128
|
+
actorUserId: string | null,
|
|
129
|
+
): Promise<CountrySummary> {
|
|
130
|
+
const existing = await this.prisma.country.findFirst({
|
|
131
|
+
where: { OR: [{ code: input.code }, { isoCode: input.isoCode }] },
|
|
132
|
+
});
|
|
133
|
+
if (existing)
|
|
134
|
+
throw new ConflictException(
|
|
135
|
+
"a country with this code or ISO code already exists",
|
|
136
|
+
);
|
|
109
137
|
|
|
110
138
|
const row = await this.prisma.country.create({
|
|
111
|
-
data: {
|
|
139
|
+
data: {
|
|
140
|
+
...input,
|
|
141
|
+
createdBy: toIdOrNull(actorUserId),
|
|
142
|
+
updatedBy: toIdOrNull(actorUserId),
|
|
143
|
+
},
|
|
112
144
|
});
|
|
113
145
|
return toCountrySummary(row);
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
async update(
|
|
148
|
+
async update(
|
|
149
|
+
countryId: string,
|
|
150
|
+
input: Partial<CountryInput>,
|
|
151
|
+
actorUserId: string | null,
|
|
152
|
+
): Promise<CountrySummary> {
|
|
117
153
|
const countryIdBig = toId(countryId);
|
|
118
|
-
const existing = await this.prisma.country.findUnique({
|
|
119
|
-
|
|
154
|
+
const existing = await this.prisma.country.findUnique({
|
|
155
|
+
where: { id: countryIdBig, isDeleted: false },
|
|
156
|
+
select: { id: true },
|
|
157
|
+
});
|
|
158
|
+
if (!existing)
|
|
159
|
+
throw new NotFoundException(`country "${countryId}" not found`);
|
|
120
160
|
|
|
121
161
|
if (input.code !== undefined || input.isoCode !== undefined) {
|
|
122
162
|
const clash = await this.prisma.country.findFirst({
|
|
123
|
-
where: {
|
|
163
|
+
where: {
|
|
164
|
+
NOT: { id: countryIdBig },
|
|
165
|
+
OR: [{ code: input.code }, { isoCode: input.isoCode }],
|
|
166
|
+
},
|
|
124
167
|
});
|
|
125
|
-
if (clash)
|
|
168
|
+
if (clash)
|
|
169
|
+
throw new ConflictException(
|
|
170
|
+
"a country with this code or ISO code already exists",
|
|
171
|
+
);
|
|
126
172
|
}
|
|
127
173
|
|
|
128
|
-
const row = await this.prisma.country.update({
|
|
174
|
+
const row = await this.prisma.country.update({
|
|
175
|
+
where: { id: countryIdBig },
|
|
176
|
+
data: { ...input, updatedBy: toIdOrNull(actorUserId) },
|
|
177
|
+
});
|
|
129
178
|
return toCountrySummary(row);
|
|
130
179
|
}
|
|
131
180
|
|
|
132
181
|
// Soft-delete, matching every other table's isDeleted/deletedAt/deletedBy/deletedReason pattern.
|
|
133
|
-
async delete(
|
|
182
|
+
async delete(
|
|
183
|
+
countryId: string,
|
|
184
|
+
actorUserId: string | null,
|
|
185
|
+
reason?: string,
|
|
186
|
+
): Promise<void> {
|
|
134
187
|
const countryIdBig = toId(countryId);
|
|
135
|
-
const existing = await this.prisma.country.findUnique({
|
|
136
|
-
|
|
188
|
+
const existing = await this.prisma.country.findUnique({
|
|
189
|
+
where: { id: countryIdBig, isDeleted: false },
|
|
190
|
+
select: { id: true },
|
|
191
|
+
});
|
|
192
|
+
if (!existing)
|
|
193
|
+
throw new NotFoundException(`country "${countryId}" not found`);
|
|
137
194
|
await this.prisma.country.update({
|
|
138
195
|
where: { id: countryIdBig },
|
|
139
|
-
data: {
|
|
196
|
+
data: {
|
|
197
|
+
isDeleted: true,
|
|
198
|
+
deletedAt: new Date(),
|
|
199
|
+
deletedBy: toIdOrNull(actorUserId),
|
|
200
|
+
deletedReason: reason ?? null,
|
|
201
|
+
},
|
|
140
202
|
});
|
|
141
203
|
}
|
|
142
204
|
|
|
143
|
-
async setActive(
|
|
205
|
+
async setActive(
|
|
206
|
+
countryId: string,
|
|
207
|
+
isActive: boolean,
|
|
208
|
+
actorUserId: string | null,
|
|
209
|
+
): Promise<void> {
|
|
144
210
|
const countryIdBig = toId(countryId);
|
|
145
|
-
const existing = await this.prisma.country.findUnique({
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
const existing = await this.prisma.country.findUnique({
|
|
212
|
+
where: { id: countryIdBig, isDeleted: false },
|
|
213
|
+
select: { id: true },
|
|
214
|
+
});
|
|
215
|
+
if (!existing)
|
|
216
|
+
throw new NotFoundException(`country "${countryId}" not found`);
|
|
217
|
+
await this.prisma.country.update({
|
|
218
|
+
where: { id: countryIdBig },
|
|
219
|
+
data: { isActive, updatedBy: toIdOrNull(actorUserId) },
|
|
220
|
+
});
|
|
148
221
|
}
|
|
149
222
|
}
|