@qwickapps/server 1.1.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +318 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +99 -60
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +683 -315
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +271 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +326 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +9 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +9 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  41. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  42. package/dist/plugins/auth/auth-plugin.js +255 -0
  43. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  45. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  47. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  48. package/dist/plugins/auth/index.d.ts +12 -0
  49. package/dist/plugins/auth/index.d.ts.map +1 -0
  50. package/dist/plugins/auth/index.js +13 -0
  51. package/dist/plugins/auth/index.js.map +1 -0
  52. package/dist/plugins/auth/types.d.ts +148 -0
  53. package/dist/plugins/auth/types.d.ts.map +1 -0
  54. package/dist/plugins/auth/types.js +14 -0
  55. package/dist/plugins/auth/types.js.map +1 -0
  56. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  57. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  58. package/dist/plugins/bans/bans-plugin.js +428 -0
  59. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  60. package/dist/plugins/bans/index.d.ts +9 -0
  61. package/dist/plugins/bans/index.d.ts.map +1 -0
  62. package/dist/plugins/bans/index.js +10 -0
  63. package/dist/plugins/bans/index.js.map +1 -0
  64. package/dist/plugins/bans/stores/index.d.ts +7 -0
  65. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  66. package/dist/plugins/bans/stores/index.js +7 -0
  67. package/dist/plugins/bans/stores/index.js.map +1 -0
  68. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  69. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  70. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  71. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  72. package/dist/plugins/bans/types.d.ts +128 -0
  73. package/dist/plugins/bans/types.d.ts.map +1 -0
  74. package/dist/plugins/bans/types.js +11 -0
  75. package/dist/plugins/bans/types.js.map +1 -0
  76. package/dist/plugins/cache-plugin.d.ts +14 -3
  77. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  78. package/dist/plugins/cache-plugin.js +27 -7
  79. package/dist/plugins/cache-plugin.js.map +1 -1
  80. package/dist/plugins/cache-plugin.test.js +96 -32
  81. package/dist/plugins/cache-plugin.test.js.map +1 -1
  82. package/dist/plugins/config-plugin.d.ts +3 -2
  83. package/dist/plugins/config-plugin.d.ts.map +1 -1
  84. package/dist/plugins/config-plugin.js +17 -10
  85. package/dist/plugins/config-plugin.js.map +1 -1
  86. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  87. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  88. package/dist/plugins/diagnostics-plugin.js +17 -10
  89. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  90. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  91. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  92. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  93. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  94. package/dist/plugins/entitlements/index.d.ts +12 -0
  95. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  96. package/dist/plugins/entitlements/index.js +16 -0
  97. package/dist/plugins/entitlements/index.js.map +1 -0
  98. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  99. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  100. package/dist/plugins/entitlements/sources/index.js +9 -0
  101. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  102. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  103. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  105. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  106. package/dist/plugins/entitlements/types.d.ts +232 -0
  107. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/types.js +11 -0
  109. package/dist/plugins/entitlements/types.js.map +1 -0
  110. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  111. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  112. package/dist/plugins/frontend-app-plugin.js +14 -9
  113. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  114. package/dist/plugins/health-plugin.d.ts +5 -2
  115. package/dist/plugins/health-plugin.d.ts.map +1 -1
  116. package/dist/plugins/health-plugin.js +20 -5
  117. package/dist/plugins/health-plugin.js.map +1 -1
  118. package/dist/plugins/index.d.ts +8 -2
  119. package/dist/plugins/index.d.ts.map +1 -1
  120. package/dist/plugins/index.js +8 -2
  121. package/dist/plugins/index.js.map +1 -1
  122. package/dist/plugins/logs-plugin.d.ts +3 -2
  123. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  124. package/dist/plugins/logs-plugin.js +21 -12
  125. package/dist/plugins/logs-plugin.js.map +1 -1
  126. package/dist/plugins/postgres-plugin.d.ts +3 -3
  127. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  128. package/dist/plugins/postgres-plugin.js +9 -7
  129. package/dist/plugins/postgres-plugin.js.map +1 -1
  130. package/dist/plugins/postgres-plugin.test.js +47 -29
  131. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  132. package/dist/plugins/users/index.d.ts +12 -0
  133. package/dist/plugins/users/index.d.ts.map +1 -0
  134. package/dist/plugins/users/index.js +13 -0
  135. package/dist/plugins/users/index.js.map +1 -0
  136. package/dist/plugins/users/stores/index.d.ts +7 -0
  137. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  138. package/dist/plugins/users/stores/index.js +7 -0
  139. package/dist/plugins/users/stores/index.js.map +1 -0
  140. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  141. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  142. package/dist/plugins/users/stores/postgres-store.js +157 -0
  143. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  144. package/dist/plugins/users/types.d.ts +189 -0
  145. package/dist/plugins/users/types.d.ts.map +1 -0
  146. package/dist/plugins/users/types.js +12 -0
  147. package/dist/plugins/users/types.js.map +1 -0
  148. package/dist/plugins/users/users-plugin.d.ts +39 -0
  149. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  150. package/dist/plugins/users/users-plugin.js +242 -0
  151. package/dist/plugins/users/users-plugin.js.map +1 -0
  152. package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
  153. package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
  154. package/dist-ui/index.html +1 -1
  155. package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
  156. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  157. package/dist-ui-lib/components/index.d.ts +18 -0
  158. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  159. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  160. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  161. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  162. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
  163. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
  164. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  165. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  166. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  167. package/dist-ui-lib/index.js +6441 -0
  168. package/dist-ui-lib/index.js.map +1 -0
  169. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  170. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  171. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  172. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  173. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  174. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  175. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  176. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  177. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  178. package/package.json +18 -6
  179. package/src/core/control-panel.ts +122 -68
  180. package/src/core/gateway.ts +870 -399
  181. package/src/core/index.ts +21 -2
  182. package/src/core/plugin-registry.ts +653 -0
  183. package/src/core/types.ts +31 -37
  184. package/src/index.ts +118 -19
  185. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  186. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  187. package/src/plugins/auth/adapters/index.ts +9 -0
  188. package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
  189. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  190. package/src/plugins/auth/auth-plugin.ts +303 -0
  191. package/src/plugins/auth/index.ts +33 -0
  192. package/src/plugins/auth/types.ts +165 -0
  193. package/src/plugins/bans/bans-plugin.ts +485 -0
  194. package/src/plugins/bans/index.ts +31 -0
  195. package/src/plugins/bans/stores/index.ts +7 -0
  196. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  197. package/src/plugins/bans/types.ts +141 -0
  198. package/src/plugins/cache-plugin.test.ts +105 -32
  199. package/src/plugins/cache-plugin.ts +40 -9
  200. package/src/plugins/config-plugin.ts +23 -12
  201. package/src/plugins/diagnostics-plugin.ts +22 -12
  202. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  203. package/src/plugins/entitlements/index.ts +51 -0
  204. package/src/plugins/entitlements/sources/index.ts +9 -0
  205. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  206. package/src/plugins/entitlements/types.ts +256 -0
  207. package/src/plugins/frontend-app-plugin.ts +24 -12
  208. package/src/plugins/health-plugin.ts +27 -7
  209. package/src/plugins/index.ts +106 -4
  210. package/src/plugins/logs-plugin.ts +28 -14
  211. package/src/plugins/postgres-plugin.test.ts +49 -29
  212. package/src/plugins/postgres-plugin.ts +11 -9
  213. package/src/plugins/users/index.ts +35 -0
  214. package/src/plugins/users/stores/index.ts +7 -0
  215. package/src/plugins/users/stores/postgres-store.ts +225 -0
  216. package/src/plugins/users/types.ts +209 -0
  217. package/src/plugins/users/users-plugin.ts +281 -0
  218. package/ui/src/App.tsx +185 -31
  219. package/ui/src/api/controlPanelApi.ts +354 -1
  220. package/ui/src/components/ControlPanelApp.tsx +209 -0
  221. package/ui/src/components/index.ts +62 -0
  222. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  223. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  224. package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
  225. package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
  226. package/ui/src/dashboard/builtInWidgets.tsx +29 -0
  227. package/ui/src/dashboard/index.ts +35 -0
  228. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  229. package/ui/src/dashboard/widgets/index.ts +7 -0
  230. package/ui/src/pages/DashboardPage.tsx +28 -149
  231. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  232. package/ui/src/pages/LogsPage.tsx +174 -8
  233. package/ui/src/pages/PluginPage.tsx +148 -0
  234. package/ui/src/pages/SystemPage.tsx +445 -0
  235. package/ui/src/pages/UsersPage.tsx +837 -0
  236. package/ui/tsconfig.lib.json +11 -0
  237. package/ui/vite.lib.config.ts +51 -0
  238. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  239. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  240. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -2,15 +2,20 @@
