@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
|
/**
|
|
@@ -48,24 +79,34 @@ const WORKSPACE_HEADER_DOC = {
|
|
|
48
79
|
@ApiBearerAuth()
|
|
49
80
|
export class WorkspaceController {
|
|
50
81
|
constructor(
|
|
51
|
-
@Inject(WorkspaceRepository)
|
|
82
|
+
@Inject(WorkspaceRepository)
|
|
83
|
+
private readonly workspaces: WorkspaceRepository,
|
|
52
84
|
@Inject(RbacRepository) private readonly rbac: RbacRepository,
|
|
53
85
|
) {}
|
|
54
86
|
|
|
55
87
|
@Post()
|
|
56
88
|
@Authenticated()
|
|
57
89
|
@UseGuards(AuthGuard)
|
|
58
|
-
@ApiOperation({
|
|
90
|
+
@ApiOperation({
|
|
91
|
+
summary: "Create a workspace",
|
|
92
|
+
description: "The creator becomes its first member, with the admin role.",
|
|
93
|
+
})
|
|
59
94
|
@ApiBody({ type: CreateWorkspaceDto })
|
|
60
95
|
@ApiResponse({ status: 201, type: WorkspaceSummaryDto })
|
|
61
96
|
async create(@Body() body: Record<string, unknown>, @Req() req: Request) {
|
|
62
|
-
return this.workspaces.create(
|
|
97
|
+
return this.workspaces.create(
|
|
98
|
+
req.auth!.sub,
|
|
99
|
+
requireString(body.name, "name"),
|
|
100
|
+
);
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
@Get()
|
|
66
104
|
@Authenticated()
|
|
67
105
|
@UseGuards(AuthGuard)
|
|
68
|
-
@ApiOperation({
|
|
106
|
+
@ApiOperation({
|
|
107
|
+
summary:
|
|
108
|
+
"List the workspaces the current user belongs to, with their roles in each",
|
|
109
|
+
})
|
|
69
110
|
@ApiResponse({ status: 200, type: [WorkspaceSummaryDto] })
|
|
70
111
|
async listMine(@Req() req: Request) {
|
|
71
112
|
return this.workspaces.listForUser(req.auth!.sub);
|
|
@@ -85,38 +126,64 @@ export class WorkspaceController {
|
|
|
85
126
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
86
127
|
@CheckAbility("members:manage")
|
|
87
128
|
@ApiHeader(WORKSPACE_HEADER_DOC)
|
|
88
|
-
@ApiOperation({
|
|
129
|
+
@ApiOperation({
|
|
130
|
+
summary: "[admin] Add an existing user to the named workspace",
|
|
131
|
+
})
|
|
89
132
|
@ApiBody({ type: AddMemberDto })
|
|
90
133
|
@ApiResponse({ status: 201, type: MembershipSummaryDto })
|
|
91
134
|
async addMember(@Body() body: Record<string, unknown>, @Req() req: Request) {
|
|
92
135
|
// No `roles` in the body means "whatever this workspace flags as the default role" — the
|
|
93
136
|
// default lives in a row, not in this line.
|
|
94
|
-
const roles =
|
|
95
|
-
|
|
137
|
+
const roles =
|
|
138
|
+
body.roles === undefined
|
|
139
|
+
? undefined
|
|
140
|
+
: requireStringArray(body.roles, "roles");
|
|
141
|
+
return this.workspaces.addMember(
|
|
142
|
+
req.authz!.workspaceId,
|
|
143
|
+
requireString(body.email, "email"),
|
|
144
|
+
roles,
|
|
145
|
+
);
|
|
96
146
|
}
|
|
97
147
|
|
|
98
148
|
@Put("members/:memberId/roles")
|
|
99
149
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
100
150
|
@CheckAbility("roles:assign")
|
|
101
151
|
@ApiHeader(WORKSPACE_HEADER_DOC)
|
|
102
|
-
@ApiOperation({
|
|
152
|
+
@ApiOperation({
|
|
153
|
+
summary: "[admin] Replace a member's roles within the named workspace",
|
|
154
|
+
})
|
|
103
155
|
@ApiParam({ name: "memberId" })
|
|
104
156
|
@ApiBody({ type: SetMemberRolesDto })
|
|
105
157
|
@ApiResponse({ status: 200, type: MemberRolesResponseDto })
|
|
106
|
-
async setMemberRoles(
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
async setMemberRoles(
|
|
159
|
+
@Param("memberId") memberId: string,
|
|
160
|
+
@Body() body: Record<string, unknown>,
|
|
161
|
+
@Req() req: Request,
|
|
162
|
+
) {
|
|
163
|
+
if (memberId === req.authz!.memberId)
|
|
164
|
+
throw new ForbiddenException("cannot change your own roles");
|
|
165
|
+
return this.rbac.setMemberRoles(
|
|
166
|
+
req.authz!.workspaceId,
|
|
167
|
+
memberId,
|
|
168
|
+
requireStringArray(body.roles, "roles"),
|
|
169
|
+
);
|
|
109
170
|
}
|
|
110
171
|
|
|
111
172
|
@Delete("members/:memberId")
|
|
112
173
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
113
174
|
@CheckAbility("members:manage")
|
|
114
175
|
@ApiHeader(WORKSPACE_HEADER_DOC)
|
|
115
|
-
@ApiOperation({
|
|
176
|
+
@ApiOperation({
|
|
177
|
+
summary: "[admin] Remove a member from the named workspace",
|
|
178
|
+
description: "Their direct permission grants go with the membership.",
|
|
179
|
+
})
|
|
116
180
|
@ApiParam({ name: "memberId" })
|
|
117
181
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
118
182
|
async removeMember(@Param("memberId") memberId: string, @Req() req: Request) {
|
|
119
|
-
if (memberId === req.authz!.memberId)
|
|
183
|
+
if (memberId === req.authz!.memberId)
|
|
184
|
+
throw new ForbiddenException(
|
|
185
|
+
"cannot remove yourself from a workspace you administer",
|
|
186
|
+
);
|
|
120
187
|
await this.workspaces.removeMember(req.authz!.workspaceId, memberId);
|
|
121
188
|
return { ok: true };
|
|
122
189
|
}
|
|
@@ -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
|
|
|
@@ -34,15 +43,23 @@ export interface AuthzContext {
|
|
|
34
43
|
* Idempotent: a route may legitimately sit behind both guards, and resolving twice would double
|
|
35
44
|
* the hot path's query count for no gain.
|
|
36
45
|
*/
|
|
37
|
-
async function resolve(
|
|
46
|
+
async function resolve(
|
|
47
|
+
rbac: RbacRepository,
|
|
48
|
+
cache: PermissionCache,
|
|
49
|
+
context: ExecutionContext,
|
|
50
|
+
): Promise<void> {
|
|
38
51
|
const req = context.switchToHttp().getRequest();
|
|
39
52
|
if (req.authz) return;
|
|
40
53
|
|
|
41
54
|
const workspaceId = req.headers[WORKSPACE_HEADER];
|
|
42
|
-
if (!req.auth || typeof workspaceId !== "string" || workspaceId.length === 0)
|
|
55
|
+
if (!req.auth || typeof workspaceId !== "string" || workspaceId.length === 0)
|
|
56
|
+
return;
|
|
43
57
|
|
|
44
58
|
const userId = req.auth.sub as string;
|
|
45
|
-
const authz = await cache.resolve<AuthzContext>(
|
|
59
|
+
const authz = await cache.resolve<AuthzContext>(
|
|
60
|
+
memberCacheKey(userId, workspaceId),
|
|
61
|
+
() => rbac.resolveAuthzContext(userId, workspaceId),
|
|
62
|
+
);
|
|
46
63
|
// Deliberately the same answer for "no such workspace" and "not your workspace": a caller
|
|
47
64
|
// outside a workspace must not be able to probe whether it exists.
|
|
48
65
|
if (!authz) throw new ForbiddenException("not a member of this workspace");
|
|
@@ -81,7 +98,8 @@ export class WorkspaceGuard implements CanActivate {
|
|
|
81
98
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
82
99
|
await resolve(this.rbac, this.cache, context);
|
|
83
100
|
const req = context.switchToHttp().getRequest();
|
|
84
|
-
if (!req.authz)
|
|
101
|
+
if (!req.authz)
|
|
102
|
+
throw new ForbiddenException(`${WORKSPACE_HEADER} header is required`);
|
|
85
103
|
return true;
|
|
86
104
|
}
|
|
87
105
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Inject, Injectable } from "@nestjs/common";
|
|
2
2
|
import { and, count, desc, eq, gte, lte, type SQL } from "drizzle-orm";
|
|
3
3
|
import type { AuditEvent } from "@/lib/auth/core/types.js";
|
|
4
|
-
import { DRIZZLE_DB, type Database } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { DRIZZLE_DB, type Database } from "../db.js";
|
|
5
|
+
import {
|
|
6
|
+
buildPageMeta,
|
|
7
|
+
normalizeLimit,
|
|
8
|
+
normalizePage,
|
|
9
|
+
type Paginated,
|
|
10
|
+
} from "../pagination.js";
|
|
11
|
+
import { auditLogs } from "../schema.js";
|
|
12
|
+
import { toIdOrNull, toIdOrUndefined } from "../id.helper.js";
|
|
8
13
|
|
|
9
14
|
export interface AuditLogEntry {
|
|
10
15
|
id: string;
|
|
@@ -50,7 +55,10 @@ export function toAuditLogEntry(row: AuditLogRow): AuditLogEntry {
|
|
|
50
55
|
/** "role_assigned" -> "Role assigned". Derived rather than passed in, so core never has to carry display text. */
|
|
51
56
|
export function humanizeAction(action: string): string {
|
|
52
57
|
const words = action.split("_");
|
|
53
|
-
return [
|
|
58
|
+
return [
|
|
59
|
+
words[0].charAt(0).toUpperCase() + words[0].slice(1),
|
|
60
|
+
...words.slice(1),
|
|
61
|
+
].join(" ");
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
@Injectable()
|
|
@@ -62,7 +70,10 @@ export class AuditLogRepository {
|
|
|
62
70
|
* those happen to a user, not inside a workspace. Callers that *are* acting in a workspace
|
|
63
71
|
* pass it, which is what scopes the admin audit-log read below.
|
|
64
72
|
*/
|
|
65
|
-
async append(
|
|
73
|
+
async append(
|
|
74
|
+
event: AuditEvent,
|
|
75
|
+
opts: { workspaceId?: string; remarks?: string } = {},
|
|
76
|
+
): Promise<void> {
|
|
66
77
|
await this.db.insert(auditLogs).values({
|
|
67
78
|
action: event.type,
|
|
68
79
|
name: humanizeAction(event.type),
|
|
@@ -80,12 +91,16 @@ export class AuditLogRepository {
|
|
|
80
91
|
|
|
81
92
|
const conditions: SQL[] = [];
|
|
82
93
|
const workspaceIdBig = toIdOrUndefined(filter.workspaceId);
|
|
83
|
-
if (workspaceIdBig !== undefined)
|
|
94
|
+
if (workspaceIdBig !== undefined)
|
|
95
|
+
conditions.push(eq(auditLogs.workspaceId, workspaceIdBig));
|
|
84
96
|
const userIdBig = toIdOrUndefined(filter.userId);
|
|
85
|
-
if (userIdBig !== undefined)
|
|
97
|
+
if (userIdBig !== undefined)
|
|
98
|
+
conditions.push(eq(auditLogs.userId, userIdBig));
|
|
86
99
|
if (filter.action) conditions.push(eq(auditLogs.action, filter.action));
|
|
87
|
-
if (filter.since)
|
|
88
|
-
|
|
100
|
+
if (filter.since)
|
|
101
|
+
conditions.push(gte(auditLogs.createdAt, new Date(filter.since)));
|
|
102
|
+
if (filter.until)
|
|
103
|
+
conditions.push(lte(auditLogs.createdAt, new Date(filter.until)));
|
|
89
104
|
const where = conditions.length ? and(...conditions) : undefined;
|
|
90
105
|
|
|
91
106
|
const rows = await this.db
|
|
@@ -95,7 +110,10 @@ export class AuditLogRepository {
|
|
|
95
110
|
.orderBy(desc(auditLogs.createdAt), desc(auditLogs.id))
|
|
96
111
|
.limit(limit)
|
|
97
112
|
.offset((page - 1) * limit);
|
|
98
|
-
const [{ value: total }] = await this.db
|
|
113
|
+
const [{ value: total }] = await this.db
|
|
114
|
+
.select({ value: count() })
|
|
115
|
+
.from(auditLogs)
|
|
116
|
+
.where(where);
|
|
99
117
|
|
|
100
118
|
return { items: rows, meta: buildPageMeta(page, limit, total) };
|
|
101
119
|
}
|