@qwickapps/server 1.0.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/LICENSE +45 -0
- package/README.md +321 -0
- package/dist/core/control-panel.d.ts +21 -0
- package/dist/core/control-panel.d.ts.map +1 -0
- package/dist/core/control-panel.js +416 -0
- package/dist/core/control-panel.js.map +1 -0
- package/dist/core/gateway.d.ts +133 -0
- package/dist/core/gateway.d.ts.map +1 -0
- package/dist/core/gateway.js +270 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health-manager.d.ts +52 -0
- package/dist/core/health-manager.d.ts.map +1 -0
- package/dist/core/health-manager.js +192 -0
- package/dist/core/health-manager.js.map +1 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logging.d.ts +83 -0
- package/dist/core/logging.d.ts.map +1 -0
- package/dist/core/logging.js +191 -0
- package/dist/core/logging.js.map +1 -0
- package/dist/core/types.d.ts +195 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/config-plugin.d.ts +15 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -0
- package/dist/plugins/config-plugin.js +96 -0
- package/dist/plugins/config-plugin.js.map +1 -0
- package/dist/plugins/diagnostics-plugin.d.ts +29 -0
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -0
- package/dist/plugins/diagnostics-plugin.js +142 -0
- package/dist/plugins/diagnostics-plugin.js.map +1 -0
- package/dist/plugins/health-plugin.d.ts +17 -0
- package/dist/plugins/health-plugin.d.ts.map +1 -0
- package/dist/plugins/health-plugin.js +25 -0
- package/dist/plugins/health-plugin.js.map +1 -0
- package/dist/plugins/index.d.ts +14 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/logs-plugin.d.ts +22 -0
- package/dist/plugins/logs-plugin.d.ts.map +1 -0
- package/dist/plugins/logs-plugin.js +242 -0
- package/dist/plugins/logs-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bk7ypbI4.js +465 -0
- package/dist-ui/assets/index-Bk7ypbI4.js.map +1 -0
- package/dist-ui/assets/index-CiizQQnb.css +1 -0
- package/dist-ui/index.html +13 -0
- package/package.json +98 -0
- package/src/core/control-panel.ts +493 -0
- package/src/core/gateway.ts +421 -0
- package/src/core/health-manager.ts +227 -0
- package/src/core/index.ts +25 -0
- package/src/core/logging.ts +234 -0
- package/src/core/types.ts +218 -0
- package/src/index.ts +55 -0
- package/src/plugins/config-plugin.ts +117 -0
- package/src/plugins/diagnostics-plugin.ts +178 -0
- package/src/plugins/health-plugin.ts +35 -0
- package/src/plugins/index.ts +17 -0
- package/src/plugins/logs-plugin.ts +314 -0
- package/ui/index.html +12 -0
- package/ui/src/App.tsx +65 -0
- package/ui/src/api/controlPanelApi.ts +148 -0
- package/ui/src/config/AppConfig.ts +18 -0
- package/ui/src/index.css +29 -0
- package/ui/src/index.tsx +11 -0
- package/ui/src/pages/ConfigPage.tsx +199 -0
- package/ui/src/pages/DashboardPage.tsx +264 -0
- package/ui/src/pages/DiagnosticsPage.tsx +315 -0
- package/ui/src/pages/HealthPage.tsx +204 -0
- package/ui/src/pages/LogsPage.tsx +267 -0
- package/ui/src/pages/NotFoundPage.tsx +41 -0
- package/ui/tsconfig.json +19 -0
- package/ui/vite.config.ts +21 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Server for @qwickapps/server
|
|
3
|
+
*
|
|
4
|
+
* Provides a production-ready gateway pattern that:
|
|
5
|
+
* 1. Serves the control panel UI (always responsive)
|
|
6
|
+
* 2. Proxies API requests to an internal service
|
|
7
|
+
* 3. Provides health and diagnostics endpoints
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* Internet → Gateway (GATEWAY_PORT, public) → Service (SERVICE_PORT, internal)
|
|
11
|
+
*
|
|
12
|
+
* The gateway is always responsive even if the internal service is down,
|
|
13
|
+
* allowing diagnostics and error visibility.
|
|
14
|
+
*
|
|
15
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Application, Request, Response, NextFunction } from 'express';
|
|
19
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
20
|
+
import type { Socket } from 'net';
|
|
21
|
+
import type { Server } from 'http';
|
|
22
|
+
import type { ControlPanelConfig, ControlPanelPlugin, Logger } from './types.js';
|
|
23
|
+
import { createControlPanel } from './control-panel.js';
|
|
24
|
+
import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './logging.js';
|
|
25
|
+
import { createProxyMiddleware, type Options } from 'http-proxy-middleware';
|
|
26
|
+
import { randomBytes } from 'crypto';
|
|
27
|
+
import express from 'express';
|
|
28
|
+
import { existsSync } from 'fs';
|
|
29
|
+
import { resolve } from 'path';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gateway configuration
|
|
33
|
+
*/
|
|
34
|
+
export interface GatewayConfig {
|
|
35
|
+
/** Port for the gateway (public-facing). Defaults to GATEWAY_PORT env or 3101 */
|
|
36
|
+
gatewayPort?: number;
|
|
37
|
+
|
|
38
|
+
/** Port for the internal service. Defaults to SERVICE_PORT env or 3100 */
|
|
39
|
+
servicePort?: number;
|
|
40
|
+
|
|
41
|
+
/** Product name for the control panel */
|
|
42
|
+
productName: string;
|
|
43
|
+
|
|
44
|
+
/** Product version */
|
|
45
|
+
version?: string;
|
|
46
|
+
|
|
47
|
+
/** Branding configuration */
|
|
48
|
+
branding?: ControlPanelConfig['branding'];
|
|
49
|
+
|
|
50
|
+
/** CORS origins */
|
|
51
|
+
corsOrigins?: string[];
|
|
52
|
+
|
|
53
|
+
/** Control panel plugins */
|
|
54
|
+
plugins?: ControlPanelPlugin[];
|
|
55
|
+
|
|
56
|
+
/** Quick links for the control panel */
|
|
57
|
+
links?: ControlPanelConfig['links'];
|
|
58
|
+
|
|
59
|
+
/** Path to custom React UI dist folder */
|
|
60
|
+
customUiPath?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* API paths to proxy to the internal service.
|
|
64
|
+
* Defaults to ['/api/v1'] if not specified.
|
|
65
|
+
* The gateway always proxies /health to the internal service.
|
|
66
|
+
*/
|
|
67
|
+
proxyPaths?: string[];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Authentication mode for the control panel (not the API).
|
|
71
|
+
* - 'none': No authentication (not recommended for production)
|
|
72
|
+
* - 'basic': HTTP Basic Auth with username/password
|
|
73
|
+
* - 'auto': Auto-generate password on startup (default)
|
|
74
|
+
*/
|
|
75
|
+
authMode?: 'none' | 'basic' | 'auto';
|
|
76
|
+
|
|
77
|
+
/** Basic auth username (defaults to 'admin') */
|
|
78
|
+
basicAuthUser?: string;
|
|
79
|
+
|
|
80
|
+
/** Basic auth password (required if authMode is 'basic') */
|
|
81
|
+
basicAuthPassword?: string;
|
|
82
|
+
|
|
83
|
+
/** Logger instance (deprecated: use logging config instead) */
|
|
84
|
+
logger?: Logger;
|
|
85
|
+
|
|
86
|
+
/** Logging configuration */
|
|
87
|
+
logging?: LoggingConfig;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Service factory function type
|
|
92
|
+
* Called with the service port, should return an object with:
|
|
93
|
+
* - app: Express application (or compatible)
|
|
94
|
+
* - server: HTTP server (created by calling listen)
|
|
95
|
+
* - shutdown: Async function to gracefully shut down the service
|
|
96
|
+
*/
|
|
97
|
+
export interface ServiceFactory {
|
|
98
|
+
(port: number): Promise<{
|
|
99
|
+
app: Application;
|
|
100
|
+
server: Server;
|
|
101
|
+
shutdown: () => Promise<void>;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Gateway instance returned by createGateway
|
|
107
|
+
*/
|
|
108
|
+
export interface GatewayInstance {
|
|
109
|
+
/** The control panel instance */
|
|
110
|
+
controlPanel: ReturnType<typeof createControlPanel>;
|
|
111
|
+
|
|
112
|
+
/** The internal service (if started) */
|
|
113
|
+
service: {
|
|
114
|
+
app: Application;
|
|
115
|
+
server: Server;
|
|
116
|
+
shutdown: () => Promise<void>;
|
|
117
|
+
} | null;
|
|
118
|
+
|
|
119
|
+
/** Start the gateway and internal service */
|
|
120
|
+
start: () => Promise<void>;
|
|
121
|
+
|
|
122
|
+
/** Stop everything gracefully */
|
|
123
|
+
stop: () => Promise<void>;
|
|
124
|
+
|
|
125
|
+
/** Gateway port */
|
|
126
|
+
gatewayPort: number;
|
|
127
|
+
|
|
128
|
+
/** Service port */
|
|
129
|
+
servicePort: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Basic auth middleware for gateway protection (control panel only)
|
|
135
|
+
* - Skips localhost requests
|
|
136
|
+
* - Skips API routes (/api/v1/*) - they have their own service auth
|
|
137
|
+
* - Skips health endpoints - these should be public
|
|
138
|
+
* - Requires valid credentials for non-localhost control panel access
|
|
139
|
+
*/
|
|
140
|
+
function createBasicAuthMiddleware(
|
|
141
|
+
username: string,
|
|
142
|
+
password: string,
|
|
143
|
+
apiPaths: string[]
|
|
144
|
+
) {
|
|
145
|
+
const expectedAuth = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
146
|
+
|
|
147
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
148
|
+
const path = req.path;
|
|
149
|
+
|
|
150
|
+
// Skip auth for API routes - they use their own authentication
|
|
151
|
+
for (const apiPath of apiPaths) {
|
|
152
|
+
if (path.startsWith(apiPath)) {
|
|
153
|
+
return next();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip auth for health endpoints - these should be publicly accessible
|
|
158
|
+
if (path === '/health' || path === '/api/health') {
|
|
159
|
+
return next();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Allow localhost without auth
|
|
163
|
+
const remoteAddress = req.ip || req.socket?.remoteAddress || '';
|
|
164
|
+
const host = req.hostname || req.headers.host || '';
|
|
165
|
+
const isLocalhost =
|
|
166
|
+
host === 'localhost' ||
|
|
167
|
+
host === '127.0.0.1' ||
|
|
168
|
+
host.startsWith('localhost:') ||
|
|
169
|
+
host.startsWith('127.0.0.1:') ||
|
|
170
|
+
remoteAddress === '127.0.0.1' ||
|
|
171
|
+
remoteAddress === '::1' ||
|
|
172
|
+
remoteAddress === '::ffff:127.0.0.1';
|
|
173
|
+
|
|
174
|
+
if (isLocalhost) {
|
|
175
|
+
return next();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for valid basic auth
|
|
179
|
+
const authHeader = req.headers.authorization;
|
|
180
|
+
if (authHeader === expectedAuth) {
|
|
181
|
+
return next();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Request authentication
|
|
185
|
+
res.setHeader('WWW-Authenticate', 'Basic realm="Control Panel"');
|
|
186
|
+
res.status(401).json({
|
|
187
|
+
error: 'Unauthorized',
|
|
188
|
+
message: 'Authentication required.',
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a gateway that proxies to an internal service
|
|
195
|
+
*
|
|
196
|
+
* @param config - Gateway configuration
|
|
197
|
+
* @param serviceFactory - Factory function to create the internal service
|
|
198
|
+
* @returns Gateway instance
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* import { createGateway } from '@qwickapps/server';
|
|
203
|
+
*
|
|
204
|
+
* const gateway = createGateway(
|
|
205
|
+
* {
|
|
206
|
+
* productName: 'My Service',
|
|
207
|
+
* gatewayPort: 3101,
|
|
208
|
+
* servicePort: 3100,
|
|
209
|
+
* },
|
|
210
|
+
* async (port) => {
|
|
211
|
+
* const app = createMyApp();
|
|
212
|
+
* const server = app.listen(port);
|
|
213
|
+
* return {
|
|
214
|
+
* app,
|
|
215
|
+
* server,
|
|
216
|
+
* shutdown: async () => { server.close(); },
|
|
217
|
+
* };
|
|
218
|
+
* }
|
|
219
|
+
* );
|
|
220
|
+
*
|
|
221
|
+
* await gateway.start();
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export function createGateway(
|
|
225
|
+
config: GatewayConfig,
|
|
226
|
+
serviceFactory: ServiceFactory
|
|
227
|
+
): GatewayInstance {
|
|
228
|
+
// Initialize logging subsystem first
|
|
229
|
+
const loggingSubsystem = initializeLogging({
|
|
230
|
+
namespace: config.productName,
|
|
231
|
+
...config.logging,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Use provided logger or get one from the logging subsystem
|
|
235
|
+
const logger = config.logger || getControlPanelLogger('Gateway');
|
|
236
|
+
|
|
237
|
+
const gatewayPort = config.gatewayPort || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3101', 10);
|
|
238
|
+
const servicePort = config.servicePort || parseInt(process.env.SERVICE_PORT || '3100', 10);
|
|
239
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
240
|
+
|
|
241
|
+
// Auth configuration
|
|
242
|
+
const authMode = config.authMode || 'auto';
|
|
243
|
+
const basicAuthUser = config.basicAuthUser || process.env.BASIC_AUTH_USER || 'admin';
|
|
244
|
+
const providedPassword = config.basicAuthPassword || process.env.BASIC_AUTH_PASSWORD;
|
|
245
|
+
const basicAuthPassword = providedPassword || (authMode === 'auto' ? randomBytes(16).toString('base64url') : '');
|
|
246
|
+
const isPasswordAutoGenerated = !providedPassword && authMode === 'auto';
|
|
247
|
+
|
|
248
|
+
// API paths to proxy
|
|
249
|
+
const proxyPaths = config.proxyPaths || ['/api/v1'];
|
|
250
|
+
|
|
251
|
+
let service: GatewayInstance['service'] = null;
|
|
252
|
+
|
|
253
|
+
// Create control panel
|
|
254
|
+
const controlPanel = createControlPanel({
|
|
255
|
+
config: {
|
|
256
|
+
productName: config.productName,
|
|
257
|
+
port: gatewayPort,
|
|
258
|
+
version: config.version || process.env.npm_package_version || '1.0.0',
|
|
259
|
+
branding: config.branding,
|
|
260
|
+
cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
|
|
261
|
+
// Skip body parsing for proxied paths
|
|
262
|
+
skipBodyParserPaths: [...proxyPaths, '/health'],
|
|
263
|
+
// Disable built-in dashboard if custom UI is provided
|
|
264
|
+
disableDashboard: !!config.customUiPath,
|
|
265
|
+
links: config.links,
|
|
266
|
+
},
|
|
267
|
+
plugins: config.plugins || [],
|
|
268
|
+
logger,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Add basic auth middleware if enabled
|
|
272
|
+
if (authMode === 'basic' || authMode === 'auto') {
|
|
273
|
+
controlPanel.app.use(createBasicAuthMiddleware(basicAuthUser, basicAuthPassword, proxyPaths));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Setup proxy middleware for API paths
|
|
277
|
+
const setupProxyMiddleware = () => {
|
|
278
|
+
const target = `http://localhost:${servicePort}`;
|
|
279
|
+
|
|
280
|
+
// Proxy each API path
|
|
281
|
+
for (const apiPath of proxyPaths) {
|
|
282
|
+
const proxyOptions: Options = {
|
|
283
|
+
target,
|
|
284
|
+
changeOrigin: false,
|
|
285
|
+
pathFilter: `${apiPath}/**`,
|
|
286
|
+
on: {
|
|
287
|
+
error: (err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
|
|
288
|
+
logger.error('Proxy error', { error: err.message, path: apiPath });
|
|
289
|
+
if (res && 'writeHead' in res && !res.headersSent) {
|
|
290
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
291
|
+
res.end(
|
|
292
|
+
JSON.stringify({
|
|
293
|
+
error: 'Service Unavailable',
|
|
294
|
+
message: 'The service is currently unavailable. Please try again later.',
|
|
295
|
+
details: nodeEnv === 'development' ? err.message : undefined,
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
controlPanel.app.use(createProxyMiddleware(proxyOptions));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Proxy /health endpoint to internal service
|
|
306
|
+
const healthProxyOptions: Options = {
|
|
307
|
+
target,
|
|
308
|
+
changeOrigin: false,
|
|
309
|
+
pathFilter: '/health',
|
|
310
|
+
on: {
|
|
311
|
+
error: (_err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
|
|
312
|
+
if (res && 'writeHead' in res && !res.headersSent) {
|
|
313
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
314
|
+
res.end(
|
|
315
|
+
JSON.stringify({
|
|
316
|
+
status: 'unhealthy',
|
|
317
|
+
error: 'Service unavailable',
|
|
318
|
+
gateway: 'healthy',
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Serve custom React UI if provided
|
|
329
|
+
const setupCustomUI = () => {
|
|
330
|
+
if (config.customUiPath && existsSync(config.customUiPath)) {
|
|
331
|
+
logger.info(`Serving custom UI from ${config.customUiPath}`);
|
|
332
|
+
controlPanel.app.use(express.static(config.customUiPath));
|
|
333
|
+
|
|
334
|
+
// SPA fallback
|
|
335
|
+
controlPanel.app.get('*', (req, res, next) => {
|
|
336
|
+
if (req.path.startsWith('/api/') || req.path === '/api') {
|
|
337
|
+
return next();
|
|
338
|
+
}
|
|
339
|
+
res.sendFile(resolve(config.customUiPath!, 'index.html'));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const start = async (): Promise<void> => {
|
|
345
|
+
logger.info('Starting gateway...');
|
|
346
|
+
|
|
347
|
+
// 1. Start internal service
|
|
348
|
+
logger.info(`Starting internal service on port ${servicePort}...`);
|
|
349
|
+
service = await serviceFactory(servicePort);
|
|
350
|
+
logger.info(`Internal service started on port ${servicePort}`);
|
|
351
|
+
|
|
352
|
+
// 2. Setup proxy middleware (after service is started)
|
|
353
|
+
setupProxyMiddleware();
|
|
354
|
+
|
|
355
|
+
// 3. Setup custom UI (after proxy middleware)
|
|
356
|
+
setupCustomUI();
|
|
357
|
+
|
|
358
|
+
// 4. Start control panel gateway
|
|
359
|
+
await controlPanel.start();
|
|
360
|
+
|
|
361
|
+
// Log startup info
|
|
362
|
+
logger.info('');
|
|
363
|
+
logger.info('========================================');
|
|
364
|
+
logger.info(` ${config.productName} Gateway`);
|
|
365
|
+
logger.info('========================================');
|
|
366
|
+
logger.info('');
|
|
367
|
+
logger.info(` Gateway Port: ${gatewayPort} (public)`);
|
|
368
|
+
logger.info(` Service Port: ${servicePort} (internal)`);
|
|
369
|
+
logger.info('');
|
|
370
|
+
|
|
371
|
+
if (authMode === 'basic' || authMode === 'auto') {
|
|
372
|
+
logger.info(' Control Panel Auth: HTTP Basic Auth');
|
|
373
|
+
logger.info(' ----------------------------------------');
|
|
374
|
+
logger.info(` Username: ${basicAuthUser}`);
|
|
375
|
+
if (isPasswordAutoGenerated) {
|
|
376
|
+
logger.info(` Password: ${basicAuthPassword}`);
|
|
377
|
+
logger.info(' (auto-generated, set BASIC_AUTH_PASSWORD to use a fixed password)');
|
|
378
|
+
} else {
|
|
379
|
+
logger.info(' Password: ********** (from environment)');
|
|
380
|
+
}
|
|
381
|
+
logger.info(' ----------------------------------------');
|
|
382
|
+
} else {
|
|
383
|
+
logger.info(' Control Panel Auth: None (not recommended)');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
logger.info('');
|
|
387
|
+
logger.info(' Endpoints:');
|
|
388
|
+
logger.info(` GET / - Control Panel UI`);
|
|
389
|
+
logger.info(` GET /api/health - Gateway health`);
|
|
390
|
+
logger.info(` GET /health - Service health (proxied)`);
|
|
391
|
+
for (const apiPath of proxyPaths) {
|
|
392
|
+
logger.info(` * ${apiPath}/* - Service API (proxied)`);
|
|
393
|
+
}
|
|
394
|
+
logger.info('========================================');
|
|
395
|
+
logger.info('');
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const stop = async (): Promise<void> => {
|
|
399
|
+
logger.info('Shutting down gateway...');
|
|
400
|
+
|
|
401
|
+
// Stop control panel
|
|
402
|
+
await controlPanel.stop();
|
|
403
|
+
|
|
404
|
+
// Stop internal service
|
|
405
|
+
if (service) {
|
|
406
|
+
await service.shutdown();
|
|
407
|
+
service.server.close();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
logger.info('Gateway shutdown complete');
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
controlPanel,
|
|
415
|
+
service,
|
|
416
|
+
start,
|
|
417
|
+
stop,
|
|
418
|
+
gatewayPort,
|
|
419
|
+
servicePort,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages health checks for various services and provides aggregated status
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HealthCheck, HealthCheckResult, HealthStatus, Logger } from './types.js';
|
|
10
|
+
|
|
11
|
+
export class HealthManager {
|
|
12
|
+
private checks: Map<string, HealthCheck> = new Map();
|
|
13
|
+
private results: Map<string, HealthCheckResult> = new Map();
|
|
14
|
+
private intervals: Map<string, ReturnType<typeof setInterval>> = new Map();
|
|
15
|
+
private logger: Logger;
|
|
16
|
+
|
|
17
|
+
constructor(logger: Logger) {
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a health check
|
|
23
|
+
*/
|
|
24
|
+
register(check: HealthCheck): void {
|
|
25
|
+
this.checks.set(check.name, check);
|
|
26
|
+
|
|
27
|
+
// Initialize result
|
|
28
|
+
this.results.set(check.name, {
|
|
29
|
+
status: 'unknown',
|
|
30
|
+
lastChecked: new Date(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Start periodic check
|
|
34
|
+
const interval = check.interval || 30000; // Default 30 seconds
|
|
35
|
+
this.runCheck(check.name);
|
|
36
|
+
|
|
37
|
+
const timer = setInterval(() => {
|
|
38
|
+
this.runCheck(check.name);
|
|
39
|
+
}, interval);
|
|
40
|
+
|
|
41
|
+
this.intervals.set(check.name, timer);
|
|
42
|
+
|
|
43
|
+
this.logger.info(`[HealthManager] Registered health check: ${check.name}`, {
|
|
44
|
+
type: check.type,
|
|
45
|
+
interval,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run a health check
|
|
51
|
+
*/
|
|
52
|
+
private async runCheck(name: string): Promise<void> {
|
|
53
|
+
const check = this.checks.get(name);
|
|
54
|
+
if (!check) return;
|
|
55
|
+
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const timeout = check.timeout || 5000;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
let result: { healthy: boolean; latency?: number; details?: Record<string, unknown> };
|
|
61
|
+
|
|
62
|
+
switch (check.type) {
|
|
63
|
+
case 'http':
|
|
64
|
+
result = await this.runHttpCheck(check, timeout);
|
|
65
|
+
break;
|
|
66
|
+
case 'tcp':
|
|
67
|
+
result = await this.runTcpCheck(check, timeout);
|
|
68
|
+
break;
|
|
69
|
+
case 'custom':
|
|
70
|
+
if (check.check) {
|
|
71
|
+
result = await Promise.race([
|
|
72
|
+
check.check(),
|
|
73
|
+
new Promise<never>((_, reject) =>
|
|
74
|
+
setTimeout(() => reject(new Error('Timeout')), timeout)
|
|
75
|
+
),
|
|
76
|
+
]);
|
|
77
|
+
} else {
|
|
78
|
+
result = { healthy: false, details: { error: 'No check function provided' } };
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
result = { healthy: false, details: { error: 'Unknown check type' } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const latency = result.latency || Date.now() - startTime;
|
|
86
|
+
|
|
87
|
+
this.results.set(name, {
|
|
88
|
+
status: result.healthy ? 'healthy' : 'unhealthy',
|
|
89
|
+
latency,
|
|
90
|
+
lastChecked: new Date(),
|
|
91
|
+
details: result.details,
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const latency = Date.now() - startTime;
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
|
|
97
|
+
this.results.set(name, {
|
|
98
|
+
status: 'unhealthy',
|
|
99
|
+
latency,
|
|
100
|
+
message,
|
|
101
|
+
lastChecked: new Date(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.logger.warn(`[HealthManager] Health check failed: ${name}`, {
|
|
105
|
+
error: message,
|
|
106
|
+
latency,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run HTTP health check
|
|
113
|
+
*/
|
|
114
|
+
private async runHttpCheck(
|
|
115
|
+
check: HealthCheck,
|
|
116
|
+
timeout: number
|
|
117
|
+
): Promise<{ healthy: boolean; latency?: number; details?: Record<string, unknown> }> {
|
|
118
|
+
if (!check.url) {
|
|
119
|
+
return { healthy: false, details: { error: 'No URL provided' } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
const response = await fetch(check.url, {
|
|
128
|
+
method: 'GET',
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
});
|
|
131
|
+
const latency = Date.now() - startTime;
|
|
132
|
+
|
|
133
|
+
clearTimeout(timeoutId);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
healthy: response.ok,
|
|
137
|
+
latency,
|
|
138
|
+
details: {
|
|
139
|
+
status: response.status,
|
|
140
|
+
statusText: response.statusText,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run TCP health check (simplified - just tries to connect)
|
|
151
|
+
*/
|
|
152
|
+
private async runTcpCheck(
|
|
153
|
+
check: HealthCheck,
|
|
154
|
+
timeout: number
|
|
155
|
+
): Promise<{ healthy: boolean; latency?: number; details?: Record<string, unknown> }> {
|
|
156
|
+
if (!check.host || !check.port) {
|
|
157
|
+
return { healthy: false, details: { error: 'Host and port required for TCP check' } };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Use HTTP check as a proxy for TCP (simplified for browser compatibility)
|
|
161
|
+
// In a real Node.js environment, you'd use net.createConnection
|
|
162
|
+
const url = `http://${check.host}:${check.port}`;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return await this.runHttpCheck({ ...check, url }, timeout);
|
|
166
|
+
} catch {
|
|
167
|
+
// TCP check failed
|
|
168
|
+
return {
|
|
169
|
+
healthy: false,
|
|
170
|
+
details: { error: `Cannot connect to ${check.host}:${check.port}` },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all health check results
|
|
177
|
+
*/
|
|
178
|
+
getResults(): Record<string, HealthCheckResult> {
|
|
179
|
+
return Object.fromEntries(this.results);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get specific health check result
|
|
184
|
+
*/
|
|
185
|
+
getResult(name: string): HealthCheckResult | undefined {
|
|
186
|
+
return this.results.get(name);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get aggregated status
|
|
191
|
+
*/
|
|
192
|
+
getAggregatedStatus(): HealthStatus {
|
|
193
|
+
const results = Array.from(this.results.values());
|
|
194
|
+
|
|
195
|
+
if (results.length === 0) return 'unknown';
|
|
196
|
+
|
|
197
|
+
const unhealthyCount = results.filter((r) => r.status === 'unhealthy').length;
|
|
198
|
+
const degradedCount = results.filter((r) => r.status === 'degraded').length;
|
|
199
|
+
|
|
200
|
+
if (unhealthyCount > 0) return 'unhealthy';
|
|
201
|
+
if (degradedCount > 0) return 'degraded';
|
|
202
|
+
|
|
203
|
+
const hasUnknown = results.some((r) => r.status === 'unknown');
|
|
204
|
+
if (hasUnknown) return 'unknown';
|
|
205
|
+
|
|
206
|
+
return 'healthy';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Force run all checks
|
|
211
|
+
*/
|
|
212
|
+
async checkAll(): Promise<void> {
|
|
213
|
+
const promises = Array.from(this.checks.keys()).map((name) => this.runCheck(name));
|
|
214
|
+
await Promise.all(promises);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Shutdown - clear all intervals
|
|
219
|
+
*/
|
|
220
|
+
shutdown(): void {
|
|
221
|
+
for (const timer of this.intervals.values()) {
|
|
222
|
+
clearInterval(timer);
|
|
223
|
+
}
|
|
224
|
+
this.intervals.clear();
|
|
225
|
+
this.logger.info('[HealthManager] Shutdown complete');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core exports for @qwickapps/server
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { createControlPanel } from './control-panel.js';
|
|
8
|
+
export type { CreateControlPanelOptions } from './control-panel.js';
|
|
9
|
+
|
|
10
|
+
export { HealthManager } from './health-manager.js';
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
ControlPanelConfig,
|
|
14
|
+
ControlPanelPlugin,
|
|
15
|
+
ControlPanelInstance,
|
|
16
|
+
PluginContext,
|
|
17
|
+
HealthCheck,
|
|
18
|
+
HealthCheckType,
|
|
19
|
+
HealthStatus,
|
|
20
|
+
HealthCheckResult,
|
|
21
|
+
LogSource,
|
|
22
|
+
ConfigDisplayOptions,
|
|
23
|
+
Logger,
|
|
24
|
+
DiagnosticsReport,
|
|
25
|
+
} from './types.js';
|