@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.
Files changed (134) hide show
  1. package/lib/copy.ts +1 -1
  2. package/package.json +1 -1
  3. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  4. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  5. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  6. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  7. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  8. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  9. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  10. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  11. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  12. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  13. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  14. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  15. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  16. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  17. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  18. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  19. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  20. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  23. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  26. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  27. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  28. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  29. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  30. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  31. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  32. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  33. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  34. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  35. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  36. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
  37. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  38. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
  39. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  40. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  41. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  42. package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
  43. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  44. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
  45. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  46. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  47. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
  48. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  49. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  50. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  53. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
  54. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  56. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  57. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  58. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  59. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  62. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  63. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  64. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  65. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  66. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  71. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  79. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  80. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  81. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  82. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  83. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  84. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  85. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  86. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  87. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  88. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  89. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  90. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  91. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
  92. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  93. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  94. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  95. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
  96. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  97. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  98. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  99. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  100. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  101. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
  102. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
  115. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  116. package/registry.json +4 -4
  117. package/simple-auth-kit.ts +42 -11
  118. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  119. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  120. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  121. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  122. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  123. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  124. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  125. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  126. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  127. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  128. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  129. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  130. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  131. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  132. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  134. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -1,29 +1,60 @@
1
- import { BadRequestException, Body, Controller, Delete, ForbiddenException, Get, Inject, Param, Post, Put, Req, UseGuards } from "@nestjs/common";
2
- import { ApiBearerAuth, ApiBody, ApiHeader, ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger";
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 "./ability.guard.js";
5
- import { AuthGuard } from "./auth.guard.js";
6
- import { WORKSPACE_HEADER, WorkspaceGuard } from "./authz.guard.js";
7
- import { Authenticated, CheckAbility } from "./route-tiers.js";
8
- import { OkResponseDto } from "./dto/auth.dto.js";
9
- import { AddMemberDto, CreateWorkspaceDto, MemberRolesResponseDto, MembershipSummaryDto, SetMemberRolesDto, WorkspaceSummaryDto } from "./dto/workspace.dto.js";
10
- import { RbacRepository } from "./rbac.repository.js";
11
- import { WorkspaceRepository } from "./workspace.repository.js";
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) throw new BadRequestException(`${field} is required`);
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")) throw new BadRequestException(`${field} must be an array of strings`);
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: "The workspace this request acts in. The caller must be a member of it.",
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) private readonly workspaces: 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({ summary: "Create a workspace", description: "The creator becomes its first member, with the admin role." })
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(req.auth!.sub, requireString(body.name, "name"));
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({ summary: "List the workspaces the current user belongs to, with their roles in each" })
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({ summary: "[admin] Add an existing user to the named workspace" })
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 = body.roles === undefined ? undefined : requireStringArray(body.roles, "roles");
83
- return this.workspaces.addMember(req.authz!.workspaceId, requireString(body.email, "email"), roles);
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({ summary: "[admin] Replace a member's roles within the named workspace" })
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(@Param("memberId") memberId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
95
- if (memberId === req.authz!.memberId) throw new ForbiddenException("cannot change your own roles");
96
- return this.rbac.setMemberRoles(req.authz!.workspaceId, memberId, requireStringArray(body.roles, "roles"));
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({ summary: "[admin] Remove a member from the named workspace", description: "Their direct permission grants go with the membership." })
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) throw new ForbiddenException("cannot remove yourself from a workspace you administer");
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 { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable } from "@nestjs/common";
2
- import { defineAbilitiesFor } from "./ability.js";
3
- import { PermissionCache } from "./permission-cache.js";
4
- import { memberCacheKey, RbacRepository } from "./rbac.repository.js";
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(rbac: RbacRepository, cache: PermissionCache, context: ExecutionContext): Promise<void> {
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) return;
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>(memberCacheKey(userId, workspaceId), () => rbac.resolveAuthzContext(userId, workspaceId));
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) throw new ForbiddenException(`${WORKSPACE_HEADER} header is required`);
83
+ if (!req.authz)
84
+ throw new ForbiddenException(`${WORKSPACE_HEADER} header is required`);
67
85
  return true;
