@simple-auth-kit/cli 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/copy.ts +1 -1
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +42 -11
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
ForbiddenException,
|
|
7
|
+
Get,
|
|
8
|
+
Inject,
|
|
9
|
+
Ip,
|
|
10
|
+
Param,
|
|
11
|
+
Patch,
|
|
12
|
+
Post,
|
|
13
|
+
Query,
|
|
14
|
+
Req,
|
|
15
|
+
UseGuards,
|
|
16
|
+
} from "@nestjs/common";
|
|
17
|
+
import {
|
|
18
|
+
ApiBearerAuth,
|
|
19
|
+
ApiBody,
|
|
20
|
+
ApiHeader,
|
|
21
|
+
ApiOperation,
|
|
22
|
+
ApiParam,
|
|
23
|
+
ApiQuery,
|
|
24
|
+
ApiResponse,
|
|
25
|
+
ApiTags,
|
|
26
|
+
} from "@nestjs/swagger";
|
|
3
27
|
import type { Request } from "express";
|
|
4
|
-
import { AbilityGuard } from "
|
|
5
|
-
import { AuthGuard } from "
|
|
6
|
-
import { AuthService } from "
|
|
7
|
-
import { WORKSPACE_HEADER, WorkspaceGuard } from "
|
|
8
|
-
import { CheckAbility } from "
|
|
9
|
-
import { WorkspaceRepository } from "
|
|
28
|
+
import { AbilityGuard } from "../guards/ability.guard.js";
|
|
29
|
+
import { AuthGuard } from "../guards/auth.guard.js";
|
|
30
|
+
import { AuthService } from "../services/auth.service.js";
|
|
31
|
+
import { WORKSPACE_HEADER, WorkspaceGuard } from "../guards/authz.guard.js";
|
|
32
|
+
import { CheckAbility } from "../route-tiers.js";
|
|
33
|
+
import { WorkspaceRepository } from "../repositories/workspace.repository.js";
|
|
10
34
|
import { hashPassword } from "@/lib/auth/core/crypto.js";
|
|
11
35
|
import {
|
|
12
36
|
AssignRoleDto,
|
|
@@ -23,23 +47,45 @@ import {
|
|
|
23
47
|
RoleSummaryDto,
|
|
24
48
|
UpdateRoleDto,
|
|
25
49
|
UpdateUserDto,
|
|
26
|
-
} from "
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import {
|
|
33
|
-
|
|
50
|
+
} from "../dto/auth.dto.js";
|
|
51
|
+
import {
|
|
52
|
+
AuditLogListResponseDto,
|
|
53
|
+
UserListResponseDto,
|
|
54
|
+
UserSummaryDto,
|
|
55
|
+
} from "../dto/admin.dto.js";
|
|
56
|
+
import {
|
|
57
|
+
CountryListResponseDto,
|
|
58
|
+
CountrySummaryDto,
|
|
59
|
+
CreateCountryDto,
|
|
60
|
+
UpdateCountryDto,
|
|
61
|
+
} from "../dto/country.dto.js";
|
|
62
|
+
import {
|
|
63
|
+
CreateCustomerDto,
|
|
64
|
+
CustomerListResponseDto,
|
|
65
|
+
CustomerSummaryDto,
|
|
66
|
+
UpdateCustomerDto,
|
|
67
|
+
} from "../dto/customer.dto.js";
|
|
68
|
+
import {
|
|
69
|
+
CreateLanguageDto,
|
|
70
|
+
LanguageListResponseDto,
|
|
71
|
+
LanguageSummaryDto,
|
|
72
|
+
UpdateLanguageDto,
|
|
73
|
+
} from "../dto/language.dto.js";
|
|
74
|
+
import { CountryRepository } from "../repositories/country.repository.js";
|
|
75
|
+
import { CustomerRepository } from "../repositories/customer.repository.js";
|
|
76
|
+
import { LanguageRepository } from "../repositories/language.repository.js";
|
|
34
77
|
|
|
35
78
|
function requireString(value: unknown, field: string): string {
|
|
36
|
-
if (typeof value !== "string" || value.length === 0)
|
|
79
|
+
if (typeof value !== "string" || value.length === 0)
|
|
80
|
+
throw new BadRequestException(`${field} is required`);
|
|
37
81
|
return value;
|
|
38
82
|
}
|
|
39
83
|
|
|
40
|
-
const optionalString = (value: unknown): string | undefined =>
|
|
84
|
+
const optionalString = (value: unknown): string | undefined =>
|
|
85
|
+
typeof value === "string" ? value : undefined;
|
|
41
86
|
|
|
42
|
-
const optionalBoolean = (value: unknown): boolean | undefined =>
|
|
87
|
+
const optionalBoolean = (value: unknown): boolean | undefined =>
|
|
88
|
+
typeof value === "boolean" ? value : undefined;
|
|
43
89
|
|
|
44
90
|
// Administration *of one workspace*, and nothing else. `WorkspaceGuard` resolves the caller's
|
|
45
91
|
// membership of the workspace named by the `X-Workspace-Id` header into the roles/permissions
|
|
@@ -49,12 +95,18 @@ const optionalBoolean = (value: unknown): boolean | undefined => (typeof value =
|
|
|
49
95
|
@ApiTags("auth")
|
|
50
96
|
@Controller("auth/admin")
|
|
51
97
|
@ApiBearerAuth()
|
|
52
|
-
@ApiHeader({
|
|
98
|
+
@ApiHeader({
|
|
99
|
+
name: WORKSPACE_HEADER,
|
|
100
|
+
required: true,
|
|
101
|
+
description:
|
|
102
|
+
"The workspace this request administers. The caller must be an admin member of it.",
|
|
103
|
+
})
|
|
53
104
|
@UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
|
|
54
105
|
export class AdminController {
|
|
55
106
|
constructor(
|
|
56
107
|
@Inject(AuthService) private readonly auth: AuthService,
|
|
57
|
-
@Inject(WorkspaceRepository)
|
|
108
|
+
@Inject(WorkspaceRepository)
|
|
109
|
+
private readonly workspaces: WorkspaceRepository,
|
|
58
110
|
@Inject(CountryRepository) private readonly countries: CountryRepository,
|
|
59
111
|
@Inject(LanguageRepository) private readonly languages: LanguageRepository,
|
|
60
112
|
@Inject(CustomerRepository) private readonly customers: CustomerRepository,
|
|
@@ -63,24 +115,48 @@ export class AdminController {
|
|
|
63
115
|
@Get("users")
|
|
64
116
|
@CheckAbility("users:read")
|
|
65
117
|
@ApiOperation({ summary: "[admin] List the members of this workspace" })
|
|
66
|
-
@ApiQuery({
|
|
67
|
-
|
|
68
|
-
|
|
118
|
+
@ApiQuery({
|
|
119
|
+
name: "search",
|
|
120
|
+
required: false,
|
|
121
|
+
description: "Email substring match",
|
|
122
|
+
})
|
|
123
|
+
@ApiQuery({
|
|
124
|
+
name: "page",
|
|
125
|
+
required: false,
|
|
126
|
+
description: "1-indexed. Defaults to 1.",
|
|
127
|
+
})
|
|
128
|
+
@ApiQuery({
|
|
129
|
+
name: "limit",
|
|
130
|
+
required: false,
|
|
131
|
+
description: "Defaults to 25, capped at 100.",
|
|
132
|
+
})
|
|
69
133
|
@ApiResponse({ status: 200, type: UserListResponseDto })
|
|
70
|
-
async listUsers(
|
|
71
|
-
|
|
134
|
+
async listUsers(
|
|
135
|
+
@Req() req: Request,
|
|
136
|
+
@Query("search") search?: string,
|
|
137
|
+
@Query("page") page?: string,
|
|
138
|
+
@Query("limit") limit?: string,
|
|
139
|
+
) {
|
|
140
|
+
return this.auth.listUsers(req.authz!, {
|
|
141
|
+
search,
|
|
142
|
+
page: page ? Number(page) : undefined,
|
|
143
|
+
limit: limit ? Number(limit) : undefined,
|
|
144
|
+
});
|
|
72
145
|
}
|
|
73
146
|
|
|
74
147
|
@Post("users")
|
|
75
148
|
@CheckAbility("users:manage")
|
|
76
149
|
@ApiOperation({
|
|
77
150
|
summary: "[admin] Create a user and add them to this workspace",
|
|
78
|
-
description:
|
|
151
|
+
description:
|
|
152
|
+
"No invitation email — the account is usable immediately with the password given here.",
|
|
79
153
|
})
|
|
80
154
|
@ApiBody({ type: CreateUserDto })
|
|
81
155
|
@ApiResponse({ status: 201, type: UserSummaryDto })
|
|
82
156
|
async createUser(@Body() body: Record<string, unknown>, @Req() req: Request) {
|
|
83
|
-
const passwordHash = await hashPassword(
|
|
157
|
+
const passwordHash = await hashPassword(
|
|
158
|
+
requireString(body.password, "password"),
|
|
159
|
+
);
|
|
84
160
|
const member = await this.workspaces.createMember(
|
|
85
161
|
req.authz!.workspaceId,
|
|
86
162
|
{
|
|
@@ -92,7 +168,11 @@ export class AdminController {
|
|
|
92
168
|
phone: optionalString(body.phone),
|
|
93
169
|
username: optionalString(body.username),
|
|
94
170
|
photo: optionalString(body.photo),
|
|
95
|
-
roles: Array.isArray(body.roles)
|
|
171
|
+
roles: Array.isArray(body.roles)
|
|
172
|
+
? body.roles.filter(
|
|
173
|
+
(role): role is string => typeof role === "string",
|
|
174
|
+
)
|
|
175
|
+
: undefined,
|
|
96
176
|
},
|
|
97
177
|
req.auth!.sub,
|
|
98
178
|
);
|
|
@@ -110,18 +190,28 @@ export class AdminController {
|
|
|
110
190
|
|
|
111
191
|
@Patch("users/:userId")
|
|
112
192
|
@CheckAbility("users:manage")
|
|
113
|
-
@ApiOperation({
|
|
193
|
+
@ApiOperation({
|
|
194
|
+
summary: "[admin] Edit a member's profile",
|
|
195
|
+
description:
|
|
196
|
+
"Profile fields only — email is the login identifier and is not editable here.",
|
|
197
|
+
})
|
|
114
198
|
@ApiParam({ name: "userId" })
|
|
115
199
|
@ApiBody({ type: UpdateUserDto })
|
|
116
200
|
@ApiResponse({ status: 200, type: UserSummaryDto })
|
|
117
|
-
async updateUser(
|
|
201
|
+
async updateUser(
|
|
202
|
+
@Param("userId") userId: string,
|
|
203
|
+
@Body() body: Record<string, unknown>,
|
|
204
|
+
@Req() req: Request,
|
|
205
|
+
) {
|
|
118
206
|
return this.auth.updateUser(
|
|
119
207
|
req.authz!,
|
|
120
208
|
userId,
|
|
121
209
|
{
|
|
122
|
-
firstName:
|
|
210
|
+
firstName:
|
|
211
|
+
body.firstName === null ? null : optionalString(body.firstName),
|
|
123
212
|
lastName: body.lastName === null ? null : optionalString(body.lastName),
|
|
124
|
-
displayName:
|
|
213
|
+
displayName:
|
|
214
|
+
body.displayName === null ? null : optionalString(body.displayName),
|
|
125
215
|
phone: body.phone === null ? null : optionalString(body.phone),
|
|
126
216
|
username: body.username === null ? null : optionalString(body.username),
|
|
127
217
|
photo: body.photo === null ? null : optionalString(body.photo),
|
|
@@ -141,21 +231,53 @@ export class AdminController {
|
|
|
141
231
|
@ApiParam({ name: "userId" })
|
|
142
232
|
@ApiBody({ type: DeleteReasonDto, required: false })
|
|
143
233
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
144
|
-
async deleteUser(
|
|
145
|
-
|
|
146
|
-
|
|
234
|
+
async deleteUser(
|
|
235
|
+
@Param("userId") userId: string,
|
|
236
|
+
@Body() body: Record<string, unknown>,
|
|
237
|
+
@Req() req: Request,
|
|
238
|
+
) {
|
|
239
|
+
if (userId === req.auth!.sub)
|
|
240
|
+
throw new ForbiddenException("cannot delete your own account");
|
|
241
|
+
await this.auth.deleteUser(
|
|
242
|
+
req.authz!,
|
|
243
|
+
userId,
|
|
244
|
+
req.auth!.sub,
|
|
245
|
+
optionalString(body?.reason),
|
|
246
|
+
);
|
|
147
247
|
return { ok: true };
|
|
148
248
|
}
|
|
149
249
|
|
|
150
250
|
@Get("audit-log")
|
|
151
251
|
@CheckAbility("audit-log:read")
|
|
152
|
-
@ApiOperation({
|
|
252
|
+
@ApiOperation({
|
|
253
|
+
summary: "[admin] List this workspace's audit log entries, newest first",
|
|
254
|
+
})
|
|
153
255
|
@ApiQuery({ name: "userId", required: false })
|
|
154
|
-
@ApiQuery({
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
256
|
+
@ApiQuery({
|
|
257
|
+
name: "action",
|
|
258
|
+
required: false,
|
|
259
|
+
description: "AuditEvent discriminant, e.g. 'role_assigned'",
|
|
260
|
+
})
|
|
261
|
+
@ApiQuery({
|
|
262
|
+
name: "since",
|
|
263
|
+
required: false,
|
|
264
|
+
description: "ISO 8601 timestamp",
|
|
265
|
+
})
|
|
266
|
+
@ApiQuery({
|
|
267
|
+
name: "until",
|
|
268
|
+
required: false,
|
|
269
|
+
description: "ISO 8601 timestamp",
|
|
270
|
+
})
|
|
271
|
+
@ApiQuery({
|
|
272
|
+
name: "page",
|
|
273
|
+
required: false,
|
|
274
|
+
description: "1-indexed. Defaults to 1.",
|
|
275
|
+
})
|
|
276
|
+
@ApiQuery({
|
|
277
|
+
name: "limit",
|
|
278
|
+
required: false,
|
|
279
|
+
description: "Defaults to 25, capped at 100.",
|
|
280
|
+
})
|
|
159
281
|
@ApiResponse({ status: 200, type: AuditLogListResponseDto })
|
|
160
282
|
async listAuditLog(
|
|
161
283
|
@Req() req: Request,
|
|
@@ -183,7 +305,13 @@ export class AdminController {
|
|
|
183
305
|
description:
|
|
184
306
|
"Grouped and ordered for a permission matrix. The catalog is global, like the Permission table: what is scoped to this workspace is which of its roles and memberships point at each row.",
|
|
185
307
|
})
|
|
186
|
-
@ApiQuery({
|
|
308
|
+
@ApiQuery({
|
|
309
|
+
name: "activeOnly",
|
|
310
|
+
required: false,
|
|
311
|
+
type: Boolean,
|
|
312
|
+
description:
|
|
313
|
+
"Pass true for a picker/dropdown — false or omitted returns everything, active or not.",
|
|
314
|
+
})
|
|
187
315
|
@ApiResponse({ status: 200, type: PermissionListResponseDto })
|
|
188
316
|
async listPermissions(@Query("activeOnly") activeOnly?: string) {
|
|
189
317
|
return this.auth.listPermissions(activeOnly === "true");
|
|
@@ -199,17 +327,23 @@ export class AdminController {
|
|
|
199
327
|
})
|
|
200
328
|
@ApiBody({ type: DefinePermissionDto })
|
|
201
329
|
@ApiResponse({ status: 201, type: PermissionSummaryDto })
|
|
202
|
-
async definePermission(
|
|
330
|
+
async definePermission(
|
|
331
|
+
@Body() body: Record<string, unknown>,
|
|
332
|
+
@Req() req: Request,
|
|
333
|
+
) {
|
|
203
334
|
return this.auth.definePermission(
|
|
204
335
|
{
|
|
205
336
|
slug: requireString(body.slug, "slug"),
|
|
206
337
|
name: optionalString(body.name),
|
|
207
338
|
displayName: optionalString(body.displayName),
|
|
208
|
-
description:
|
|
339
|
+
description:
|
|
340
|
+
body.description === null ? null : optionalString(body.description),
|
|
209
341
|
group: optionalString(body.group),
|
|
210
|
-
groupOrder:
|
|
342
|
+
groupOrder:
|
|
343
|
+
typeof body.groupOrder === "number" ? body.groupOrder : undefined,
|
|
211
344
|
order: typeof body.order === "number" ? body.order : undefined,
|
|
212
|
-
isActive:
|
|
345
|
+
isActive:
|
|
346
|
+
typeof body.isActive === "boolean" ? body.isActive : undefined,
|
|
213
347
|
},
|
|
214
348
|
req.auth!.sub,
|
|
215
349
|
);
|
|
@@ -218,9 +352,18 @@ export class AdminController {
|
|
|
218
352
|
@Get("roles")
|
|
219
353
|
@CheckAbility("roles:manage")
|
|
220
354
|
@ApiOperation({ summary: "[admin] List the roles this workspace defines" })
|
|
221
|
-
@ApiQuery({
|
|
355
|
+
@ApiQuery({
|
|
356
|
+
name: "activeOnly",
|
|
357
|
+
required: false,
|
|
358
|
+
type: Boolean,
|
|
359
|
+
description:
|
|
360
|
+
"Pass true for a picker/dropdown — false or omitted returns everything, active or not.",
|
|
361
|
+
})
|
|
222
362
|
@ApiResponse({ status: 200, type: RoleListResponseDto })
|
|
223
|
-
async listRoles(
|
|
363
|
+
async listRoles(
|
|
364
|
+
@Req() req: Request,
|
|
365
|
+
@Query("activeOnly") activeOnly?: string,
|
|
366
|
+
) {
|
|
224
367
|
return this.auth.listRoles(req.authz!, activeOnly === "true");
|
|
225
368
|
}
|
|
226
369
|
|
|
@@ -248,15 +391,21 @@ export class AdminController {
|
|
|
248
391
|
@ApiParam({ name: "roleId" })
|
|
249
392
|
@ApiBody({ type: UpdateRoleDto })
|
|
250
393
|
@ApiResponse({ status: 200, type: RoleSummaryDto })
|
|
251
|
-
async updateRole(
|
|
394
|
+
async updateRole(
|
|
395
|
+
@Param("roleId") roleId: string,
|
|
396
|
+
@Body() body: Record<string, unknown>,
|
|
397
|
+
@Req() req: Request,
|
|
398
|
+
) {
|
|
252
399
|
return this.auth.updateRole(
|
|
253
400
|
req.authz!,
|
|
254
401
|
roleId,
|
|
255
402
|
{
|
|
256
403
|
name: optionalString(body.name),
|
|
257
404
|
displayName: optionalString(body.displayName),
|
|
258
|
-
description:
|
|
259
|
-
|
|
405
|
+
description:
|
|
406
|
+
body.description === null ? null : optionalString(body.description),
|
|
407
|
+
isActive:
|
|
408
|
+
typeof body.isActive === "boolean" ? body.isActive : undefined,
|
|
260
409
|
},
|
|
261
410
|
req.auth!.sub,
|
|
262
411
|
);
|
|
@@ -266,93 +415,167 @@ export class AdminController {
|
|
|
266
415
|
@CheckAbility("roles:manage")
|
|
267
416
|
@ApiOperation({
|
|
268
417
|
summary: "[admin] Delete one of this workspace's roles",
|
|
269
|
-
description:
|
|
418
|
+
description:
|
|
419
|
+
"Soft-delete: existing assignments are left in place rather than cascade-deleted, and the role simply stops being resolved.",
|
|
270
420
|
})
|
|
271
421
|
@ApiParam({ name: "roleId" })
|
|
272
422
|
@ApiBody({ type: DeleteReasonDto, required: false })
|
|
273
423
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
274
|
-
async deleteRole(
|
|
275
|
-
|
|
424
|
+
async deleteRole(
|
|
425
|
+
@Param("roleId") roleId: string,
|
|
426
|
+
@Body() body: Record<string, unknown>,
|
|
427
|
+
@Req() req: Request,
|
|
428
|
+
) {
|
|
429
|
+
await this.auth.deleteRole(
|
|
430
|
+
req.authz!,
|
|
431
|
+
roleId,
|
|
432
|
+
req.auth!.sub,
|
|
433
|
+
optionalString(body?.reason),
|
|
434
|
+
);
|
|
276
435
|
return { ok: true };
|
|
277
436
|
}
|
|
278
437
|
|
|
279
438
|
@Post("roles/:roleId/permissions")
|
|
280
439
|
@CheckAbility("roles:manage")
|
|
281
|
-
@ApiOperation({
|
|
440
|
+
@ApiOperation({
|
|
441
|
+
summary: "[admin] Attach a permission to one of this workspace's roles",
|
|
442
|
+
})
|
|
282
443
|
@ApiParam({ name: "roleId" })
|
|
283
444
|
@ApiBody({ type: AttachPermissionDto })
|
|
284
445
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
285
|
-
async attachPermissionToRole(
|
|
286
|
-
|
|
446
|
+
async attachPermissionToRole(
|
|
447
|
+
@Param("roleId") roleId: string,
|
|
448
|
+
@Body() body: Record<string, unknown>,
|
|
449
|
+
@Req() req: Request,
|
|
450
|
+
) {
|
|
451
|
+
await this.auth.attachPermissionToRole(
|
|
452
|
+
req.authz!,
|
|
453
|
+
roleId,
|
|
454
|
+
requireString(body.permission, "permission"),
|
|
455
|
+
req.auth!.sub,
|
|
456
|
+
);
|
|
287
457
|
return { ok: true };
|
|
288
458
|
}
|
|
289
459
|
|
|
290
460
|
@Post("roles/:roleId/permissions/:permissionSlug/revoke")
|
|
291
461
|
@CheckAbility("roles:manage")
|
|
292
|
-
@ApiOperation({
|
|
462
|
+
@ApiOperation({
|
|
463
|
+
summary: "[admin] Detach a permission from one of this workspace's roles",
|
|
464
|
+
})
|
|
293
465
|
@ApiParam({ name: "roleId" })
|
|
294
466
|
@ApiParam({ name: "permissionSlug" })
|
|
295
467
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
296
|
-
async detachPermissionFromRole(
|
|
297
|
-
|
|
468
|
+
async detachPermissionFromRole(
|
|
469
|
+
@Param("roleId") roleId: string,
|
|
470
|
+
@Param("permissionSlug") permissionSlug: string,
|
|
471
|
+
@Req() req: Request,
|
|
472
|
+
) {
|
|
473
|
+
await this.auth.detachPermissionFromRole(
|
|
474
|
+
req.authz!,
|
|
475
|
+
roleId,
|
|
476
|
+
permissionSlug,
|
|
477
|
+
);
|
|
298
478
|
return { ok: true };
|
|
299
479
|
}
|
|
300
480
|
|
|
301
481
|
@Post("users/:userId/roles")
|
|
302
482
|
@CheckAbility("roles:assign")
|
|
303
|
-
@ApiOperation({
|
|
483
|
+
@ApiOperation({
|
|
484
|
+
summary: "[admin] Assign a role to a member of this workspace",
|
|
485
|
+
})
|
|
304
486
|
@ApiParam({ name: "userId" })
|
|
305
487
|
@ApiBody({ type: AssignRoleDto })
|
|
306
488
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
307
|
-
async assignRole(
|
|
308
|
-
|
|
489
|
+
async assignRole(
|
|
490
|
+
@Param("userId") userId: string,
|
|
491
|
+
@Body() body: Record<string, unknown>,
|
|
492
|
+
@Req() req: Request,
|
|
493
|
+
) {
|
|
494
|
+
await this.auth.assignRole(
|
|
495
|
+
req.authz!,
|
|
496
|
+
userId,
|
|
497
|
+
requireString(body.role, "role"),
|
|
498
|
+
);
|
|
309
499
|
return { ok: true };
|
|
310
500
|
}
|
|
311
501
|
|
|
312
502
|
@Post("users/:userId/roles/:roleSlug/revoke")
|
|
313
503
|
@CheckAbility("roles:assign")
|
|
314
|
-
@ApiOperation({
|
|
504
|
+
@ApiOperation({
|
|
505
|
+
summary: "[admin] Revoke a role from a member of this workspace",
|
|
506
|
+
})
|
|
315
507
|
@ApiParam({ name: "userId" })
|
|
316
508
|
@ApiParam({ name: "roleSlug" })
|
|
317
509
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
318
|
-
async revokeRole(
|
|
510
|
+
async revokeRole(
|
|
511
|
+
@Param("userId") userId: string,
|
|
512
|
+
@Param("roleSlug") roleSlug: string,
|
|
513
|
+
@Req() req: Request,
|
|
514
|
+
) {
|
|
319
515
|
// Revoking your own admin role would strip the very permission that authorized the call,
|
|
320
516
|
// with no way back in. Assigning to yourself is fine; it can't lock anyone out.
|
|
321
|
-
if (userId === req.auth!.sub)
|
|
517
|
+
if (userId === req.auth!.sub)
|
|
518
|
+
throw new ForbiddenException("cannot change your own roles");
|
|
322
519
|
await this.auth.revokeRole(req.authz!, userId, roleSlug);
|
|
323
520
|
return { ok: true };
|
|
324
521
|
}
|
|
325
522
|
|
|
326
523
|
@Post("users/:userId/permissions")
|
|
327
524
|
@CheckAbility("permissions:grant")
|
|
328
|
-
@ApiOperation({
|
|
525
|
+
@ApiOperation({
|
|
526
|
+
summary: "[admin] Grant a permission directly to a member, bypassing roles",
|
|
527
|
+
description: "The grant is scoped to this workspace.",
|
|
528
|
+
})
|
|
329
529
|
@ApiParam({ name: "userId" })
|
|
330
530
|
@ApiBody({ type: GrantPermissionDto })
|
|
331
531
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
332
|
-
async grantPermission(
|
|
333
|
-
|
|
532
|
+
async grantPermission(
|
|
533
|
+
@Param("userId") userId: string,
|
|
534
|
+
@Body() body: Record<string, unknown>,
|
|
535
|
+
@Req() req: Request,
|
|
536
|
+
) {
|
|
537
|
+
await this.auth.grantPermission(
|
|
538
|
+
req.authz!,
|
|
539
|
+
userId,
|
|
540
|
+
requireString(body.permission, "permission"),
|
|
541
|
+
req.auth!.sub,
|
|
542
|
+
);
|
|
334
543
|
return { ok: true };
|
|
335
544
|
}
|
|
336
545
|
|
|
337
546
|
@Post("users/:userId/permissions/:permissionSlug/revoke")
|
|
338
547
|
@CheckAbility("permissions:grant")
|
|
339
|
-
@ApiOperation({
|
|
548
|
+
@ApiOperation({
|
|
549
|
+
summary: "[admin] Revoke a direct permission grant from a member",
|
|
550
|
+
})
|
|
340
551
|
@ApiParam({ name: "userId" })
|
|
341
552
|
@ApiParam({ name: "permissionSlug" })
|
|
342
553
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
343
|
-
async revokePermission(
|
|
554
|
+
async revokePermission(
|
|
555
|
+
@Param("userId") userId: string,
|
|
556
|
+
@Param("permissionSlug") permissionSlug: string,
|
|
557
|
+
@Req() req: Request,
|
|
558
|
+
) {
|
|
344
559
|
await this.auth.revokePermission(req.authz!, userId, permissionSlug);
|
|
345
560
|
return { ok: true };
|
|
346
561
|
}
|
|
347
562
|
|
|
348
563
|
@Post("users/:userId/block")
|
|
349
564
|
@CheckAbility("users:block")
|
|
350
|
-
@ApiOperation({
|
|
351
|
-
|
|
565
|
+
@ApiOperation({
|
|
566
|
+
summary: "[admin] Block a member, revoking all their sessions immediately",
|
|
567
|
+
description:
|
|
568
|
+
"Blocking disables the whole account, so it is only allowed against a member of the workspace you administer.",
|
|
569
|
+
})
|
|
352
570
|
@ApiParam({ name: "userId" })
|
|
353
571
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
354
|
-
async block(
|
|
355
|
-
|
|
572
|
+
async block(
|
|
573
|
+
@Param("userId") userId: string,
|
|
574
|
+
@Req() req: Request,
|
|
575
|
+
@Ip() ip: string,
|
|
576
|
+
) {
|
|
577
|
+
if (userId === req.auth!.sub)
|
|
578
|
+
throw new ForbiddenException("cannot block your own account");
|
|
356
579
|
await this.auth.block(req.authz!, userId, { userId: req.auth!.sub, ip });
|
|
357
580
|
return { ok: true };
|
|
358
581
|
}
|
|
@@ -362,7 +585,11 @@ export class AdminController {
|
|
|
362
585
|
@ApiOperation({ summary: "[admin] Unblock a member" })
|
|
363
586
|
@ApiParam({ name: "userId" })
|
|
364
587
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
365
|
-
async unblock(
|
|
588
|
+
async unblock(
|
|
589
|
+
@Param("userId") userId: string,
|
|
590
|
+
@Req() req: Request,
|
|
591
|
+
@Ip() ip: string,
|
|
592
|
+
) {
|
|
366
593
|
await this.auth.unblock(req.authz!, userId, { userId: req.auth!.sub, ip });
|
|
367
594
|
return { ok: true };
|
|
368
595
|
}
|
|
@@ -370,14 +597,24 @@ export class AdminController {
|
|
|
370
597
|
@Post("users/:userId/deactivate")
|
|
371
598
|
@CheckAbility("users:block")
|
|
372
599
|
@ApiOperation({
|
|
373
|
-
summary:
|
|
374
|
-
|
|
600
|
+
summary:
|
|
601
|
+
"[admin] Deactivate a member, revoking all their sessions immediately",
|
|
602
|
+
description:
|
|
603
|
+
"Distinct from block/unblock — a routine administrative toggle, not a security action. Both independently deny login.",
|
|
375
604
|
})
|
|
376
605
|
@ApiParam({ name: "userId" })
|
|
377
606
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
378
|
-
async deactivate(
|
|
379
|
-
|
|
380
|
-
|
|
607
|
+
async deactivate(
|
|
608
|
+
@Param("userId") userId: string,
|
|
609
|
+
@Req() req: Request,
|
|
610
|
+
@Ip() ip: string,
|
|
611
|
+
) {
|
|
612
|
+
if (userId === req.auth!.sub)
|
|
613
|
+
throw new ForbiddenException("cannot deactivate your own account");
|
|
614
|
+
await this.auth.deactivate(req.authz!, userId, {
|
|
615
|
+
userId: req.auth!.sub,
|
|
616
|
+
ip,
|
|
617
|
+
});
|
|
381
618
|
return { ok: true };
|
|
382
619
|
}
|
|
383
620
|
|
|
@@ -386,7 +623,11 @@ export class AdminController {
|
|
|
386
623
|
@ApiOperation({ summary: "[admin] Reactivate a member" })
|
|
387
624
|
@ApiParam({ name: "userId" })
|
|
388
625
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
389
|
-
async activate(
|
|
626
|
+
async activate(
|
|
627
|
+
@Param("userId") userId: string,
|
|
628
|
+
@Req() req: Request,
|
|
629
|
+
@Ip() ip: string,
|
|
630
|
+
) {
|
|
390
631
|
await this.auth.activate(req.authz!, userId, { userId: req.auth!.sub, ip });
|
|
391
632
|
return { ok: true };
|
|
392
633
|
}
|
|
@@ -396,10 +637,28 @@ export class AdminController {
|
|
|
396
637
|
@Get("countries")
|
|
397
638
|
@CheckAbility("countries:read")
|
|
398
639
|
@ApiOperation({ summary: "[admin] List this workspace's countries" })
|
|
399
|
-
@ApiQuery({
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
640
|
+
@ApiQuery({
|
|
641
|
+
name: "search",
|
|
642
|
+
required: false,
|
|
643
|
+
description: "Matches name/code/isoCode",
|
|
644
|
+
})
|
|
645
|
+
@ApiQuery({
|
|
646
|
+
name: "page",
|
|
647
|
+
required: false,
|
|
648
|
+
description: "1-indexed. Defaults to 1.",
|
|
649
|
+
})
|
|
650
|
+
@ApiQuery({
|
|
651
|
+
name: "limit",
|
|
652
|
+
required: false,
|
|
653
|
+
description: "Defaults to 25, capped at 100.",
|
|
654
|
+
})
|
|
655
|
+
@ApiQuery({
|
|
656
|
+
name: "activeOnly",
|
|
657
|
+
required: false,
|
|
658
|
+
type: Boolean,
|
|
659
|
+
description:
|
|
660
|
+
"Pass true for a picker/dropdown — false or omitted returns everything, active or not.",
|
|
661
|
+
})
|
|
403
662
|
@ApiResponse({ status: 200, type: CountryListResponseDto })
|
|
404
663
|
async listCountries(
|
|
405
664
|
@Req() req: Request,
|
|
@@ -421,7 +680,10 @@ export class AdminController {
|
|
|
421
680
|
@ApiOperation({ summary: "[admin] Create a country in this workspace" })
|
|
422
681
|
@ApiBody({ type: CreateCountryDto })
|
|
423
682
|
@ApiResponse({ status: 201, type: CountrySummaryDto })
|
|
424
|
-
async createCountry(
|
|
683
|
+
async createCountry(
|
|
684
|
+
@Body() body: Record<string, unknown>,
|
|
685
|
+
@Req() req: Request,
|
|
686
|
+
) {
|
|
425
687
|
return this.countries.create(
|
|
426
688
|
req.authz!.workspaceId,
|
|
427
689
|
{
|
|
@@ -441,7 +703,9 @@ export class AdminController {
|
|
|
441
703
|
|
|
442
704
|
@Get("countries/:countryId")
|
|
443
705
|
@CheckAbility("countries:read")
|
|
444
|
-
@ApiOperation({
|
|
706
|
+
@ApiOperation({
|
|
707
|
+
summary: "[admin] Fetch a single country from this workspace",
|
|
708
|
+
})
|
|
445
709
|
@ApiParam({ name: "countryId" })
|
|
446
710
|
@ApiResponse({ status: 200, type: CountrySummaryDto })
|
|
447
711
|
async getCountry(@Param("countryId") countryId: string, @Req() req: Request) {
|
|
@@ -454,7 +718,11 @@ export class AdminController {
|
|
|
454
718
|
@ApiParam({ name: "countryId" })
|
|
455
719
|
@ApiBody({ type: UpdateCountryDto })
|
|
456
720
|
@ApiResponse({ status: 200, type: CountrySummaryDto })
|
|
457
|
-
async updateCountry(
|
|
721
|
+
async updateCountry(
|
|
722
|
+
@Param("countryId") countryId: string,
|
|
723
|
+
@Body() body: Record<string, unknown>,
|
|
724
|
+
@Req() req: Request,
|
|
725
|
+
) {
|
|
458
726
|
return this.countries.update(
|
|
459
727
|
req.authz!.workspaceId,
|
|
460
728
|
countryId,
|
|
@@ -475,12 +743,25 @@ export class AdminController {
|
|
|
475
743
|
|
|
476
744
|
@Delete("countries/:countryId")
|
|
477
745
|
@CheckAbility("countries:manage")
|
|
478
|
-
@ApiOperation({
|
|
746
|
+
@ApiOperation({
|
|
747
|
+
summary: "[admin] Delete a country from this workspace",
|
|
748
|
+
description:
|
|
749
|
+
"Soft-delete: the row survives for audit purposes and stops appearing in listings.",
|
|
750
|
+
})
|
|
479
751
|
@ApiParam({ name: "countryId" })
|
|
480
752
|
@ApiBody({ type: DeleteReasonDto, required: false })
|
|
481
753
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
482
|
-
async deleteCountry(
|
|
483
|
-
|
|
754
|
+
async deleteCountry(
|
|
755
|
+
@Param("countryId") countryId: string,
|
|
756
|
+
@Body() body: Record<string, unknown>,
|
|
757
|
+
@Req() req: Request,
|
|
758
|
+
) {
|
|
759
|
+
await this.countries.delete(
|
|
760
|
+
req.authz!.workspaceId,
|
|
761
|
+
countryId,
|
|
762
|
+
req.auth!.sub,
|
|
763
|
+
optionalString(body?.reason),
|
|
764
|
+
);
|
|
484
765
|
return { ok: true };
|
|
485
766
|
}
|
|
486
767
|
|
|
@@ -489,8 +770,16 @@ export class AdminController {
|
|
|
489
770
|
@ApiOperation({ summary: "[admin] Reactivate a country" })
|
|
490
771
|
@ApiParam({ name: "countryId" })
|
|
491
772
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
492
|
-
async activateCountry(
|
|
493
|
-
|
|
773
|
+
async activateCountry(
|
|
774
|
+
@Param("countryId") countryId: string,
|
|
775
|
+
@Req() req: Request,
|
|
776
|
+
) {
|
|
777
|
+
await this.countries.setActive(
|
|
778
|
+
req.authz!.workspaceId,
|
|
779
|
+
countryId,
|
|
780
|
+
true,
|
|
781
|
+
req.auth!.sub,
|
|
782
|
+
);
|
|
494
783
|
return { ok: true };
|
|
495
784
|
}
|
|
496
785
|
|
|
@@ -499,8 +788,16 @@ export class AdminController {
|
|
|
499
788
|
@ApiOperation({ summary: "[admin] Deactivate a country" })
|
|
500
789
|
@ApiParam({ name: "countryId" })
|
|
501
790
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
502
|
-
async deactivateCountry(
|
|
503
|
-
|
|
791
|
+
async deactivateCountry(
|
|
792
|
+
@Param("countryId") countryId: string,
|
|
793
|
+
@Req() req: Request,
|
|
794
|
+
) {
|
|
795
|
+
await this.countries.setActive(
|
|
796
|
+
req.authz!.workspaceId,
|
|
797
|
+
countryId,
|
|
798
|
+
false,
|
|
799
|
+
req.auth!.sub,
|
|
800
|
+
);
|
|
504
801
|
return { ok: true };
|
|
505
802
|
}
|
|
506
803
|
|
|
@@ -509,10 +806,28 @@ export class AdminController {
|
|
|
509
806
|
@Get("languages")
|
|
510
807
|
@CheckAbility("languages:read")
|
|
511
808
|
@ApiOperation({ summary: "[admin] List this workspace's languages" })
|
|
512
|
-
@ApiQuery({
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
809
|
+
@ApiQuery({
|
|
810
|
+
name: "search",
|
|
811
|
+
required: false,
|
|
812
|
+
description: "Matches name/code/nativeName",
|
|
813
|
+
})
|
|
814
|
+
@ApiQuery({
|
|
815
|
+
name: "page",
|
|
816
|
+
required: false,
|
|
817
|
+
description: "1-indexed. Defaults to 1.",
|
|
818
|
+
})
|
|
819
|
+
@ApiQuery({
|
|
820
|
+
name: "limit",
|
|
821
|
+
required: false,
|
|
822
|
+
description: "Defaults to 25, capped at 100.",
|
|
823
|
+
})
|
|
824
|
+
@ApiQuery({
|
|
825
|
+
name: "activeOnly",
|
|
826
|
+
required: false,
|
|
827
|
+
type: Boolean,
|
|
828
|
+
description:
|
|
829
|
+
"Pass true for a picker/dropdown — false or omitted returns everything, active or not.",
|
|
830
|
+
})
|
|
516
831
|
@ApiResponse({ status: 200, type: LanguageListResponseDto })
|
|
517
832
|
async listLanguages(
|
|
518
833
|
@Req() req: Request,
|
|
@@ -534,7 +849,10 @@ export class AdminController {
|
|
|
534
849
|
@ApiOperation({ summary: "[admin] Create a language in this workspace" })
|
|
535
850
|
@ApiBody({ type: CreateLanguageDto })
|
|
536
851
|
@ApiResponse({ status: 201, type: LanguageSummaryDto })
|
|
537
|
-
async createLanguage(
|
|
852
|
+
async createLanguage(
|
|
853
|
+
@Body() body: Record<string, unknown>,
|
|
854
|
+
@Req() req: Request,
|
|
855
|
+
) {
|
|
538
856
|
return this.languages.create(
|
|
539
857
|
req.authz!.workspaceId,
|
|
540
858
|
{
|
|
@@ -551,10 +869,15 @@ export class AdminController {
|
|
|
551
869
|
|
|
552
870
|
@Get("languages/:languageId")
|
|
553
871
|
@CheckAbility("languages:read")
|
|
554
|
-
@ApiOperation({
|
|
872
|
+
@ApiOperation({
|
|
873
|
+
summary: "[admin] Fetch a single language from this workspace",
|
|
874
|
+
})
|
|
555
875
|
@ApiParam({ name: "languageId" })
|
|
556
876
|
@ApiResponse({ status: 200, type: LanguageSummaryDto })
|
|
557
|
-
async getLanguage(
|
|
877
|
+
async getLanguage(
|
|
878
|
+
@Param("languageId") languageId: string,
|
|
879
|
+
@Req() req: Request,
|
|
880
|
+
) {
|
|
558
881
|
return this.languages.get(req.authz!.workspaceId, languageId);
|
|
559
882
|
}
|
|
560
883
|
|
|
@@ -564,7 +887,11 @@ export class AdminController {
|
|
|
564
887
|
@ApiParam({ name: "languageId" })
|
|
565
888
|
@ApiBody({ type: UpdateLanguageDto })
|
|
566
889
|
@ApiResponse({ status: 200, type: LanguageSummaryDto })
|
|
567
|
-
async updateLanguage(
|
|
890
|
+
async updateLanguage(
|
|
891
|
+
@Param("languageId") languageId: string,
|
|
892
|
+
@Body() body: Record<string, unknown>,
|
|
893
|
+
@Req() req: Request,
|
|
894
|
+
) {
|
|
568
895
|
return this.languages.update(
|
|
569
896
|
req.authz!.workspaceId,
|
|
570
897
|
languageId,
|
|
@@ -582,12 +909,25 @@ export class AdminController {
|
|
|
582
909
|
|
|
583
910
|
@Delete("languages/:languageId")
|
|
584
911
|
@CheckAbility("languages:manage")
|
|
585
|
-
@ApiOperation({
|
|
912
|
+
@ApiOperation({
|
|
913
|
+
summary: "[admin] Delete a language from this workspace",
|
|
914
|
+
description:
|
|
915
|
+
"Soft-delete: the row survives for audit purposes and stops appearing in listings.",
|
|
916
|
+
})
|
|
586
917
|
@ApiParam({ name: "languageId" })
|
|
587
918
|
@ApiBody({ type: DeleteReasonDto, required: false })
|
|
588
919
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
589
|
-
async deleteLanguage(
|
|
590
|
-
|
|
920
|
+
async deleteLanguage(
|
|
921
|
+
@Param("languageId") languageId: string,
|
|
922
|
+
@Body() body: Record<string, unknown>,
|
|
923
|
+
@Req() req: Request,
|
|
924
|
+
) {
|
|
925
|
+
await this.languages.delete(
|
|
926
|
+
req.authz!.workspaceId,
|
|
927
|
+
languageId,
|
|
928
|
+
req.auth!.sub,
|
|
929
|
+
optionalString(body?.reason),
|
|
930
|
+
);
|
|
591
931
|
return { ok: true };
|
|
592
932
|
}
|
|
593
933
|
|
|
@@ -596,8 +936,16 @@ export class AdminController {
|
|
|
596
936
|
@ApiOperation({ summary: "[admin] Reactivate a language" })
|
|
597
937
|
@ApiParam({ name: "languageId" })
|
|
598
938
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
599
|
-
async activateLanguage(
|
|
600
|
-
|
|
939
|
+
async activateLanguage(
|
|
940
|
+
@Param("languageId") languageId: string,
|
|
941
|
+
@Req() req: Request,
|
|
942
|
+
) {
|
|
943
|
+
await this.languages.setActive(
|
|
944
|
+
req.authz!.workspaceId,
|
|
945
|
+
languageId,
|
|
946
|
+
true,
|
|
947
|
+
req.auth!.sub,
|
|
948
|
+
);
|
|
601
949
|
return { ok: true };
|
|
602
950
|
}
|
|
603
951
|
|
|
@@ -606,8 +954,16 @@ export class AdminController {
|
|
|
606
954
|
@ApiOperation({ summary: "[admin] Deactivate a language" })
|
|
607
955
|
@ApiParam({ name: "languageId" })
|
|
608
956
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
609
|
-
async deactivateLanguage(
|
|
610
|
-
|
|
957
|
+
async deactivateLanguage(
|
|
958
|
+
@Param("languageId") languageId: string,
|
|
959
|
+
@Req() req: Request,
|
|
960
|
+
) {
|
|
961
|
+
await this.languages.setActive(
|
|
962
|
+
req.authz!.workspaceId,
|
|
963
|
+
languageId,
|
|
964
|
+
false,
|
|
965
|
+
req.auth!.sub,
|
|
966
|
+
);
|
|
611
967
|
return { ok: true };
|
|
612
968
|
}
|
|
613
969
|
|
|
@@ -617,10 +973,28 @@ export class AdminController {
|
|
|
617
973
|
@Get("customers")
|
|
618
974
|
@CheckAbility("customers:read")
|
|
619
975
|
@ApiOperation({ summary: "[admin] List this workspace's customers" })
|
|
620
|
-
@ApiQuery({
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
976
|
+
@ApiQuery({
|
|
977
|
+
name: "search",
|
|
978
|
+
required: false,
|
|
979
|
+
description: "Matches name/email/username/phone",
|
|
980
|
+
})
|
|
981
|
+
@ApiQuery({
|
|
982
|
+
name: "page",
|
|
983
|
+
required: false,
|
|
984
|
+
description: "1-indexed. Defaults to 1.",
|
|
985
|
+
})
|
|
986
|
+
@ApiQuery({
|
|
987
|
+
name: "limit",
|
|
988
|
+
required: false,
|
|
989
|
+
description: "Defaults to 25, capped at 100.",
|
|
990
|
+
})
|
|
991
|
+
@ApiQuery({
|
|
992
|
+
name: "activeOnly",
|
|
993
|
+
required: false,
|
|
994
|
+
type: Boolean,
|
|
995
|
+
description:
|
|
996
|
+
"Pass true for a picker/dropdown — false or omitted returns everything, active or not.",
|
|
997
|
+
})
|
|
624
998
|
@ApiResponse({ status: 200, type: CustomerListResponseDto })
|
|
625
999
|
async listCustomers(
|
|
626
1000
|
@Req() req: Request,
|
|
@@ -642,7 +1016,10 @@ export class AdminController {
|
|
|
642
1016
|
@ApiOperation({ summary: "[admin] Create a customer in this workspace" })
|
|
643
1017
|
@ApiBody({ type: CreateCustomerDto })
|
|
644
1018
|
@ApiResponse({ status: 201, type: CustomerSummaryDto })
|
|
645
|
-
async createCustomer(
|
|
1019
|
+
async createCustomer(
|
|
1020
|
+
@Body() body: Record<string, unknown>,
|
|
1021
|
+
@Req() req: Request,
|
|
1022
|
+
) {
|
|
646
1023
|
return this.customers.create(
|
|
647
1024
|
req.authz!.workspaceId,
|
|
648
1025
|
{
|
|
@@ -665,10 +1042,15 @@ export class AdminController {
|
|
|
665
1042
|
|
|
666
1043
|
@Get("customers/:customerId")
|
|
667
1044
|
@CheckAbility("customers:read")
|
|
668
|
-
@ApiOperation({
|
|
1045
|
+
@ApiOperation({
|
|
1046
|
+
summary: "[admin] Fetch a single customer from this workspace",
|
|
1047
|
+
})
|
|
669
1048
|
@ApiParam({ name: "customerId" })
|
|
670
1049
|
@ApiResponse({ status: 200, type: CustomerSummaryDto })
|
|
671
|
-
async getCustomer(
|
|
1050
|
+
async getCustomer(
|
|
1051
|
+
@Param("customerId") customerId: string,
|
|
1052
|
+
@Req() req: Request,
|
|
1053
|
+
) {
|
|
672
1054
|
return this.customers.get(req.authz!.workspaceId, customerId);
|
|
673
1055
|
}
|
|
674
1056
|
|
|
@@ -678,13 +1060,18 @@ export class AdminController {
|
|
|
678
1060
|
@ApiParam({ name: "customerId" })
|
|
679
1061
|
@ApiBody({ type: UpdateCustomerDto })
|
|
680
1062
|
@ApiResponse({ status: 200, type: CustomerSummaryDto })
|
|
681
|
-
async updateCustomer(
|
|
1063
|
+
async updateCustomer(
|
|
1064
|
+
@Param("customerId") customerId: string,
|
|
1065
|
+
@Body() body: Record<string, unknown>,
|
|
1066
|
+
@Req() req: Request,
|
|
1067
|
+
) {
|
|
682
1068
|
return this.customers.update(
|
|
683
1069
|
req.authz!.workspaceId,
|
|
684
1070
|
customerId,
|
|
685
1071
|
{
|
|
686
1072
|
email: optionalString(body.email),
|
|
687
|
-
firstName:
|
|
1073
|
+
firstName:
|
|
1074
|
+
body.firstName === null ? null : optionalString(body.firstName),
|
|
688
1075
|
lastName: body.lastName === null ? null : optionalString(body.lastName),
|
|
689
1076
|
username: body.username === null ? null : optionalString(body.username),
|
|
690
1077
|
phone: body.phone === null ? null : optionalString(body.phone),
|
|
@@ -702,12 +1089,25 @@ export class AdminController {
|
|
|
702
1089
|
|
|
703
1090
|
@Delete("customers/:customerId")
|
|
704
1091
|
@CheckAbility("customers:manage")
|
|
705
|
-
@ApiOperation({
|
|
1092
|
+
@ApiOperation({
|
|
1093
|
+
summary: "[admin] Delete a customer from this workspace",
|
|
1094
|
+
description:
|
|
1095
|
+
"Soft-delete: the row survives for audit purposes and stops appearing in listings.",
|
|
1096
|
+
})
|
|
706
1097
|
@ApiParam({ name: "customerId" })
|
|
707
1098
|
@ApiBody({ type: DeleteReasonDto, required: false })
|
|
708
1099
|
@ApiResponse({ status: 200, type: OkResponseDto })
|
|
709
|
-
async deleteCustomer(
|
|
710
|
-
|
|
1100
|
+
async deleteCustomer(
|
|
1101
|
+
@Param("customerId") customerId: string,
|
|
1102
|
+
@Body() body: Record<string, unknown>,
|
|
1103
|
+
@Req() req: Request,
|
|
1104
|
+
) {
|
|
1105
|
+
await this.customers.delete(
|
|
1106
|
+
req.authz!.workspaceId,
|
|
1107
|
+
customerId,
|
|
1108
|
+
req.auth!.sub,
|
|
1109
|
+
optionalString(body?.reason),
|
|
1110
|
+
);
|
|
711
1111
|
return { ok: true };
|
|
712
1112
|
}
|
|
713
1113
|
|
|
@@ -716,8 +1116,16 @@ export class AdminController {
|
|
|
716
1116
|
@ApiOperation({ summary: "[admin] Reactivate a customer" })
|
|
717
1117
|
@ApiParam({ name: "customerId" })
|
|
718
1118
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
719
|
-
async activateCustomer(
|
|
720
|
-
|
|
1119
|
+
async activateCustomer(
|
|
1120
|
+
@Param("customerId") customerId: string,
|
|
1121
|
+
@Req() req: Request,
|
|
1122
|
+
) {
|
|
1123
|
+
await this.customers.setActive(
|
|
1124
|
+
req.authz!.workspaceId,
|
|
1125
|
+
customerId,
|
|
1126
|
+
true,
|
|
1127
|
+
req.auth!.sub,
|
|
1128
|
+
);
|
|
721
1129
|
return { ok: true };
|
|
722
1130
|
}
|
|
723
1131
|
|
|
@@ -726,8 +1134,16 @@ export class AdminController {
|
|
|
726
1134
|
@ApiOperation({ summary: "[admin] Deactivate a customer" })
|
|
727
1135
|
@ApiParam({ name: "customerId" })
|
|
728
1136
|
@ApiResponse({ status: 201, type: OkResponseDto })
|
|
729
|
-
async deactivateCustomer(
|
|
730
|
-
|
|
1137
|
+
async deactivateCustomer(
|
|
1138
|
+
@Param("customerId") customerId: string,
|
|
1139
|
+
@Req() req: Request,
|
|
1140
|
+
) {
|
|
1141
|
+
await this.customers.setActive(
|
|
1142
|
+
req.authz!.workspaceId,
|
|
1143
|
+
customerId,
|
|
1144
|
+
false,
|
|
1145
|
+
req.auth!.sub,
|
|
1146
|
+
);
|
|
731
1147
|
return { ok: true };
|
|
732
1148
|
}
|
|
733
1149
|
}
|