@simple-auth-kit/cli 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +6 -4
- package/simple-auth-kit.ts +582 -124
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -3,27 +3,33 @@ import swaggerUi from "swagger-ui-express";
|
|
|
3
3
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
4
4
|
import { Pool } from "pg";
|
|
5
5
|
import type { RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
|
|
6
|
-
import { createAdminRouter } from "./admin.router.js";
|
|
7
|
-
import { AuditLogRepository } from "./audit-log.repository.js";
|
|
8
|
-
import { authCoreErrorMiddleware } from "./auth-core-error.middleware.js";
|
|
6
|
+
import { createAdminRouter } from "./routers/admin.router.js";
|
|
7
|
+
import { AuditLogRepository } from "./repositories/audit-log.repository.js";
|
|
8
|
+
import { authCoreErrorMiddleware } from "./middleware/auth-core-error.middleware.js";
|
|
9
9
|
import { AuthConfig, defaultAuthConfig } from "./auth.config.js";
|
|
10
|
-
import { createAuthMiddleware } from "./auth.middleware.js";
|
|
11
|
-
import { createAuthRouter } from "./auth.router.js";
|
|
12
|
-
import { AuthService } from "./auth.service.js";
|
|
13
|
-
import {
|
|
10
|
+
import { createAuthMiddleware } from "./middleware/auth.middleware.js";
|
|
11
|
+
import { createAuthRouter } from "./routers/auth.router.js";
|
|
12
|
+
import { AuthService } from "./services/auth.service.js";
|
|
13
|
+
import {
|
|
14
|
+
createAuthzMiddleware,
|
|
15
|
+
createWorkspaceMiddleware,
|
|
16
|
+
} from "./middleware/authz.middleware.js";
|
|
14
17
|
import { KeyProviderService } from "./key-provider.js";
|
|
15
|
-
import { OAuthRepository } from "./oauth.repository.js";
|
|
18
|
+
import { OAuthRepository } from "./repositories/oauth.repository.js";
|
|
16
19
|
import { openApiSpec } from "./openapi-spec.js";
|
|
17
|
-
import { PasswordResetRepository } from "./password-reset.repository.js";
|
|
18
|
-
import {
|
|
20
|
+
import { PasswordResetRepository } from "./repositories/password-reset.repository.js";
|
|
21
|
+
import {
|
|
22
|
+
InMemoryPermissionCacheStore,
|
|
23
|
+
PermissionCache,
|
|
24
|
+
} from "./permission-cache.js";
|
|
19
25
|
import { InMemoryRateLimitStore } from "./rate-limit.store.js";
|
|
20
|
-
import { RbacRepository } from "./rbac.repository.js";
|
|
21
|
-
import { responseEnvelope } from "./response-envelope.middleware.js";
|
|
26
|
+
import { RbacRepository } from "./repositories/rbac.repository.js";
|
|
27
|
+
import { responseEnvelope } from "./middleware/response-envelope.middleware.js";
|
|
22
28
|
import * as schema from "./schema.js";
|
|
23
|
-
import { SessionRepository } from "./session.repository.js";
|
|
24
|
-
import { TwoFactorRepository } from "./two-factor.repository.js";
|
|
25
|
-
import { createWorkspaceRouter } from "./workspace.router.js";
|
|
26
|
-
import { WorkspaceRepository } from "./workspace.repository.js";
|
|
29
|
+
import { SessionRepository } from "./repositories/session.repository.js";
|
|
30
|
+
import { TwoFactorRepository } from "./repositories/two-factor.repository.js";
|
|
31
|
+
import { createWorkspaceRouter } from "./routers/workspace.router.js";
|
|
32
|
+
import { WorkspaceRepository } from "./repositories/workspace.repository.js";
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* Factory wiring a Drizzle `NodePgDatabase` (from a `pg.Pool`) plus all the plain classes,
|
|
@@ -41,23 +47,41 @@ export function createAuthApp(config: Partial<AuthConfig> = {}): Express {
|
|
|
41
47
|
const keys = new KeyProviderService();
|
|
42
48
|
// Swap the store for a Redis-backed one by passing `rateLimitStore` in `config` — nothing in
|
|
43
49
|
// this library's source changes.
|
|
44
|
-
const rateLimit: RateLimitDeps =
|
|
50
|
+
const rateLimit: RateLimitDeps =
|
|
51
|
+
resolvedConfig.rateLimitStore ?? new InMemoryRateLimitStore();
|
|
45
52
|
// The cache seam. Swap the store for a Redis-backed one by passing `permissionCacheStore` in
|
|
46
53
|
// `config` — nothing in this library's source changes. Keys are namespaced simpleauthkit:authz:*.
|
|
47
|
-
const permissionCache = new PermissionCache(
|
|
54
|
+
const permissionCache = new PermissionCache(
|
|
55
|
+
resolvedConfig.permissionCacheStore ?? new InMemoryPermissionCacheStore(),
|
|
56
|
+
resolvedConfig,
|
|
57
|
+
);
|
|
48
58
|
const rbac = new RbacRepository(db, permissionCache);
|
|
49
59
|
const workspaces = new WorkspaceRepository(db, rbac);
|
|
50
60
|
const twoFactor = new TwoFactorRepository(db);
|
|
51
61
|
const oauth = new OAuthRepository(db);
|
|
52
62
|
const passwordReset = new PasswordResetRepository(db);
|
|
53
|
-
const authService = new AuthService(
|
|
63
|
+
const authService = new AuthService(
|
|
64
|
+
db,
|
|
65
|
+
sessions,
|
|
66
|
+
keys,
|
|
67
|
+
rateLimit,
|
|
68
|
+
auditLog,
|
|
69
|
+
rbac,
|
|
70
|
+
twoFactor,
|
|
71
|
+
oauth,
|
|
72
|
+
passwordReset,
|
|
73
|
+
resolvedConfig,
|
|
74
|
+
);
|
|
54
75
|
|
|
55
76
|
const requireAuth = createAuthMiddleware(keys, sessions);
|
|
56
77
|
// Roles belong to a workspace membership, resolved from the database on the request that names
|
|
57
78
|
// the workspace. `requireAuthz` tolerates a request that names none (GET /auth/me answers with
|
|
58
79
|
// empty roles); `requireWorkspace` does not. See authz.middleware.ts.
|
|
59
80
|
const requireAuthz = createAuthzMiddleware({ rbac, cache: permissionCache });
|
|
60
|
-
const requireWorkspace = createWorkspaceMiddleware({
|
|
81
|
+
const requireWorkspace = createWorkspaceMiddleware({
|
|
82
|
+
rbac,
|
|
83
|
+
cache: permissionCache,
|
|
84
|
+
});
|
|
61
85
|
|
|
62
86
|
if (!resolvedConfig.permissionCacheStore || !resolvedConfig.rateLimitStore) {
|
|
63
87
|
console.warn(
|
|
@@ -71,11 +95,33 @@ export function createAuthApp(config: Partial<AuthConfig> = {}): Express {
|
|
|
71
95
|
const app = express();
|
|
72
96
|
app.use(express.json());
|
|
73
97
|
app.use(responseEnvelope());
|
|
74
|
-
app.use(
|
|
75
|
-
|
|
98
|
+
app.use(
|
|
99
|
+
"/auth/admin",
|
|
100
|
+
createAdminRouter({
|
|
101
|
+
auth: authService,
|
|
102
|
+
workspaces,
|
|
103
|
+
authentication: requireAuth,
|
|
104
|
+
workspaceScope: requireWorkspace,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
app.use(
|
|
108
|
+
"/auth",
|
|
109
|
+
createAuthRouter({
|
|
110
|
+
auth: authService,
|
|
111
|
+
config: resolvedConfig,
|
|
112
|
+
authentication: requireAuth,
|
|
113
|
+
authorization: requireAuthz,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
76
116
|
app.use(
|
|
77
117
|
"/workspaces",
|
|
78
|
-
createWorkspaceRouter({
|
|
118
|
+
createWorkspaceRouter({
|
|
119
|
+
workspaces,
|
|
120
|
+
rbac,
|
|
121
|
+
authentication: requireAuth,
|
|
122
|
+
authorization: requireAuthz,
|
|
123
|
+
workspaceScope: requireWorkspace,
|
|
124
|
+
}),
|
|
79
125
|
);
|
|
80
126
|
// Hand-authored OpenAPI spec (see openapi-spec.ts) — this combo has no NestJS decorators to
|
|
81
127
|
// generate one from, so it's served as static JSON, same as the nestjs-* combos' /docs-json.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { NextFunction, Request, RequestHandler, Response } from "express";
|
|
2
|
-
import { defineAbilitiesFor } from "
|
|
3
|
-
import { HttpError } from "
|
|
4
|
-
import type { PermissionCache } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
2
|
+
import { defineAbilitiesFor } from "../ability.js";
|
|
3
|
+
import { HttpError } from "../http-error.js";
|
|
4
|
+
import type { PermissionCache } from "../permission-cache.js";
|
|
5
|
+
import {
|
|
6
|
+
memberCacheKey,
|
|
7
|
+
type RbacRepository,
|
|
8
|
+
} from "../repositories/rbac.repository.js";
|
|
9
|
+
import "../request-context.js";
|
|
7
10
|
|
|
8
11
|
export const WORKSPACE_HEADER = "x-workspace-id";
|
|
9
12
|
|
|
@@ -45,10 +48,14 @@ async function resolve(deps: AuthzMiddlewareDeps, req: Request): Promise<void> {
|
|
|
45
48
|
if (req.authz) return;
|
|
46
49
|
|
|
47
50
|
const workspaceId = req.headers[WORKSPACE_HEADER];
|
|
48
|
-
if (!req.auth || typeof workspaceId !== "string" || workspaceId.length === 0)
|
|
51
|
+
if (!req.auth || typeof workspaceId !== "string" || workspaceId.length === 0)
|
|
52
|
+
return;
|
|
49
53
|
|
|
50
54
|
const userId = req.auth.sub;
|
|
51
|
-
const authz = await deps.cache.resolve<AuthzContext>(
|
|
55
|
+
const authz = await deps.cache.resolve<AuthzContext>(
|
|
56
|
+
memberCacheKey(userId, workspaceId),
|
|
57
|
+
() => deps.rbac.resolveAuthzContext(userId, workspaceId),
|
|
58
|
+
);
|
|
52
59
|
// Deliberately the same answer for "no such workspace" and "not your workspace": a caller
|
|
53
60
|
// outside a workspace must not be able to probe whether it exists.
|
|
54
61
|
if (!authz) throw new HttpError(403, "not a member of this workspace");
|
|
@@ -65,8 +72,14 @@ async function resolve(deps: AuthzMiddlewareDeps, req: Request): Promise<void> {
|
|
|
65
72
|
*
|
|
66
73
|
* Replaces the reference combo's `AuthzGuard`.
|
|
67
74
|
*/
|
|
68
|
-
export function createAuthzMiddleware(
|
|
69
|
-
|
|
75
|
+
export function createAuthzMiddleware(
|
|
76
|
+
deps: AuthzMiddlewareDeps,
|
|
77
|
+
): RequestHandler {
|
|
78
|
+
return async function authzMiddleware(
|
|
79
|
+
req: Request,
|
|
80
|
+
_res: Response,
|
|
81
|
+
next: NextFunction,
|
|
82
|
+
): Promise<void> {
|
|
70
83
|
try {
|
|
71
84
|
await resolve(deps, req);
|
|
72
85
|
next();
|
|
@@ -81,11 +94,18 @@ export function createAuthzMiddleware(deps: AuthzMiddlewareDeps): RequestHandler
|
|
|
81
94
|
*
|
|
82
95
|
* Replaces the reference combo's `WorkspaceGuard`.
|
|
83
96
|
*/
|
|
84
|
-
export function createWorkspaceMiddleware(
|
|
85
|
-
|
|
97
|
+
export function createWorkspaceMiddleware(
|
|
98
|
+
deps: AuthzMiddlewareDeps,
|
|
99
|
+
): RequestHandler {
|
|
100
|
+
return async function workspaceMiddleware(
|
|
101
|
+
req: Request,
|
|
102
|
+
_res: Response,
|
|
103
|
+
next: NextFunction,
|
|
104
|
+
): Promise<void> {
|
|
86
105
|
try {
|
|
87
106
|
await resolve(deps, req);
|
|
88
|
-
if (!req.authz)
|
|
107
|
+
if (!req.authz)
|
|
108
|
+
throw new HttpError(403, `${WORKSPACE_HEADER} header is required`);
|
|
89
109
|
next();
|
|
90
110
|
} catch (err) {
|
|
91
111
|
next(err);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// The workspace-membership half of the OpenAPI document. Mirrors the reference combo's
|
|
2
2
|
// `dto/workspace.dto.ts`; merged into the administration fragment by openapi-admin.ts, which is
|
|
3
3
|
// what openapi-spec.ts assembles into the document.
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
errorResponse,
|
|
6
|
+
missingPermission,
|
|
7
|
+
requiresPermission,
|
|
8
|
+
type OpenApiFragment,
|
|
9
|
+
} from "./openapi-fragment.js";
|
|
10
|
+
import { WORKSPACE_HEADER } from "./middleware/authz.middleware.js";
|
|
6
11
|
|
|
7
12
|
/** Every route that acts *inside* a workspace names it with this header, never a path segment. */
|
|
8
13
|
export const workspaceHeaderParameter = {
|
|
@@ -10,7 +15,8 @@ export const workspaceHeaderParameter = {
|
|
|
10
15
|
in: "header",
|
|
11
16
|
required: true,
|
|
12
17
|
schema: { type: "string" },
|
|
13
|
-
description:
|
|
18
|
+
description:
|
|
19
|
+
"The workspace this request acts in. The caller must be a member of it.",
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
export const workspaceSpec: OpenApiFragment = {
|
|
@@ -18,7 +24,14 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
18
24
|
|
|
19
25
|
// `GET /auth/me` works with or without it: named, it answers with that workspace's roles;
|
|
20
26
|
// omitted, with none.
|
|
21
|
-
scopeParameters: [
|
|
27
|
+
scopeParameters: [
|
|
28
|
+
{
|
|
29
|
+
...workspaceHeaderParameter,
|
|
30
|
+
required: false,
|
|
31
|
+
description:
|
|
32
|
+
"Answer for this workspace. Omitted, `roles` and `permissions` come back empty.",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
22
35
|
|
|
23
36
|
schemas: {
|
|
24
37
|
CreateWorkspaceRequest: {
|
|
@@ -32,15 +45,27 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
32
45
|
type: "object",
|
|
33
46
|
required: ["email"],
|
|
34
47
|
properties: {
|
|
35
|
-
email: {
|
|
36
|
-
|
|
48
|
+
email: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description:
|
|
51
|
+
"Must already be a registered user — this library has no invite flow",
|
|
52
|
+
},
|
|
53
|
+
roles: {
|
|
54
|
+
type: "array",
|
|
55
|
+
items: { type: "string" },
|
|
56
|
+
description: 'Defaults to ["member"]',
|
|
57
|
+
},
|
|
37
58
|
},
|
|
38
59
|
},
|
|
39
60
|
SetMemberRolesRequest: {
|
|
40
61
|
type: "object",
|
|
41
62
|
required: ["roles"],
|
|
42
63
|
properties: {
|
|
43
|
-
roles: {
|
|
64
|
+
roles: {
|
|
65
|
+
type: "array",
|
|
66
|
+
items: { type: "string" },
|
|
67
|
+
description: "Replaces the member's whole role set",
|
|
68
|
+
},
|
|
44
69
|
},
|
|
45
70
|
},
|
|
46
71
|
WorkspaceSummary: {
|
|
@@ -50,7 +75,11 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
50
75
|
id: { type: "string" },
|
|
51
76
|
name: { type: "string" },
|
|
52
77
|
createdAt: { type: "string", format: "date-time" },
|
|
53
|
-
roles: {
|
|
78
|
+
roles: {
|
|
79
|
+
type: "array",
|
|
80
|
+
items: { type: "string" },
|
|
81
|
+
description: "The calling user's roles in this workspace",
|
|
82
|
+
},
|
|
54
83
|
},
|
|
55
84
|
},
|
|
56
85
|
MembershipSummary: {
|
|
@@ -84,23 +113,43 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
84
113
|
security: [{ bearerAuth: [] }],
|
|
85
114
|
requestBody: {
|
|
86
115
|
required: true,
|
|
87
|
-
content: {
|
|
116
|
+
content: {
|
|
117
|
+
"application/json": {
|
|
118
|
+
schema: { $ref: "#/components/schemas/CreateWorkspaceRequest" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
88
121
|
},
|
|
89
122
|
responses: {
|
|
90
|
-
"201": {
|
|
123
|
+
"201": {
|
|
124
|
+
description: "Created",
|
|
125
|
+
content: {
|
|
126
|
+
"application/json": {
|
|
127
|
+
schema: { $ref: "#/components/schemas/WorkspaceSummary" },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
91
131
|
"400": errorResponse("Missing required field"),
|
|
92
132
|
"401": errorResponse("Missing or invalid access token"),
|
|
93
133
|
},
|
|
94
134
|
},
|
|
95
135
|
get: {
|
|
96
136
|
tags: ["workspaces"],
|
|
97
|
-
summary:
|
|
98
|
-
|
|
137
|
+
summary:
|
|
138
|
+
"List the workspaces the current user belongs to, with their roles in each",
|
|
139
|
+
description:
|
|
140
|
+
"Deliberately not permission-gated: it acts outside every workspace and only ever answers with the caller's own memberships.",
|
|
99
141
|
security: [{ bearerAuth: [] }],
|
|
100
142
|
responses: {
|
|
101
143
|
"200": {
|
|
102
144
|
description: "OK",
|
|
103
|
-
content: {
|
|
145
|
+
content: {
|
|
146
|
+
"application/json": {
|
|
147
|
+
schema: {
|
|
148
|
+
type: "array",
|
|
149
|
+
items: { $ref: "#/components/schemas/WorkspaceSummary" },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
104
153
|
},
|
|
105
154
|
"401": errorResponse("Missing or invalid access token"),
|
|
106
155
|
},
|
|
@@ -110,16 +159,26 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
110
159
|
get: {
|
|
111
160
|
tags: ["workspaces"],
|
|
112
161
|
summary: "List the members of the named workspace",
|
|
113
|
-
description:
|
|
162
|
+
description:
|
|
163
|
+
"Gated on membership of the named workspace rather than on a permission — seeing who else is in a room you are in is not an administrative capability.",
|
|
114
164
|
security: [{ bearerAuth: [] }],
|
|
115
165
|
parameters: [workspaceHeaderParameter],
|
|
116
166
|
responses: {
|
|
117
167
|
"200": {
|
|
118
168
|
description: "OK",
|
|
119
|
-
content: {
|
|
169
|
+
content: {
|
|
170
|
+
"application/json": {
|
|
171
|
+
schema: {
|
|
172
|
+
type: "array",
|
|
173
|
+
items: { $ref: "#/components/schemas/MembershipSummary" },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
120
177
|
},
|
|
121
178
|
"401": errorResponse("Missing or invalid access token"),
|
|
122
|
-
"403": errorResponse(
|
|
179
|
+
"403": errorResponse(
|
|
180
|
+
"Not a member of this workspace, or no workspace named",
|
|
181
|
+
),
|
|
123
182
|
},
|
|
124
183
|
},
|
|
125
184
|
post: {
|
|
@@ -130,13 +189,27 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
130
189
|
parameters: [workspaceHeaderParameter],
|
|
131
190
|
requestBody: {
|
|
132
191
|
required: true,
|
|
133
|
-
content: {
|
|
192
|
+
content: {
|
|
193
|
+
"application/json": {
|
|
194
|
+
schema: { $ref: "#/components/schemas/AddMemberRequest" },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
134
197
|
},
|
|
135
198
|
responses: {
|
|
136
|
-
"201": {
|
|
199
|
+
"201": {
|
|
200
|
+
description: "Created",
|
|
201
|
+
content: {
|
|
202
|
+
"application/json": {
|
|
203
|
+
schema: { $ref: "#/components/schemas/MembershipSummary" },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
137
207
|
"400": errorResponse("Missing required field"),
|
|
138
208
|
"401": errorResponse("Missing or invalid access token"),
|
|
139
|
-
"403": missingPermission(
|
|
209
|
+
"403": missingPermission(
|
|
210
|
+
"members:manage",
|
|
211
|
+
"the request named no workspace you belong to",
|
|
212
|
+
),
|
|
140
213
|
"404": errorResponse("No user with that email"),
|
|
141
214
|
"409": errorResponse("Already a member of this workspace"),
|
|
142
215
|
},
|
|
@@ -150,17 +223,42 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
150
223
|
requiresPermission("roles:assign") +
|
|
151
224
|
" It deliberately shares that key with `POST /auth/admin/users/{userId}/roles` rather than minting one of its own: it is the same capability reached by a different path.",
|
|
152
225
|
security: [{ bearerAuth: [] }],
|
|
153
|
-
parameters: [
|
|
226
|
+
parameters: [
|
|
227
|
+
workspaceHeaderParameter,
|
|
228
|
+
{
|
|
229
|
+
name: "memberId",
|
|
230
|
+
in: "path",
|
|
231
|
+
required: true,
|
|
232
|
+
schema: { type: "string" },
|
|
233
|
+
},
|
|
234
|
+
],
|
|
154
235
|
requestBody: {
|
|
155
236
|
required: true,
|
|
156
|
-
content: {
|
|
237
|
+
content: {
|
|
238
|
+
"application/json": {
|
|
239
|
+
schema: { $ref: "#/components/schemas/SetMemberRolesRequest" },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
157
242
|
},
|
|
158
243
|
responses: {
|
|
159
|
-
"200": {
|
|
244
|
+
"200": {
|
|
245
|
+
description: "OK",
|
|
246
|
+
content: {
|
|
247
|
+
"application/json": {
|
|
248
|
+
schema: { $ref: "#/components/schemas/MemberRolesResponse" },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
160
252
|
"400": errorResponse("Missing required field"),
|
|
161
253
|
"401": errorResponse("Missing or invalid access token"),
|
|
162
|
-
"403": missingPermission(
|
|
163
|
-
|
|
254
|
+
"403": missingPermission(
|
|
255
|
+
"roles:assign",
|
|
256
|
+
"cannot change your own roles",
|
|
257
|
+
"the request named no workspace you belong to",
|
|
258
|
+
),
|
|
259
|
+
"404": errorResponse(
|
|
260
|
+
"Member not found in this workspace, or a role is not defined in it",
|
|
261
|
+
),
|
|
164
262
|
},
|
|
165
263
|
},
|
|
166
264
|
},
|
|
@@ -168,13 +266,34 @@ export const workspaceSpec: OpenApiFragment = {
|
|
|
168
266
|
delete: {
|
|
169
267
|
tags: ["workspaces"],
|
|
170
268
|
summary: "[admin] Remove a member from the named workspace",
|
|
171
|
-
description:
|
|
269
|
+
description:
|
|
270
|
+
requiresPermission("members:manage") +
|
|
271
|
+
" Their direct permission grants go with the membership.",
|
|
172
272
|
security: [{ bearerAuth: [] }],
|
|
173
|
-
parameters: [
|
|
273
|
+
parameters: [
|
|
274
|
+
workspaceHeaderParameter,
|
|
275
|
+
{
|
|
276
|
+
name: "memberId",
|
|
277
|
+
in: "path",
|
|
278
|
+
required: true,
|
|
279
|
+
schema: { type: "string" },
|
|
280
|
+
},
|
|
281
|
+
],
|
|
174
282
|
responses: {
|
|
175
|
-
"200": {
|
|
283
|
+
"200": {
|
|
284
|
+
description: "OK",
|
|
285
|
+
content: {
|
|
286
|
+
"application/json": {
|
|
287
|
+
schema: { $ref: "#/components/schemas/OkResponse" },
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
176
291
|
"401": errorResponse("Missing or invalid access token"),
|
|
177
|
-
"403": missingPermission(
|
|
292
|
+
"403": missingPermission(
|
|
293
|
+
"members:manage",
|
|
294
|
+
"cannot remove yourself from a workspace you administer",
|
|
295
|
+
"the request named no workspace you belong to",
|
|
296
|
+
),
|
|
178
297
|
"404": errorResponse("Member not found in this workspace"),
|
|
179
298
|
},
|
|
180
299
|
},
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { and, count, desc, eq, gte, lte, type SQL } from "drizzle-orm";
|
|
2
2
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
3
|
import type { AuditEvent } from "@/lib/auth/core/types.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
buildPageMeta,
|
|
6
|
+
normalizeLimit,
|
|
7
|
+
normalizePage,
|
|
8
|
+
type Paginated,
|
|
9
|
+
} from "../pagination.js";
|
|
10
|
+
import * as schema from "../schema.js";
|
|
11
|
+
import { toIdOrNull, toIdOrUndefined } from "../id.helper.js";
|
|
7
12
|
|
|
8
13
|
export interface AuditLogEntry {
|
|
9
14
|
id: string;
|
|
@@ -49,7 +54,10 @@ export function toAuditLogEntry(row: AuditLogRow): AuditLogEntry {
|
|
|
49
54
|
/** "role_assigned" -> "Role assigned". Derived rather than passed in, so core never has to carry display text. */
|
|
50
55
|
export function humanizeAction(action: string): string {
|
|
51
56
|
const words = action.split("_");
|
|
52
|
-
return [
|
|
57
|
+
return [
|
|
58
|
+
words[0].charAt(0).toUpperCase() + words[0].slice(1),
|
|
59
|
+
...words.slice(1),
|
|
60
|
+
].join(" ");
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
@@ -65,7 +73,10 @@ export class AuditLogRepository {
|
|
|
65
73
|
* those happen to a user, not inside a workspace. Callers that *are* acting in a workspace
|
|
66
74
|
* pass it, which is what scopes the admin audit-log read below.
|
|
67
75
|
*/
|
|
68
|
-
async append(
|
|
76
|
+
async append(
|
|
77
|
+
event: AuditEvent,
|
|
78
|
+
opts: { workspaceId?: string; remarks?: string } = {},
|
|
79
|
+
): Promise<void> {
|
|
69
80
|
await this.db.insert(schema.auditLogs).values({
|
|
70
81
|
action: event.type,
|
|
71
82
|
name: humanizeAction(event.type),
|
|
@@ -83,12 +94,17 @@ export class AuditLogRepository {
|
|
|
83
94
|
const conditions: SQL[] = [];
|
|
84
95
|
|
|
85
96
|
const workspaceIdBig = toIdOrUndefined(filter.workspaceId);
|
|
86
|
-
if (workspaceIdBig !== undefined)
|
|
97
|
+
if (workspaceIdBig !== undefined)
|
|
98
|
+
conditions.push(eq(schema.auditLogs.workspaceId, workspaceIdBig));
|
|
87
99
|
const userIdBig = toIdOrUndefined(filter.userId);
|
|
88
|
-
if (userIdBig !== undefined)
|
|
89
|
-
|
|
90
|
-
if (filter.
|
|
91
|
-
|
|
100
|
+
if (userIdBig !== undefined)
|
|
101
|
+
conditions.push(eq(schema.auditLogs.userId, userIdBig));
|
|
102
|
+
if (filter.action)
|
|
103
|
+
conditions.push(eq(schema.auditLogs.action, filter.action));
|
|
104
|
+
if (filter.since)
|
|
105
|
+
conditions.push(gte(schema.auditLogs.createdAt, new Date(filter.since)));
|
|
106
|
+
if (filter.until)
|
|
107
|
+
conditions.push(lte(schema.auditLogs.createdAt, new Date(filter.until)));
|
|
92
108
|
const where = conditions.length ? and(...conditions) : undefined;
|
|
93
109
|
|
|
94
110
|
const rows = await this.db
|
|
@@ -98,7 +114,10 @@ export class AuditLogRepository {
|
|
|
98
114
|
.orderBy(desc(schema.auditLogs.createdAt), desc(schema.auditLogs.id))
|
|
99
115
|
.limit(limit)
|
|
100
116
|
.offset((page - 1) * limit);
|
|
101
|
-
const [{ value: total }] = await this.db
|
|
117
|
+
const [{ value: total }] = await this.db
|
|
118
|
+
.select({ value: count() })
|
|
119
|
+
.from(schema.auditLogs)
|
|
120
|
+
.where(where);
|
|
102
121
|
|
|
103
122
|
return { items: rows, meta: buildPageMeta(page, limit, total) };
|
|
104
123
|
}
|