@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.
Files changed (80) hide show
  1. package/README.md +179 -80
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +37 -45
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/gateway.d.ts +32 -13
  6. package/dist/core/gateway.d.ts.map +1 -1
  7. package/dist/core/gateway.js +150 -99
  8. package/dist/core/gateway.js.map +1 -1
  9. package/dist/core/guards.d.ts +22 -0
  10. package/dist/core/guards.d.ts.map +1 -0
  11. package/dist/core/guards.js +167 -0
  12. package/dist/core/guards.js.map +1 -0
  13. package/dist/core/health-manager.d.ts.map +1 -1
  14. package/dist/core/health-manager.js +3 -9
  15. package/dist/core/health-manager.js.map +1 -1
  16. package/dist/core/logging.d.ts.map +1 -1
  17. package/dist/core/logging.js +1 -5
  18. package/dist/core/logging.js.map +1 -1
  19. package/dist/core/types.d.ts +104 -11
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +9 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/plugins/cache-plugin.d.ts +219 -0
  26. package/dist/plugins/cache-plugin.d.ts.map +1 -0
  27. package/dist/plugins/cache-plugin.js +326 -0
  28. package/dist/plugins/cache-plugin.js.map +1 -0
  29. package/dist/plugins/cache-plugin.test.d.ts +8 -0
  30. package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
  31. package/dist/plugins/cache-plugin.test.js +188 -0
  32. package/dist/plugins/cache-plugin.test.js.map +1 -0
  33. package/dist/plugins/config-plugin.js +1 -1
  34. package/dist/plugins/config-plugin.js.map +1 -1
  35. package/dist/plugins/diagnostics-plugin.js +1 -1
  36. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  37. package/dist/plugins/frontend-app-plugin.d.ts +39 -0
  38. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -0
  39. package/dist/plugins/frontend-app-plugin.js +176 -0
  40. package/dist/plugins/frontend-app-plugin.js.map +1 -0
  41. package/dist/plugins/health-plugin.js +1 -1
  42. package/dist/plugins/health-plugin.js.map +1 -1
  43. package/dist/plugins/index.d.ts +8 -0
  44. package/dist/plugins/index.d.ts.map +1 -1
  45. package/dist/plugins/index.js +5 -0
  46. package/dist/plugins/index.js.map +1 -1
  47. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  48. package/dist/plugins/logs-plugin.js +1 -3
  49. package/dist/plugins/logs-plugin.js.map +1 -1
  50. package/dist/plugins/postgres-plugin.d.ts +155 -0
  51. package/dist/plugins/postgres-plugin.d.ts.map +1 -0
  52. package/dist/plugins/postgres-plugin.js +244 -0
  53. package/dist/plugins/postgres-plugin.js.map +1 -0
  54. package/dist/plugins/postgres-plugin.test.d.ts +8 -0
  55. package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
  56. package/dist/plugins/postgres-plugin.test.js +165 -0
  57. package/dist/plugins/postgres-plugin.test.js.map +1 -0
  58. package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
  59. package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
  60. package/dist-ui/index.html +1 -1
  61. package/package.json +18 -2
  62. package/src/core/control-panel.ts +41 -53
  63. package/src/core/gateway.ts +193 -124
  64. package/src/core/guards.ts +190 -0
  65. package/src/core/health-manager.ts +3 -9
  66. package/src/core/logging.ts +1 -5
  67. package/src/core/types.ts +115 -9
  68. package/src/index.ts +40 -0
  69. package/src/plugins/cache-plugin.test.ts +241 -0
  70. package/src/plugins/cache-plugin.ts +503 -0
  71. package/src/plugins/config-plugin.ts +1 -1
  72. package/src/plugins/diagnostics-plugin.ts +1 -1
  73. package/src/plugins/frontend-app-plugin.ts +211 -0
  74. package/src/plugins/health-plugin.ts +1 -1
  75. package/src/plugins/index.ts +13 -0
  76. package/src/plugins/logs-plugin.ts +1 -3
  77. package/src/plugins/postgres-plugin.test.ts +213 -0
  78. package/src/plugins/postgres-plugin.ts +345 -0
  79. package/ui/src/api/controlPanelApi.ts +1 -1
  80. package/ui/src/pages/LogsPage.tsx +6 -10
