@simple-auth-kit/cli 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  3. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  4. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  5. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  6. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  7. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  8. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  9. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  10. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  11. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  12. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  13. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  14. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  15. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  16. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  17. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  18. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  19. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  20. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  23. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  26. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  27. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  28. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  29. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  30. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  31. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  32. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  33. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  34. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  35. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
  36. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  37. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
  38. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  39. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  40. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  41. package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
  42. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  43. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
  44. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  45. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  46. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
  47. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  48. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  49. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  50. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
  53. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  54. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  56. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  57. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  58. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  59. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  62. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  63. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  64. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  65. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  66. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  71. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  79. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  80. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  81. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  82. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  83. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  84. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  85. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  86. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  87. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  88. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  89. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  90. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
  91. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  92. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  93. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  94. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
  95. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  96. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  97. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  98. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  99. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  100. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
  101. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  102. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  115. package/registry.json +6 -4
  116. package/simple-auth-kit.ts +582 -124
  117. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  118. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  119. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  120. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  121. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  122. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  123. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  124. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  125. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  126. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  127. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  128. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  130. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  131. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  132. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
@@ -2,29 +2,36 @@ import { Pool } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
  import { DynamicModule, Module } from "@nestjs/common";
4
4
  import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
5
- import { AbilityGuard } from "./ability.guard.js";
6
- import { AdminController } from "./admin.controller.js";
7
- import { AuditLogRepository } from "./audit-log.repository.js";
8
- import { AuthCoreErrorFilter } from "./auth-core-error.filter.js";
5
+ import { AbilityGuard } from "./guards/ability.guard.js";
6
+ import { AdminController } from "./controllers/admin.controller.js";
7
+ import { AuditLogRepository } from "./repositories/audit-log.repository.js";
8
+ import { AuthCoreErrorFilter } from "./filters/auth-core-error.filter.js";
9
9
  import { AUTH_CONFIG, AuthConfig, defaultAuthConfig } from "./auth.config.js";
10
- import { AuthController } from "./auth.controller.js";
11
- import { AuthGuard } from "./auth.guard.js";
12
- import { AuthService } from "./auth.service.js";
13
- import { AuthzGuard, WorkspaceGuard } from "./authz.guard.js";
10
+ import { AuthController } from "./controllers/auth.controller.js";
11
+ import { AuthGuard } from "./guards/auth.guard.js";
12
+ import { AuthService } from "./services/auth.service.js";
13
+ import { AuthzGuard, WorkspaceGuard } from "./guards/authz.guard.js";
14
14
  import { DRIZZLE_DB } from "./db.js";
15
15
  import { KeyProviderService } from "./key-provider.js";
16
- import { OAuthRepository } from "./oauth.repository.js";
17
- import { PasswordResetRepository } from "./password-reset.repository.js";
18
- import { InMemoryPermissionCacheStore, PERMISSION_CACHE_STORE, PermissionCache } from "./permission-cache.js";
19
- import { InMemoryRateLimitStore, RATE_LIMIT_STORE } from "./rate-limit.store.js";
20
- import { RbacRepository } from "./rbac.repository.js";
21
- import { ResponseInterceptor } from "./response.interceptor.js";
16
+ import { OAuthRepository } from "./repositories/oauth.repository.js";
17
+ import { PasswordResetRepository } from "./repositories/password-reset.repository.js";
18
+ import {
19
+ InMemoryPermissionCacheStore,
20
+ PERMISSION_CACHE_STORE,
21
+ PermissionCache,
22
+ } from "./permission-cache.js";
23
+ import {
24
+ InMemoryRateLimitStore,
25
+ RATE_LIMIT_STORE,
26
+ } from "./rate-limit.store.js";
27
+ import { RbacRepository } from "./repositories/rbac.repository.js";
28
+ import { ResponseInterceptor } from "./interceptors/response.interceptor.js";
22
29
  import { assertEveryRouteDeclaresATier } from "./route-tiers.js";
