@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
package/README.md CHANGED
@@ -61,6 +61,8 @@ For production deployments, use `createGateway` to run a gateway that:
61
61
  1. Serves the control panel UI (always responsive, even if the API crashes)
62
62
  2. Proxies API requests to an internal service
63
63
  3. Handles graceful error responses when the internal service is down
64
+ 4. Supports maintenance mode with customizable status pages
65
+ 5. Shows service unavailable pages when mounted apps are unreachable
64
66
 
65
67
  ```typescript
66
68
  import { createGateway, createHealthPlugin } from '@qwickapps/server';
@@ -115,6 +117,68 @@ Internet → Gateway (3101, public) → API Service (3100, internal)
115
117
 
116
118
  The gateway is always responsive even if the internal API service crashes, allowing you to view diagnostics and error information.
117
119
 
120
+ ### Mounted Apps with Maintenance Mode
121
+
122
+ Mount frontend apps or proxy services with full maintenance and fallback support:
123
+
124
+ ```typescript
125
+ const gateway = createGateway({
126
+ // ... base config
127
+ mountedApps: [
128
+ {
129
+ path: '/app',
130
+ name: 'Main App',
131
+ type: 'proxy',
132
+ target: 'http://localhost:4000',
133
+ maintenance: {
134
+ enabled: false, // Toggle to enable maintenance mode
135
+ title: 'Scheduled Maintenance',
136
+ message: 'We are upgrading our systems.',
137
+ expectedBackAt: '2 hours', // or ISO date, or "soon"
138
+ contactUrl: 'https://status.example.com',
139
+ bypassPaths: ['/app/health', '/app/api/status'],
140
+ },
141
+ fallback: {
142
+ title: 'Service Unavailable',
143
+ message: 'The application is temporarily unavailable.',
144
+ showRetry: true,
145
+ autoRefresh: 30, // seconds
146
+ },
147
+ },
148
+ {
149
+ path: '/docs',
150
+ name: 'Documentation',
151
+ type: 'static',
152
+ staticPath: './docs-dist',
153
+ },
154
+ ],
155
+ });
156
+ ```
157
+
158
+ ### Maintenance and Fallback Configuration
159
+
160
+ **MaintenanceConfig** - Shown when maintenance mode is enabled:
161
+
162
+ | Property | Type | Description |
163
+ |----------|------|-------------|
164
+ | `enabled` | `boolean` | Enable/disable maintenance mode |
165
+ | `title` | `string` | Page title (default: "Under Maintenance") |
166
+ | `message` | `string` | Custom message to display |
167
+ | `expectedBackAt` | `string` | ETA: ISO date, relative time ("2 hours"), or "soon" |
168
+ | `contactUrl` | `string` | Link to status page or contact |
169
+ | `bypassPaths` | `string[]` | Paths that bypass maintenance (e.g., health checks) |
170
+
171
+ **FallbackConfig** - Shown when the proxied service is unreachable:
172
+
173
+ | Property | Type | Description |
174
+ |----------|------|-------------|
175
+ | `title` | `string` | Page title (default: "Service Unavailable") |
176
+ | `message` | `string` | Custom message to display |
177
+ | `showRetry` | `boolean` | Show retry button (default: true) |
178
+ | `autoRefresh` | `number` | Auto-refresh countdown in seconds (default: 30) |
179
+
180
+ Both pages feature modern, responsive designs with automatic dark mode support.
181
+
118
182
  ## Configuration
119
183
 
120
184
  ### ControlPanelConfig
@@ -130,6 +194,7 @@ The gateway is always responsive even if the internal API service crashes, allow
130
194
  | `cors` | `object` | No | CORS origins configuration |
131
195
  | `links` | `array` | No | Quick links for the dashboard |
132
196
  | `skipBodyParserPaths` | `string[]` | No | Paths to skip body parsing (for proxy) |
197
+ | `logoUrl` | `string` | No | Custom logo URL for the landing page |
133
198
 
134
199
  ### Route Guards
135
200
 
@@ -297,6 +362,259 @@ createLogsPlugin({
297
362
  });
298
363
  ```
