@qwickapps/server 1.0.0 → 1.1.7
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/README.md +179 -80
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +37 -45
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +32 -13
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +150 -99
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +167 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +3 -9
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/logging.js +1 -5
- package/dist/core/logging.js.map +1 -1
- package/dist/core/types.d.ts +104 -11
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache-plugin.d.ts +219 -0
- package/dist/plugins/cache-plugin.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.js +326 -0
- package/dist/plugins/cache-plugin.js.map +1 -0
- package/dist/plugins/cache-plugin.test.d.ts +8 -0
- package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.test.js +188 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -0
- package/dist/plugins/config-plugin.js +1 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +1 -1
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/frontend-app-plugin.d.ts +39 -0
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -0
- package/dist/plugins/frontend-app-plugin.js +176 -0
- package/dist/plugins/frontend-app-plugin.js.map +1 -0
- package/dist/plugins/health-plugin.js +1 -1
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +5 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +1 -3
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +155 -0
- package/dist/plugins/postgres-plugin.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.js +244 -0
- package/dist/plugins/postgres-plugin.js.map +1 -0
- package/dist/plugins/postgres-plugin.test.d.ts +8 -0
- package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.test.js +165 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -0
- package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
- package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/package.json +18 -2
- package/src/core/control-panel.ts +41 -53
- package/src/core/gateway.ts +193 -124
- package/src/core/guards.ts +190 -0
- package/src/core/health-manager.ts +3 -9
- package/src/core/logging.ts +1 -5
- package/src/core/types.ts +115 -9
- package/src/index.ts +40 -0
- package/src/plugins/cache-plugin.test.ts +241 -0
- package/src/plugins/cache-plugin.ts +503 -0
- package/src/plugins/config-plugin.ts +1 -1
- package/src/plugins/diagnostics-plugin.ts +1 -1
- package/src/plugins/frontend-app-plugin.ts +211 -0
- package/src/plugins/health-plugin.ts +1 -1
- package/src/plugins/index.ts +13 -0
- package/src/plugins/logs-plugin.ts +1 -3
- package/src/plugins/postgres-plugin.test.ts +213 -0
- package/src/plugins/postgres-plugin.ts +345 -0
- package/ui/src/api/controlPanelApi.ts +1 -1
- package/ui/src/pages/LogsPage.tsx +6 -10
package/dist-ui/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Control Panel</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CW1BviRn.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CiizQQnb.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qwickapps/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,9 +55,13 @@
|
|
|
55
55
|
"@types/cors": "^2.8.17",
|
|
56
56
|
"@types/express": "^4.17.21",
|
|
57
57
|
"@types/node": "^20.10.5",
|
|
58
|
+
"@types/pg": "^8.11.0",
|
|
58
59
|
"@types/react": "^18.2.0",
|
|
60
|
+
"ioredis": "^5.4.0",
|
|
61
|
+
"pg": "^8.13.0",
|
|
59
62
|
"@types/react-dom": "^18.2.0",
|
|
60
63
|
"@vitejs/plugin-react": "^4.3.4",
|
|
64
|
+
"express-openid-connect": "^2.19.3",
|
|
61
65
|
"react": "^18.2.0",
|
|
62
66
|
"react-dom": "^18.2.0",
|
|
63
67
|
"react-router-dom": "^6.30.1",
|
|
@@ -66,11 +70,23 @@
|
|
|
66
70
|
"vitest": "^2.1.0"
|
|
67
71
|
},
|
|
68
72
|
"peerDependencies": {
|
|
69
|
-
"@qwickapps/react-framework": ">=1.0.0"
|
|
73
|
+
"@qwickapps/react-framework": ">=1.0.0",
|
|
74
|
+
"express-openid-connect": ">=2.0.0",
|
|
75
|
+
"ioredis": ">=5.0.0",
|
|
76
|
+
"pg": ">=8.0.0"
|
|
70
77
|
},
|
|
71
78
|
"peerDependenciesMeta": {
|
|
72
79
|
"@qwickapps/react-framework": {
|
|
73
80
|
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"express-openid-connect": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"ioredis": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"pg": {
|
|
89
|
+
"optional": true
|
|
74
90
|
}
|
|
75
91
|
},
|
|
76
92
|
"keywords": [
|
|
@@ -14,7 +14,8 @@ import { existsSync } from 'node:fs';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { HealthManager } from './health-manager.js';
|
|
17
|
-
import { initializeLogging, getControlPanelLogger,
|
|
17
|
+
import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './logging.js';
|
|
18
|
+
import { createRouteGuard } from './guards.js';
|
|
18
19
|
import type {
|
|
19
20
|
ControlPanelConfig,
|
|
20
21
|
ControlPanelPlugin,
|
|
@@ -94,35 +95,15 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
94
95
|
}
|
|
95
96
|
app.use(compression());
|
|
96
97
|
|
|
97
|
-
//
|
|
98
|
-
if (config.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
102
|
-
res.set('WWW-Authenticate', 'Basic realm="Control Panel"');
|
|
103
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const base64Credentials = authHeader.substring(6);
|
|
107
|
-
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
108
|
-
const [username, password] = credentials.split(':');
|
|
109
|
-
|
|
110
|
-
const validUser = config.auth!.users!.find(
|
|
111
|
-
(u) => u.username === username && u.password === password
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
if (!validUser) {
|
|
115
|
-
return res.status(401).json({ error: 'Invalid credentials' });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
next();
|
|
119
|
-
});
|
|
98
|
+
// Apply route guard if configured
|
|
99
|
+
if (config.guard && config.guard.type !== 'none') {
|
|
100
|
+
const guardMiddleware = createRouteGuard(config.guard);
|
|
101
|
+
app.use(guardMiddleware);
|
|
120
102
|
}
|
|
121
103
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
104
|
+
// Get mount path (defaults to /cpanel)
|
|
105
|
+
const mountPath = config.mountPath || '/cpanel';
|
|
106
|
+
const apiBasePath = mountPath === '/' ? '/api' : `${mountPath}/api`;
|
|
126
107
|
|
|
127
108
|
// Request logging
|
|
128
109
|
app.use((req, _res, next) => {
|
|
@@ -130,8 +111,8 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
130
111
|
next();
|
|
131
112
|
});
|
|
132
113
|
|
|
133
|
-
// Mount router
|
|
134
|
-
app.use(
|
|
114
|
+
// Mount router at the configured path
|
|
115
|
+
app.use(apiBasePath, router);
|
|
135
116
|
|
|
136
117
|
// Built-in routes
|
|
137
118
|
|
|
@@ -173,7 +154,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
173
154
|
});
|
|
174
155
|
|
|
175
156
|
/**
|
|
176
|
-
* Serve dashboard UI
|
|
157
|
+
* Serve dashboard UI at the configured mount path
|
|
177
158
|
*
|
|
178
159
|
* Priority:
|
|
179
160
|
* 1. If useRichUI is true and dist-ui exists, serve React SPA
|
|
@@ -185,25 +166,33 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
185
166
|
const hasRichUI = existsSync(effectiveUiPath);
|
|
186
167
|
const useRichUI = config.useRichUI !== false && hasRichUI;
|
|
187
168
|
|
|
188
|
-
logger.debug(`Dashboard config:
|
|
169
|
+
logger.debug(`Dashboard config: mountPath=${mountPath}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
|
|
189
170
|
|
|
190
171
|
if (useRichUI) {
|
|
191
|
-
logger.
|
|
192
|
-
// Serve static assets from dist-ui
|
|
193
|
-
app.use(express.static(effectiveUiPath));
|
|
194
|
-
|
|
195
|
-
// SPA fallback - serve index.html for all non-API routes
|
|
196
|
-
app.get(
|
|
197
|
-
// Skip API routes
|
|
198
|
-
if (req.path.startsWith(
|
|
172
|
+
logger.debug(`Serving React UI from ${effectiveUiPath}`);
|
|
173
|
+
// Serve static assets from dist-ui at the mount path
|
|
174
|
+
app.use(mountPath, express.static(effectiveUiPath));
|
|
175
|
+
|
|
176
|
+
// SPA fallback - serve index.html for all non-API routes under the mount path
|
|
177
|
+
app.get(`${mountPath}/*`, (req: Request, res: Response, next) => {
|
|
178
|
+
// Skip API routes
|
|
179
|
+
if (req.path.startsWith(apiBasePath)) {
|
|
199
180
|
return next();
|
|
200
181
|
}
|
|
201
182
|
res.sendFile(join(effectiveUiPath, 'index.html'));
|
|
202
183
|
});
|
|
184
|
+
|
|
185
|
+
// Also serve the mount path root
|
|
186
|
+
if (mountPath !== '/') {
|
|
187
|
+
app.get(mountPath, (_req: Request, res: Response) => {
|
|
188
|
+
res.sendFile(join(effectiveUiPath, 'index.html'));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
203
191
|
} else {
|
|
204
|
-
logger.
|
|
205
|
-
|
|
206
|
-
|
|
192
|
+
logger.debug(`Serving basic HTML dashboard`);
|
|
193
|
+
const dashboardPath = mountPath === '/' ? '/' : mountPath;
|
|
194
|
+
app.get(dashboardPath, (_req: Request, res: Response) => {
|
|
195
|
+
const html = generateDashboardHtml(config, healthManager.getResults(), mountPath);
|
|
207
196
|
res.type('html').send(html);
|
|
208
197
|
});
|
|
209
198
|
}
|
|
@@ -220,7 +209,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
220
209
|
|
|
221
210
|
// Register plugin
|
|
222
211
|
const registerPlugin = async (plugin: ControlPanelPlugin): Promise<void> => {
|
|
223
|
-
logger.
|
|
212
|
+
logger.debug(`Registering plugin: ${plugin.name}`);
|
|
224
213
|
|
|
225
214
|
// Register routes
|
|
226
215
|
if (plugin.routes) {
|
|
@@ -249,7 +238,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
249
238
|
}
|
|
250
239
|
|
|
251
240
|
registeredPlugins.push(plugin);
|
|
252
|
-
logger.
|
|
241
|
+
logger.debug(`Plugin registered: ${plugin.name}`);
|
|
253
242
|
};
|
|
254
243
|
|
|
255
244
|
// Get diagnostics report
|
|
@@ -287,10 +276,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
287
276
|
|
|
288
277
|
return new Promise((resolve) => {
|
|
289
278
|
server = app.listen(config.port, () => {
|
|
290
|
-
logger.info(`Control panel
|
|
291
|
-
logger.info(`Dashboard: http://localhost:${config.port}`);
|
|
292
|
-
logger.info(`Health: http://localhost:${config.port}/api/health`);
|
|
293
|
-
logger.info(`Diagnostics: http://localhost:${config.port}/api/diagnostics`);
|
|
279
|
+
logger.info(`Control panel listening on port ${config.port}`);
|
|
294
280
|
resolve();
|
|
295
281
|
});
|
|
296
282
|
});
|
|
@@ -334,8 +320,10 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
334
320
|
*/
|
|
335
321
|
function generateDashboardHtml(
|
|
336
322
|
config: ControlPanelConfig,
|
|
337
|
-
health: Record<string, { status: string; latency?: number; lastChecked: Date }
|
|
323
|
+
health: Record<string, { status: string; latency?: number; lastChecked: Date }>,
|
|
324
|
+
mountPath: string = '/cpanel'
|
|
338
325
|
): string {
|
|
326
|
+
const apiBasePath = mountPath === '/' ? '/api' : `${mountPath}/api`;
|
|
339
327
|
const healthEntries = Object.entries(health);
|
|
340
328
|
const overallStatus = healthEntries.every((e) => e[1].status === 'healthy')
|
|
341
329
|
? 'healthy'
|
|
@@ -478,9 +466,9 @@ function generateDashboardHtml(
|
|
|
478
466
|
|
|
479
467
|
<div class="api-links">
|
|
480
468
|
<strong>API:</strong>
|
|
481
|
-
<a href="/
|
|
482
|
-
<a href="/
|
|
483
|
-
<a href="/
|
|
469
|
+
<a href="${apiBasePath}/health">Health</a>
|
|
470
|
+
<a href="${apiBasePath}/info">Info</a>
|
|
471
|
+
<a href="${apiBasePath}/diagnostics">Diagnostics</a>
|
|
484
472
|
</div>
|
|
485
473
|
</div>
|
|
486
474
|
|
package/src/core/gateway.ts
CHANGED
|
@@ -23,7 +23,6 @@ import type { ControlPanelConfig, ControlPanelPlugin, Logger } from './types.js'
|
|
|
23
23
|
import { createControlPanel } from './control-panel.js';
|
|
24
24
|
import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './logging.js';
|
|
25
25
|
import { createProxyMiddleware, type Options } from 'http-proxy-middleware';
|
|
26
|
-
import { randomBytes } from 'crypto';
|
|
27
26
|
import express from 'express';
|
|
28
27
|
import { existsSync } from 'fs';
|
|
29
28
|
import { resolve } from 'path';
|
|
@@ -56,31 +55,47 @@ export interface GatewayConfig {
|
|
|
56
55
|
/** Quick links for the control panel */
|
|
57
56
|
links?: ControlPanelConfig['links'];
|
|
58
57
|
|
|
59
|
-
/** Path to custom React UI dist folder */
|
|
58
|
+
/** Path to custom React UI dist folder for the control panel */
|
|
60
59
|
customUiPath?: string;
|
|
61
60
|
|
|
62
61
|
/**
|
|
63
|
-
*
|
|
64
|
-
* Defaults to
|
|
65
|
-
* The gateway always proxies /health to the internal service.
|
|
62
|
+
* Mount path for the control panel.
|
|
63
|
+
* Defaults to '/cpanel'.
|
|
66
64
|
*/
|
|
67
|
-
|
|
65
|
+
controlPanelPath?: string;
|
|
68
66
|
|
|
69
67
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* - 'basic': HTTP Basic Auth with username/password
|
|
73
|
-
* - 'auto': Auto-generate password on startup (default)
|
|
68
|
+
* Route guard for the control panel.
|
|
69
|
+
* Defaults to auto-generated basic auth.
|
|
74
70
|
*/
|
|
75
|
-
|
|
71
|
+
controlPanelGuard?: ControlPanelConfig['guard'];
|
|
76
72
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Frontend app configuration for the root path (/).
|
|
75
|
+
* If not provided, root path is not handled by the gateway.
|
|
76
|
+
*/
|
|
77
|
+
frontendApp?: {
|
|
78
|
+
/** Redirect to another URL */
|
|
79
|
+
redirectUrl?: string;
|
|
80
|
+
/** Path to static files to serve */
|
|
81
|
+
staticPath?: string;
|
|
82
|
+
/** Landing page configuration */
|
|
83
|
+
landingPage?: {
|
|
84
|
+
title: string;
|
|
85
|
+
heading?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
links?: Array<{ label: string; url: string }>;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
79
90
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
91
|
+
/**
|
|
92
|
+
* API paths to proxy to the internal service.
|
|
93
|
+
* Defaults to ['/api/v1'] if not specified.
|
|
94
|
+
* The gateway always proxies /health to the internal service.
|
|
95
|
+
*/
|
|
96
|
+
proxyPaths?: string[];
|
|
82
97
|
|
|
83
|
-
/** Logger instance
|
|
98
|
+
/** Logger instance */
|
|
84
99
|
logger?: Logger;
|
|
85
100
|
|
|
86
101
|
/** Logging configuration */
|
|
@@ -131,63 +146,106 @@ export interface GatewayInstance {
|
|
|
131
146
|
|
|
132
147
|
|
|
133
148
|
/**
|
|
134
|
-
*
|
|
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
|
|
149
|
+
* Generate landing page HTML for the frontend app
|
|
139
150
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
function generateLandingPageHtml(
|
|
152
|
+
config: NonNullable<GatewayConfig['frontendApp']>['landingPage'],
|
|
153
|
+
controlPanelPath: string
|
|
154
|
+
): string {
|
|
155
|
+
if (!config) return '';
|
|
156
|
+
|
|
157
|
+
const primaryColor = '#6366f1';
|
|
158
|
+
|
|
159
|
+
const links = config.links || [
|
|
160
|
+
{ label: 'Control Panel', url: controlPanelPath },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const linksHtml = links
|
|
164
|
+
.map(
|
|
165
|
+
(link) =>
|
|
166
|
+
`<a href="${link.url}" class="link">${link.label}</a>`
|
|
167
|
+
)
|
|
168
|
+
.join('');
|
|
169
|
+
|
|
170
|
+
return `<!DOCTYPE html>
|
|
171
|
+
<html lang="en">
|
|
172
|
+
<head>
|
|
173
|
+
<meta charset="UTF-8">
|
|
174
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
175
|
+
<title>${config.title}</title>
|
|
176
|
+
<style>
|
|
177
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
178
|
+
body {
|
|
179
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
180
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
181
|
+
color: #e2e8f0;
|
|
182
|
+
min-height: 100vh;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
155
186
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
.container {
|
|
188
|
+
text-align: center;
|
|
189
|
+
max-width: 600px;
|
|
190
|
+
padding: 2rem;
|
|
160
191
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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();
|
|
192
|
+
h1 {
|
|
193
|
+
font-size: 2.5rem;
|
|
194
|
+
color: ${primaryColor};
|
|
195
|
+
margin-bottom: 1rem;
|
|
176
196
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
p {
|
|
198
|
+
font-size: 1.125rem;
|
|
199
|
+
color: #94a3b8;
|
|
200
|
+
margin-bottom: 2rem;
|
|
201
|
+
line-height: 1.6;
|
|
182
202
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
.links {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
gap: 1rem;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
}
|
|
209
|
+
.link {
|
|
210
|
+
display: inline-block;
|
|
211
|
+
padding: 0.875rem 2rem;
|
|
212
|
+
background: ${primaryColor};
|
|
213
|
+
color: white;
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
border-radius: 0.5rem;
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
transition: all 0.2s;
|
|
218
|
+
}
|
|
219
|
+
.link:hover {
|
|
220
|
+
transform: translateY(-2px);
|
|
221
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.3);
|
|
222
|
+
}
|
|
223
|
+
.footer {
|
|
224
|
+
position: fixed;
|
|
225
|
+
bottom: 1rem;
|
|
226
|
+
left: 0;
|
|
227
|
+
right: 0;
|
|
228
|
+
text-align: center;
|
|
229
|
+
color: #64748b;
|
|
230
|
+
font-size: 0.875rem;
|
|
231
|
+
}
|
|
232
|
+
.footer a {
|
|
233
|
+
color: ${primaryColor};
|
|
234
|
+
text-decoration: none;
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
237
|
+
</head>
|
|
238
|
+
<body>
|
|
239
|
+
<div class="container">
|
|
240
|
+
<h1>${config.heading || config.title}</h1>
|
|
241
|
+
${config.description ? `<p>${config.description}</p>` : ''}
|
|
242
|
+
${linksHtml ? `<div class="links">${linksHtml}</div>` : ''}
|
|
243
|
+
</div>
|
|
244
|
+
<div class="footer">
|
|
245
|
+
Powered by <a href="https://qwickapps.com" target="_blank">QwickApps</a>
|
|
246
|
+
</div>
|
|
247
|
+
</body>
|
|
248
|
+
</html>`;
|
|
191
249
|
}
|
|
192
250
|
|
|
193
251
|
/**
|
|
@@ -238,12 +296,11 @@ export function createGateway(
|
|
|
238
296
|
const servicePort = config.servicePort || parseInt(process.env.SERVICE_PORT || '3100', 10);
|
|
239
297
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
240
298
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const isPasswordAutoGenerated = !providedPassword && authMode === 'auto';
|
|
299
|
+
// Control panel mount path (defaults to /cpanel)
|
|
300
|
+
const controlPanelPath = config.controlPanelPath || '/cpanel';
|
|
301
|
+
|
|
302
|
+
// Guard configuration for control panel
|
|
303
|
+
const guardConfig = config.controlPanelGuard;
|
|
247
304
|
|
|
248
305
|
// API paths to proxy
|
|
249
306
|
const proxyPaths = config.proxyPaths || ['/api/v1'];
|
|
@@ -260,19 +317,18 @@ export function createGateway(
|
|
|
260
317
|
cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
|
|
261
318
|
// Skip body parsing for proxied paths
|
|
262
319
|
skipBodyParserPaths: [...proxyPaths, '/health'],
|
|
263
|
-
//
|
|
264
|
-
|
|
320
|
+
// Mount path for control panel
|
|
321
|
+
mountPath: controlPanelPath,
|
|
322
|
+
// Route guard
|
|
323
|
+
guard: guardConfig,
|
|
324
|
+
// Custom UI path
|
|
325
|
+
customUiPath: config.customUiPath,
|
|
265
326
|
links: config.links,
|
|
266
327
|
},
|
|
267
328
|
plugins: config.plugins || [],
|
|
268
329
|
logger,
|
|
269
330
|
});
|
|
270
331
|
|
|
271
|
-
// Add basic auth middleware if enabled
|
|
272
|
-
if (authMode === 'basic' || authMode === 'auto') {
|
|
273
|
-
controlPanel.app.use(createBasicAuthMiddleware(basicAuthUser, basicAuthPassword, proxyPaths));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
332
|
// Setup proxy middleware for API paths
|
|
277
333
|
const setupProxyMiddleware = () => {
|
|
278
334
|
const target = `http://localhost:${servicePort}`;
|
|
@@ -325,18 +381,41 @@ export function createGateway(
|
|
|
325
381
|
controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
|
|
326
382
|
};
|
|
327
383
|
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
if (config.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
384
|
+
// Setup frontend app at root path
|
|
385
|
+
const setupFrontendApp = () => {
|
|
386
|
+
if (!config.frontendApp) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { redirectUrl, staticPath, landingPage } = config.frontendApp;
|
|
391
|
+
|
|
392
|
+
// Priority 1: Redirect
|
|
393
|
+
if (redirectUrl) {
|
|
394
|
+
logger.info(`Frontend app: Redirecting / to ${redirectUrl}`);
|
|
395
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
396
|
+
res.redirect(redirectUrl);
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Priority 2: Serve static files
|
|
402
|
+
if (staticPath && existsSync(staticPath)) {
|
|
403
|
+
logger.info(`Frontend app: Serving static files from ${staticPath}`);
|
|
404
|
+
controlPanel.app.use('/', express.static(staticPath));
|
|
405
|
+
|
|
406
|
+
// SPA fallback for root
|
|
407
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
408
|
+
res.sendFile(resolve(staticPath, 'index.html'));
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Priority 3: Landing page
|
|
414
|
+
if (landingPage) {
|
|
415
|
+
logger.info(`Frontend app: Serving landing page`);
|
|
416
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
417
|
+
const html = generateLandingPageHtml(landingPage, controlPanelPath);
|
|
418
|
+
res.type('html').send(html);
|
|
340
419
|
});
|
|
341
420
|
}
|
|
342
421
|
};
|
|
@@ -352,47 +431,37 @@ export function createGateway(
|
|
|
352
431
|
// 2. Setup proxy middleware (after service is started)
|
|
353
432
|
setupProxyMiddleware();
|
|
354
433
|
|
|
355
|
-
// 3. Setup
|
|
356
|
-
|
|
434
|
+
// 3. Setup frontend app at root path
|
|
435
|
+
setupFrontendApp();
|
|
357
436
|
|
|
358
437
|
// 4. Start control panel gateway
|
|
359
438
|
await controlPanel.start();
|
|
360
439
|
|
|
440
|
+
// Calculate API base path
|
|
441
|
+
const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
|
|
442
|
+
|
|
361
443
|
// Log startup info
|
|
362
|
-
logger.info(
|
|
363
|
-
logger.info(
|
|
364
|
-
logger.info(` ${
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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(' ----------------------------------------');
|
|
444
|
+
logger.info(`${config.productName} Gateway`);
|
|
445
|
+
logger.info(`Gateway Port: ${gatewayPort} (public)`);
|
|
446
|
+
logger.info(`Service Port: ${servicePort} (internal)`);
|
|
447
|
+
|
|
448
|
+
if (guardConfig && guardConfig.type === 'basic') {
|
|
449
|
+
logger.info(`Control Panel Auth: HTTP Basic Auth - Username: ${guardConfig.username}`);
|
|
450
|
+
} else if (guardConfig && guardConfig.type !== 'none') {
|
|
451
|
+
logger.info(`Control Panel Auth: ${guardConfig.type}`);
|
|
382
452
|
} else {
|
|
383
|
-
logger.info('
|
|
453
|
+
logger.info('Control Panel Auth: None (not recommended)');
|
|
384
454
|
}
|
|
385
455
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
logger.info(`
|
|
390
|
-
logger.info(`
|
|
456
|
+
if (config.frontendApp) {
|
|
457
|
+
logger.info(`Frontend App: GET /`);
|
|
458
|
+
}
|
|
459
|
+
logger.info(`Control Panel UI: GET ${controlPanelPath.padEnd(20)}`);
|
|
460
|
+
logger.info(`Gateway Health: GET ${apiBasePath}/health`);
|
|
461
|
+
logger.info(`Service Health: GET /health`);
|
|
391
462
|
for (const apiPath of proxyPaths) {
|
|
392
|
-
logger.info(`
|
|
463
|
+
logger.info(`Service API: * ${apiPath}/*`);
|
|
393
464
|
}
|
|
394
|
-
logger.info('========================================');
|
|
395
|
-
logger.info('');
|
|
396
465
|
};
|
|
397
466
|
|
|
398
467
|
const stop = async (): Promise<void> => {
|