2
2
  * Gateway Server for @qwickapps/server
3
3
  *
4
4
  * Provides a production-ready gateway pattern that:
5
- * 1. Serves the control panel UI (always responsive)
6
- * 2. Proxies API requests to an internal service
7
- * 3. Provides health and diagnostics endpoints
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 (GATEWAY_PORT, public) → Service (SERVICE_PORT, internal)
11
+ * Internet → Gateway (:3000) → [Control Panel (:3001), Admin (:3002), API (:3003), ...]
11
12
  *
12
- * The gateway is always responsive even if the internal service is down,
13
- * allowing diagnostics and error visibility.
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, apiBasePath, version, logoUrl) {
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
- background-position: 0% 0%, 100% 100%, 50% 50%;
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: 80px;
271
- height: 80px;
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
- ${logoUrl
408
- ? `<div class="logo custom"><img src="${logoUrl}" alt="${productName} logo" /></div>`
315
+ ${logoIconUrl
316
+ ? `<div class="logo"><img src="${logoIconUrl}" alt="${productName} logo"></div>`
409
317
  : `<div class="logo default">
410
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
411
- <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
412
- </svg>
413
- </div>`}
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 loading" id="status-badge">
418
- <div class="status-dot" id="status-dot"></div>
419
- <span class="status-text" id="status-text">Checking status...</span>
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,259 +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 ${version}</a>
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
+ }
442
484
 
443
- <script>
444
- async function checkStatus() {
445
- const badge = document.getElementById('status-badge');
446
- const dot = document.getElementById('status-dot');
447
- const text = document.getElementById('status-text');
448
- const desc = document.getElementById('description');
449
-
450
- try {
451
- const res = await fetch('${apiBasePath}/health');
452
- const data = await res.json();
453
-
454
- badge.classList.remove('loading');
455
-
456
- if (data.status === 'healthy') {
457
- dot.className = 'status-dot';
458
- text.textContent = 'All systems operational';
459
- desc.textContent = 'The service is running smoothly and ready to handle requests.';
460
- } else if (data.status === 'degraded') {
461
- dot.className = 'status-dot degraded';
462
- text.textContent = 'Degraded performance';
463
- desc.textContent = 'Some services may be experiencing issues. Core functionality remains available.';
464
- } else {
465
- dot.className = 'status-dot unhealthy';
466
- text.textContent = 'System maintenance';
467
- desc.textContent = 'The service is currently undergoing maintenance. Please check back shortly.';
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
+ }
516
+
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>`;
468
542
  }
469
- } catch (e) {
470
- badge.classList.remove('loading');
471
- dot.className = 'status-dot unhealthy';
472
- text.textContent = 'Unable to connect';
473
- desc.textContent = 'Could not reach the service. Please try again later.';
474
- }
475
- }
476
-
477
- // Check status on load and every 30 seconds
478
- checkStatus();
479
- setInterval(checkStatus, 30000);
480
- </script>
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>`;
552
+ }
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>
481
661
  </body>
482
662
  </html>`;
483
663
  }
484
664
  /**
485
- * Create a gateway that proxies to an internal service
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
486
774
  *
487
775
  * @param config - Gateway configuration
488
- * @param serviceFactory - Factory function to create the internal service
489
776
  * @returns Gateway instance
490
777
  *
491
778
  * @example
492
779
  * ```typescript
493
780
  * import { createGateway } from '@qwickapps/server';
494
781
  *
495
- * const gateway = createGateway(
496
- * {
497
- * productName: 'My Service',
498
- * gatewayPort: 3101,
499
- * servicePort: 3100,
782
+ * const gateway = createGateway({
783
+ * productName: 'My Product',
784
+ * port: 3000,
785
+ * controlPanel: {
786
+ * path: '/cpanel',
787
+ * port: 3001,
788
+ * plugins: [...],
500
789
  * },
501
- * async (port) => {
502
- * const app = createMyApp();
503
- * const server = app.listen(port);
504
- * return {
505
- * app,
506
- * server,
507
- * shutdown: async () => { server.close(); },
508
- * };
509
- * }
510
- * );
790
+ * apps: [
791
+ * { path: '/api', source: { type: 'proxy', target: 'http://localhost:3002' } },
792
+ * { path: '/docs', source: { type: 'static', directory: './docs' } },
793
+ * ],
794
+ * });
511
795
  *
512
796
  * await gateway.start();
513
797
  * ```
514
798
  */
515
- export function createGateway(config, serviceFactory) {
516
- // Initialize logging subsystem first
517
- const loggingSubsystem = initializeLogging({
799
+ export function createGateway(config) {
800
+ // Initialize logging (side effect - subsystem is initialized globally)
801
+ initializeLogging({
518
802
  namespace: config.productName,
519
803
  ...config.logging,
520
804
  });
521
- // Use provided logger or get one from the logging subsystem
522
805
  const logger = config.logger || getControlPanelLogger('Gateway');
523
- const gatewayPort = config.gatewayPort || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3101', 10);
524
- const servicePort = config.servicePort || parseInt(process.env.SERVICE_PORT || '3100', 10);
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);
525
808
  const nodeEnv = process.env.NODE_ENV || 'development';
526
- // Control panel mount path (defaults to /cpanel)
527
- const controlPanelPath = config.controlPanelPath || '/cpanel';
528
- // Guard configuration for control panel
529
- const guardConfig = config.controlPanelGuard;
530
- // API paths to proxy
531
- const proxyPaths = config.proxyPaths || ['/api/v1'];
532
- // Version for display
533
809
  const version = config.version || process.env.npm_package_version || '1.0.0';
534
- let service = null;
535
- // Create control panel
536
- const controlPanel = createControlPanel({
537
- config: {
538
- productName: config.productName,
539
- port: gatewayPort,
540
- version,
541
- branding: config.branding,
542
- cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
543
- // Skip body parsing for proxied paths
544
- skipBodyParserPaths: [...proxyPaths, '/health'],
545
- // Mount path for control panel
546
- mountPath: controlPanelPath,
547
- // Route guard
548
- guard: guardConfig,
549
- // Custom UI path
550
- customUiPath: config.customUiPath,
551
- links: config.links,
552
- },
553
- plugins: config.plugins || [],
554
- logger,
555
- });
556
- // Setup proxy middleware for API paths
557
- const setupProxyMiddleware = () => {
558
- const target = `http://localhost:${servicePort}`;
559
- // Proxy each API path
560
- for (const apiPath of proxyPaths) {
561
- const proxyOptions = {
562
- target,
563
- changeOrigin: false,
564
- pathFilter: `${apiPath}/**`,
565
- on: {
566
- error: (err, _req, res) => {
567
- logger.error('Proxy error', { error: err.message, path: apiPath });
568
- if (res && 'writeHead' in res && !res.headersSent) {
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
569
861
  res.writeHead(503, { 'Content-Type': 'application/json' });
570
862
  res.end(JSON.stringify({
571
863
  error: 'Service Unavailable',
572
- message: 'The service is currently unavailable. Please try again later.',
864
+ message: `The service at ${path} is currently unavailable.`,
573
865
  details: nodeEnv === 'development' ? err.message : undefined,
574
866
  }));
575
867
  }
576
- },
577
- },
578
- };
579
- controlPanel.app.use(createProxyMiddleware(proxyOptions));
580
- }
581
- // Proxy /health endpoint to internal service
582
- const healthProxyOptions = {
583
- target,
584
- changeOrigin: false,
585
- pathFilter: '/health',
586
- on: {
587
- error: (_err, _req, res) => {
588
- if (res && 'writeHead' in res && !res.headersSent) {
589
- res.writeHead(503, { 'Content-Type': 'application/json' });
590
- res.end(JSON.stringify({
591
- status: 'unhealthy',
592
- error: 'Service unavailable',
593
- gateway: 'healthy',
594
- }));
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
+ }
595
874
  }
596
875
  },
597
876
  },
598
877
  };
599
- controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
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 });
600
890
  };
