@qwickapps/server 1.2.0 → 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 +238 -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 +92 -54
  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 +679 -319
  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 +114 -61
  180. package/src/core/gateway.ts +863 -403
  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="/logo.svg" 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,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 ${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
+ }
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
- <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
- // Use /health (public, proxied from service) instead of control panel API
452
- const res = await fetch('/health');
453
- const data = await res.json();
454
-
455
- badge.classList.remove('loading');
456
-
457
- if (data.status === 'healthy') {
458
- dot.className = 'status-dot';
459
- text.textContent = 'All systems operational';
460
- desc.textContent = 'The service is running smoothly and ready to handle requests.';
461
- } else if (data.status === 'degraded') {
462
- dot.className = 'status-dot degraded';
463
- text.textContent = 'Degraded performance';
464
- desc.textContent = 'Some services may be experiencing issues. Core functionality remains available.';
465
- } else {
466
- dot.className = 'status-dot unhealthy';
467
- text.textContent = 'System maintenance';
468
- desc.textContent = 'The service is currently undergoing maintenance. Please check back shortly.';
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
- } catch (e) {
471
- badge.classList.remove('loading');
472
- dot.className = 'status-dot unhealthy';
473
- text.textContent = 'Unable to connect';
474
- desc.textContent = 'Could not reach the service. Please try again later.';
475
- }
476
- }
477
-
478
- // Check status on load and every 30 seconds
479
- checkStatus();
480
- setInterval(checkStatus, 30000);
481
- </script>
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
- * 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
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
- * productName: 'My Service',
499
- * gatewayPort: 3101,
500
- * servicePort: 3100,
782
+ * const gateway = createGateway({
783
+ * productName: 'My Product',
784
+ * port: 3000,
785
+ * controlPanel: {
786
+ * path: '/cpanel',
787
+ * port: 3001,
788
+ * plugins: [...],
501
789
  * },
502
- * async (port) => {
503
- * const app = createMyApp();
504
- * const server = app.listen(port);
505
- * return {
506
- * app,
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, serviceFactory) {
517
- // Initialize logging subsystem first
518
- const loggingSubsystem = initializeLogging({
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
- const gatewayPort = config.gatewayPort || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3101', 10);
525
- 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);
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
- let service = null;
536
- // Create control panel
537
- const controlPanel = createControlPanel({
538
- config: {
539
- productName: config.productName,
540
- port: gatewayPort,
541
- version,
542
- branding: config.branding,
543
- cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
544
- // Skip body parsing for proxied paths
545
- skipBodyParserPaths: [...proxyPaths, '/health'],
546
- // Mount path for control panel
547
- mountPath: controlPanelPath,
548
- // Route guard
549
- guard: guardConfig,
550
- // Custom UI path
551
- customUiPath: config.customUiPath,
552
- links: config.links,
553
- },
554
- plugins: config.plugins || [],
555
- logger,
556
- });
557
- // Setup proxy middleware for API paths
558
- const setupProxyMiddleware = () => {
559
- const target = `http://localhost:${servicePort}`;
560
- // Proxy each API path
561
- for (const apiPath of proxyPaths) {
562
- const proxyOptions = {
563
- target,
564
- changeOrigin: false,
565
- pathFilter: `${apiPath}/**`,
566
- on: {
567
- error: (err, _req, res) => {
568
- logger.error('Proxy error', { error: err.message, path: apiPath });
569
- 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
570
861
  res.writeHead(503, { 'Content-Type': 'application/json' });
571
862
  res.end(JSON.stringify({
572
863
  error: 'Service Unavailable',
573
- message: 'The service is currently unavailable. Please try again later.',
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
- controlPanel.app.use(createProxyMiddleware(proxyOptions));
581
- }
582
- // Proxy /health endpoint to internal service
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
- 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 });
601
890
  };
602
- // Calculate API base path for landing page
603
- const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
604
- // Setup frontend app at root path
605
- const setupFrontendApp = () => {
606
- // Serve logo at /logo.svg if logoUrl is configured and customUiPath exists
607
- if (config.logoUrl && config.customUiPath) {
608
- const logoPath = resolve(config.customUiPath, 'logo.svg');
609
- if (existsSync(logoPath)) {
610
- controlPanel.app.get('/logo.svg', (_req, res) => {
611
- res.sendFile(logoPath);
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
- // If no frontend app configured, serve default landing page with status
617
- if (!config.frontendApp) {
618
- logger.debug('Frontend app: Serving default landing page');
619
- controlPanel.app.get('/', (_req, res) => {
620
- const html = generateDefaultLandingPageHtml(config.productName, controlPanelPath, apiBasePath, version, config.logoUrl);
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 } = config.frontendApp;
947
+ const { redirectUrl, staticPath, landingPage } = frontendApp;
626
948
  // Priority 1: Redirect
627
949
  if (redirectUrl) {
628
- logger.debug(`Frontend app: Redirecting / to ${redirectUrl}`);
629
- controlPanel.app.get('/', (_req, res) => {
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: Serve static files
954
+ // Priority 2: Static files
635
955
  if (staticPath && existsSync(staticPath)) {
636
- logger.debug(`Frontend app: Serving static files from ${staticPath}`);
637
- controlPanel.app.use('/', express.static(staticPath));
638
- // SPA fallback for root
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(`Frontend app: Serving landing page`);
647
- controlPanel.app.get('/', (_req, res) => {
648
- const html = generateLandingPageHtml(landingPage, controlPanelPath);
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 service
656
- logger.debug(`Starting internal service on port ${servicePort}...`);
657
- service = await serviceFactory(servicePort);
658
- logger.debug(`Internal service started on port ${servicePort}`);
659
- // 2. Setup proxy middleware (after service is started)
660
- setupProxyMiddleware();
661
- // 3. Setup frontend app at root path
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
- // 4. Start control panel gateway
664
- await controlPanel.start();
665
- // Log concise startup info
666
- const authInfo = guardConfig?.type === 'basic'
667
- ? `(auth: ${guardConfig.username})`
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 detailed route info at debug level
673
- logger.debug(`Gateway Port: ${gatewayPort} (public)`);
674
- logger.debug(`Service Port: ${servicePort} (internal)`);
675
- logger.debug(`Frontend App: GET /`);
676
- logger.debug(`Control Panel UI: GET ${controlPanelPath}`);
677
- logger.debug(`Gateway Health: GET ${apiBasePath}/health`);
678
- logger.debug(`Service Health: GET /health`);
679
- for (const apiPath of proxyPaths) {
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
- await controlPanel.stop();
687
- // Stop internal service
688
- if (service) {
689
- await service.shutdown();
690
- 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()));
691
1050
  }
692
1051
  logger.debug('Gateway shutdown complete');
693
1052
  };
694
1053
  return {
695
- controlPanel,
696
- service,
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