68
86
  }
69
87
  }
@@ -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": { displayName: "List members", description: "GET /auth/admin/users, GET /auth/admin/users/:userId", group: "Users", order: 1 },
26
- "users:block": { displayName: "Block and unblock members", description: "POST /auth/admin/users/:userId/block, .../unblock", group: "Users", order: 2 },
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: "Assign and revoke a member's roles — POST /auth/admin/users/:userId/roles, .../roles/:roleSlug/revoke, PUT /workspaces/members/:memberId/roles",
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": { displayName: "Read the permission catalog", description: "GET /auth/admin/permissions", group: "Permissions", order: 1 },
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: "Create a permission, rename it, or deactivate it — POST /auth/admin/permissions. The authority that defines all the others.",
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: "Grant and revoke a permission straight to a member, bypassing roles — POST /auth/admin/users/:userId/permissions, .../permissions/:slug/revoke",
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: "POST /workspaces/members, DELETE /workspaces/members/:memberId",
80
+ description:
81
+ "POST /workspaces/members, DELETE /workspaces/members/:memberId",
63
82
  group: "Members",
64
83
  order: 1,
65
84
  },
66
- "audit-log:read": { displayName: "Read the audit log", description: "GET /auth/admin/audit-log", group: "Audit", order: 1 },
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": { displayName: "List countries", description: "GET /auth/admin/countries, GET /auth/admin/countries/:countryId", group: "Countries", order: 1 },
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: "POST /auth/admin/countries, PATCH /auth/admin/countries/:countryId, DELETE /auth/admin/countries/:countryId",
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: "POST /auth/admin/countries/:countryId/activate, .../deactivate",
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": { displayName: "List languages", description: "GET /auth/admin/languages, GET /auth/admin/languages/:languageId", group: "Languages", order: 1 },
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: "POST /auth/admin/languages, PATCH /auth/admin/languages/:languageId, DELETE /auth/admin/languages/:languageId",
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: "POST /auth/admin/languages/:languageId/activate, .../deactivate",
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": { displayName: "List customers", description: "GET /auth/admin/customers, GET /auth/admin/customers/:customerId", group: "Customers", order: 1 },
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: "POST /auth/admin/customers, PATCH /auth/admin/customers/:customerId, DELETE /auth/admin/customers/:customerId",
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: "POST /auth/admin/customers/:customerId/activate, .../deactivate",
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(PERMISSION_CATALOG) as PermissionSlug[];
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[] = [...new Set(PERMISSION_SLUGS.map((slug) => PERMISSION_CATALOG[slug].group))];
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: "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).",
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: "The default for a new membership. Carries no administrative permission.",
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<PrismaClient, "permission" | "role" | "permissionRole">;
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(db: RbacWriter, workspaceId: bigint): Promise<void> {
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(PERMISSION_CATALOG[slug].group),
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({ where: { slug: { in: PERMISSION_SLUGS } }, select: { id: true, slug: true } });
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({ where: { workspaceId, slug: { in: DEFAULT_ROLES.map((r) => r.slug) } }, select: { id: true, slug: true } });
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) => ({ roleId: role.id, permissionId: permissionId.get(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) await db.permissionRole.createMany({ data: rolePermissions, skipDuplicates: true });
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 "../generated/prisma/client.js";
4
- import { toIdOrNull, toIdOrUndefined } from "./id.helper.js";
5
- import { buildPageMeta, normalizeLimit, normalizePage, type Paginated } from "./pagination.js";
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 [words[0].charAt(0).toUpperCase() + words[0].slice(1), ...words.slice(1)].join(" ");
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(event: AuditEvent, opts: { workspaceId?: string; remarks?: string } = {}): Promise<void> {
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,