601
- // Calculate API base path for landing page
602
- const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
603
- // Setup frontend app at root path
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());
928
+ });
929
+ }
930
+ }
931
+ mountedApps.push({ path, type: 'static' });
932
+ };
933
+ /**
934
+ * Setup frontend app at root path
935
+ */
604
936
  const setupFrontendApp = () => {
605
- // If no frontend app configured, serve default landing page with status
606
- if (!config.frontendApp) {
607
- logger.info('Frontend app: Serving default landing page');
608
- controlPanel.app.get('/', (_req, res) => {
609
- const html = generateDefaultLandingPageHtml(config.productName, controlPanelPath, apiBasePath, version, config.logoUrl);
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);
610
943
  res.type('html').send(html);
611
944
  });
612
945
  return;
613
946
  }
614
- const { redirectUrl, staticPath, landingPage } = config.frontendApp;
947
+ const { redirectUrl, staticPath, landingPage } = frontendApp;
615
948
  // Priority 1: Redirect
616
949
  if (redirectUrl) {
617
- logger.info(`Frontend app: Redirecting / to ${redirectUrl}`);
618
- controlPanel.app.get('/', (_req, res) => {
619
- res.redirect(redirectUrl);
620
- });
950
+ logger.debug(`Frontend: Redirecting / to ${redirectUrl}`);
951
+ app.get('/', (_req, res) => res.redirect(redirectUrl));
621
952
  return;
622
953
  }