@@ -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-Bk7ypbI4.js"></script>
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.0.0",
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, getLoggingSubsystem, type LoggingConfig } from './logging.js';
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
- // Basic auth middleware if configured
98
- if (config.auth?.enabled && config.auth.provider === 'basic' && config.auth.users) {
99
- app.use((req, res, next) => {
100
- const authHeader = req.headers.authorization;
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
- // Custom auth middleware
123
- if (config.auth?.enabled && config.auth.provider === 'custom' && config.auth.customMiddleware) {
124
- app.use(config.auth.customMiddleware);
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('/api', router);
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: __dirname=${__dirname}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
169
+ logger.debug(`Dashboard config: mountPath=${mountPath}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
189
170
 
190
171
  if (useRichUI) {
191
- logger.info(`Serving rich React UI from ${effectiveUiPath}`);
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('*', (req: Request, res: Response, next) => {
197
- // Skip API routes (must check for /api/ with trailing slash to avoid matching /api-keys etc)
198
- if (req.path.startsWith('/api/') || req.path === '/api') {
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.info('Serving basic HTML dashboard');
205
- app.get('/', (_req: Request, res: Response) => {
206
- const html = generateDashboardHtml(config, healthManager.getResults());
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.info(`Registering plugin: ${plugin.name}`);
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.info(`Plugin registered: ${plugin.name}`);
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 started on port ${config.port}`);
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="/api/health">Health</a>
482
- <a href="/api/info">Info</a>
483
- <a href="/api/diagnostics">Diagnostics</a>
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
 
@@ -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
- * 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.
62
+ * Mount path for the control panel.
63
+ * Defaults to '/cpanel'.
66
64
  */
67
- proxyPaths?: string[];
65
+ controlPanelPath?: string;
68
66
 
69
67
  /**
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)
68
+ * Route guard for the control panel.
69
+ * Defaults to auto-generated basic auth.
74
70
  */
75
- authMode?: 'none' | 'basic' | 'auto';
71
+ controlPanelGuard?: ControlPanelConfig['guard'];
76
72
 
77
- /** Basic auth username (defaults to 'admin') */
78
- basicAuthUser?: string;
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
- /** Basic auth password (required if authMode is 'basic') */
81
- basicAuthPassword?: string;
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 (deprecated: use logging config instead) */
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
- * 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
149
+ * Generate landing page HTML for the frontend app
139
150
  */
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
- }
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
- // Skip auth for health endpoints - these should be publicly accessible
158
- if (path === '/health' || path === '/api/health') {
159
- return next();
187
+ .container {
188
+ text-align: center;
189
+ max-width: 600px;
190
+ padding: 2rem;
160
191
  }
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();
192
+ h1 {
193
+ font-size: 2.5rem;
194
+ color: ${primaryColor};
195
+ margin-bottom: 1rem;
176
196
  }
177
-
178
- // Check for valid basic auth
179
- const authHeader = req.headers.authorization;
180
- if (authHeader === expectedAuth) {
181
- return next();
197
+ p {
198
+ font-size: 1.125rem;
199
+ color: #94a3b8;
200
+ margin-bottom: 2rem;
201
+ line-height: 1.6;
182
202
  }
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
- };
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
- // 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';
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
- // Disable built-in dashboard if custom UI is provided
264
- disableDashboard: !!config.customUiPath,
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
- // 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'));
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 custom UI (after proxy middleware)
356
- setupCustomUI();
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(` ${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(' ----------------------------------------');
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(' Control Panel Auth: None (not recommended)');
453
+ logger.info('Control Panel Auth: None (not recommended)');
384
454
  }
385
455
 
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)`);
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(` * ${apiPath}/* - Service API (proxied)`);
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> => {