@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,29 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
ForbiddenException,
|
|
7
|
+
Get,
|
|
8
|
+
Inject,
|
|
9
|
+
Param,
|
|
10
|
+
Post,
|
|
11
|
+
Put,
|
|
12
|
+
Req,
|
|
13
|
+
UseGuards,
|
|
14
|
+
} from "@nestjs/common";
|
|
15
|
+
import {
|
|
16
|
+
ApiBearerAuth,
|
|
17
|
+
ApiBody,
|
|
18
|
+
ApiHeader,
|
|
19
|
+
ApiOperation,
|
|
20
|
+
ApiParam,
|
|
21
|
+
ApiResponse,
|
|
22
|
+
ApiTags,
|
|
23
|
+
} from "@nestjs/swagger";
|
|
3
24
|
import type { Request } from "express";
|
|
4
|
-
import { AbilityGuard } from "
|
|
5
|
-
import { AuthGuard } from "
|
|
6
|
-
import { WORKSPACE_HEADER, WorkspaceGuard } from "
|
|
7
|
-
import { Authenticated, CheckAbility } from "
|
|
8
|
-
import { OkResponseDto } from "
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
import { AbilityGuard } from "../guards/ability.guard.js";
|
|
26
|
+
import { AuthGuard } from "../guards/auth.guard.js";
|
|
27
|
+
import { WORKSPACE_HEADER, WorkspaceGuard } from "../guards/authz.guard.js";
|
|
28
|
+
import { Authenticated, CheckAbility } from "../route-tiers.js";
|
|
29
|
+
import { OkResponseDto } from "../dto/auth.dto.js";
|
|
30
|
+
import {
|
|
31
|
+
AddMemberDto,
|
|
32
|
+
CreateWorkspaceDto,
|
|
33
|
+
MemberRolesResponseDto,
|
|
34
|
+
MembershipSummaryDto,
|
|
35
|
+
SetMemberRolesDto,
|
|
36
|
+
WorkspaceSummaryDto,
|
|
37
|
+
} from "../dto/workspace.dto.js";
|
|
38
|
+
import { RbacRepository } from "../repositories/rbac.repository.js";
|
|
39
|
+
import { WorkspaceRepository } from "../repositories/workspace.repository.js";
|
|
12
40
|
|
|
13
41
|
function requireString(value: unknown, field: string): string {
|
|
14
|
-
if (typeof value !== "string" || value.length === 0)
|
|
42
|
+
if (typeof value !== "string" || value.length === 0)
|
|
43
|
+
throw new BadRequestException(`${field} is required`);
|
|
15
44
|
return value;
|
|
16
45
|
}
|
|
17
46
|
|
|
18
47
|
function requireStringArray(value: unknown, field: string): string[] {
|
|
19
|
-
if (!Array.isArray(value) || value.some((v) => typeof v !== "string"))
|
|
48
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string"))
|
|
49
|
+
throw new BadRequestException(`${field} must be an array of strings`);
|
|
20
50
|
return value as string[];
|
|
21
51
|
}
|
|
22
52
|
|
|
23
53
|
const WORKSPACE_HEADER_DOC = {
|
|
24
54
|
name: WORKSPACE_HEADER,
|
|
25
55
|
required: true,
|
|
26
|
-
description:
|
|
56
|
+
description:
|
|
57
|
+
"The workspace this request acts in. The caller must be a member of it.",
|
|
27
58
|
};
|
|
28
59
|
|
|
29
60
|
// Every route below that acts *inside* a workspace names it with the `X-Workspace-Id` header
|
|
@@ -37,24 +68,34 @@ const WORKSPACE_HEADER_DOC = {
|
|
|
37
68
|
@ApiBearerAuth()
|
|
38
69
|
export class WorkspaceController {
|
|
39
70
|
constructor(
|
|
40
|
-
@Inject(WorkspaceRepository)
|
|
71
|
+
@Inject(WorkspaceRepository)
|
|
72
|
+
private readonly workspaces: WorkspaceRepository,
|
|
41
73
|
@Inject(RbacRepository) private readonly rbac: RbacRepository,
|
|
42
74
|
) {}
|
|
43
75
|
|
|
44
76
|
@Post()
|
|
45
77
|
@Authenticated()
|
|
46
78
|
@UseGuards(AuthGuard)
|
|
47
|
-
@ApiOperation({
|
|
79
|
+
@ApiOperation({
|
|
80
|
+
summary: "Create a workspace",
|
|
81
|
+
description: "The creator becomes its first member, with the admin role.",
|
|
82
|
+
})
|
|
48
83
|
@ApiBody({ type: CreateWorkspaceDto })
|
|
49
84
|
@ApiResponse({ status: 201, type: WorkspaceSummaryDto })
|
|
50
85
|
async create(@Body() body: Record<string, unknown>, @Req() req: Request) {
|
|
51
|
-
return this.workspaces.create(
|
|
86
|
+
return this.workspaces.create(
|
|
87
|
+
req.auth!.sub,
|
|
88
|
+
requireString(body.name, "name"),
|
|
89
|
+
);
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
@Get()
|
|
55
93
|
@Authenticated()
|
|
56
94
|
@UseGuards(AuthGuard)
|
|
57
|
-
@ApiOperation({
|
|
95
|
+
@ApiOperation({
|
|
96
|
+
summary:
|
|
97
|
+
"List the workspaces the current user belongs to, with their roles in each",
|
|
98
|
+
})
|
|
58
99
|
@ApiResponse({ status: 200, type: [WorkspaceSummaryDto] })
|
|
59
100
|
async listMine(@Req() req: Request) {
|
|
60
101
|
return this.workspaces.listForUser(req.auth!.sub);
|
|
@@ -74,37 +115,63 @@ export class WorkspaceController {
|
|
|
74
115
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
75
116
|
@CheckAbility("members:manage")
|
|
76
117
|
@ApiHeader(WORKSPACE_HEADER_DOC)
|
|
77
|
-
@ApiOperation({
|
|
118
|
+
@ApiOperation({
|
|
119
|
+
summary: "[admin] Add an existing user to the named workspace",
|
|
120
|
+
})
|
|
78
121
|
@ApiBody({ type: AddMemberDto })
|
|
79
122
|
@ApiResponse({ status: 201, type: MembershipSummaryDto })
|
|
80
123
|
async addMember(@Body() body: Record<string, unknown>, @Req() req: Request) {
|
|
81
124
|
// No `roles` in the body means "whatever this workspace flags as the default role".
|
|
82
|
-
const roles =
|
|
83
|
-
|
|
125
|
+
const roles =
|
|
126
|
+
body.roles === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: requireStringArray(body.roles, "roles");
|
|
129
|
+
return this.workspaces.addMember(
|
|
130
|
+
req.authz!.workspaceId,
|
|
131
|
+
requireString(body.email, "email"),
|
|
132
|
+
roles,
|
|
133
|
+
);
|
|
84
134
|
}
|
|
85
135
|
|
|
86
136
|
@Put("members/:memberId/roles")
|
|
87
137
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
88
138
|
@CheckAbility("roles:assign")
|
|
89
139
|
@ApiHeader(WORKSPACE_HEADER_DOC)
|
|
90
|
-
@ApiOperation({
|
|
140
|
+
@ApiOperation({
|
|
141
|
+
summary: "[admin] Replace a member's roles within the named workspace",
|
|
142
|
+
})
|
|
91
143
|
@ApiParam({ name: "memberId" })
|
|
92
144
|
@ApiBody({ type: SetMemberRolesDto })
|
|
93
145
|
@ApiResponse({ status: 200, type: MemberRolesResponseDto })
|
|
94
|
-
async setMemberRoles(
|
|
95
|
-
|
|
96
|
-
|
|
146
|
+
async setMemberRoles(
|
|
147
|
+
@Param("memberId") memberId: string,
|
|
148
|
+
@Body() body: Record<string, unknown>,
|
|
149
|
+
@Req() req: Request,
|
|
150
|
+
) {
|
|
151
|
+
if (memberId === req.authz!.memberId)
|
|
152
|
+
throw new ForbiddenException("cannot change your own roles");
|
|
153
|
+
return this.rbac.setMemberRoles(
|
|
154
|
+
req.authz!.workspaceId,
|
|
155
|
+
memberId,
|
|
156
|
+
requireStringArray(body.roles, "roles"),
|
|
157
|
+
);
|
|
97
158
|
}
|
|
98
159
|
|
|
99
160
|
@Delete("members/:memberId")
|
|
100
161
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
101
162
|
@CheckAbility("members:manage")
|
|
102
163
|
@ApiHeader(WORKSPACE_HEADER_DOC)
|
|
103
|
-
@ApiOperation({
|
|
164
|
+
@ApiOperation({
|
|
165
|
+
summary: "[admin] Remove a member from the named workspace",
|
|
166
|
+
description: "Their direct permission grants go with the membership.",
|
|
167
|
+
})
|
|
104
168
|
@ApiParam({ name: "memberId" })
|
|
105
169
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
106
170
|
async removeMember(@Param("memberId") memberId: string, @Req() req: Request) {
|
|
107
|
-
if (memberId === req.authz!.memberId)
|
|
171
|
+
if (memberId === req.authz!.memberId)
|
|
172
|
+
throw new ForbiddenException(
|
|
173
|
+
"cannot remove yourself from a workspace you administer",
|
|
174
|
+
);
|
|
108
175
|
await this.workspaces.removeMember(req.authz!.workspaceId, memberId);
|
|
109
176
|
return { ok: true };
|
|
110
177
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
Inject,
|
|
6
|
+
Injectable,
|
|
7
|
+
} from "@nestjs/common";
|
|
8
|
+
import { defineAbilitiesFor } from "../ability.js";
|
|
9
|
+
import { PermissionCache } from "../permission-cache.js";
|
|
10
|
+
import {
|
|
11
|
+
memberCacheKey,
|
|
12
|
+
RbacRepository,
|
|
13
|
+
} from "../repositories/rbac.repository.js";
|
|
5
14
|
|
|
6
15
|
export const WORKSPACE_HEADER = "x-workspace-id";
|
|
7
16
|
|
|
@@ -20,15 +29,23 @@ export interface AuthzContext {
|
|
|
20
29
|
// steady-state cost is a cache read. A non-member is never cached, so being added to a
|
|
21
30
|
// workspace is effective immediately. Idempotent: a route may sit behind both guards, and
|
|
22
31
|
// resolving twice would double the hot path's query count for no gain.
|
|
23
|
-
async function resolve(
|
|
32
|
+
async function resolve(
|
|
33
|
+
rbac: RbacRepository,
|
|
34
|
+
cache: PermissionCache,
|
|
35
|
+
context: ExecutionContext,
|
|
36
|
+
): Promise<void> {
|
|
24
37
|
const req = context.switchToHttp().getRequest();
|
|
25
38
|
if (req.authz) return;
|
|
26
39
|
|
|
27
40
|
const workspaceId = req.headers[WORKSPACE_HEADER];
|
|
28
|
-
if (!req.auth || typeof workspaceId !== "string" || workspaceId.length === 0)
|
|
41
|
+
if (!req.auth || typeof workspaceId !== "string" || workspaceId.length === 0)
|
|
42
|
+
return;
|
|
29
43
|
|
|
30
44
|
const userId = req.auth.sub as string;
|
|
31
|
-
const authz = await cache.resolve<AuthzContext>(
|
|
45
|
+
const authz = await cache.resolve<AuthzContext>(
|
|
46
|
+
memberCacheKey(userId, workspaceId),
|
|
47
|
+
() => rbac.resolveAuthzContext(userId, workspaceId),
|
|
48
|
+
);
|
|
32
49
|
// Same answer for "no such workspace" and "not your workspace" — a caller outside a workspace
|
|
33
50
|
// must not be able to probe whether it exists.
|
|
34
51
|
if (!authz) throw new ForbiddenException("not a member of this workspace");
|
|
@@ -63,7 +80,8 @@ export class WorkspaceGuard implements CanActivate {
|
|
|
63
80
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
64
81
|
await resolve(this.rbac, this.cache, context);
|
|
65
82
|
const req = context.switchToHttp().getRequest();
|
|
66
|
-
if (!req.authz)
|
|
83
|
+
if (!req.authz)
|
|
84
|
+
throw new ForbiddenException(`${WORKSPACE_HEADER} header is required`);
|
|
67
85
|
return true;
|
|
68
86
|
}
|
|
69
87
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// thing that creates a workspace — callers are `src/seed.ts` (the first workspace) and
|
|
10
10
|
// `src/workspace.repository.ts` (every workspace created afterward, in the same transaction
|
|
11
11
|
// that creates it).
|
|
12
|
-
import type { PrismaClient } from "
|
|
12
|
+
import type { PrismaClient } from "./../generated/prisma/client.js";
|
|
13
13
|
|
|
14
14
|
export interface PermissionSeed {
|
|
15
15
|
displayName: string;
|
|
@@ -22,8 +22,18 @@ export interface PermissionSeed {
|
|
|
22
22
|
// is the `Permission` table); the authority they confer is scoped to the workspace the request
|
|
23
23
|
// names, because it's that workspace's roles and that membership's grants that point at them.
|
|
24
24
|
export const PERMISSION_CATALOG = {
|
|
25
|
-
"users:read": {
|
|
26
|
-
|
|
25
|
+
"users:read": {
|
|
26
|
+
displayName: "List members",
|
|
27
|
+
description: "GET /auth/admin/users, GET /auth/admin/users/:userId",
|
|
28
|
+
group: "Users",
|
|
29
|
+
order: 1,
|
|
30
|
+
},
|
|
31
|
+
"users:block": {
|
|
32
|
+
displayName: "Block and unblock members",
|
|
33
|
+
description: "POST /auth/admin/users/:userId/block, .../unblock",
|
|
34
|
+
group: "Users",
|
|
35
|
+
order: 2,
|
|
36
|
+
},
|
|
27
37
|
"users:manage": {
|
|
28
38
|
displayName: "Create, edit, and delete members",
|
|
29
39
|
description:
|
|
@@ -40,69 +50,107 @@ export const PERMISSION_CATALOG = {
|
|
|
40
50
|
},
|
|
41
51
|
"roles:assign": {
|
|
42
52
|
displayName: "Assign roles",
|
|
43
|
-
description:
|
|
53
|
+
description:
|
|
54
|
+
"Assign and revoke a member's roles — POST /auth/admin/users/:userId/roles, .../roles/:roleSlug/revoke, PUT /workspaces/members/:memberId/roles",
|
|
44
55
|
group: "Roles",
|
|
45
56
|
order: 2,
|
|
46
57
|
},
|
|
47
|
-
"permissions:read": {
|
|
58
|
+
"permissions:read": {
|
|
59
|
+
displayName: "Read the permission catalog",
|
|
60
|
+
description: "GET /auth/admin/permissions",
|
|
61
|
+
group: "Permissions",
|
|
62
|
+
order: 1,
|
|
63
|
+
},
|
|
48
64
|
"permissions:define": {
|
|
49
65
|
displayName: "Define permissions",
|
|
50
|
-
description:
|
|
66
|
+
description:
|
|
67
|
+
"Create a permission, rename it, or deactivate it — POST /auth/admin/permissions. The authority that defines all the others.",
|
|
51
68
|
group: "Permissions",
|
|
52
69
|
order: 2,
|
|
53
70
|
},
|
|
54
71
|
"permissions:grant": {
|
|
55
72
|
displayName: "Grant permissions directly",
|
|
56
|
-
description:
|
|
73
|
+
description:
|
|
74
|
+
"Grant and revoke a permission straight to a member, bypassing roles — POST /auth/admin/users/:userId/permissions, .../permissions/:slug/revoke",
|
|
57
75
|
group: "Permissions",
|
|
58
76
|
order: 3,
|
|
59
77
|
},
|
|
60
78
|
"members:manage": {
|
|
61
79
|
displayName: "Add and remove members",
|
|
62
|
-
description:
|
|
80
|
+
description:
|
|
81
|
+
"POST /workspaces/members, DELETE /workspaces/members/:memberId",
|
|
63
82
|
group: "Members",
|
|
64
83
|
order: 1,
|
|
65
84
|
},
|
|
66
|
-
"audit-log:read": {
|
|
85
|
+
"audit-log:read": {
|
|
86
|
+
displayName: "Read the audit log",
|
|
87
|
+
description: "GET /auth/admin/audit-log",
|
|
88
|
+
group: "Audit",
|
|
89
|
+
order: 1,
|
|
90
|
+
},
|
|
67
91
|
|
|
68
|
-
"countries:read": {
|
|
92
|
+
"countries:read": {
|
|
93
|
+
displayName: "List countries",
|
|
94
|
+
description:
|
|
95
|
+
"GET /auth/admin/countries, GET /auth/admin/countries/:countryId",
|
|
96
|
+
group: "Countries",
|
|
97
|
+
order: 1,
|
|
98
|
+
},
|
|
69
99
|
"countries:manage": {
|
|
70
100
|
displayName: "Create, edit, and delete countries",
|
|
71
|
-
description:
|
|
101
|
+
description:
|
|
102
|
+
"POST /auth/admin/countries, PATCH /auth/admin/countries/:countryId, DELETE /auth/admin/countries/:countryId",
|
|
72
103
|
group: "Countries",
|
|
73
104
|
order: 2,
|
|
74
105
|
},
|
|
75
106
|
"countries:status": {
|
|
76
107
|
displayName: "Activate and deactivate countries",
|
|
77
|
-
description:
|
|
108
|
+
description:
|
|
109
|
+
"POST /auth/admin/countries/:countryId/activate, .../deactivate",
|
|
78
110
|
group: "Countries",
|
|
79
111
|
order: 3,
|
|
80
112
|
},
|
|
81
113
|
|
|
82
|
-
"languages:read": {
|
|
114
|
+
"languages:read": {
|
|
115
|
+
displayName: "List languages",
|
|
116
|
+
description:
|
|
117
|
+
"GET /auth/admin/languages, GET /auth/admin/languages/:languageId",
|
|
118
|
+
group: "Languages",
|
|
119
|
+
order: 1,
|
|
120
|
+
},
|
|
83
121
|
"languages:manage": {
|
|
84
122
|
displayName: "Create, edit, and delete languages",
|
|
85
|
-
description:
|
|
123
|
+
description:
|
|
124
|
+
"POST /auth/admin/languages, PATCH /auth/admin/languages/:languageId, DELETE /auth/admin/languages/:languageId",
|
|
86
125
|
group: "Languages",
|
|
87
126
|
order: 2,
|
|
88
127
|
},
|
|
89
128
|
"languages:status": {
|
|
90
129
|
displayName: "Activate and deactivate languages",
|
|
91
|
-
description:
|
|
130
|
+
description:
|
|
131
|
+
"POST /auth/admin/languages/:languageId/activate, .../deactivate",
|
|
92
132
|
group: "Languages",
|
|
93
133
|
order: 3,
|
|
94
134
|
},
|
|
95
135
|
|
|
96
|
-
"customers:read": {
|
|
136
|
+
"customers:read": {
|
|
137
|
+
displayName: "List customers",
|
|
138
|
+
description:
|
|
139
|
+
"GET /auth/admin/customers, GET /auth/admin/customers/:customerId",
|
|
140
|
+
group: "Customers",
|
|
141
|
+
order: 1,
|
|
142
|
+
},
|
|
97
143
|
"customers:manage": {
|
|
98
144
|
displayName: "Create, edit, and delete customers",
|
|
99
|
-
description:
|
|
145
|
+
description:
|
|
146
|
+
"POST /auth/admin/customers, PATCH /auth/admin/customers/:customerId, DELETE /auth/admin/customers/:customerId",
|
|
100
147
|
group: "Customers",
|
|
101
148
|
order: 2,
|
|
102
149
|
},
|
|
103
150
|
"customers:status": {
|
|
104
151
|
displayName: "Activate and deactivate customers",
|
|
105
|
-
description:
|
|
152
|
+
description:
|
|
153
|
+
"POST /auth/admin/customers/:customerId/activate, .../deactivate",
|
|
106
154
|
group: "Customers",
|
|
107
155
|
order: 3,
|
|
108
156
|
},
|
|
@@ -113,10 +161,14 @@ export const PERMISSION_CATALOG = {
|
|
|
113
161
|
// type at compile time.
|
|
114
162
|
export type PermissionSlug = keyof typeof PERMISSION_CATALOG;
|
|
115
163
|
|
|
116
|
-
export const PERMISSION_SLUGS = Object.keys(
|
|
164
|
+
export const PERMISSION_SLUGS = Object.keys(
|
|
165
|
+
PERMISSION_CATALOG,
|
|
166
|
+
) as PermissionSlug[];
|
|
117
167
|
|
|
118
168
|
/** Groups, in render order for an admin console. Derived from the catalog so it can't drift. */
|
|
119
|
-
export const PERMISSION_GROUP_ORDER: string[] = [
|
|
169
|
+
export const PERMISSION_GROUP_ORDER: string[] = [
|
|
170
|
+
...new Set(PERMISSION_SLUGS.map((slug) => PERMISSION_CATALOG[slug].group)),
|
|
171
|
+
];
|
|
120
172
|
|
|
121
173
|
export interface RoleSeed {
|
|
122
174
|
slug: string;
|
|
@@ -143,7 +195,8 @@ export const DEFAULT_ROLES: readonly RoleSeed[] = [
|
|
|
143
195
|
{
|
|
144
196
|
slug: "superadmin",
|
|
145
197
|
displayName: "Super Admin",
|
|
146
|
-
description:
|
|
198
|
+
description:
|
|
199
|
+
"Carries every permission in the catalog — same authority as admin, held by the seeded super_admin account's membership (see SEED_SUPERADMIN_* in seed.ts).",
|
|
147
200
|
isDefault: false,
|
|
148
201
|
order: 1,
|
|
149
202
|
permissions: PERMISSION_SLUGS,
|
|
@@ -151,7 +204,8 @@ export const DEFAULT_ROLES: readonly RoleSeed[] = [
|
|
|
151
204
|
{
|
|
152
205
|
slug: "member",
|
|
153
206
|
displayName: "Member",
|
|
154
|
-
description:
|
|
207
|
+
description:
|
|
208
|
+
"The default for a new membership. Carries no administrative permission.",
|
|
155
209
|
isDefault: true,
|
|
156
210
|
order: 2,
|
|
157
211
|
permissions: [],
|
|
@@ -166,14 +220,20 @@ export const WORKSPACE_CREATOR_ROLES: string[] = ["admin", "member"];
|
|
|
166
220
|
/** Role slugs given to the seeded super_admin's membership. `member` is included so they're also an ordinary member. */
|
|
167
221
|
export const SEED_SUPERADMIN_ROLES: string[] = ["superadmin", "member"];
|
|
168
222
|
|
|
169
|
-
export type RbacWriter = Pick<
|
|
223
|
+
export type RbacWriter = Pick<
|
|
224
|
+
PrismaClient,
|
|
225
|
+
"permission" | "role" | "permissionRole"
|
|
226
|
+
>;
|
|
170
227
|
|
|
171
228
|
// `db` may be a transaction client — `WorkspaceRepository.create` passes one, so the workspace,
|
|
172
229
|
// its creator's membership, and its roles all land together or not at all. Idempotent and safe
|
|
173
230
|
// to run concurrently: every insert is `skipDuplicates` on the natural unique key.
|
|
174
231
|
// Takes bigint directly: both callers (`WorkspaceRepository.create`, `seed.ts`) already have the
|
|
175
232
|
// workspace row's id in hand from their own Prisma call.
|
|
176
|
-
export async function provisionDefaultRoles(
|
|
233
|
+
export async function provisionDefaultRoles(
|
|
234
|
+
db: RbacWriter,
|
|
235
|
+
workspaceId: bigint,
|
|
236
|
+
): Promise<void> {
|
|
177
237
|
await db.permission.createMany({
|
|
178
238
|
data: PERMISSION_SLUGS.map((slug) => ({
|
|
179
239
|
slug,
|
|
@@ -181,12 +241,17 @@ export async function provisionDefaultRoles(db: RbacWriter, workspaceId: bigint)
|
|
|
181
241
|
displayName: PERMISSION_CATALOG[slug].displayName,
|
|
182
242
|
description: PERMISSION_CATALOG[slug].description,
|
|
183
243
|
group: PERMISSION_CATALOG[slug].group,
|
|
184
|
-
groupOrder: PERMISSION_GROUP_ORDER.indexOf(
|
|
244
|
+
groupOrder: PERMISSION_GROUP_ORDER.indexOf(
|
|
245
|
+
PERMISSION_CATALOG[slug].group,
|
|
246
|
+
),
|
|
185
247
|
order: PERMISSION_CATALOG[slug].order,
|
|
186
248
|
})),
|
|
187
249
|
skipDuplicates: true,
|
|
188
250
|
});
|
|
189
|
-
const permissions = await db.permission.findMany({
|
|
251
|
+
const permissions = await db.permission.findMany({
|
|
252
|
+
where: { slug: { in: PERMISSION_SLUGS } },
|
|
253
|
+
select: { id: true, slug: true },
|
|
254
|
+
});
|
|
190
255
|
const permissionId = new Map(permissions.map((p) => [p.slug, p.id]));
|
|
191
256
|
|
|
192
257
|
await db.role.createMany({
|
|
@@ -201,11 +266,21 @@ export async function provisionDefaultRoles(db: RbacWriter, workspaceId: bigint)
|
|
|
201
266
|
})),
|
|
202
267
|
skipDuplicates: true,
|
|
203
268
|
});
|
|
204
|
-
const roles = await db.role.findMany({
|
|
269
|
+
const roles = await db.role.findMany({
|
|
270
|
+
where: { workspaceId, slug: { in: DEFAULT_ROLES.map((r) => r.slug) } },
|
|
271
|
+
select: { id: true, slug: true },
|
|
272
|
+
});
|
|
205
273
|
const seedBySlug = new Map(DEFAULT_ROLES.map((role) => [role.slug, role]));
|
|
206
274
|
|
|
207
275
|
const rolePermissions = roles.flatMap((role) =>
|
|
208
|
-
(seedBySlug.get(role.slug)?.permissions ?? []).map((slug) => ({
|
|
276
|
+
(seedBySlug.get(role.slug)?.permissions ?? []).map((slug) => ({
|
|
277
|
+
roleId: role.id,
|
|
278
|
+
permissionId: permissionId.get(slug)!,
|
|
279
|
+
})),
|
|
209
280
|
);
|
|
210
|
-
if (rolePermissions.length)
|
|
281
|
+
if (rolePermissions.length)
|
|
282
|
+
await db.permissionRole.createMany({
|
|
283
|
+
data: rolePermissions,
|
|
284
|
+
skipDuplicates: true,
|
|
285
|
+
});
|
|
211
286
|
}
|
|
@@ -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;
|
|
@@ -29,7 +34,10 @@ export interface AuditLogListFilter {
|
|
|
29
34
|
// "role_assigned" -> "Role assigned". Derived rather than passed in, so core never carries display text.
|
|
30
35
|
export function humanizeAction(action: string): string {
|
|
31
36
|
const words = action.split("_");
|
|
32
|
-
return [
|
|
37
|
+
return [
|
|
38
|
+
words[0].charAt(0).toUpperCase() + words[0].slice(1),
|
|
39
|
+
...words.slice(1),
|
|
40
|
+
].join(" ");
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/** An `AuditLog` row as `findMany` returns it. */
|
|
@@ -56,7 +64,10 @@ export class AuditLogRepository {
|
|
|
56
64
|
|
|
57
65
|
// workspaceId is left null for events core raises (login, logout, password reset, ...) —
|
|
58
66
|
// those happen to a user, not inside a workspace.
|
|
59
|
-
async append(
|
|
67
|
+
async append(
|
|
68
|
+
event: AuditEvent,
|
|
69
|
+
opts: { workspaceId?: string; remarks?: string } = {},
|
|
70
|
+
): Promise<void> {
|
|
60
71
|
await this.prisma.auditLog.create({
|
|
61
72
|
data: {
|
|
62
73
|
action: event.type,
|