299
364
 
365
+ #### PostgreSQL Plugin
366
+
367
+ Provides connection pooling, transactions, and health checks for PostgreSQL databases.
368
+
369
+ ```typescript
370
+ import { createPostgresPlugin, getPostgres } from '@qwickapps/server';
371
+
372
+ // Register the plugin
373
+ createPostgresPlugin({
374
+ connectionString: process.env.DATABASE_URL,
375
+ // Or individual options:
376
+ // host: 'localhost',
377
+ // port: 5432,
378
+ // database: 'mydb',
379
+ // user: 'postgres',
380
+ // password: 'secret',
381
+ maxConnections: 20,
382
+ healthCheckInterval: 30000,
383
+ });
384
+
385
+ // Use in your code
386
+ const pg = getPostgres();
387
+ const result = await pg.query('SELECT * FROM users WHERE id = $1', [userId]);
388
+
389
+ // With transactions
390
+ await pg.withTransaction(async (client) => {
391
+ await client.query('INSERT INTO orders ...');
392
+ await client.query('UPDATE inventory ...');
393
+ });
394
+ ```
395
+
396
+ **Exports:**
397
+ - `createPostgresPlugin(config)` - Create and register the plugin
398
+ - `getPostgres(name?)` - Get a PostgreSQL instance (throws if not registered)
399
+ - `hasPostgres(name?)` - Check if an instance is registered
400
+ - `PostgresInstance` - TypeScript type for the instance
401
+
402
+ #### Cache Plugin
403
+
404
+ Redis-based caching with key prefixing, TTL support, and pattern operations.
405
+
406
+ ```typescript
407
+ import { createCachePlugin, getCache } from '@qwickapps/server';
408
+
409
+ // Register the plugin
410
+ createCachePlugin({
411
+ url: process.env.REDIS_URL,
412
+ // Or individual options:
413
+ // host: 'localhost',
414
+ // port: 6379,
415
+ // password: 'secret',
416
+ keyPrefix: 'myapp:',
417
+ defaultTTL: 3600, // 1 hour default
418
+ healthCheckInterval: 30000,
419
+ });
420
+
421
+ // Use in your code
422
+ const cache = getCache();
423
+
424
+ // Basic operations
425
+ await cache.set('user:123', userData, 600); // TTL in seconds
426
+ const user = await cache.get('user:123');
427
+ await cache.delete('user:123');
428
+
429
+ // Pattern operations
430
+ const keys = await cache.keys('user:*');
431
+ await cache.deletePattern('session:*');
432
+
433
+ // Stats and maintenance
434
+ const stats = await cache.getStats();
435
+ await cache.flush(); // Clear all keys with prefix
436
+ ```
437
+
438
+ **Exports:**
439
+ - `createCachePlugin(config)` - Create and register the plugin
440
+ - `getCache(name?)` - Get a cache instance (throws if not registered)
441
+ - `hasCache(name?)` - Check if an instance is registered
442
+ - `CacheInstance` - TypeScript type for the instance
443
+
444
+ #### Auth Plugin
445
+
446
+ Pluggable authentication with support for multiple providers via the adapter pattern.
447
+
448
+ ```typescript
449
+ import { createAuthPlugin, auth0Adapter, basicAdapter } from '@qwickapps/server';
450
+
451
+ // Auth0 with RBAC and domain restrictions
452
+ createAuthPlugin({
453
+ adapter: auth0Adapter({
454
+ domain: process.env.AUTH0_DOMAIN!,
455
+ clientId: process.env.AUTH0_CLIENT_ID!,
456
+ clientSecret: process.env.AUTH0_CLIENT_SECRET!,
457
+ baseUrl: 'https://myapp.example.com',
458
+ secret: process.env.SESSION_SECRET!,
459
+ audience: process.env.AUTH0_AUDIENCE, // For API access tokens
460
+ allowedRoles: ['admin', 'support'], // RBAC filtering
461
+ allowedDomains: ['@company.com'], // Domain whitelist
462
+ exposeAccessToken: true, // For downstream API calls
463
+ }),
464
+ excludePaths: ['/health', '/api/public'],
465
+ });
466
+
467
+ // Basic auth fallback
468
+ createAuthPlugin({
469
+ adapter: basicAdapter({
470
+ username: 'admin',
471
+ password: process.env.ADMIN_PASSWORD!,
472
+ }),
473
+ });
474
+ ```
475
+
476
+ **Available Adapters:**
477
+ - `auth0Adapter` - Auth0 OIDC (requires `express-openid-connect`)
478
+ - `supabaseAdapter` - Supabase JWT validation
479
+ - `basicAdapter` - HTTP Basic authentication
480
+
481
+ **Helper Functions:**
482
+ ```typescript
483
+ import { isAuthenticated, getAuthenticatedUser, getAccessToken } from '@qwickapps/server';
484
+
485
+ // In your route handlers
486
+ if (isAuthenticated(req)) {
487
+ const user = getAuthenticatedUser(req);
488
+ // { id, email, name, picture, emailVerified, roles }
489
+
490
+ const accessToken = getAccessToken(req);
491
+ // Use for downstream API calls
492
+ }
493
+ ```
494
+
495
+ **Middleware Helpers:**
496
+ ```typescript
497
+ import { requireAuth, requireRoles, requireAnyRole } from '@qwickapps/server';
498
+
499
+ // Require authentication
500
+ app.get('/admin', requireAuth(), (req, res) => { ... });
501
+
502
+ // Require specific roles (all required)
503
+ app.get('/admin/users', requireRoles('admin', 'user-manager'), (req, res) => { ... });
504
+
505
+ // Require any of the roles
506
+ app.get('/dashboard', requireAnyRole('admin', 'editor', 'viewer'), (req, res) => { ... });
507
+ ```
508
+
509
+ #### Users Plugin
510
+
511
+ Storage-agnostic user management with ban support.
512
+
513
+ ```typescript
514
+ import { createUsersPlugin, postgresUserStore, getPostgres } from '@qwickapps/server';
515
+ import { Pool } from 'pg';
516
+
517
+ // Create with PostgreSQL storage
518
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
519
+
520
+ createUsersPlugin({
521
+ store: postgresUserStore({
522
+ pool,
523
+ usersTable: 'users',
524
+ bansTable: 'user_bans',
525
+ autoCreateTables: true,
526
+ }),
527
+ bans: {
528
+ enabled: true,
529
+ supportTemporary: true, // Enable expiring bans
530
+ onBan: async (user, ban) => {
531
+ // Notify external systems, revoke sessions, etc.
532
+ console.log(`User ${user.email} banned: ${ban.reason}`);
533
+ },
534
+ onUnban: async (user) => {
535
+ console.log(`User ${user.email} unbanned`);
536
+ },
537
+ },
538
+ api: {
539
+ prefix: '/api/users',
540
+ crud: true, // GET/POST/PUT/DELETE /api/users
541
+ search: true, // GET /api/users?q=...
542
+ bans: true, // Ban management endpoints
543
+ },
544
+ });
545
+ ```
546
+
547
+ **REST API Endpoints:**
548
+ | Endpoint | Method | Description |
549
+ |----------|--------|-------------|
550
+ | `/api/users` | GET | List/search users |
551
+ | `/api/users` | POST | Create user |
552
+ | `/api/users/:id` | GET | Get user by ID |
553
+ | `/api/users/:id` | PUT | Update user |
554
+ | `/api/users/:id` | DELETE | Delete user |
555
+ | `/api/users/bans` | GET | List active bans |
556
+ | `/api/users/:id/ban` | GET | Get user's ban status |
557
+ | `/api/users/:id/ban` | POST | Ban user |
558
+ | `/api/users/:id/ban` | DELETE | Unban user |
559
+ | `/api/users/:id/bans` | GET | Get user's ban history |
560
+
561
+ **Helper Functions:**
562
+ ```typescript
563
+ import { getUserById, getUserByEmail, isUserBanned, findOrCreateUser } from '@qwickapps/server';
564
+
565
+ // Get user
566
+ const user = await getUserById('user-123');
567
+ const userByEmail = await getUserByEmail('test@example.com');
568
+
569
+ // Check ban status
570
+ const banned = await isUserBanned('user-123');
571
+
572
+ // Find or create from auth provider
573
+ const user = await findOrCreateUser({
574
+ email: 'user@example.com',
575
+ name: 'Test User',
576
+ external_id: 'auth0|12345',
577
+ provider: 'auth0',
578
+ });
579
+ ```
580
+
581
+ **Email Ban Support (for auth-only scenarios):**
582
+
583
+ For cases where you don't store users locally but need to ban by email:
584
+
585
+ ```typescript
586
+ import { isEmailBanned, getEmailBan, banEmail, unbanEmail } from '@qwickapps/server';
587
+
588
+ // Check if email is banned
589
+ const banned = await isEmailBanned('user@example.com');
590
+
591
+ // Get ban details
592
+ const ban = await getEmailBan('user@example.com');
593
+
594
+ // Ban an email
595
+ await banEmail({
596
+ email: 'user@example.com',
597
+ reason: 'Spam activity',
598
+ banned_by: 'admin@company.com',
599
+ duration: 86400, // 24 hours (optional, null = permanent)
600
+ });
601
+
602
+ // Unban an email
603
+ await unbanEmail({
604
+ email: 'user@example.com',
605
+ unbanned_by: 'admin@company.com',
606
+ note: 'Cleared after review',
607
+ });
608
+ ```
609
+
610
+ **Email Ban API Endpoints:**
611
+ | Endpoint | Method | Description |
612
+ |----------|--------|-------------|
613
+ | `/api/users/email-bans` | GET | List active email bans |
614
+ | `/api/users/email-bans/:email` | GET | Check email ban status |
615
+ | `/api/users/email-bans` | POST | Ban an email |
616
+ | `/api/users/email-bans/:email` | DELETE | Unban an email |
617
+
300
618
  ### Creating Custom Plugins