23
30
  import * as schema from "./schema.js";
24
- import { SessionRepository } from "./session.repository.js";
25
- import { TwoFactorRepository } from "./two-factor.repository.js";
26
- import { WorkspaceController } from "./workspace.controller.js";
27
- import { WorkspaceRepository } from "./workspace.repository.js";
31
+ import { SessionRepository } from "./repositories/session.repository.js";
32
+ import { TwoFactorRepository } from "./repositories/two-factor.repository.js";
33
+ import { WorkspaceController } from "./controllers/workspace.controller.js";
34
+ import { WorkspaceRepository } from "./repositories/workspace.repository.js";
28
35
 
29
36
  /**
30
37
  * Every controller this library ships, in one place. It is both what the module registers and
@@ -41,7 +48,10 @@ export class AuthModule {
41
48
  // stops the process here, by name, rather than shipping as an open endpoint nobody noticed.
42
49
  // Nothing above this line allocates a connection pool or a port, so a failed boot leaves
43
50
  // nothing behind.
44
- assertEveryRouteDeclaresATier(AUTH_CONTROLLERS, { authentication: AuthGuard, ability: AbilityGuard });
51
+ assertEveryRouteDeclaresATier(AUTH_CONTROLLERS, {
52
+ authentication: AuthGuard,
53
+ ability: AbilityGuard,
54
+ });
45
55
 
46
56
  const pool = new Pool({ connectionString: process.env["DATABASE_URL"] });
47
57
  const db = drizzle(pool, { schema });
@@ -62,14 +72,21 @@ export class AuthModule {
62
72
  { provide: DRIZZLE_DB, useValue: db },
63
73
  // The cache seam. Swap the store for a Redis-backed one by passing `permissionCacheStore`
64
74
  // to forRoot — nothing in this library's source changes. Keys are namespaced simpleauthkit:authz:*.
65
- { provide: PERMISSION_CACHE_STORE, useValue: config.permissionCacheStore ?? new InMemoryPermissionCacheStore() },
75
+ {
76
+ provide: PERMISSION_CACHE_STORE,
77
+ useValue:
78
+ config.permissionCacheStore ?? new InMemoryPermissionCacheStore(),
79
+ },
66
80
  PermissionCache,
67
81
  { provide: APP_FILTER, useClass: AuthCoreErrorFilter },
68
82
  { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor },
69
83
  AuditLogRepository,
70
84
  SessionRepository,
71
85
  KeyProviderService,
72
- { provide: RATE_LIMIT_STORE, useValue: config.rateLimitStore ?? new InMemoryRateLimitStore() },
86
+ {
87
+ provide: RATE_LIMIT_STORE,
88
+ useValue: config.rateLimitStore ?? new InMemoryRateLimitStore(),
89
+ },
73
90
  RbacRepository,
74
91
  WorkspaceRepository,
75
92
  TwoFactorRepository,
@@ -81,7 +98,16 @@ export class AuthModule {
81
98
  WorkspaceGuard,
82
99
  AbilityGuard,
83
100
  ],
84
- exports: [AuthService, AuthGuard, AuthzGuard, WorkspaceGuard, AbilityGuard, PermissionCache, WorkspaceRepository, DRIZZLE_DB],
101
+ exports: [
102
+ AuthService,
103
+ AuthGuard,
104
+ AuthzGuard,
105
+ WorkspaceGuard,
106
+ AbilityGuard,
107
+ PermissionCache,
108
+ WorkspaceRepository,
109
+ DRIZZLE_DB,
110
+ ],
85
111
  };
86
112
  }
87
113
  }
@@ -1,12 +1,36 @@
1
- import { BadRequestException, Body, Controller, Delete, ForbiddenException, Get, Inject, Ip, Param, Patch, Post, Query, Req, UseGuards } from "@nestjs/common";
2
- import { ApiBearerAuth, ApiBody, ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
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 "./ability.guard.js";
5
- import { AuthGuard } from "./auth.guard.js";
6
- import { AuthService } from "./auth.service.js";
7
- import { WORKSPACE_HEADER, WorkspaceGuard } from "./authz.guard.js";
8
- import { CheckAbility } from "./route-tiers.js";
9
- import { WorkspaceRepository } from "./workspace.repository.js";
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,15 +47,21 @@ import {
23
47
  RoleSummaryDto,
24
48
  UpdateRoleDto,
25
49
  UpdateUserDto,
26
- } from "./dto/auth.dto.js";
27
- import { AuditLogListResponseDto, UserListResponseDto, UserSummaryDto } from "./dto/admin.dto.js";
50
+ } from "../dto/auth.dto.js";
51
+ import {
52
+ AuditLogListResponseDto,
53
+ UserListResponseDto,
54
+ UserSummaryDto,
55
+ } from "../dto/admin.dto.js";
28
56
 
29
57
  function requireString(value: unknown, field: string): string {
30
- if (typeof value !== "string" || value.length === 0) throw new BadRequestException(`${field} is required`);
58
+ if (typeof value !== "string" || value.length === 0)
59
+ throw new BadRequestException(`${field} is required`);
31
60
  return value;
32
61
  }
33
62
 
34
- const optionalString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
63
+ const optionalString = (value: unknown): string | undefined =>
64
+ typeof value === "string" ? value : undefined;
35
65
 
36
66
  /**
37
67
  * Administration *of one workspace*, and of nothing else. Authority is a permission, never a role
@@ -55,35 +85,65 @@ const optionalString = (value: unknown): string | undefined => (typeof value ===
55
85
  @ApiTags("auth")
56
86
  @Controller("auth/admin")
57
87
  @ApiBearerAuth()
58
- @ApiHeader({ name: WORKSPACE_HEADER, required: true, description: "The workspace this request administers. The caller must be an admin member of it." })
88
+ @ApiHeader({
89
+ name: WORKSPACE_HEADER,
90
+ required: true,
91
+ description:
92
+ "The workspace this request administers. The caller must be an admin member of it.",
93
+ })
59
94
  @UseGuards(AuthGuard, WorkspaceGuard, AbilityGuard)
60
95
  export class AdminController {
61
96
  constructor(
62
97
  @Inject(AuthService) private readonly auth: AuthService,
63
- @Inject(WorkspaceRepository) private readonly workspaces: WorkspaceRepository,
98
+ @Inject(WorkspaceRepository)
99
+ private readonly workspaces: WorkspaceRepository,
64
100
  ) {}
65
101
 
66
102
  @Get("users")
67
103
  @CheckAbility("users:read")
68
104
  @ApiOperation({ summary: "[admin] List the members of this workspace" })
69
- @ApiQuery({ name: "search", required: false, description: "Email substring match" })
70
- @ApiQuery({ name: "page", required: false, description: "1-indexed. Defaults to 1." })
71
- @ApiQuery({ name: "limit", required: false, description: "Defaults to 25, capped at 100." })
105
+ @ApiQuery({
106
+ name: "search",
107
+ required: false,
108
+ description: "Email substring match",
109
+ })
110
+ @ApiQuery({
111
+ name: "page",
112
+ required: false,
113
+ description: "1-indexed. Defaults to 1.",
114
+ })
115
+ @ApiQuery({
116
+ name: "limit",
117
+ required: false,
118
+ description: "Defaults to 25, capped at 100.",
119
+ })
72
120
  @ApiResponse({ status: 200, type: UserListResponseDto })
73
- async listUsers(@Req() req: Request, @Query("search") search?: string, @Query("page") page?: string, @Query("limit") limit?: string) {
74
- return this.auth.listUsers(req.authz!, { search, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined });
121
+ async listUsers(
122
+ @Req() req: Request,
123
+ @Query("search") search?: string,
124
+ @Query("page") page?: string,
125
+ @Query("limit") limit?: string,
126
+ ) {
127
+ return this.auth.listUsers(req.authz!, {
128
+ search,
129
+ page: page ? Number(page) : undefined,
130
+ limit: limit ? Number(limit) : undefined,
131
+ });
75
132
  }
76
133
 
77
134
  @Post("users")
78
135
  @CheckAbility("users:manage")
79
136
  @ApiOperation({
80
137
  summary: "[admin] Create a user and add them to this workspace",
81
- description: "No invitation email — the account is usable immediately with the password given here.",
138
+ description:
139
+ "No invitation email — the account is usable immediately with the password given here.",
82
140
  })
83
141
  @ApiBody({ type: CreateUserDto })
84
142
  @ApiResponse({ status: 201, type: UserSummaryDto })
85
143
  async createUser(@Body() body: Record<string, unknown>, @Req() req: Request) {
86
- const passwordHash = await hashPassword(requireString(body.password, "password"));
144
+ const passwordHash = await hashPassword(
145
+ requireString(body.password, "password"),
146
+ );
87
147
  const member = await this.workspaces.createMember(
88
148
  req.authz!.workspaceId,
89
149
  {
@@ -94,7 +154,11 @@ export class AdminController {
94
154
  displayName: optionalString(body.displayName),
95
155
  phone: optionalString(body.phone),
96
156
  username: optionalString(body.username),
97
- roles: Array.isArray(body.roles) ? body.roles.filter((role): role is string => typeof role === "string") : undefined,
157
+ roles: Array.isArray(body.roles)
158
+ ? body.roles.filter(
159
+ (role): role is string => typeof role === "string",
160
+ )
161
+ : undefined,
98
162
  },
99
163
  req.auth!.sub,
100
164
  );
@@ -112,18 +176,28 @@ export class AdminController {
112
176
 
113
177
  @Patch("users/:userId")
114
178
  @CheckAbility("users:manage")
115
- @ApiOperation({ summary: "[admin] Edit a member's profile", description: "Profile fields only — email is the login identifier and is not editable here." })
179
+ @ApiOperation({
180
+ summary: "[admin] Edit a member's profile",
181
+ description:
182
+ "Profile fields only — email is the login identifier and is not editable here.",
183
+ })
116
184
  @ApiParam({ name: "userId" })
117
185
  @ApiBody({ type: UpdateUserDto })
118
186
  @ApiResponse({ status: 200, type: UserSummaryDto })
119
- async updateUser(@Param("userId") userId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
187
+ async updateUser(
188
+ @Param("userId") userId: string,
189
+ @Body() body: Record<string, unknown>,
190
+ @Req() req: Request,
191
+ ) {
120
192
  return this.auth.updateUser(
121
193
  req.authz!,
122
194
  userId,
123
195
  {
124
- firstName: body.firstName === null ? null : optionalString(body.firstName),
196
+ firstName:
197
+ body.firstName === null ? null : optionalString(body.firstName),
125
198
  lastName: body.lastName === null ? null : optionalString(body.lastName),
126
- displayName: body.displayName === null ? null : optionalString(body.displayName),
199
+ displayName:
200
+ body.displayName === null ? null : optionalString(body.displayName),
127
201
  phone: body.phone === null ? null : optionalString(body.phone),
128
202
  username: body.username === null ? null : optionalString(body.username),
129
203
  photo: body.photo === null ? null : optionalString(body.photo),
@@ -143,21 +217,53 @@ export class AdminController {
143
217
  @ApiParam({ name: "userId" })
144
218
  @ApiBody({ type: DeleteReasonDto, required: false })
145
219
  @ApiResponse({ status: 200, type: OkResponseDto })
146
- async deleteUser(@Param("userId") userId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
147
- if (userId === req.auth!.sub) throw new ForbiddenException("cannot delete your own account");
148
- await this.auth.deleteUser(req.authz!, userId, req.auth!.sub, optionalString(body?.reason));
220
+ async deleteUser(
221
+ @Param("userId") userId: string,
222
+ @Body() body: Record<string, unknown>,
223
+ @Req() req: Request,
224
+ ) {
225
+ if (userId === req.auth!.sub)
226
+ throw new ForbiddenException("cannot delete your own account");
227
+ await this.auth.deleteUser(
228
+ req.authz!,
229
+ userId,
230
+ req.auth!.sub,
231
+ optionalString(body?.reason),
232
+ );
149
233
  return { ok: true };
150
234
  }
151
235
 
152
236
  @Get("audit-log")
153
237
  @CheckAbility("audit-log:read")
154
- @ApiOperation({ summary: "[admin] List this workspace's audit log entries, newest first" })
238
+ @ApiOperation({
239
+ summary: "[admin] List this workspace's audit log entries, newest first",
240
+ })
155
241
  @ApiQuery({ name: "userId", required: false })
156
- @ApiQuery({ name: "action", required: false, description: "AuditEvent discriminant, e.g. 'role_assigned'" })
157
- @ApiQuery({ name: "since", required: false, description: "ISO 8601 timestamp" })
158
- @ApiQuery({ name: "until", required: false, description: "ISO 8601 timestamp" })
159
- @ApiQuery({ name: "page", required: false, description: "1-indexed. Defaults to 1." })
160
- @ApiQuery({ name: "limit", required: false, description: "Defaults to 25, capped at 100." })
242
+ @ApiQuery({
243
+ name: "action",
244
+ required: false,
245
+ description: "AuditEvent discriminant, e.g. 'role_assigned'",
246
+ })
247
+ @ApiQuery({
248
+ name: "since",
249
+ required: false,
250
+ description: "ISO 8601 timestamp",
251
+ })
252
+ @ApiQuery({
253
+ name: "until",
254
+ required: false,
255
+ description: "ISO 8601 timestamp",
256
+ })
257
+ @ApiQuery({
258
+ name: "page",
259
+ required: false,
260
+ description: "1-indexed. Defaults to 1.",
261
+ })
262
+ @ApiQuery({
263
+ name: "limit",
264
+ required: false,
265
+ description: "Defaults to 25, capped at 100.",
266
+ })
161
267
  @ApiResponse({ status: 200, type: AuditLogListResponseDto })
162
268
  async listAuditLog(
163
269
  @Req() req: Request,
@@ -200,17 +306,23 @@ export class AdminController {
200
306
  })
201
307
  @ApiBody({ type: DefinePermissionDto })
202
308
  @ApiResponse({ status: 201, type: PermissionSummaryDto })
203
- async definePermission(@Body() body: Record<string, unknown>, @Req() req: Request) {
309
+ async definePermission(
310
+ @Body() body: Record<string, unknown>,
311
+ @Req() req: Request,
312
+ ) {
204
313
  return this.auth.definePermission(
205
314
  {
206
315
  slug: requireString(body.slug, "slug"),
207
316
  name: optionalString(body.name),
208
317
  displayName: optionalString(body.displayName),
209
- description: body.description === null ? null : optionalString(body.description),
318
+ description:
319
+ body.description === null ? null : optionalString(body.description),
210
320
  group: optionalString(body.group),
211
- groupOrder: typeof body.groupOrder === "number" ? body.groupOrder : undefined,
321
+ groupOrder:
322
+ typeof body.groupOrder === "number" ? body.groupOrder : undefined,
212
323
  order: typeof body.order === "number" ? body.order : undefined,
213
- isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
324
+ isActive:
325
+ typeof body.isActive === "boolean" ? body.isActive : undefined,
214
326
  },
215
327
  req.auth!.sub,
216
328
  );
@@ -248,15 +360,21 @@ export class AdminController {
248
360
  @ApiParam({ name: "roleId" })
249
361
  @ApiBody({ type: UpdateRoleDto })
250
362
  @ApiResponse({ status: 200, type: RoleSummaryDto })
251
- async updateRole(@Param("roleId") roleId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
363
+ async updateRole(
364
+ @Param("roleId") roleId: string,
365
+ @Body() body: Record<string, unknown>,
366
+ @Req() req: Request,
367
+ ) {
252
368
  return this.auth.updateRole(
253
369
  req.authz!,
254
370
  roleId,
255
371
  {
256
372
  name: optionalString(body.name),
257
373
  displayName: optionalString(body.displayName),
258
- description: body.description === null ? null : optionalString(body.description),
259
- isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
374
+ description:
375
+ body.description === null ? null : optionalString(body.description),
376
+ isActive:
377
+ typeof body.isActive === "boolean" ? body.isActive : undefined,
260
378
  },
261
379
  req.auth!.sub,
262
380
  );
@@ -266,83 +384,147 @@ export class AdminController {
266
384
  @CheckAbility("roles:manage")
267
385
  @ApiOperation({
268
386
  summary: "[admin] Delete one of this workspace's roles",
269
- description: "Soft-delete: existing assignments are left in place rather than cascade-deleted, and the role simply stops being resolved.",
387
+ description:
388
+ "Soft-delete: existing assignments are left in place rather than cascade-deleted, and the role simply stops being resolved.",
270
389
  })
271
390
  @ApiParam({ name: "roleId" })
272
391
  @ApiBody({ type: DeleteReasonDto, required: false })
273
392
  @ApiResponse({ status: 200, type: OkResponseDto })
274
- async deleteRole(@Param("roleId") roleId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
275
- await this.auth.deleteRole(req.authz!, roleId, req.auth!.sub, optionalString(body?.reason));
393
+ async deleteRole(
394
+ @Param("roleId") roleId: string,
395
+ @Body() body: Record<string, unknown>,
396
+ @Req() req: Request,
397
+ ) {
398
+ await this.auth.deleteRole(
399
+ req.authz!,
400
+ roleId,
401
+ req.auth!.sub,
402
+ optionalString(body?.reason),
403
+ );
276
404
  return { ok: true };
277
405
  }
278
406
 
279
407
  @Post("roles/:roleId/permissions")
280
408
  @CheckAbility("roles:manage")
281
- @ApiOperation({ summary: "[admin] Attach a permission to one of this workspace's roles" })
409
+ @ApiOperation({
410
+ summary: "[admin] Attach a permission to one of this workspace's roles",
411
+ })
282
412
  @ApiParam({ name: "roleId" })
283
413
  @ApiBody({ type: AttachPermissionDto })
284
414
  @ApiResponse({ status: 201, type: OkResponseDto })
285
- async attachPermissionToRole(@Param("roleId") roleId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
286
- await this.auth.attachPermissionToRole(req.authz!, roleId, requireString(body.permission, "permission"), req.auth!.sub);
415
+ async attachPermissionToRole(
416
+ @Param("roleId") roleId: string,
417
+ @Body() body: Record<string, unknown>,
418
+ @Req() req: Request,
419
+ ) {
420
+ await this.auth.attachPermissionToRole(
421
+ req.authz!,
422
+ roleId,
423
+ requireString(body.permission, "permission"),
424
+ req.auth!.sub,
425
+ );
287
426
  return { ok: true };
288
427
  }
289
428
 
290
429
  @Post("users/:userId/roles")
291
430
  @CheckAbility("roles:assign")
292
- @ApiOperation({ summary: "[admin] Assign a role to a member of this workspace" })
431
+ @ApiOperation({
432
+ summary: "[admin] Assign a role to a member of this workspace",
433
+ })
293
434
  @ApiParam({ name: "userId" })
294
435
  @ApiBody({ type: AssignRoleDto })
295
436
  @ApiResponse({ status: 201, type: OkResponseDto })
296
- async assignRole(@Param("userId") userId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
297
- await this.auth.assignRole(req.authz!, userId, requireString(body.role, "role"));
437
+ async assignRole(
438
+ @Param("userId") userId: string,
439
+ @Body() body: Record<string, unknown>,
440
+ @Req() req: Request,
441
+ ) {
442
+ await this.auth.assignRole(
443
+ req.authz!,
444
+ userId,
445
+ requireString(body.role, "role"),
446
+ );
298
447
  return { ok: true };
299
448
  }
300
449
 
301
450
  @Post("users/:userId/roles/:roleSlug/revoke")
302
451
  @CheckAbility("roles:assign")
303
- @ApiOperation({ summary: "[admin] Revoke a role from a member of this workspace" })
452
+ @ApiOperation({
453
+ summary: "[admin] Revoke a role from a member of this workspace",
454
+ })
304
455
  @ApiParam({ name: "userId" })
305
456
  @ApiParam({ name: "roleSlug" })
306
457
  @ApiResponse({ status: 201, type: OkResponseDto })
307
- async revokeRole(@Param("userId") userId: string, @Param("roleSlug") roleSlug: string, @Req() req: Request) {
458
+ async revokeRole(
459
+ @Param("userId") userId: string,
460
+ @Param("roleSlug") roleSlug: string,
461
+ @Req() req: Request,
462
+ ) {
308
463
  // Revoking your own `admin` would strip the very permission that authorised the call. In a
309
464
  // workspace there is no way back in afterwards, so the route refuses it the same way
310
465
  // self-block does. Assigning to yourself is fine; it can't lock anyone out.
311
- if (userId === req.auth!.sub) throw new ForbiddenException("cannot change your own roles");
466
+ if (userId === req.auth!.sub)
467
+ throw new ForbiddenException("cannot change your own roles");
312
468
  await this.auth.revokeRole(req.authz!, userId, roleSlug);
313
469
  return { ok: true };
314
470
  }
315
471
 
316
472
  @Post("users/:userId/permissions")
317
473
  @CheckAbility("permissions:grant")
318
- @ApiOperation({ summary: "[admin] Grant a permission directly to a member, bypassing roles", description: "The grant is scoped to this workspace." })
474
+ @ApiOperation({
475
+ summary: "[admin] Grant a permission directly to a member, bypassing roles",
476
+ description: "The grant is scoped to this workspace.",
477
+ })
319
478
  @ApiParam({ name: "userId" })
320
479
  @ApiBody({ type: GrantPermissionDto })
321
480
  @ApiResponse({ status: 201, type: OkResponseDto })
322
- async grantPermission(@Param("userId") userId: string, @Body() body: Record<string, unknown>, @Req() req: Request) {
323
- await this.auth.grantPermission(req.authz!, userId, requireString(body.permission, "permission"), req.auth!.sub);
481
+ async grantPermission(
482
+ @Param("userId") userId: string,
483
+ @Body() body: Record<string, unknown>,
484
+ @Req() req: Request,
485
+ ) {
486
+ await this.auth.grantPermission(
487
+ req.authz!,
488
+ userId,
489
+ requireString(body.permission, "permission"),
490
+ req.auth!.sub,
491
+ );
324
492
  return { ok: true };
325
493
  }
326
494
 
327
495
  @Post("users/:userId/permissions/:permissionSlug/revoke")
328
496
  @CheckAbility("permissions:grant")
329
- @ApiOperation({ summary: "[admin] Revoke a direct permission grant from a member" })
497
+ @ApiOperation({
498
+ summary: "[admin] Revoke a direct permission grant from a member",
499
+ })
330
500
  @ApiParam({ name: "userId" })
331
501
  @ApiParam({ name: "permissionSlug" })
332
502
  @ApiResponse({ status: 201, type: OkResponseDto })
333
- async revokePermission(@Param("userId") userId: string, @Param("permissionSlug") permissionSlug: string, @Req() req: Request) {
503
+ async revokePermission(
504
+ @Param("userId") userId: string,
505
+ @Param("permissionSlug") permissionSlug: string,
506
+ @Req() req: Request,
507
+ ) {
334
508
  await this.auth.revokePermission(req.authz!, userId, permissionSlug);
335
509
  return { ok: true };
336
510
  }
337
511
 
338
512
  @Post("users/:userId/block")
339
513
  @CheckAbility("users:block")
340
- @ApiOperation({ summary: "[admin] Block a member, revoking all their sessions immediately",
341
- description: "Blocking disables the whole account, so it is only allowed against a member of the workspace you administer." })
514
+ @ApiOperation({
515
+ summary: "[admin] Block a member, revoking all their sessions immediately",
516
+ description:
517
+ "Blocking disables the whole account, so it is only allowed against a member of the workspace you administer.",
518
+ })
342
519
  @ApiParam({ name: "userId" })
343
520
  @ApiResponse({ status: 201, type: OkResponseDto })
344
- async block(@Param("userId") userId: string, @Req() req: Request, @Ip() ip: string) {
345
- if (userId === req.auth!.sub) throw new ForbiddenException("cannot block your own account");
521
+ async block(
522
+ @Param("userId") userId: string,
523
+ @Req() req: Request,
524
+ @Ip() ip: string,
525
+ ) {
526
+ if (userId === req.auth!.sub)
527
+ throw new ForbiddenException("cannot block your own account");
346
528
  await this.auth.block(req.authz!, userId, { userId: req.auth!.sub, ip });
347
529
  return { ok: true };
348
530
  }
@@ -353,21 +535,34 @@ export class AdminController {
353
535
  @ApiParam({ name: "userId" })
354
536
  @ApiResponse({ status: 201, type: OkResponseDto })
355
537
  async unblock(@Param("userId") userId: string, @Req() req: Request) {
356
- await this.auth.unblock(req.authz!, userId, { userId: req.auth!.sub, ip: req.ip });
538
+ await this.auth.unblock(req.authz!, userId, {
539
+ userId: req.auth!.sub,
540
+ ip: req.ip,
541
+ });
357
542
  return { ok: true };
358
543
  }
359
544
 
360
545
  @Post("users/:userId/deactivate")
361
546
  @CheckAbility("users:block")
362
547
  @ApiOperation({
363
- summary: "[admin] Deactivate a member, revoking all their sessions immediately",
364
- description: "Distinct from block/unblock — a routine administrative toggle, not a security action. Both independently deny login.",
548
+ summary:
549
+ "[admin] Deactivate a member, revoking all their sessions immediately",
550
+ description:
551
+ "Distinct from block/unblock — a routine administrative toggle, not a security action. Both independently deny login.",
365
552
  })
366
553
  @ApiParam({ name: "userId" })
367
554
  @ApiResponse({ status: 201, type: OkResponseDto })
368
- async deactivate(@Param("userId") userId: string, @Req() req: Request, @Ip() ip: string) {
369
- if (userId === req.auth!.sub) throw new ForbiddenException("cannot deactivate your own account");
370
- await this.auth.deactivate(req.authz!, userId, { userId: req.auth!.sub, ip });
555
+ async deactivate(
556
+ @Param("userId") userId: string,
557
+ @Req() req: Request,
558
+ @Ip() ip: string,
559
+ ) {
560
+ if (userId === req.auth!.sub)
561
+ throw new ForbiddenException("cannot deactivate your own account");
562
+ await this.auth.deactivate(req.authz!, userId, {
563
+ userId: req.auth!.sub,
564
+ ip,
565
+ });
371
566
  return { ok: true };
372
567
  }
373
568
 
@@ -376,7 +571,11 @@ export class AdminController {
376
571
  @ApiOperation({ summary: "[admin] Reactivate a member" })
377
572
  @ApiParam({ name: "userId" })
378
573
  @ApiResponse({ status: 201, type: OkResponseDto })
379
- async activate(@Param("userId") userId: string, @Req() req: Request, @Ip() ip: string) {
574
+ async activate(
575
+ @Param("userId") userId: string,
576
+ @Req() req: Request,
577
+ @Ip() ip: string,
578
+ ) {
380
579
  await this.auth.activate(req.authz!, userId, { userId: req.auth!.sub, ip });
381
580
  return { ok: true };
382
581
  }