@qwickapps/server 1.2.0 → 1.3.1
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 +392 -0
- package/dist/core/control-panel.d.ts +7 -2
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +120 -54
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +159 -79
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +679 -319
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +307 -0
- package/dist/core/plugin-registry.d.ts.map +1 -0
- package/dist/core/plugin-registry.js +352 -0
- package/dist/core/plugin-registry.js.map +1 -0
- package/dist/core/types.d.ts +16 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
- package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +10 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/index.js +10 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts +40 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +255 -0
- package/dist/plugins/auth/auth-plugin.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.js +147 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +88 -0
- package/dist/plugins/auth/env-config.d.ts.map +1 -0
- package/dist/plugins/auth/env-config.js +489 -0
- package/dist/plugins/auth/env-config.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +14 -0
- package/dist/plugins/auth/index.d.ts.map +1 -0
- package/dist/plugins/auth/index.js +16 -0
- package/dist/plugins/auth/index.js.map +1 -0
- package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
- package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
- package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
- package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +218 -0
- package/dist/plugins/auth/types.d.ts.map +1 -0
- package/dist/plugins/auth/types.js +14 -0
- package/dist/plugins/auth/types.js.map +1 -0
- package/dist/plugins/bans/bans-plugin.d.ts +59 -0
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
- package/dist/plugins/bans/bans-plugin.js +428 -0
- package/dist/plugins/bans/bans-plugin.js.map +1 -0
- package/dist/plugins/bans/index.d.ts +9 -0
- package/dist/plugins/bans/index.d.ts.map +1 -0
- package/dist/plugins/bans/index.js +10 -0
- package/dist/plugins/bans/index.js.map +1 -0
- package/dist/plugins/bans/stores/index.d.ts +7 -0
- package/dist/plugins/bans/stores/index.d.ts.map +1 -0
- package/dist/plugins/bans/stores/index.js +7 -0
- package/dist/plugins/bans/stores/index.js.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.js +132 -0
- package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
- package/dist/plugins/bans/types.d.ts +128 -0
- package/dist/plugins/bans/types.d.ts.map +1 -0
- package/dist/plugins/bans/types.js +11 -0
- package/dist/plugins/bans/types.js.map +1 -0
- package/dist/plugins/cache-plugin.d.ts +14 -3
- package/dist/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/plugins/cache-plugin.js +27 -7
- package/dist/plugins/cache-plugin.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +99 -32
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +3 -2
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +17 -10
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.d.ts +2 -2
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +17 -10
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
- package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
- package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
- package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
- package/dist/plugins/entitlements/index.d.ts +12 -0
- package/dist/plugins/entitlements/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/index.js +16 -0
- package/dist/plugins/entitlements/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/index.d.ts +9 -0
- package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/index.js +9 -0
- package/dist/plugins/entitlements/sources/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
- package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
- package/dist/plugins/entitlements/types.d.ts +232 -0
- package/dist/plugins/entitlements/types.d.ts.map +1 -0
- package/dist/plugins/entitlements/types.js +11 -0
- package/dist/plugins/entitlements/types.js.map +1 -0
- package/dist/plugins/frontend-app-plugin.d.ts +9 -3
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +14 -9
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.d.ts +5 -2
- package/dist/plugins/health-plugin.d.ts.map +1 -1
- package/dist/plugins/health-plugin.js +20 -5
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +10 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +10 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts +3 -2
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +21 -12
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +3 -3
- package/dist/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/plugins/postgres-plugin.js +9 -7
- package/dist/plugins/postgres-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +50 -29
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
- package/dist/plugins/preferences/index.d.ts +12 -0
- package/dist/plugins/preferences/index.d.ts.map +1 -0
- package/dist/plugins/preferences/index.js +13 -0
- package/dist/plugins/preferences/index.js.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.js +226 -0
- package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
- package/dist/plugins/preferences/stores/index.d.ts +9 -0
- package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/index.js +9 -0
- package/dist/plugins/preferences/stores/index.js.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.js +181 -0
- package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
- package/dist/plugins/preferences/types.d.ts +91 -0
- package/dist/plugins/preferences/types.d.ts.map +1 -0
- package/dist/plugins/preferences/types.js +10 -0
- package/dist/plugins/preferences/types.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
- package/dist/plugins/users/index.d.ts +12 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +13 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/stores/index.d.ts +7 -0
- package/dist/plugins/users/stores/index.d.ts.map +1 -0
- package/dist/plugins/users/stores/index.js +7 -0
- package/dist/plugins/users/stores/index.js.map +1 -0
- package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/users/stores/postgres-store.js +157 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -0
- package/dist/plugins/users/types.d.ts +225 -0
- package/dist/plugins/users/types.d.ts.map +1 -0
- package/dist/plugins/users/types.js +12 -0
- package/dist/plugins/users/types.js.map +1 -0
- package/dist/plugins/users/users-plugin.d.ts +45 -0
- package/dist/plugins/users/users-plugin.d.ts.map +1 -0
- package/dist/plugins/users/users-plugin.js +359 -0
- package/dist/plugins/users/users-plugin.js.map +1 -0
- package/dist-ui/assets/index-BY8OxNgO.js +465 -0
- package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
- package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
- package/dist-ui-lib/components/index.d.ts +18 -0
- package/dist-ui-lib/config/AppConfig.d.ts +7 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
- package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
- package/dist-ui-lib/dashboard/index.d.ts +13 -0
- package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
- package/dist-ui-lib/index.js +5172 -0
- package/dist-ui-lib/index.js.map +1 -0
- package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
- package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
- package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
- package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
- package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
- package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
- package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
- package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
- package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
- package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
- package/package.json +24 -7
- package/src/core/control-panel.ts +145 -61
- package/src/core/gateway.ts +863 -403
- package/src/core/index.ts +21 -2
- package/src/core/plugin-registry.ts +716 -0
- package/src/core/types.ts +31 -37
- package/src/index.ts +125 -19
- package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
- package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
- package/src/plugins/auth/adapters/index.ts +10 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
- package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
- package/src/plugins/auth/auth-plugin.test.ts +176 -0
- package/src/plugins/auth/auth-plugin.ts +303 -0
- package/src/plugins/auth/env-config.ts +572 -0
- package/src/plugins/auth/index.ts +42 -0
- package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
- package/src/plugins/auth/types.ts +245 -0
- package/src/plugins/bans/bans-plugin.ts +485 -0
- package/src/plugins/bans/index.ts +31 -0
- package/src/plugins/bans/stores/index.ts +7 -0
- package/src/plugins/bans/stores/postgres-store.ts +195 -0
- package/src/plugins/bans/types.ts +141 -0
- package/src/plugins/cache-plugin.test.ts +108 -32
- package/src/plugins/cache-plugin.ts +40 -9
- package/src/plugins/config-plugin.ts +23 -12
- package/src/plugins/diagnostics-plugin.ts +22 -12
- package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
- package/src/plugins/entitlements/index.ts +51 -0
- package/src/plugins/entitlements/sources/index.ts +9 -0
- package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
- package/src/plugins/entitlements/types.ts +256 -0
- package/src/plugins/frontend-app-plugin.ts +24 -12
- package/src/plugins/health-plugin.ts +27 -7
- package/src/plugins/index.ts +132 -4
- package/src/plugins/logs-plugin.ts +28 -14
- package/src/plugins/postgres-plugin.test.ts +52 -29
- package/src/plugins/postgres-plugin.ts +11 -9
- package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
- package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
- package/src/plugins/preferences/index.ts +30 -0
- package/src/plugins/preferences/preferences-plugin.ts +270 -0
- package/src/plugins/preferences/stores/index.ts +9 -0
- package/src/plugins/preferences/stores/postgres-store.ts +252 -0
- package/src/plugins/preferences/types.ts +100 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
- package/src/plugins/users/index.ts +38 -0
- package/src/plugins/users/stores/index.ts +7 -0
- package/src/plugins/users/stores/postgres-store.ts +225 -0
- package/src/plugins/users/types.ts +247 -0
- package/src/plugins/users/users-plugin.ts +418 -0
- package/ui/src/App.tsx +188 -31
- package/ui/src/api/controlPanelApi.ts +453 -1
- package/ui/src/components/ControlPanelApp.tsx +212 -0
- package/ui/src/components/index.ts +62 -0
- package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
- package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
- package/ui/src/dashboard/builtInWidgets.tsx +35 -0
- package/ui/src/dashboard/index.ts +35 -0
- package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
- package/ui/src/dashboard/widgets/index.ts +7 -0
- package/ui/src/pages/AuthPage.tsx +259 -0
- package/ui/src/pages/DashboardPage.tsx +28 -149
- package/ui/src/pages/EntitlementsPage.tsx +557 -0
- package/ui/src/pages/LogsPage.tsx +174 -8
- package/ui/src/pages/PluginPage.tsx +148 -0
- package/ui/src/pages/PluginsPage.tsx +394 -0
- package/ui/src/pages/SystemPage.tsx +445 -0
- package/ui/src/pages/UsersPage.tsx +837 -0
- package/ui/tsconfig.lib.json +11 -0
- package/ui/vite.lib.config.ts +56 -0
- package/dist-ui/assets/index-CW1BviRn.js +0 -465
- package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
- package/ui/src/pages/HealthPage.tsx +0 -204
package/dist/core/gateway.js
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* Gateway Server for @qwickapps/server
|
|
3
3
|
*
|
|
4
4
|
* Provides a production-ready gateway pattern that:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
5
|
+
* 1. Proxies multiple apps mounted at configurable paths
|
|
6
|
+
* 2. Each app runs at `/` on its own internal port
|
|
7
|
+
* 3. Gateway handles path rewriting automatically
|
|
8
|
+
* 4. Provides health and diagnostics endpoints
|
|
8
9
|
*
|
|
9
10
|
* Architecture:
|
|
10
|
-
* Internet → Gateway (
|
|
11
|
+
* Internet → Gateway (:3000) → [Control Panel (:3001), Admin (:3002), API (:3003), ...]
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Port Scheme:
|
|
14
|
+
* - 3000: Gateway (public)
|
|
15
|
+
* - 3001: Control Panel
|
|
16
|
+
* - 3002+: Additional apps
|
|
17
|
+
*
|
|
18
|
+
* Each app is isolated and can be served from any mount path without rebuilding.
|
|
14
19
|
*
|
|
15
20
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
16
21
|
*/
|
|
@@ -18,15 +23,21 @@ import { createControlPanel } from './control-panel.js';
|
|
|
18
23
|
import { initializeLogging, getControlPanelLogger } from './logging.js';
|
|
19
24
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
20
25
|
import express from 'express';
|
|
21
|
-
import { existsSync } from 'fs';
|
|
22
|
-
import { resolve } from 'path';
|
|
26
|
+
import { existsSync, readFileSync } from 'fs';
|
|
27
|
+
import { resolve, join, dirname } from 'path';
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
// Get QwickApps Server version from package.json
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = dirname(__filename);
|
|
32
|
+
const serverPackageJson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
33
|
+
const QWICKAPPS_SERVER_VERSION = serverPackageJson.version || '1.0.0';
|
|
23
34
|
/**
|
|
24
35
|
* Generate landing page HTML for the frontend app
|
|
25
36
|
*/
|
|
26
37
|
function generateLandingPageHtml(config, controlPanelPath) {
|
|
27
38
|
if (!config)
|
|
28
39
|
return '';
|
|
29
|
-
const primaryColor = '#6366f1';
|
|
40
|
+
const primaryColor = config.branding?.primaryColor || '#6366f1';
|
|
30
41
|
const links = config.links || [
|
|
31
42
|
{ label: 'Control Panel', url: controlPanelPath },
|
|
32
43
|
];
|
|
@@ -115,9 +126,8 @@ function generateLandingPageHtml(config, controlPanelPath) {
|
|
|
115
126
|
}
|
|
116
127
|
/**
|
|
117
128
|
* Generate default landing page HTML when no frontend app is configured
|
|
118
|
-
* Shows system status with animated background
|
|
119
129
|
*/
|
|
120
|
-
function generateDefaultLandingPageHtml(productName, controlPanelPath,
|
|
130
|
+
function generateDefaultLandingPageHtml(productName, controlPanelPath, logoIconUrl) {
|
|
121
131
|
return `<!DOCTYPE html>
|
|
122
132
|
<html lang="en">
|
|
123
133
|
<head>
|
|
@@ -150,81 +160,18 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
150
160
|
justify-content: center;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
/* Animated gradient background */
|
|
154
163
|
.bg-gradient {
|
|
155
164
|
position: fixed;
|
|
156
|
-
top: 0;
|
|
157
|
-
left: 0;
|
|
158
|
-
right: 0;
|
|
159
|
-
bottom: 0;
|
|
165
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
160
166
|
background:
|
|
161
167
|
radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
|
|
162
|
-
radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%)
|
|
163
|
-
radial-gradient(ellipse at 50% 50%, rgba(59, 130, 246, 0.05) 0%, transparent 70%);
|
|
168
|
+
radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
|
|
164
169
|
animation: gradientShift 15s ease-in-out infinite;
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
@keyframes gradientShift {
|
|
168
|
-
0%, 100% {
|
|
169
|
-
|
|
170
|
-
opacity: 1;
|
|
171
|
-
}
|
|
172
|
-
50% {
|
|
173
|
-
background-position: 100% 0%, 0% 100%, 50% 50%;
|
|
174
|
-
opacity: 0.8;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/* Floating particles */
|
|
179
|
-
.particles {
|
|
180
|
-
position: fixed;
|
|
181
|
-
top: 0;
|
|
182
|
-
left: 0;
|
|
183
|
-
right: 0;
|
|
184
|
-
bottom: 0;
|
|
185
|
-
overflow: hidden;
|
|
186
|
-
pointer-events: none;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.particle {
|
|
190
|
-
position: absolute;
|
|
191
|
-
width: 4px;
|
|
192
|
-
height: 4px;
|
|
193
|
-
background: var(--primary);
|
|
194
|
-
border-radius: 50%;
|
|
195
|
-
opacity: 0.3;
|
|
196
|
-
animation: float 20s infinite;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.particle:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
|
200
|
-
.particle:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 20s; }
|
|
201
|
-
.particle:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 28s; }
|
|
202
|
-
.particle:nth-child(4) { left: 40%; animation-delay: 1s; animation-duration: 22s; }
|
|
203
|
-
.particle:nth-child(5) { left: 50%; animation-delay: 3s; animation-duration: 24s; }
|
|
204
|
-
.particle:nth-child(6) { left: 60%; animation-delay: 5s; animation-duration: 26s; }
|
|
205
|
-
.particle:nth-child(7) { left: 70%; animation-delay: 2s; animation-duration: 21s; }
|
|
206
|
-
.particle:nth-child(8) { left: 80%; animation-delay: 4s; animation-duration: 23s; }
|
|
207
|
-
.particle:nth-child(9) { left: 90%; animation-delay: 1s; animation-duration: 27s; }
|
|
208
|
-
|
|
209
|
-
@keyframes float {
|
|
210
|
-
0% { transform: translateY(100vh) scale(0); opacity: 0; }
|
|
211
|
-
10% { opacity: 0.3; }
|
|
212
|
-
90% { opacity: 0.3; }
|
|
213
|
-
100% { transform: translateY(-100vh) scale(1); opacity: 0; }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/* Grid pattern overlay */
|
|
217
|
-
.grid-overlay {
|
|
218
|
-
position: fixed;
|
|
219
|
-
top: 0;
|
|
220
|
-
left: 0;
|
|
221
|
-
right: 0;
|
|
222
|
-
bottom: 0;
|
|
223
|
-
background-image:
|
|
224
|
-
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
|
225
|
-
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
|
226
|
-
background-size: 60px 60px;
|
|
227
|
-
pointer-events: none;
|
|
173
|
+
0%, 100% { opacity: 1; }
|
|
174
|
+
50% { opacity: 0.8; }
|
|
228
175
|
}
|
|
229
176
|
|
|
230
177
|
.container {
|
|
@@ -251,10 +198,6 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
251
198
|
box-shadow: 0 20px 40px var(--primary-glow);
|
|
252
199
|
}
|
|
253
200
|
|
|
254
|
-
.logo.custom {
|
|
255
|
-
filter: drop-shadow(0 20px 40px var(--primary-glow));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
201
|
@keyframes logoFloat {
|
|
259
202
|
0%, 100% { transform: translateY(0); }
|
|
260
203
|
50% { transform: translateY(-10px); }
|
|
@@ -267,8 +210,8 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
267
210
|
}
|
|
268
211
|
|
|
269
212
|
.logo img {
|
|
270
|
-
width:
|
|
271
|
-
height:
|
|
213
|
+
width: 64px;
|
|
214
|
+
height: 64px;
|
|
272
215
|
object-fit: contain;
|
|
273
216
|
}
|
|
274
217
|
|
|
@@ -303,17 +246,6 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
303
246
|
animation: pulse 2s ease-in-out infinite;
|
|
304
247
|
}
|
|
305
248
|
|
|
306
|
-
.status-dot.degraded {
|
|
307
|
-
background: var(--warning);
|
|
308
|
-
box-shadow: 0 0 10px var(--warning);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.status-dot.unhealthy {
|
|
312
|
-
background: var(--error);
|
|
313
|
-
box-shadow: 0 0 10px var(--error);
|
|
314
|
-
animation: none;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
249
|
@keyframes pulse {
|
|
318
250
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
319
251
|
50% { opacity: 0.7; transform: scale(1.1); }
|
|
@@ -322,7 +254,6 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
322
254
|
.status-text {
|
|
323
255
|
font-size: 0.95rem;
|
|
324
256
|
font-weight: 500;
|
|
325
|
-
color: var(--text-primary);
|
|
326
257
|
}
|
|
327
258
|
|
|
328
259
|
.description {
|
|
@@ -375,54 +306,27 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
375
306
|
text-decoration: none;
|
|
376
307
|
font-weight: 500;
|
|
377
308
|
}
|
|
378
|
-
|
|
379
|
-
.footer a:hover {
|
|
380
|
-
text-decoration: underline;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/* Loading state */
|
|
384
|
-
.loading .status-dot {
|
|
385
|
-
background: var(--text-secondary);
|
|
386
|
-
box-shadow: none;
|
|
387
|
-
animation: none;
|
|
388
|
-
}
|
|
389
309
|
</style>
|
|
390
310
|
</head>
|
|
391
311
|
<body>
|
|
392
312
|
<div class="bg-gradient"></div>
|
|
393
|
-
<div class="particles">
|
|
394
|
-
<div class="particle"></div>
|
|
395
|
-
<div class="particle"></div>
|
|
396
|
-
<div class="particle"></div>
|
|
397
|
-
<div class="particle"></div>
|
|
398
|
-
<div class="particle"></div>
|
|
399
|
-
<div class="particle"></div>
|
|
400
|
-
<div class="particle"></div>
|
|
401
|
-
<div class="particle"></div>
|
|
402
|
-
<div class="particle"></div>
|
|
403
|
-
</div>
|
|
404
|
-
<div class="grid-overlay"></div>
|
|
405
313
|
|
|
406
314
|
<div class="container">
|
|
407
|
-
${
|
|
408
|
-
? `<div class="logo
|
|
315
|
+
${logoIconUrl
|
|
316
|
+
? `<div class="logo"><img src="${logoIconUrl}" alt="${productName} logo"></div>`
|
|
409
317
|
: `<div class="logo default">
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
318
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
319
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
320
|
+
</svg>
|
|
321
|
+
</div>`}
|
|
414
322
|
|
|
415
323
|
<h1>${productName}</h1>
|
|
416
324
|
|
|
417
|
-
<div class="status-badge
|
|
418
|
-
<div class="status-dot"
|
|
419
|
-
<span class="status-text"
|
|
325
|
+
<div class="status-badge">
|
|
326
|
+
<div class="status-dot"></div>
|
|
327
|
+
<span class="status-text">Gateway Online</span>
|
|
420
328
|
</div>
|
|
421
329
|
|
|
422
|
-
<p class="description" id="description">
|
|
423
|
-
Enterprise-grade service powered by QwickApps
|
|
424
|
-
</p>
|
|
425
|
-
|
|
426
330
|
<div class="links">
|
|
427
331
|
<a href="${controlPanelPath}" class="link">
|
|
428
332
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -437,267 +341,723 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
437
341
|
</div>
|
|
438
342
|
|
|
439
343
|
<div class="footer">
|
|
440
|
-
Powered by <a href="https://qwickapps.com" target="_blank">QwickApps Server</a> - <a href="https://github.com/qwickapps/server" target="_blank">Version ${
|
|
344
|
+
Enterprise Services Powered by <a href="https://qwickapps.com" target="_blank">QwickApps Server</a> - <a href="https://github.com/qwickapps/server" target="_blank">Version ${QWICKAPPS_SERVER_VERSION}</a>
|
|
441
345
|
</div>
|
|
346
|
+
</body>
|
|
347
|
+
</html>`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Shared CSS styles for status pages (maintenance, service unavailable)
|
|
351
|
+
*/
|
|
352
|
+
const statusPageStyles = `
|
|
353
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
354
|
+
|
|
355
|
+
:root {
|
|
356
|
+
--bg-dark: #0a0a0f;
|
|
357
|
+
--bg-card: rgba(255, 255, 255, 0.03);
|
|
358
|
+
--text-primary: #f1f5f9;
|
|
359
|
+
--text-secondary: #94a3b8;
|
|
360
|
+
--border-color: rgba(255, 255, 255, 0.08);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
body {
|
|
364
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
|
365
|
+
background: var(--bg-dark);
|
|
366
|
+
color: var(--text-primary);
|
|
367
|
+
min-height: 100vh;
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
justify-content: center;
|
|
371
|
+
overflow: hidden;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.bg-gradient {
|
|
375
|
+
position: fixed;
|
|
376
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
377
|
+
animation: gradientShift 15s ease-in-out infinite;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@keyframes gradientShift {
|
|
381
|
+
0%, 100% { opacity: 1; }
|
|
382
|
+
50% { opacity: 0.8; }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.container {
|
|
386
|
+
position: relative;
|
|
387
|
+
z-index: 10;
|
|
388
|
+
text-align: center;
|
|
389
|
+
max-width: 480px;
|
|
390
|
+
padding: 3rem 2rem;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.icon-wrapper {
|
|
394
|
+
width: 100px;
|
|
395
|
+
height: 100px;
|
|
396
|
+
margin: 0 auto 2rem;
|
|
397
|
+
border-radius: 50%;
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
justify-content: center;
|
|
401
|
+
animation: iconPulse 3s ease-in-out infinite;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@keyframes iconPulse {
|
|
405
|
+
0%, 100% { transform: scale(1); }
|
|
406
|
+
50% { transform: scale(1.05); }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.icon-wrapper svg {
|
|
410
|
+
width: 48px;
|
|
411
|
+
height: 48px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
h1 {
|
|
415
|
+
font-size: 2rem;
|
|
416
|
+
font-weight: 700;
|
|
417
|
+
margin-bottom: 0.75rem;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.subtitle {
|
|
421
|
+
color: var(--text-secondary);
|
|
422
|
+
font-size: 1.05rem;
|
|
423
|
+
line-height: 1.6;
|
|
424
|
+
margin-bottom: 2rem;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.status-card {
|
|
428
|
+
background: var(--bg-card);
|
|
429
|
+
border: 1px solid var(--border-color);
|
|
430
|
+
border-radius: 16px;
|
|
431
|
+
padding: 1.5rem;
|
|
432
|
+
margin-bottom: 2rem;
|
|
433
|
+
backdrop-filter: blur(10px);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.status-row {
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
justify-content: center;
|
|
440
|
+
gap: 0.75rem;
|
|
441
|
+
color: var(--text-secondary);
|
|
442
|
+
font-size: 0.95rem;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.status-row svg {
|
|
446
|
+
width: 20px;
|
|
447
|
+
height: 20px;
|
|
448
|
+
flex-shrink: 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.eta-badge {
|
|
452
|
+
display: inline-flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
gap: 0.5rem;
|
|
455
|
+
padding: 0.75rem 1.25rem;
|
|
456
|
+
background: var(--bg-card);
|
|
457
|
+
border: 1px solid var(--border-color);
|
|
458
|
+
border-radius: 100px;
|
|
459
|
+
font-size: 0.9rem;
|
|
460
|
+
color: var(--text-secondary);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.actions {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-wrap: wrap;
|
|
466
|
+
gap: 1rem;
|
|
467
|
+
justify-content: center;
|
|
468
|
+
margin-top: 2rem;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.btn {
|
|
472
|
+
display: inline-flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 0.5rem;
|
|
475
|
+
padding: 0.875rem 1.5rem;
|
|
476
|
+
border-radius: 12px;
|
|
477
|
+
font-weight: 500;
|
|
478
|
+
font-size: 0.95rem;
|
|
479
|
+
text-decoration: none;
|
|
480
|
+
transition: all 0.3s ease;
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
border: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.btn-primary {
|
|
486
|
+
background: var(--accent-color);
|
|
487
|
+
color: white;
|
|
488
|
+
box-shadow: 0 4px 15px var(--accent-glow);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.btn-primary:hover {
|
|
492
|
+
transform: translateY(-2px);
|
|
493
|
+
box-shadow: 0 8px 25px var(--accent-glow);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.btn-secondary {
|
|
497
|
+
background: var(--bg-card);
|
|
498
|
+
border: 1px solid var(--border-color);
|
|
499
|
+
color: var(--text-primary);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.btn-secondary:hover {
|
|
503
|
+
background: rgba(255, 255, 255, 0.08);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.footer {
|
|
507
|
+
position: fixed;
|
|
508
|
+
bottom: 1.5rem;
|
|
509
|
+
left: 0;
|
|
510
|
+
right: 0;
|
|
511
|
+
text-align: center;
|
|
512
|
+
color: var(--text-secondary);
|
|
513
|
+
font-size: 0.85rem;
|
|
514
|
+
z-index: 10;
|
|
515
|
+
}
|
|
442
516
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
517
|
+
@media (max-width: 480px) {
|
|
518
|
+
.container { padding: 2rem 1.5rem; }
|
|
519
|
+
h1 { font-size: 1.5rem; }
|
|
520
|
+
.icon-wrapper { width: 80px; height: 80px; }
|
|
521
|
+
.icon-wrapper svg { width: 40px; height: 40px; }
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
/**
|
|
525
|
+
* Generate a maintenance page HTML
|
|
526
|
+
*/
|
|
527
|
+
function generateMaintenancePageHtml(appName, config, productName) {
|
|
528
|
+
const title = config.title || 'Under Maintenance';
|
|
529
|
+
const message = config.message || `${appName} is currently undergoing scheduled maintenance.`;
|
|
530
|
+
let etaHtml = '';
|
|
531
|
+
if (config.expectedBackAt) {
|
|
532
|
+
const eta = config.expectedBackAt;
|
|
533
|
+
if (eta === 'soon') {
|
|
534
|
+
etaHtml = `
|
|
535
|
+
<div class="eta-badge">
|
|
536
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
537
|
+
<circle cx="12" cy="12" r="10"/>
|
|
538
|
+
<path d="M12 6v6l4 2"/>
|
|
539
|
+
</svg>
|
|
540
|
+
Back online soon
|
|
541
|
+
</div>`;
|
|
542
|
+
}
|
|
543
|
+
else if (eta.includes('hour') || eta.includes('minute')) {
|
|
544
|
+
etaHtml = `
|
|
545
|
+
<div class="eta-badge">
|
|
546
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
547
|
+
<circle cx="12" cy="12" r="10"/>
|
|
548
|
+
<path d="M12 6v6l4 2"/>
|
|
549
|
+
</svg>
|
|
550
|
+
Expected back in ${eta}
|
|
551
|
+
</div>`;
|
|
469
552
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
553
|
+
else {
|
|
554
|
+
// ISO date string
|
|
555
|
+
etaHtml = `
|
|
556
|
+
<div class="eta-badge">
|
|
557
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
558
|
+
<circle cx="12" cy="12" r="10"/>
|
|
559
|
+
<path d="M12 6v6l4 2"/>
|
|
560
|
+
</svg>
|
|
561
|
+
<span id="eta-countdown">Calculating...</span>
|
|
562
|
+
</div>
|
|
563
|
+
<script>
|
|
564
|
+
(function() {
|
|
565
|
+
const target = new Date('${eta}');
|
|
566
|
+
const el = document.getElementById('eta-countdown');
|
|
567
|
+
function update() {
|
|
568
|
+
const now = new Date();
|
|
569
|
+
const diff = target - now;
|
|
570
|
+
if (diff <= 0) {
|
|
571
|
+
el.textContent = 'Should be back now';
|
|
572
|
+
setTimeout(() => location.reload(), 5000);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const hours = Math.floor(diff / 3600000);
|
|
576
|
+
const mins = Math.floor((diff % 3600000) / 60000);
|
|
577
|
+
if (hours > 0) {
|
|
578
|
+
el.textContent = 'Back in ' + hours + 'h ' + mins + 'm';
|
|
579
|
+
} else {
|
|
580
|
+
el.textContent = 'Back in ' + mins + ' minutes';
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
update();
|
|
584
|
+
setInterval(update, 60000);
|
|
585
|
+
})();
|
|
586
|
+
</script>`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const contactHtml = config.contactUrl
|
|
590
|
+
? `<a href="${config.contactUrl}" class="btn btn-secondary">
|
|
591
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
592
|
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
|
593
|
+
<polyline points="22,6 12,13 2,6"/>
|
|
594
|
+
</svg>
|
|
595
|
+
Contact Support
|
|
596
|
+
</a>`
|
|
597
|
+
: '';
|
|
598
|
+
return `<!DOCTYPE html>
|
|
599
|
+
<html lang="en">
|
|
600
|
+
<head>
|
|
601
|
+
<meta charset="UTF-8">
|
|
602
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
603
|
+
<title>${title} - ${productName}</title>
|
|
604
|
+
<style>
|
|
605
|
+
${statusPageStyles}
|
|
606
|
+
:root {
|
|
607
|
+
--accent-color: #f59e0b;
|
|
608
|
+
--accent-glow: rgba(245, 158, 11, 0.3);
|
|
609
|
+
}
|
|
610
|
+
.bg-gradient {
|
|
611
|
+
background:
|
|
612
|
+
radial-gradient(ellipse at 30% 30%, rgba(245, 158, 11, 0.12) 0%, transparent 50%),
|
|
613
|
+
radial-gradient(ellipse at 70% 70%, rgba(234, 179, 8, 0.08) 0%, transparent 50%);
|
|
614
|
+
}
|
|
615
|
+
.icon-wrapper {
|
|
616
|
+
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
617
|
+
box-shadow: 0 20px 40px rgba(245, 158, 11, 0.3);
|
|
618
|
+
}
|
|
619
|
+
</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div class="bg-gradient"></div>
|
|
623
|
+
|
|
624
|
+
<div class="container">
|
|
625
|
+
<div class="icon-wrapper">
|
|
626
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
|
627
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
|
628
|
+
</svg>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<h1>${title}</h1>
|
|
632
|
+
<p class="subtitle">${message}</p>
|
|
633
|
+
|
|
634
|
+
<div class="status-card">
|
|
635
|
+
<div class="status-row">
|
|
636
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
637
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
638
|
+
</svg>
|
|
639
|
+
<span>We're performing upgrades to improve your experience</span>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
${etaHtml}
|
|
644
|
+
|
|
645
|
+
<div class="actions">
|
|
646
|
+
<button onclick="location.reload()" class="btn btn-primary">
|
|
647
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
648
|
+
<path d="M23 4v6h-6"/>
|
|
649
|
+
<path d="M1 20v-6h6"/>
|
|
650
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
651
|
+
</svg>
|
|
652
|
+
Check Again
|
|
653
|
+
</button>
|
|
654
|
+
${contactHtml}
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<div class="footer">
|
|
659
|
+
${productName}
|
|
660
|
+
</div>
|
|
482
661
|
</body>
|
|
483
662
|
</html>`;
|
|
484
663
|
}
|
|
485
664
|
/**
|
|
486
|
-
*
|
|
665
|
+
* Generate a service unavailable page HTML (when proxy fails)
|
|
666
|
+
*/
|
|
667
|
+
function generateServiceUnavailablePageHtml(appName, path, config, productName) {
|
|
668
|
+
const title = config?.title || 'Service Unavailable';
|
|
669
|
+
const message = config?.message || `${appName} is temporarily unavailable. Our team has been notified.`;
|
|
670
|
+
const showRetry = config?.showRetry !== false;
|
|
671
|
+
const autoRefresh = config?.autoRefresh ?? 30;
|
|
672
|
+
const autoRefreshScript = autoRefresh > 0
|
|
673
|
+
? `<script>
|
|
674
|
+
let countdown = ${autoRefresh};
|
|
675
|
+
const el = document.getElementById('refresh-countdown');
|
|
676
|
+
setInterval(() => {
|
|
677
|
+
countdown--;
|
|
678
|
+
if (countdown <= 0) location.reload();
|
|
679
|
+
el.textContent = countdown;
|
|
680
|
+
}, 1000);
|
|
681
|
+
</script>`
|
|
682
|
+
: '';
|
|
683
|
+
const autoRefreshHtml = autoRefresh > 0
|
|
684
|
+
? `<div class="status-row" style="margin-top: 1rem;">
|
|
685
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
686
|
+
<circle cx="12" cy="12" r="10"/>
|
|
687
|
+
<path d="M12 6v6l4 2"/>
|
|
688
|
+
</svg>
|
|
689
|
+
<span>Auto-refreshing in <strong id="refresh-countdown">${autoRefresh}</strong>s</span>
|
|
690
|
+
</div>`
|
|
691
|
+
: '';
|
|
692
|
+
const retryButtonHtml = showRetry
|
|
693
|
+
? `<button onclick="location.reload()" class="btn btn-primary">
|
|
694
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
695
|
+
<path d="M23 4v6h-6"/>
|
|
696
|
+
<path d="M1 20v-6h6"/>
|
|
697
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
698
|
+
</svg>
|
|
699
|
+
Try Again
|
|
700
|
+
</button>`
|
|
701
|
+
: '';
|
|
702
|
+
return `<!DOCTYPE html>
|
|
703
|
+
<html lang="en">
|
|
704
|
+
<head>
|
|
705
|
+
<meta charset="UTF-8">
|
|
706
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
707
|
+
<title>${title} - ${productName}</title>
|
|
708
|
+
<style>
|
|
709
|
+
${statusPageStyles}
|
|
710
|
+
:root {
|
|
711
|
+
--accent-color: #ef4444;
|
|
712
|
+
--accent-glow: rgba(239, 68, 68, 0.3);
|
|
713
|
+
}
|
|
714
|
+
.bg-gradient {
|
|
715
|
+
background:
|
|
716
|
+
radial-gradient(ellipse at 30% 30%, rgba(239, 68, 68, 0.1) 0%, transparent 50%),
|
|
717
|
+
radial-gradient(ellipse at 70% 70%, rgba(220, 38, 38, 0.08) 0%, transparent 50%);
|
|
718
|
+
}
|
|
719
|
+
.icon-wrapper {
|
|
720
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
721
|
+
box-shadow: 0 20px 40px rgba(239, 68, 68, 0.3);
|
|
722
|
+
}
|
|
723
|
+
</style>
|
|
724
|
+
</head>
|
|
725
|
+
<body>
|
|
726
|
+
<div class="bg-gradient"></div>
|
|
727
|
+
|
|
728
|
+
<div class="container">
|
|
729
|
+
<div class="icon-wrapper">
|
|
730
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
|
731
|
+
<circle cx="12" cy="12" r="10"/>
|
|
732
|
+
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
733
|
+
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
734
|
+
</svg>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<h1>${title}</h1>
|
|
738
|
+
<p class="subtitle">${message}</p>
|
|
739
|
+
|
|
740
|
+
<div class="status-card">
|
|
741
|
+
<div class="status-row">
|
|
742
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
743
|
+
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
|
744
|
+
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
|
745
|
+
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
|
746
|
+
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
|
747
|
+
</svg>
|
|
748
|
+
<span>The service at <code style="background: rgba(255,255,255,0.1); padding: 0.2rem 0.4rem; border-radius: 4px;">${path}</code> is not responding</span>
|
|
749
|
+
</div>
|
|
750
|
+
${autoRefreshHtml}
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<div class="actions">
|
|
754
|
+
${retryButtonHtml}
|
|
755
|
+
<a href="/" class="btn btn-secondary">
|
|
756
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
757
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
|
758
|
+
<polyline points="9,22 9,12 15,12 15,22"/>
|
|
759
|
+
</svg>
|
|
760
|
+
Go Home
|
|
761
|
+
</a>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<div class="footer">
|
|
766
|
+
${productName}
|
|
767
|
+
</div>
|
|
768
|
+
${autoRefreshScript}
|
|
769
|
+
</body>
|
|
770
|
+
</html>`;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Create a gateway that proxies to multiple internal services
|
|
487
774
|
*
|
|
488
775
|
* @param config - Gateway configuration
|
|
489
|
-
* @param serviceFactory - Factory function to create the internal service
|
|
490
776
|
* @returns Gateway instance
|
|
491
777
|
*
|
|
492
778
|
* @example
|
|
493
779
|
* ```typescript
|
|
494
780
|
* import { createGateway } from '@qwickapps/server';
|
|
495
781
|
*
|
|
496
|
-
* const gateway = createGateway(
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
782
|
+
* const gateway = createGateway({
|
|
783
|
+
* productName: 'My Product',
|
|
784
|
+
* port: 3000,
|
|
785
|
+
* controlPanel: {
|
|
786
|
+
* path: '/cpanel',
|
|
787
|
+
* port: 3001,
|
|
788
|
+
* plugins: [...],
|
|
501
789
|
* },
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* server,
|
|
508
|
-
* shutdown: async () => { server.close(); },
|
|
509
|
-
* };
|
|
510
|
-
* }
|
|
511
|
-
* );
|
|
790
|
+
* apps: [
|
|
791
|
+
* { path: '/api', source: { type: 'proxy', target: 'http://localhost:3002' } },
|
|
792
|
+
* { path: '/docs', source: { type: 'static', directory: './docs' } },
|
|
793
|
+
* ],
|
|
794
|
+
* });
|
|
512
795
|
*
|
|
513
796
|
* await gateway.start();
|
|
514
797
|
* ```
|
|
515
798
|
*/
|
|
516
|
-
export function createGateway(config
|
|
517
|
-
// Initialize logging subsystem
|
|
518
|
-
|
|
799
|
+
export function createGateway(config) {
|
|
800
|
+
// Initialize logging (side effect - subsystem is initialized globally)
|
|
801
|
+
initializeLogging({
|
|
519
802
|
namespace: config.productName,
|
|
520
803
|
...config.logging,
|
|
521
804
|
});
|
|
522
|
-
// Use provided logger or get one from the logging subsystem
|
|
523
805
|
const logger = config.logger || getControlPanelLogger('Gateway');
|
|
524
|
-
|
|
525
|
-
const
|
|
806
|
+
// Port configuration - new scheme: 3000 gateway, 3001 cpanel, 3002+ apps
|
|
807
|
+
const gatewayPort = config.port || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3000', 10);
|
|
526
808
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
527
|
-
// Control panel mount path (defaults to /cpanel)
|
|
528
|
-
const controlPanelPath = config.controlPanelPath || '/cpanel';
|
|
529
|
-
// Guard configuration for control panel
|
|
530
|
-
const guardConfig = config.controlPanelGuard;
|
|
531
|
-
// API paths to proxy
|
|
532
|
-
const proxyPaths = config.proxyPaths || ['/api/v1'];
|
|
533
|
-
// Version for display
|
|
534
809
|
const version = config.version || process.env.npm_package_version || '1.0.0';
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
810
|
+
// Control panel configuration
|
|
811
|
+
const cpConfig = config.controlPanel ?? { enabled: true };
|
|
812
|
+
const cpEnabled = cpConfig.enabled !== false;
|
|
813
|
+
const cpPath = cpConfig.path || '/cpanel';
|
|
814
|
+
const cpPort = cpConfig.port || 3001;
|
|
815
|
+
// Create gateway Express app
|
|
816
|
+
const app = express();
|
|
817
|
+
let server = null;
|
|
818
|
+
let controlPanelInstance = null;
|
|
819
|
+
const mountedApps = [];
|
|
820
|
+
/**
|
|
821
|
+
* Setup proxy middleware for an app
|
|
822
|
+
*/
|
|
823
|
+
const setupProxyApp = (appConfig, httpServer) => {
|
|
824
|
+
const { path, source, stripPrefix = true, name, maintenance, fallback } = appConfig;
|
|
825
|
+
if (source.type !== 'proxy')
|
|
826
|
+
return;
|
|
827
|
+
const appName = name || path.replace(/^\//, '') || 'Service';
|
|
828
|
+
logger.debug(`Setting up proxy: ${path} -> ${source.target}`);
|
|
829
|
+
// Maintenance mode middleware - intercepts all requests when enabled
|
|
830
|
+
if (maintenance?.enabled) {
|
|
831
|
+
logger.info(`Maintenance mode enabled for ${path}`);
|
|
832
|
+
app.use(path, (req, res, next) => {
|
|
833
|
+
// Check bypass paths
|
|
834
|
+
if (maintenance.bypassPaths?.some(bp => req.path.startsWith(bp))) {
|
|
835
|
+
return next();
|
|
836
|
+
}
|
|
837
|
+
const html = generateMaintenancePageHtml(appName, maintenance, config.productName);
|
|
838
|
+
res.status(503).type('html').send(html);
|
|
839
|
+
});
|
|
840
|
+
mountedApps.push({ path, type: 'proxy', target: source.target });
|
|
841
|
+
return; // Don't setup proxy when in maintenance mode
|
|
842
|
+
}
|
|
843
|
+
const proxyOptions = {
|
|
844
|
+
target: source.target,
|
|
845
|
+
changeOrigin: true,
|
|
846
|
+
ws: source.ws ?? false,
|
|
847
|
+
pathRewrite: stripPrefix ? { [`^${path}`]: '' } : undefined,
|
|
848
|
+
on: {
|
|
849
|
+
proxyReq: (proxyReq) => {
|
|
850
|
+
// Add X-Forwarded headers so app knows its mounted path
|
|
851
|
+
proxyReq.setHeader('X-Forwarded-Prefix', path);
|
|
852
|
+
},
|
|
853
|
+
error: (err, req, res) => {
|
|
854
|
+
logger.error(`Proxy error for ${path}`, { error: err.message });
|
|
855
|
+
if (res && 'writeHead' in res && !res.headersSent) {
|
|
856
|
+
// Check if this looks like an API request (Accept: application/json or /api/ path)
|
|
857
|
+
const acceptHeader = req.headers['accept'] || '';
|
|
858
|
+
const isApiRequest = acceptHeader.includes('application/json') || req.url?.includes('/api/');
|
|
859
|
+
if (isApiRequest) {
|
|
860
|
+
// Return JSON error for API requests
|
|
570
861
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
571
862
|
res.end(JSON.stringify({
|
|
572
863
|
error: 'Service Unavailable',
|
|
573
|
-
message:
|
|
864
|
+
message: `The service at ${path} is currently unavailable.`,
|
|
574
865
|
details: nodeEnv === 'development' ? err.message : undefined,
|
|
575
866
|
}));
|
|
576
867
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const healthProxyOptions = {
|
|
584
|
-
target,
|
|
585
|
-
changeOrigin: false,
|
|
586
|
-
pathFilter: '/health',
|
|
587
|
-
on: {
|
|
588
|
-
error: (_err, _req, res) => {
|
|
589
|
-
if (res && 'writeHead' in res && !res.headersSent) {
|
|
590
|
-
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
591
|
-
res.end(JSON.stringify({
|
|
592
|
-
status: 'unhealthy',
|
|
593
|
-
error: 'Service unavailable',
|
|
594
|
-
gateway: 'healthy',
|
|
595
|
-
}));
|
|
868
|
+
else {
|
|
869
|
+
// Return beautiful HTML page for browser requests
|
|
870
|
+
const html = generateServiceUnavailablePageHtml(appName, path, fallback, config.productName);
|
|
871
|
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
872
|
+
res.end(html);
|
|
873
|
+
}
|
|
596
874
|
}
|
|
597
875
|
},
|
|
598
876
|
},
|
|
599
877
|
};
|
|
600
|
-
|
|
878
|
+
const proxy = createProxyMiddleware(proxyOptions);
|
|
879
|
+
// Mount proxy
|
|
880
|
+
app.use(path, proxy);
|
|
881
|
+
// WebSocket upgrade handling
|
|
882
|
+
if (source.ws && httpServer) {
|
|
883
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
884
|
+
if (req.url?.startsWith(path)) {
|
|
885
|
+
proxy.upgrade?.(req, socket, head);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
mountedApps.push({ path, type: 'proxy', target: source.target });
|
|
601
890
|
};
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
891
|
+
/**
|
|
892
|
+
* Setup static file serving for an app
|
|
893
|
+
*/
|
|
894
|
+
const setupStaticApp = (appConfig) => {
|
|
895
|
+
const { path, source } = appConfig;
|
|
896
|
+
if (source.type !== 'static')
|
|
897
|
+
return;
|
|
898
|
+
logger.debug(`Setting up static: ${path} -> ${source.directory}`);
|
|
899
|
+
if (!existsSync(source.directory)) {
|
|
900
|
+
logger.warn(`Static directory not found: ${source.directory}`);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// Serve static files
|
|
904
|
+
app.use(path, express.static(source.directory, { index: false }));
|
|
905
|
+
// SPA fallback
|
|
906
|
+
if (source.spa) {
|
|
907
|
+
const indexPath = join(source.directory, 'index.html');
|
|
908
|
+
// Read and cache index.html with path rewriting
|
|
909
|
+
let cachedHtml = null;
|
|
910
|
+
const getIndexHtml = () => {
|
|
911
|
+
if (cachedHtml)
|
|
912
|
+
return cachedHtml;
|
|
913
|
+
let html = readFileSync(indexPath, 'utf-8');
|
|
914
|
+
// Rewrite asset paths for non-root mount
|
|
915
|
+
if (path !== '/') {
|
|
916
|
+
html = html.replace(/src="\/assets\//g, `src="${path}/assets/`);
|
|
917
|
+
html = html.replace(/href="\/assets\//g, `href="${path}/assets/`);
|
|
918
|
+
}
|
|
919
|
+
cachedHtml = html;
|
|
920
|
+
return html;
|
|
921
|
+
};
|
|
922
|
+
app.get(`${path}/*`, (_req, res) => {
|
|
923
|
+
res.type('html').send(getIndexHtml());
|
|
924
|
+
});
|
|
925
|
+
if (path !== '/') {
|
|
926
|
+
app.get(path, (_req, res) => {
|
|
927
|
+
res.type('html').send(getIndexHtml());
|
|
612
928
|
});
|
|
613
|
-
logger.debug('Frontend app: Serving logo at /logo.svg');
|
|
614
929
|
}
|
|
615
930
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
931
|
+
mountedApps.push({ path, type: 'static' });
|
|
932
|
+
};
|
|
933
|
+
/**
|
|
934
|
+
* Setup frontend app at root path
|
|
935
|
+
*/
|
|
936
|
+
const setupFrontendApp = () => {
|
|
937
|
+
const { frontendApp, logoIconUrl } = config;
|
|
938
|
+
// Default landing page
|
|
939
|
+
if (!frontendApp) {
|
|
940
|
+
logger.debug('Frontend: Serving default landing page');
|
|
941
|
+
app.get('/', (_req, res) => {
|
|
942
|
+
const html = generateDefaultLandingPageHtml(config.productName, cpPath, logoIconUrl);
|
|
621
943
|
res.type('html').send(html);
|
|
622
944
|
});
|
|
623
945
|
return;
|
|
624
946
|
}
|
|
625
|
-
const { redirectUrl, staticPath, landingPage } =
|
|
947
|
+
const { redirectUrl, staticPath, landingPage } = frontendApp;
|
|
626
948
|
// Priority 1: Redirect
|
|
627
949
|
if (redirectUrl) {
|
|
628
|
-
logger.debug(`Frontend
|
|
629
|
-
|
|
630
|
-
res.redirect(redirectUrl);
|
|
631
|
-
});
|
|
950
|
+
logger.debug(`Frontend: Redirecting / to ${redirectUrl}`);
|
|
951
|
+
app.get('/', (_req, res) => res.redirect(redirectUrl));
|
|
632
952
|
return;
|
|
633
953
|
}
|
|
634
|
-
// Priority 2:
|
|
954
|
+
// Priority 2: Static files
|
|
635
955
|
if (staticPath && existsSync(staticPath)) {
|
|
636
|
-
logger.debug(`Frontend
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
controlPanel.app.get('/', (_req, res) => {
|
|
956
|
+
logger.debug(`Frontend: Serving static from ${staticPath}`);
|
|
957
|
+
app.use('/', express.static(staticPath));
|
|
958
|
+
app.get('/', (_req, res) => {
|
|
640
959
|
res.sendFile(resolve(staticPath, 'index.html'));
|
|
641
960
|
});
|
|
642
961
|
return;
|
|
643
962
|
}
|
|
644
963
|
// Priority 3: Landing page
|
|
645
964
|
if (landingPage) {
|
|
646
|
-
logger.debug(
|
|
647
|
-
|
|
648
|
-
const html = generateLandingPageHtml(landingPage,
|
|
965
|
+
logger.debug('Frontend: Serving custom landing page');
|
|
966
|
+
app.get('/', (_req, res) => {
|
|
967
|
+
const html = generateLandingPageHtml(landingPage, cpPath);
|
|
649
968
|
res.type('html').send(html);
|
|
650
969
|
});
|
|
651
970
|
}
|
|
652
971
|
};
|
|
972
|
+
/**
|
|
973
|
+
* Start the gateway
|
|
974
|
+
*/
|
|
653
975
|
const start = async () => {
|
|
654
976
|
logger.debug('Starting gateway...');
|
|
655
|
-
// 1. Start internal
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
977
|
+
// 1. Start internal control panel if enabled
|
|
978
|
+
if (cpEnabled) {
|
|
979
|
+
logger.debug(`Starting control panel on port ${cpPort}...`);
|
|
980
|
+
controlPanelInstance = createControlPanel({
|
|
981
|
+
config: {
|
|
982
|
+
productName: config.productName,
|
|
983
|
+
port: cpPort,
|
|
984
|
+
version,
|
|
985
|
+
logoIconUrl: config.logoIconUrl,
|
|
986
|
+
branding: config.branding,
|
|
987
|
+
cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
|
|
988
|
+
mountPath: '/', // Control panel runs at / internally
|
|
989
|
+
guard: cpConfig.guard,
|
|
990
|
+
customUiPath: cpConfig.customUiPath,
|
|
991
|
+
links: cpConfig.links,
|
|
992
|
+
},
|
|
993
|
+
plugins: cpConfig.plugins || [],
|
|
994
|
+
logger,
|
|
995
|
+
});
|
|
996
|
+
await controlPanelInstance.start();
|
|
997
|
+
logger.debug(`Control panel started on port ${cpPort}`);
|
|
998
|
+
}
|
|
999
|
+
// 2. Create HTTP server
|
|
1000
|
+
server = app.listen(gatewayPort);
|
|
1001
|
+
// 3. Setup mounted apps (proxy and static)
|
|
1002
|
+
const apps = config.apps || [];
|
|
1003
|
+
// Add control panel as a proxy app if enabled
|
|
1004
|
+
if (cpEnabled) {
|
|
1005
|
+
setupProxyApp({
|
|
1006
|
+
path: cpPath,
|
|
1007
|
+
source: { type: 'proxy', target: `http://localhost:${cpPort}` },
|
|
1008
|
+
}, server);
|
|
1009
|
+
}
|
|
1010
|
+
// Setup additional apps
|
|
1011
|
+
for (const appConfig of apps) {
|
|
1012
|
+
if (appConfig.source.type === 'proxy') {
|
|
1013
|
+
setupProxyApp(appConfig, server);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
setupStaticApp(appConfig);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// 4. Setup frontend app at root
|
|
662
1020
|
setupFrontendApp();
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
: guardConfig?.type && guardConfig.type !== 'none'
|
|
669
|
-
? `(auth: ${guardConfig.type})`
|
|
1021
|
+
// Log startup info
|
|
1022
|
+
const authInfo = cpConfig.guard?.type === 'basic'
|
|
1023
|
+
? `(auth: ${cpConfig.guard.username})`
|
|
1024
|
+
: cpConfig.guard?.type && cpConfig.guard.type !== 'none'
|
|
1025
|
+
? `(auth: ${cpConfig.guard.type})`
|
|
670
1026
|
: '(no auth)';
|
|
671
|
-
logger.info(`${config.productName} started on port ${gatewayPort} ${authInfo}`);
|
|
672
|
-
// Log
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
logger.debug(`Service API: * ${apiPath}/*`);
|
|
1027
|
+
logger.info(`${config.productName} gateway started on port ${gatewayPort} ${authInfo}`);
|
|
1028
|
+
// Log mounted apps
|
|
1029
|
+
for (const mounted of mountedApps) {
|
|
1030
|
+
if (mounted.type === 'proxy') {
|
|
1031
|
+
logger.debug(` ${mounted.path}/* -> ${mounted.target}`);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
logger.debug(` ${mounted.path}/* -> [static]`);
|
|
1035
|
+
}
|
|
681
1036
|
}
|
|
682
1037
|
};
|
|
1038
|
+
/**
|
|
1039
|
+
* Stop the gateway
|
|
1040
|
+
*/
|
|
683
1041
|
const stop = async () => {
|
|
684
1042
|
logger.debug('Shutting down gateway...');
|
|
685
1043
|
// Stop control panel
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1044
|
+
if (controlPanelInstance) {
|
|
1045
|
+
await controlPanelInstance.stop();
|
|
1046
|
+
}
|
|
1047
|
+
// Stop gateway server
|
|
1048
|
+
if (server) {
|
|
1049
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
691
1050
|
}
|
|
692
1051
|
logger.debug('Gateway shutdown complete');
|
|
693
1052
|
};
|
|
694
1053
|
return {
|
|
695
|
-
|
|
696
|
-
|
|
1054
|
+
app,
|
|
1055
|
+
server,
|
|
1056
|
+
controlPanel: controlPanelInstance,
|
|
1057
|
+
mountedApps,
|
|
697
1058
|
start,
|
|
698
1059
|
stop,
|
|
699
|
-
gatewayPort,
|
|
700
|
-
servicePort,
|
|
1060
|
+
port: gatewayPort,
|
|
701
1061
|
};
|
|
702
1062
|
}
|
|
703
1063
|
//# sourceMappingURL=gateway.js.map
|