301
619
 
302
620
  ```typescript
@@ -6,10 +6,15 @@
6
6
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
7
  */
8
8
  import { type LoggingConfig } from './logging.js';
9
- import type { ControlPanelConfig, ControlPanelPlugin, ControlPanelInstance, Logger } from './types.js';
9
+ import type { ControlPanelConfig, ControlPanelInstance, Logger } from './types.js';
10
+ import { type Plugin, type PluginConfig } from './plugin-registry.js';
10
11
  export interface CreateControlPanelOptions {
11
12
  config: ControlPanelConfig;
12
- plugins?: ControlPanelPlugin[];
13
+ /** Plugins to start with the control panel */
14
+ plugins?: Array<{
15
+ plugin: Plugin;
16
+ config?: PluginConfig;
17
+ }>;
13
18
  logger?: Logger;
14
19
  /** Logging configuration */
15
20
  logging?: LoggingConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"control-panel.d.ts","sourceRoot":"","sources":["../../src/core/control-panel.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,EAA4C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAE5F,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EAIpB,MAAM,EACP,MAAM,YAAY,CAAC;AAWpB,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,oBAAoB,CA2Q3F"}
1
+ {"version":3,"file":"control-panel.d.ts","sourceRoot":"","sources":["../../src/core/control-panel.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,EAA4C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAE5F,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAGpB,MAAM,EACP,MAAM,YAAY,CAAC;AACpB,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,YAAY,EAElB,MAAM,sBAAsB,CAAC;AAuB9B,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC,CAAC;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,oBAAoB,CAgT3F"}
@@ -9,12 +9,13 @@ import express from 'express';
9
9
  import helmet from 'helmet';
10
10
  import cors from 'cors';
11
11
  import compression from 'compression';
12
- import { existsSync } from 'node:fs';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
13
  import { fileURLToPath } from 'node:url';
14
14
  import { dirname, join } from 'node:path';
15
15
  import { HealthManager } from './health-manager.js';
16
16
  import { initializeLogging, getControlPanelLogger } from './logging.js';
17
17
  import { createRouteGuard } from './guards.js';
18
+ import { createPluginRegistry, } from './plugin-registry.js';
18
19
  // Get the package root directory for serving UI assets
19
20
  const __filename = fileURLToPath(import.meta.url);
20
21
  const __dirname = dirname(__filename);
@@ -23,6 +24,18 @@ const packageRoot = __dirname.includes('/src/')
23
24
  ? join(__dirname, '..', '..')
24
25
  : join(__dirname, '..', '..');
25
26
  const uiDistPath = join(packageRoot, 'dist-ui');
27
+ // Read @qwickapps/server package version
28
+ let frameworkVersion = '1.0.0';
29
+ try {
30
+ const packageJsonPath = join(packageRoot, 'package.json');
31
+ if (existsSync(packageJsonPath)) {
32
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
33
+ frameworkVersion = packageJson.version || '1.0.0';
34
+ }
35
+ }
36
+ catch {
37
+ // Keep default version if reading fails
38
+ }
26
39
  /**
27
40
  * Create a control panel instance
28
41
  */
@@ -38,9 +51,10 @@ export function createControlPanel(options) {
38
51
  const app = express();
39
52
  const router = express.Router();
40
53
  const healthManager = new HealthManager(logger);
41
- const registeredPlugins = [];
42
54
  let server = null;
43
55
  const startTime = Date.now();
56
+ // Initialize the new plugin registry
57
+ const pluginRegistry = createPluginRegistry(app, router, logger, healthManager, getControlPanelLogger);
44
58
  // Security middleware
45
59
  app.use(helmet({
46
60
  contentSecurityPolicy: false, // Allow inline scripts for simple UI
@@ -65,13 +79,14 @@ export function createControlPanel(options) {
65
79
  app.use(express.json());
66
80
  }
67
81
  app.use(compression());
68
- // Apply route guard if configured
82
+ // Get mount path (defaults to /cpanel)
83
+ const mountPath = config.mountPath || '/cpanel';
84
+ // Apply route guard if configured - only to the control panel mount path
69
85
  if (config.guard && config.guard.type !== 'none') {
70
86
  const guardMiddleware = createRouteGuard(config.guard);
71
- app.use(guardMiddleware);
87
+ // Only protect the control panel path, not the root or other paths
88
+ app.use(mountPath, guardMiddleware);
72
89
  }
73
- // Get mount path (defaults to /cpanel)
74
- const mountPath = config.mountPath || '/cpanel';
75
90
  const apiBasePath = mountPath === '/' ? '/api' : `${mountPath}/api`;
76
91
  // Request logging
77
92
  app.use((req, _res, next) => {
@@ -101,6 +116,8 @@ export function createControlPanel(options) {
101
116
  router.get('/info', (_req, res) => {
102
117
  res.json({
103
118
  product: config.productName,
119
+ logoName: config.logoName || config.productName,
120
+ logoIconUrl: config.logoIconUrl,
104
121
  version: config.version || 'unknown',
105
122
  uptime: Date.now() - startTime,
106
123
  links: config.links || [],
@@ -114,6 +131,28 @@ export function createControlPanel(options) {
114
131
  const report = getDiagnostics();
115
132
  res.json(report);
116
133
  });
134
+ /**
135
+ * GET /api/ui-contributions - UI contributions from all plugins
136
+ *
137
+ * Returns menu items, pages, and widgets registered by plugins.
138
+ * Used by the React UI to build dynamic navigation and pages.
139
+ */
140
+ router.get('/ui-contributions', (_req, res) => {
141
+ res.json({
142
+ menuItems: pluginRegistry.getMenuItems(),
143
+ pages: pluginRegistry.getPages(),
144
+ widgets: pluginRegistry.getWidgets(),
145
+ plugins: pluginRegistry.listPlugins(),
146
+ });
147
+ });
148
+ /**
149
+ * GET /api/plugins - List all registered plugins
150
+ */
151
+ router.get('/plugins', (_req, res) => {
152
+ res.json({
153
+ plugins: pluginRegistry.listPlugins(),
154
+ });
155
+ });
117
156
  /**
118
157
  * Serve dashboard UI at the configured mount path
119
158
  *
@@ -129,20 +168,53 @@ export function createControlPanel(options) {
129
168
  logger.debug(`Dashboard config: mountPath=${mountPath}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