623
- // Priority 2: Serve static files
954
+ // Priority 2: Static files
624
955
  if (staticPath && existsSync(staticPath)) {
625
- logger.info(`Frontend app: Serving static files from ${staticPath}`);
626
- controlPanel.app.use('/', express.static(staticPath));
627
- // SPA fallback for root
628
- controlPanel.app.get('/', (_req, res) => {
956
+ logger.debug(`Frontend: Serving static from ${staticPath}`);
957
+ app.use('/', express.static(staticPath));
958
+ app.get('/', (_req, res) => {
629
959
  res.sendFile(resolve(staticPath, 'index.html'));
630
960
  });
631
961
  return;
632
962
  }
633
963
  // Priority 3: Landing page
634
964
  if (landingPage) {
635
- logger.info(`Frontend app: Serving landing page`);
636
- controlPanel.app.get('/', (_req, res) => {
637
- const html = generateLandingPageHtml(landingPage, controlPanelPath);
965
+ logger.debug('Frontend: Serving custom landing page');
966
+ app.get('/', (_req, res) => {
967
+ const html = generateLandingPageHtml(landingPage, cpPath);
638
968
  res.type('html').send(html);
639
969
  });
640
970
  }
641
971
  };
972
+ /**
973
+ * Start the gateway
974
+ */
642
975
  const start = async () => {
643
- logger.info('Starting gateway...');
644
- // 1. Start internal service
645
- logger.info(`Starting internal service on port ${servicePort}...`);
646
- service = await serviceFactory(servicePort);
647
- logger.info(`Internal service started on port ${servicePort}`);
648
- // 2. Setup proxy middleware (after service is started)
649
- setupProxyMiddleware();
650
- // 3. Setup frontend app at root path
651
- setupFrontendApp();
652
- // 4. Start control panel gateway
653
- await controlPanel.start();
654
- // Log startup info
655
- logger.info(`${config.productName} Gateway`);
656
- logger.info(`Gateway Port: ${gatewayPort} (public)`);
657
- logger.info(`Service Port: ${servicePort} (internal)`);
658
- if (guardConfig && guardConfig.type === 'basic') {
659
- logger.info(`Control Panel Auth: HTTP Basic Auth - Username: ${guardConfig.username}`);
976
+ logger.debug('Starting gateway...');
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}`);
660
998
  }
661
- else if (guardConfig && guardConfig.type !== 'none') {
662
- logger.info(`Control Panel Auth: ${guardConfig.type}`);
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);
663
1009
  }
664
- else {
665
- logger.info('Control Panel Auth: None (not recommended)');
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
+ }
666
1018
  }
667
- logger.info(`Frontend App: GET /`);
668
- logger.info(`Control Panel UI: GET ${controlPanelPath.padEnd(20)}`);
669
- logger.info(`Gateway Health: GET ${apiBasePath}/health`);
670
- logger.info(`Service Health: GET /health`);
671
- for (const apiPath of proxyPaths) {
672
- logger.info(`Service API: * ${apiPath}/*`);
1019
+ // 4. Setup frontend app at root
1020
+ setupFrontendApp();
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})`
1026
+ : '(no auth)';
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
+ }
673
1036
  }
674
1037
  };
1038
+ /**
1039
+ * Stop the gateway
1040
+ */
675
1041
  const stop = async () => {
676
- logger.info('Shutting down gateway...');
1042
+ logger.debug('Shutting down gateway...');
677
1043
  // Stop control panel
678
- await controlPanel.stop();
679
- // Stop internal service
680
- if (service) {
681
- await service.shutdown();
682
- service.server.close();
1044
+ if (controlPanelInstance) {
1045
+ await controlPanelInstance.stop();
1046
+ }
1047
+ // Stop gateway server
1048
+ if (server) {
1049
+ await new Promise((resolve) => server.close(() => resolve()));
683
1050
  }
684
- logger.info('Gateway shutdown complete');
1051
+ logger.debug('Gateway shutdown complete');
685
1052
  };
686
1053
  return {
687
- controlPanel,
688
- service,
1054
+ app,
1055
+ server,
1056
+ controlPanel: controlPanelInstance,
1057
+ mountedApps,
689
1058
  start,
690
1059
  stop,
691
- gatewayPort,
692
- servicePort,
1060
+ port: gatewayPort,
693
1061
  };
694
1062
  }
695
1063
  //# sourceMappingURL=gateway.js.map