@bluealba/platform-cli 1.0.1 → 1.0.2
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/dist/index.js +277 -9
- package/docs/404.mdx +5 -0
- package/docs/architecture/api-explorer.mdx +478 -0
- package/docs/architecture/architecture-diagrams.mdx +12 -0
- package/docs/architecture/authentication-system.mdx +903 -0
- package/docs/architecture/authorization-system.mdx +886 -0
- package/docs/architecture/bootstrap.mdx +1442 -0
- package/docs/architecture/gateway-architecture.mdx +845 -0
- package/docs/architecture/multi-tenancy.mdx +1150 -0
- package/docs/architecture/overview.mdx +776 -0
- package/docs/architecture/scheduler.mdx +818 -0
- package/docs/architecture/shell.mdx +885 -0
- package/docs/architecture/ui-extension-points.mdx +781 -0
- package/docs/architecture/user-states.mdx +794 -0
- package/docs/development/overview.mdx +21 -0
- package/docs/development/workflow.mdx +914 -0
- package/docs/getting-started/core-concepts.mdx +892 -0
- package/docs/getting-started/installation.mdx +780 -0
- package/docs/getting-started/overview.mdx +83 -0
- package/docs/getting-started/quick-start.mdx +940 -0
- package/docs/guides/adding-documentation-sites.mdx +1367 -0
- package/docs/guides/creating-services.mdx +1736 -0
- package/docs/guides/creating-ui-modules.mdx +1860 -0
- package/docs/guides/identity-providers.mdx +1007 -0
- package/docs/guides/mermaid-diagrams.mdx +212 -0
- package/docs/guides/using-feature-flags.mdx +1059 -0
- package/docs/guides/working-with-rooms.mdx +566 -0
- package/docs/index.mdx +57 -0
- package/docs/platform-cli/commands.mdx +604 -0
- package/docs/platform-cli/overview.mdx +195 -0
- package/package.json +5 -2
- package/skills/ba-platform/platform-cli.skill.md +26 -0
- package/skills/ba-platform/platform.skill.md +35 -0
- package/templates/application-monorepo-template/gitignore +95 -0
- package/templates/bootstrap-service-template/gitignore +57 -0
- package/templates/bootstrap-service-template/src/main.ts +6 -16
- package/templates/customization-ui-module-template/gitignore +73 -0
- package/templates/nestjs-service-module-template/gitignore +56 -0
- package/templates/platform-init-template/{{platformName}}-core/gitignore +97 -0
- package/templates/react-ui-module-template/Dockerfile +1 -1
- package/templates/react-ui-module-template/caddy/Caddyfile +1 -1
- package/templates/react-ui-module-template/gitignore +72 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Gateway Service Architecture
|
|
3
|
+
description: Deep dive into the PAE NestJS Gateway Service - routing, proxying, HTTP/2, WebSocket, catalog integration, and platform context
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
7
|
+
|
|
8
|
+
The **PAE NestJS Gateway Service** is the central entry point for all platform traffic. Built with NestJS and Fastify, it handles authentication, authorization, routing, and request proxying to backend services and micro-frontends.
|
|
9
|
+
|
|
10
|
+
## Role and Responsibilities
|
|
11
|
+
|
|
12
|
+
The gateway acts as a **reverse proxy** and **API gateway** with the following responsibilities:
|
|
13
|
+
|
|
14
|
+
<CardGrid stagger>
|
|
15
|
+
<Card title="Traffic Entry Point" icon="star">
|
|
16
|
+
Single entry point for all HTTP and WebSocket traffic, handling SSL termination and HTTP/2
|
|
17
|
+
</Card>
|
|
18
|
+
|
|
19
|
+
<Card title="Authentication" icon="approve-check">
|
|
20
|
+
Validates JWT tokens, API keys, and service credentials for every request
|
|
21
|
+
</Card>
|
|
22
|
+
|
|
23
|
+
<Card title="Authorization" icon="shield">
|
|
24
|
+
Enforces RBAC policies and operation-based access control before proxying requests
|
|
25
|
+
</Card>
|
|
26
|
+
|
|
27
|
+
<Card title="Request Routing" icon="random">
|
|
28
|
+
Resolves target services dynamically using the catalog and forwards requests
|
|
29
|
+
</Card>
|
|
30
|
+
|
|
31
|
+
<Card title="Context Management" icon="seti:json">
|
|
32
|
+
Manages tenant, user, and application context throughout request lifecycle
|
|
33
|
+
</Card>
|
|
34
|
+
|
|
35
|
+
<Card title="Header Enrichment" icon="seti:config">
|
|
36
|
+
Adds platform context headers to forwarded requests for downstream services
|
|
37
|
+
</Card>
|
|
38
|
+
</CardGrid>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Architecture Overview
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ PAE NestJS Gateway │
|
|
47
|
+
│ │
|
|
48
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
49
|
+
│ │ HTTP/2 Server (Fastify) │ │
|
|
50
|
+
│ │ SSL/TLS Termination │ │
|
|
51
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
52
|
+
│ │ │
|
|
53
|
+
│ ┌────────────────────▼───────────────────────────────────────┐ │
|
|
54
|
+
│ │ Authentication Middleware │ │
|
|
55
|
+
│ │ • JWT Validation (Cookie-based) │ │
|
|
56
|
+
│ │ • API Key Validation │ │
|
|
57
|
+
│ │ • Service-to-Service Auth │ │
|
|
58
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
59
|
+
│ │ │
|
|
60
|
+
│ ┌────────────────────▼───────────────────────────────────────┐ │
|
|
61
|
+
│ │ Tenant Resolution Middleware │ │
|
|
62
|
+
│ │ • Subdomain Pattern Matching │ │
|
|
63
|
+
│ │ • URL Pattern Matching │ │
|
|
64
|
+
│ │ • Cookie/Header-based Resolution │ │
|
|
65
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
66
|
+
│ │ │
|
|
67
|
+
│ ┌────────────────────▼───────────────────────────────────────┐ │
|
|
68
|
+
│ │ Platform Context Setup │ │
|
|
69
|
+
│ │ • Store tenant, user, app in request context │ │
|
|
70
|
+
│ │ • Validate tenant access for user │ │
|
|
71
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
72
|
+
│ │ │
|
|
73
|
+
│ ┌────────────────────▼───────────────────────────────────────┐ │
|
|
74
|
+
│ │ Catalog Route Resolution │ │
|
|
75
|
+
│ │ • Query catalog for target module │ │
|
|
76
|
+
│ │ • Match request path to module routes │ │
|
|
77
|
+
│ │ • Handle tenant-based module restrictions │ │
|
|
78
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
79
|
+
│ │ │
|
|
80
|
+
│ ┌────────────────────▼───────────────────────────────────────┐ │
|
|
81
|
+
│ │ Authorization Guard │ │
|
|
82
|
+
│ │ • Check user has required operations │ │
|
|
83
|
+
│ │ • Evaluate authorization rules │ │
|
|
84
|
+
│ │ • Enforce tenant-scoped access │ │
|
|
85
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
86
|
+
│ │ │
|
|
87
|
+
│ ┌────────────────────▼───────────────────────────────────────┐ │
|
|
88
|
+
│ │ Proxy Middleware │ │
|
|
89
|
+
│ │ • Add forwarding headers │ │
|
|
90
|
+
│ │ • Sign requests for cloud functions │ │
|
|
91
|
+
│ │ • Forward to target service │ │
|
|
92
|
+
│ └────────────────────┬───────────────────────────────────────┘ │
|
|
93
|
+
└────────────────────────┼───────────────────────────────────────┘
|
|
94
|
+
│
|
|
95
|
+
┌────────────────┼────────────────┐
|
|
96
|
+
▼ ▼ ▼
|
|
97
|
+
┌─────────┐ ┌──────────┐ ┌──────────────┐
|
|
98
|
+
│ Service │ │ UI │ │ Lambda │
|
|
99
|
+
│ A │ │ Shell │ │ Function │
|
|
100
|
+
└─────────┘ └──────────┘ └──────────────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Core Modules
|
|
106
|
+
|
|
107
|
+
### 1. Authentication Module
|
|
108
|
+
|
|
109
|
+
Handles all authentication mechanisms.
|
|
110
|
+
|
|
111
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/authentication/`
|
|
112
|
+
|
|
113
|
+
**Key Components**:
|
|
114
|
+
- `authentication.service.ts` - Core authentication logic
|
|
115
|
+
- `guards/auth.guard.ts` - Authentication guard for routes
|
|
116
|
+
- `guards/auth.middleware.ts` - Authentication middleware
|
|
117
|
+
- `api-keys/` - API key authentication
|
|
118
|
+
- `oauth-providers/` - Multi-IDP OAuth support
|
|
119
|
+
- `impersonation/` - User impersonation feature
|
|
120
|
+
|
|
121
|
+
**Supported Authentication Methods**:
|
|
122
|
+
|
|
123
|
+
<Tabs>
|
|
124
|
+
<TabItem label="JWT Cookie">
|
|
125
|
+
```typescript
|
|
126
|
+
// Extract JWT from HTTP-only cookie
|
|
127
|
+
const token = request.cookies[AUTH_COOKIE_NAME];
|
|
128
|
+
const user = await jwtService.verify(token, { secret });
|
|
129
|
+
|
|
130
|
+
// Decoded JWT contains:
|
|
131
|
+
{
|
|
132
|
+
id: 'user-123',
|
|
133
|
+
username: 'john@acme.com',
|
|
134
|
+
displayName: 'John Doe',
|
|
135
|
+
tenantId: 'acme',
|
|
136
|
+
groups: ['admins', 'users'],
|
|
137
|
+
applications: ['app-1', 'app-2']
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
</TabItem>
|
|
141
|
+
|
|
142
|
+
<TabItem label="API Key">
|
|
143
|
+
```typescript
|
|
144
|
+
// Extract API key from Authorization header
|
|
145
|
+
const apiKey = extractBearerToken(request);
|
|
146
|
+
|
|
147
|
+
// Validate against database
|
|
148
|
+
const user = await apiKeysService.getUserForApiKey(apiKey);
|
|
149
|
+
|
|
150
|
+
// Returns user context if valid
|
|
151
|
+
```
|
|
152
|
+
</TabItem>
|
|
153
|
+
|
|
154
|
+
<TabItem label="Service-to-Service">
|
|
155
|
+
```typescript
|
|
156
|
+
// Special case: SERVICE_ACCESS_SECRET
|
|
157
|
+
if (apiKey === process.env.SERVICE_ACCESS_SECRET) {
|
|
158
|
+
return {
|
|
159
|
+
id: 'service',
|
|
160
|
+
username: 'service',
|
|
161
|
+
displayName: 'Service Account',
|
|
162
|
+
authProviderName: 'service-access',
|
|
163
|
+
groups: [],
|
|
164
|
+
applications: []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
</TabItem>
|
|
169
|
+
</Tabs>
|
|
170
|
+
|
|
171
|
+
<Aside type="note">
|
|
172
|
+
The authentication guard runs on **every request** except public routes. Public routes are marked with `@Public()` decorator or match configured passthrough patterns.
|
|
173
|
+
</Aside>
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### 2. Authorization Module
|
|
178
|
+
|
|
179
|
+
Enforces access control policies.
|
|
180
|
+
|
|
181
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/authorization/`
|
|
182
|
+
|
|
183
|
+
**Key Components**:
|
|
184
|
+
- `authorization.service.ts` - Authorization logic
|
|
185
|
+
- `authorization.guard.ts` - Authorization guard
|
|
186
|
+
|
|
187
|
+
**Authorization Flow**:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
async authorizeRequest(context: ExecutionContext, username: string, tenantId?: number) {
|
|
191
|
+
// 1. Get user's allowed resources (applications + operations), filtered by tenant
|
|
192
|
+
const allowedResources = await authzService.getAuthorizedResourcesForUser(username, tenantId);
|
|
193
|
+
|
|
194
|
+
// 2. Get catalog and resolve target module
|
|
195
|
+
const catalog = await catalogService.getCatalog();
|
|
196
|
+
const targetModule = catalogService.resolveTargetModule(catalog, requestPath);
|
|
197
|
+
|
|
198
|
+
// 3. Check if user is authorized for this module/path/method
|
|
199
|
+
const isAuthorized = paeInstance.isAuthorized(
|
|
200
|
+
targetModule,
|
|
201
|
+
catalog,
|
|
202
|
+
allowedResources,
|
|
203
|
+
requestPath,
|
|
204
|
+
requestMethod
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// 4. Add authorization headers for downstream services
|
|
208
|
+
this.addAuthzHeaders(context, allowedResources);
|
|
209
|
+
|
|
210
|
+
return isAuthorized;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Authorization Headers Added**:
|
|
215
|
+
|
|
216
|
+
```http
|
|
217
|
+
x-forwarded-user-operations: users::read,users::write,orders::read
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### 3. Tenant Module
|
|
223
|
+
|
|
224
|
+
Manages multi-tenancy.
|
|
225
|
+
|
|
226
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/tenants/`
|
|
227
|
+
|
|
228
|
+
**Key Components**:
|
|
229
|
+
- `tenants.service.ts` - Tenant CRUD operations
|
|
230
|
+
- `tenant-resolver.service.ts` - Tenant resolution from requests
|
|
231
|
+
- `pattern-matchers/` - URL pattern matching strategies
|
|
232
|
+
- `tenant.guard.ts` - Tenant validation guard
|
|
233
|
+
|
|
234
|
+
**Tenant Resolution Strategies**:
|
|
235
|
+
|
|
236
|
+
<Tabs>
|
|
237
|
+
<TabItem label="Subdomain">
|
|
238
|
+
```typescript
|
|
239
|
+
// Extract tenant from subdomain
|
|
240
|
+
// acme.platform.com → tenant: 'acme'
|
|
241
|
+
|
|
242
|
+
const host = req.headers.host; // 'acme.platform.com'
|
|
243
|
+
const subdomain = host.split('.')[0]; // 'acme'
|
|
244
|
+
const tenant = await tenantsService.findByCode(subdomain);
|
|
245
|
+
```
|
|
246
|
+
</TabItem>
|
|
247
|
+
|
|
248
|
+
<TabItem label="Path">
|
|
249
|
+
```typescript
|
|
250
|
+
// Extract tenant from URL path
|
|
251
|
+
// platform.com/tenants/acme/app → tenant: 'acme'
|
|
252
|
+
|
|
253
|
+
const pathPattern = '/tenants/:tenantCode/*';
|
|
254
|
+
const match = matchPath(req.url, pathPattern);
|
|
255
|
+
const tenant = await tenantsService.findByCode(match.tenantCode);
|
|
256
|
+
```
|
|
257
|
+
</TabItem>
|
|
258
|
+
|
|
259
|
+
<TabItem label="Header">
|
|
260
|
+
```typescript
|
|
261
|
+
// Extract tenant from custom header
|
|
262
|
+
// x-tenant-id: acme → tenant: 'acme'
|
|
263
|
+
|
|
264
|
+
const tenantCode = req.headers['x-tenant-id'];
|
|
265
|
+
const tenant = await tenantsService.findByCode(tenantCode);
|
|
266
|
+
```
|
|
267
|
+
</TabItem>
|
|
268
|
+
|
|
269
|
+
<TabItem label="Cookie">
|
|
270
|
+
```typescript
|
|
271
|
+
// Extract tenant from cookie
|
|
272
|
+
// tenant=acme → tenant: 'acme'
|
|
273
|
+
|
|
274
|
+
const tenantCode = req.cookies['tenant'];
|
|
275
|
+
const tenant = await tenantsService.findByCode(tenantCode);
|
|
276
|
+
```
|
|
277
|
+
</TabItem>
|
|
278
|
+
|
|
279
|
+
<TabItem label="Query Param">
|
|
280
|
+
```typescript
|
|
281
|
+
// Extract tenant from query parameter
|
|
282
|
+
// ?tenant=acme → tenant: 'acme'
|
|
283
|
+
|
|
284
|
+
const tenantCode = req.query.tenant;
|
|
285
|
+
const tenant = await tenantsService.findByCode(tenantCode);
|
|
286
|
+
```
|
|
287
|
+
</TabItem>
|
|
288
|
+
</Tabs>
|
|
289
|
+
|
|
290
|
+
**Tenant URL Patterns**:
|
|
291
|
+
|
|
292
|
+
Tenants can be configured with multiple URL patterns:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
interface TenantUrlPattern {
|
|
296
|
+
id: string;
|
|
297
|
+
tenantId: string;
|
|
298
|
+
type: 'SUBDOMAIN' | 'PATH' | 'CUSTOM_DOMAIN' | 'QUERY_PARAM' | 'HEADER';
|
|
299
|
+
pattern: string;
|
|
300
|
+
priority: number; // Higher priority patterns checked first
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Examples:
|
|
304
|
+
{
|
|
305
|
+
type: 'SUBDOMAIN',
|
|
306
|
+
pattern: 'acme.platform.com'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
type: 'CUSTOM_DOMAIN',
|
|
311
|
+
pattern: 'acme-corp.com'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
{
|
|
315
|
+
type: 'PATH',
|
|
316
|
+
pattern: '/t/acme/*'
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### 4. Catalog Module
|
|
323
|
+
|
|
324
|
+
Dynamic service registry and route resolution.
|
|
325
|
+
|
|
326
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/catalog/`
|
|
327
|
+
|
|
328
|
+
**Key Components**:
|
|
329
|
+
- `catalog.service.ts` - Catalog management
|
|
330
|
+
- `catalog.repository.ts` - Data persistence
|
|
331
|
+
- `catalog-gateway.service.ts` - Gateway-specific catalog logic
|
|
332
|
+
|
|
333
|
+
**Catalog Structure**:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
interface CatalogItem {
|
|
337
|
+
id: string;
|
|
338
|
+
name: string;
|
|
339
|
+
code: string;
|
|
340
|
+
type: 'service' | 'app' | 'tool' | 'cloud-function' | 'utility' | 'documentation';
|
|
341
|
+
baseUrl: string;
|
|
342
|
+
|
|
343
|
+
protocol?: 'http' | 'https';
|
|
344
|
+
port: number;
|
|
345
|
+
|
|
346
|
+
ui?: {
|
|
347
|
+
route: string;
|
|
348
|
+
bundleFile: string;
|
|
349
|
+
mountAtSelector: string;
|
|
350
|
+
customProps: {};
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
// Authorization
|
|
355
|
+
authorization: {
|
|
356
|
+
operations?: string[];
|
|
357
|
+
isPublic?: boolean;
|
|
358
|
+
routes?: {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Metadata
|
|
362
|
+
createdAt: Date;
|
|
363
|
+
createdBy: string;
|
|
364
|
+
updatedAt: Date;
|
|
365
|
+
updatedBy: string;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Route Resolution**:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Example: Resolve target for /api/habits/123
|
|
373
|
+
const catalog = await catalogService.getCatalog();
|
|
374
|
+
const targetModule = catalogService.resolveTargetModule(catalog, '/api/habits/123');
|
|
375
|
+
|
|
376
|
+
// Returns:
|
|
377
|
+
{
|
|
378
|
+
name: 'habits-service',
|
|
379
|
+
type: 'service',
|
|
380
|
+
baseUrl: '/api/habits',
|
|
381
|
+
protocol: 'http',
|
|
382
|
+
port: 80
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Tenant-Scoped Catalog**:
|
|
387
|
+
|
|
388
|
+
The catalog can be filtered by tenant to restrict available modules:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Get catalog filtered by tenant
|
|
392
|
+
const catalog = await catalogService.getCatalog(tenantId);
|
|
393
|
+
|
|
394
|
+
// Only returns modules assigned to this tenant
|
|
395
|
+
// via tenant_application_restrictions table
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### 5. Proxy Module
|
|
401
|
+
|
|
402
|
+
Forwards requests to target services.
|
|
403
|
+
|
|
404
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/proxy/`
|
|
405
|
+
|
|
406
|
+
**Key Components**:
|
|
407
|
+
- `proxy.service.ts` - Request forwarding logic
|
|
408
|
+
- `proxy.middleware.ts` - Proxy middleware
|
|
409
|
+
- `build-forward-url.ts` - URL construction
|
|
410
|
+
|
|
411
|
+
**Proxy Flow**:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
async forwardRequest(targetModule: CatalogItem, req: FastifyRequest, reply: FastifyReply) {
|
|
415
|
+
// 1. Build target URL
|
|
416
|
+
const targetUrl = buildForwardURL(targetModule, req, tenantConfig, impersonationMode);
|
|
417
|
+
|
|
418
|
+
// 2. Add forward headers
|
|
419
|
+
const forwardHeaders = {
|
|
420
|
+
'x-forwarded-host': req.headers.host,
|
|
421
|
+
'x-forwarded-server': localIpAddress,
|
|
422
|
+
'x-forwarded-uri': req.url,
|
|
423
|
+
'x-forwarded-proto': req.protocol,
|
|
424
|
+
'x-forwarded-for': req.ip,
|
|
425
|
+
'x-forwarded-user-id': user.id,
|
|
426
|
+
'x-forwarded-user': user.username,
|
|
427
|
+
'x-forwarded-user-name': user.displayName,
|
|
428
|
+
'x-forwarded-user-operations': Array.from(allowedOperations).join(','),
|
|
429
|
+
'x-forwarded-tenant-config': base64(tenant),
|
|
430
|
+
'x-forwarded-tenant': base64(tenant), // only in multi-tenant mode
|
|
431
|
+
'x-forwarded-allowed-tenants': base64(tenant), // only in multi-tenant mode
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// 3. Sign request if cloud function requires it
|
|
435
|
+
if (targetModule.cloudFunction?.requiresSignature) {
|
|
436
|
+
const signature = await signCloudFunctionRequest(req, targetModule, targetUrl);
|
|
437
|
+
Object.assign(forwardHeaders, signature);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 4. Forward request using Fastify reply-from
|
|
441
|
+
reply.from(targetUrl, {
|
|
442
|
+
rewriteRequestHeaders: (_, headers) => ({
|
|
443
|
+
...headers,
|
|
444
|
+
...forwardHeaders
|
|
445
|
+
})
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Forward Headers**:
|
|
451
|
+
|
|
452
|
+
The gateway adds these headers to every proxied request:
|
|
453
|
+
|
|
454
|
+
```http
|
|
455
|
+
x-forwarded-host: platform.com
|
|
456
|
+
x-forwarded-server: 192.168.1.10
|
|
457
|
+
x-forwarded-uri: /api/habits/123
|
|
458
|
+
x-forwarded-protocol: https
|
|
459
|
+
x-forwarded-for: 203.0.113.45
|
|
460
|
+
x-forwarded-user-id: john@acme.com
|
|
461
|
+
x-forwarded-user: john@acme.com
|
|
462
|
+
x-forwarded-user-name: John Doe
|
|
463
|
+
x-forwarded-auth-provider-name: okta
|
|
464
|
+
x-forwarded-tenant: eyJ0ZW5hbnRJZCI6ImFjbWUifQ==
|
|
465
|
+
x-forwarded-tenant-config: eyJtb2RlIjoibXVsdGktdGVuYW50In0=
|
|
466
|
+
x-forwarded-allowed-tenants: W3siaWQiOiJhY21lIn1d
|
|
467
|
+
x-forwarded-user-operations: habits.read,habits.write,habits.delete
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
<Aside type="tip" title="Service Implementation">
|
|
471
|
+
Backend services should extract these headers to get platform context. The `pae-service-nestjs-sdk` provides decorators to automatically extract context:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
@Get()
|
|
475
|
+
async findAll(@PlatformContext() ctx: PlatformContext) {
|
|
476
|
+
// ctx contains tenant, user, and other context
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
</Aside>
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
### 6. Platform Context Module
|
|
484
|
+
|
|
485
|
+
Manages request-scoped context.
|
|
486
|
+
|
|
487
|
+
**Location**: `apps/pae-nestjs-gateway-service/src/platform-context/`
|
|
488
|
+
|
|
489
|
+
**Key Component**:
|
|
490
|
+
- `context/context.service.ts` - Context management using nestjs-cls
|
|
491
|
+
|
|
492
|
+
**Context Storage**:
|
|
493
|
+
|
|
494
|
+
The gateway uses `nestjs-cls` (Continuation-Local Storage) to store request-scoped data:
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
@Injectable()
|
|
498
|
+
export class PlatformContextService {
|
|
499
|
+
constructor(private readonly cls: ClsService) {}
|
|
500
|
+
|
|
501
|
+
// Store session
|
|
502
|
+
setSession(session: Session) {
|
|
503
|
+
this.cls.set('pae::session', session);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Store user
|
|
507
|
+
setAuthUser(user: AuthUserWithApplications) {
|
|
508
|
+
this.cls.set('pae::authUser', user);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Store tenant
|
|
512
|
+
setTenant(tenant: Tenant | null) {
|
|
513
|
+
this.cls.set('tenant', tenant);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Store target module
|
|
517
|
+
setTargetModule(targetModule: CatalogItem) {
|
|
518
|
+
this.cls.set('targetModule', targetModule);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Retrieve from context
|
|
522
|
+
getAuthUser(): AuthUserWithApplications {
|
|
523
|
+
return this.cls.get('pae::authUser');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Context Flow**:
|
|
529
|
+
|
|
530
|
+
```
|
|
531
|
+
1. Request arrives
|
|
532
|
+
2. Authentication middleware → setSession(), setAuthUser()
|
|
533
|
+
3. Tenant middleware → setTenant()
|
|
534
|
+
4. Catalog middleware → setTargetModule()
|
|
535
|
+
5. Any service/guard can access context via PlatformContextService
|
|
536
|
+
6. Request completes → context automatically cleaned up
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## HTTP/2 and WebSocket Support
|
|
542
|
+
|
|
543
|
+
### HTTP/2 Configuration
|
|
544
|
+
|
|
545
|
+
The gateway uses Fastify with HTTP/2 support:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
// apps/pae-nestjs-gateway-service/src/main.ts
|
|
549
|
+
const httpsOptions = {
|
|
550
|
+
http2: true,
|
|
551
|
+
https: {
|
|
552
|
+
key: fs.readFileSync(process.env.SSL_KEY_PATH),
|
|
553
|
+
cert: fs.readFileSync(process.env.SSL_CERT_PATH)
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const app = await NestFactory.create<FastifyAdapter>(
|
|
558
|
+
AppModule,
|
|
559
|
+
new FastifyAdapter(httpsOptions)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
await app.listen(443, '0.0.0.0');
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**HTTP/2 Benefits**:
|
|
566
|
+
- Multiplexing: Multiple requests over single connection
|
|
567
|
+
- Header compression: Reduces bandwidth
|
|
568
|
+
- Server push: Proactive resource delivery
|
|
569
|
+
- Binary protocol: Faster parsing
|
|
570
|
+
|
|
571
|
+
### WebSocket Support
|
|
572
|
+
|
|
573
|
+
The gateway supports WebSocket connections:
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
// WebSocket proxying
|
|
577
|
+
const targetWsUrl = targetModule.serviceUrl.replace('http', 'ws');
|
|
578
|
+
|
|
579
|
+
// Forward WebSocket upgrade
|
|
580
|
+
reply.hijack();
|
|
581
|
+
const ws = new WebSocket(targetWsUrl, {
|
|
582
|
+
headers: forwardHeaders
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Pipe messages bidirectionally
|
|
586
|
+
clientWs.on('message', data => ws.send(data));
|
|
587
|
+
ws.on('message', data => clientWs.send(data));
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Lambda Function Integration
|
|
593
|
+
|
|
594
|
+
The gateway can route to AWS Lambda functions (or other cloud functions).
|
|
595
|
+
|
|
596
|
+
**Lambda Configuration**:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// Register Lambda function in catalog
|
|
600
|
+
{
|
|
601
|
+
code: 'pdf-generator',
|
|
602
|
+
name: 'PDF Generator Function',
|
|
603
|
+
type: 'CLOUD_FUNCTION',
|
|
604
|
+
baseUrl: '/api/pdf',
|
|
605
|
+
cloudFunction: {
|
|
606
|
+
provider: 'AWS_LAMBDA',
|
|
607
|
+
functionName: 'pae-pdf-generator',
|
|
608
|
+
region: 'us-east-1',
|
|
609
|
+
requiresSignature: true
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Request Signing**:
|
|
615
|
+
|
|
616
|
+
For functions that require AWS Signature V4:
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// apps/pae-nestjs-gateway-service/src/proxy/create-cloud-function-signer.ts
|
|
620
|
+
export const createCloudFunctionSigner = (module: CatalogItem, targetUrl: string) => {
|
|
621
|
+
return async (req: FastifyRequest) => {
|
|
622
|
+
const signature = aws4.sign({
|
|
623
|
+
service: 'lambda',
|
|
624
|
+
region: module.cloudFunction.region,
|
|
625
|
+
method: req.method,
|
|
626
|
+
path: new URL(targetUrl).pathname,
|
|
627
|
+
headers: {
|
|
628
|
+
'content-type': req.headers['content-type'],
|
|
629
|
+
'host': new URL(targetUrl).host
|
|
630
|
+
},
|
|
631
|
+
body: req.body
|
|
632
|
+
}, {
|
|
633
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
634
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return signature.headers;
|
|
638
|
+
};
|
|
639
|
+
};
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Configuration
|
|
645
|
+
|
|
646
|
+
### Environment Variables
|
|
647
|
+
|
|
648
|
+
The gateway requires these environment variables:
|
|
649
|
+
|
|
650
|
+
<Tabs>
|
|
651
|
+
<TabItem label="Core">
|
|
652
|
+
```bash
|
|
653
|
+
# Service Access
|
|
654
|
+
SERVICE_ACCESS_SECRET=secret-for-service-to-service-auth
|
|
655
|
+
JWT_SECRET=secret-for-jwt-validation
|
|
656
|
+
|
|
657
|
+
# Authentication Service
|
|
658
|
+
AUTHN_SERVICE_URL=http://pae-authn-service:4000
|
|
659
|
+
|
|
660
|
+
# Ports
|
|
661
|
+
HTTP_PORT=80
|
|
662
|
+
HTTPS_PORT=443
|
|
663
|
+
```
|
|
664
|
+
</TabItem>
|
|
665
|
+
|
|
666
|
+
<TabItem label="HTTPS">
|
|
667
|
+
```bash
|
|
668
|
+
# HTTPS Configuration
|
|
669
|
+
HTTPS=true
|
|
670
|
+
HTTP2=true
|
|
671
|
+
SSL_CERT_PATH=/etc/ssl/cert.pem
|
|
672
|
+
SSL_KEY_PATH=/etc/ssl/key.pem
|
|
673
|
+
```
|
|
674
|
+
</TabItem>
|
|
675
|
+
|
|
676
|
+
<TabItem label="Database">
|
|
677
|
+
```bash
|
|
678
|
+
# Database
|
|
679
|
+
DB_CLIENT=pg
|
|
680
|
+
DB_URL=postgresql://user:pass@host:5432/db
|
|
681
|
+
```
|
|
682
|
+
</TabItem>
|
|
683
|
+
|
|
684
|
+
<TabItem label="Optional">
|
|
685
|
+
```bash
|
|
686
|
+
# Logging
|
|
687
|
+
LOG_LEVEL=debug
|
|
688
|
+
LOG_JSON=true
|
|
689
|
+
|
|
690
|
+
# Observability
|
|
691
|
+
TRACE_URL=http://jaeger:14268/api/traces
|
|
692
|
+
|
|
693
|
+
# Auth Passthrough (regex)
|
|
694
|
+
AUTH_PASSTHROUGH_PATTERN=^/_/.*
|
|
695
|
+
|
|
696
|
+
# AWS (for Lambda)
|
|
697
|
+
AWS_REGION=us-east-1
|
|
698
|
+
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
|
699
|
+
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
700
|
+
```
|
|
701
|
+
</TabItem>
|
|
702
|
+
</Tabs>
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## Performance Considerations
|
|
707
|
+
|
|
708
|
+
### Connection Pooling
|
|
709
|
+
|
|
710
|
+
The gateway maintains connection pools to backend services:
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
// Fastify reply-from configuration
|
|
714
|
+
app.register(fastifyReplyFrom, {
|
|
715
|
+
base: targetUrl,
|
|
716
|
+
// Connection pool settings
|
|
717
|
+
http: {
|
|
718
|
+
connections: 100, // Max connections per host
|
|
719
|
+
pipelining: 10 // Max pipelined requests
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### Caching
|
|
725
|
+
|
|
726
|
+
The catalog is cached to avoid database queries on every request:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
// Cache catalog in memory with TTL
|
|
730
|
+
private catalogCache: {
|
|
731
|
+
data: CatalogItem[];
|
|
732
|
+
timestamp: number;
|
|
733
|
+
ttl: number; // 5 minutes
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
async getCatalog() {
|
|
737
|
+
if (this.isCacheValid()) {
|
|
738
|
+
return this.catalogCache.data;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const catalog = await this.catalogRepository.findAll();
|
|
742
|
+
this.catalogCache = {
|
|
743
|
+
data: catalog,
|
|
744
|
+
timestamp: Date.now(),
|
|
745
|
+
ttl: 5 * 60 * 1000
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
return catalog;
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Request Timeouts
|
|
753
|
+
|
|
754
|
+
Configure timeouts to prevent hanging requests:
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
// Fastify timeout configuration
|
|
758
|
+
const app = await NestFactory.create(AppModule, new FastifyAdapter({
|
|
759
|
+
connectionTimeout: 30000, // 30 seconds
|
|
760
|
+
keepAliveTimeout: 5000 // 5 seconds
|
|
761
|
+
}));
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## Error Handling
|
|
767
|
+
|
|
768
|
+
The gateway implements comprehensive error handling:
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
// Global exception filter
|
|
772
|
+
@Catch()
|
|
773
|
+
export class GlobalExceptionFilter implements ExceptionFilter {
|
|
774
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
775
|
+
const ctx = host.switchToHttp();
|
|
776
|
+
const response = ctx.getResponse();
|
|
777
|
+
const request = ctx.getRequest();
|
|
778
|
+
|
|
779
|
+
// Log error with context
|
|
780
|
+
logger.error({
|
|
781
|
+
error: exception,
|
|
782
|
+
path: request.url,
|
|
783
|
+
method: request.method,
|
|
784
|
+
user: request.user?.username,
|
|
785
|
+
tenant: request.tenant?.code
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Send appropriate response
|
|
789
|
+
if (exception instanceof HttpException) {
|
|
790
|
+
response.status(exception.getStatus()).send({
|
|
791
|
+
statusCode: exception.getStatus(),
|
|
792
|
+
message: exception.message,
|
|
793
|
+
timestamp: new Date().toISOString(),
|
|
794
|
+
path: request.url
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
response.status(500).send({
|
|
798
|
+
statusCode: 500,
|
|
799
|
+
message: 'Internal server error',
|
|
800
|
+
timestamp: new Date().toISOString(),
|
|
801
|
+
path: request.url
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## Health Checks
|
|
811
|
+
|
|
812
|
+
The gateway exposes health check endpoints:
|
|
813
|
+
|
|
814
|
+
```http
|
|
815
|
+
GET /_/health
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
Response:
|
|
819
|
+
```json
|
|
820
|
+
{
|
|
821
|
+
healthy: 'ok'
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## Security Considerations
|
|
828
|
+
|
|
829
|
+
<Aside type="caution" title="Security Best Practices">
|
|
830
|
+
1. **Always use HTTPS** in production environments
|
|
831
|
+
2. **Rotate secrets regularly** (JWT_SECRET, SERVICE_ACCESS_SECRET)
|
|
832
|
+
3. **Keep SSL certificates up to date** - automate renewal with Let's Encrypt
|
|
833
|
+
4. **Rate limit endpoints** to prevent abuse
|
|
834
|
+
5. **Validate all inputs** before proxying to services
|
|
835
|
+
6. **Log security events** (failed auth, authorization denials)
|
|
836
|
+
7. **Implement CSRF protection** for browser-based requests
|
|
837
|
+
</Aside>
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Next Steps
|
|
842
|
+
|
|
843
|
+
- **[Authentication System](/_/docs/architecture/authentication-system/)** - Deep dive into authentication flows
|
|
844
|
+
- **[Authorization System](/_/docs/architecture/authorization-system/)** - Understanding RBAC and permissions
|
|
845
|
+
- **[Multi-Tenancy](/_/docs/architecture/multi-tenancy/)** - Tenant isolation and context
|