130
169
  if (useRichUI) {
131
170
  logger.debug(`Serving React UI from ${effectiveUiPath}`);
171
+ // Read index.html template
172
+ const indexHtmlPath = join(effectiveUiPath, 'index.html');
173
+ const indexHtmlTemplate = readFileSync(indexHtmlPath, 'utf-8');
174
+ /**
175
+ * Get index.html with the base path injected.
176
+ *
177
+ * The server injects the base path as window.__APP_BASE_PATH__ so the React app
178
+ * can read it at runtime without complex detection logic. This is the standard
179
+ * pattern used by frameworks like Next.js (__NEXT_DATA__).
180
+ *
181
+ * When served behind a gateway proxy, use X-Forwarded-Prefix to determine
182
+ * the public path for assets and the React Router basename.
183
+ */
184
+ const getIndexHtml = (req) => {
185
+ // Determine the effective public path:
186
+ // - If X-Forwarded-Prefix header is set (proxied), use that
187
+ // - Otherwise, use the configured mountPath
188
+ const forwardedPrefix = req.get('X-Forwarded-Prefix');
189
+ const effectivePath = forwardedPrefix || mountPath;
190
+ const normalizedPath = effectivePath === '/' ? '' : effectivePath;
191
+ // Inject base path as global variable before other scripts
192
+ const basePathScript = `<script>window.__APP_BASE_PATH__="${normalizedPath}";</script>`;
193
+ let html = indexHtmlTemplate.replace('<head>', `<head>\n ${basePathScript}`);
194
+ // Rewrite asset paths if mounted at a subpath
195
+ if (normalizedPath) {
196
+ html = html.replace(/src="\/assets\//g, `src="${normalizedPath}/assets/`);
197
+ html = html.replace(/href="\/assets\//g, `href="${normalizedPath}/assets/`);
198
+ }
199
+ return html;
200
+ };
132
201
  // Serve static assets from dist-ui at the mount path
133
- app.use(mountPath, express.static(effectiveUiPath));
202
+ // Disable index: false to prevent serving index.html automatically
203
+ // We handle index.html separately with rewritten asset paths
204
+ app.use(mountPath, express.static(effectiveUiPath, { index: false }));
134
205
  // SPA fallback - serve index.html for all non-API routes under the mount path
135
- app.get(`${mountPath}/*`, (req, res, next) => {
206
+ const spaFallbackPath = mountPath === '/' ? '/*' : `${mountPath}/*`;
207
+ app.get(spaFallbackPath, (req, res, next) => {
136
208
  // Skip API routes
137
209
  if (req.path.startsWith(apiBasePath)) {
138
210
  return next();
139
211
  }
140
- res.sendFile(join(effectiveUiPath, 'index.html'));
212
+ res.type('html').send(getIndexHtml(req));
141
213
  });
142
214
  // Also serve the mount path root
143
215
  if (mountPath !== '/') {
144
- app.get(mountPath, (_req, res) => {
145
- res.sendFile(join(effectiveUiPath, 'index.html'));
216
+ app.get(mountPath, (req, res) => {
217
+ res.type('html').send(getIndexHtml(req));
146
218
  });
147
219
  }
148
220
  }
@@ -155,43 +227,9 @@ export function createControlPanel(options) {
155
227
  });
156
228
  }
157
229
  }
158
- // Plugin context factory - creates context with plugin-specific logger
159
- const createPluginContext = (pluginName) => ({
160
- config,
161
- app,
162
- router,
163
- logger: getControlPanelLogger(pluginName),
164
- registerHealthCheck: (check) => healthManager.register(check),
165
- });
166
- // Register plugin
167
- const registerPlugin = async (plugin) => {
168
- logger.debug(`Registering plugin: ${plugin.name}`);
169
- // Register routes
170
- if (plugin.routes) {
171
- for (const route of plugin.routes) {
172
- switch (route.method) {
173
- case 'get':
174
- router.get(route.path, route.handler);
175
- break;
176
- case 'post':
177
- router.post(route.path, route.handler);
178
- break;
179
- case 'put':
180
- router.put(route.path, route.handler);
181
- break;
182
- case 'delete':
183
- router.delete(route.path, route.handler);
184
- break;
185
- }
186
- logger.debug(`Registered route: ${route.method.toUpperCase()} ${route.path}`);
187
- }
188
- }
189
- // Initialize plugin with plugin-specific logger
190
- if (plugin.onInit) {
191
- await plugin.onInit(createPluginContext(plugin.name));
192
- }
193
- registeredPlugins.push(plugin);
194
- logger.debug(`Plugin registered: ${plugin.name}`);
230
+ // Start a plugin with the registry
231
+ const startPlugin = async (plugin, pluginConfig = {}) => {
232
+ return pluginRegistry.startPlugin(plugin, pluginConfig);
195
233
  };
196
234
  // Get diagnostics report
197
235
  const getDiagnostics = () => {
@@ -200,6 +238,7 @@ export function createControlPanel(options) {
200
238
  timestamp: new Date().toISOString(),
201
239
  product: config.productName,
202
240
  version: config.version,
241
+ frameworkVersion,
203
242
  uptime: Date.now() - startTime,
204
243
  health: healthManager.getResults(),
205
244
  system: {
@@ -219,32 +258,31 @@ export function createControlPanel(options) {
219
258
  };
220
259
  // Start server
221
260
  const start = async () => {
222
- // Register initial plugins
223
- for (const plugin of plugins) {
224
- await registerPlugin(plugin);
261
+ // Start initial plugins via registry
262
+ for (const { plugin, config: pluginConfig } of plugins) {
263
+ const success = await pluginRegistry.startPlugin(plugin, pluginConfig || {});
264
+ if (!success) {
265
+ logger.error(`Failed to start plugin: ${plugin.id}`);
266
+ }
225
267
  }
226
268
  return new Promise((resolve) => {
227
269
  server = app.listen(config.port, () => {
228
- logger.info(`Control panel listening on port ${config.port}`);
270
+ logger.debug(`Control panel listening on port ${config.port}`);
229
271
  resolve();
230
272
  });
231
273
  });
232
274
  };
233
275
  // Stop server
234
276
  const stop = async () => {
235
- // Shutdown plugins
236
- for (const plugin of registeredPlugins) {
237
- if (plugin.onShutdown) {
238
- await plugin.onShutdown();
239
- }
240
- }
277
+ // Stop all plugins via registry
278
+ await pluginRegistry.stopAllPlugins();
241
279
  // Shutdown health manager
242
280
  healthManager.shutdown();
243
281
  // Close server
244
282
  if (server) {
245
283
  return new Promise((resolve) => {
246
284
  server.close(() => {
247
- logger.info('Control panel stopped');
285
+ logger.debug('Control panel stopped');
248
286
  resolve();
249
287
  });
250
288
  });
@@ -254,9 +292,10 @@ export function createControlPanel(options) {
254
292
  app,
255
293
  start,
256
294
  stop,
257
- registerPlugin,
295
+ startPlugin,
258
296
  getHealthStatus: () => healthManager.getResults(),
259
297
  getDiagnostics,
298
+ getPluginRegistry: () => pluginRegistry,
260
299
  };
261
300
  }
262
301
  /**