@qwickapps/server 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +392 -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 +120 -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 +307 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +352 -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 +10 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +10 -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/adapters/supertokens-adapter.d.ts +18 -0
  41. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  42. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  43. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  45. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.js +255 -0
  47. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  48. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  49. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  51. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  52. package/dist/plugins/auth/env-config.d.ts +88 -0
  53. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  54. package/dist/plugins/auth/env-config.js +489 -0
  55. package/dist/plugins/auth/env-config.js.map +1 -0
  56. package/dist/plugins/auth/index.d.ts +14 -0
  57. package/dist/plugins/auth/index.d.ts.map +1 -0
  58. package/dist/plugins/auth/index.js +16 -0
  59. package/dist/plugins/auth/index.js.map +1 -0
  60. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  61. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  62. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  63. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  64. package/dist/plugins/auth/types.d.ts +218 -0
  65. package/dist/plugins/auth/types.d.ts.map +1 -0
  66. package/dist/plugins/auth/types.js +14 -0
  67. package/dist/plugins/auth/types.js.map +1 -0
  68. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  69. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  70. package/dist/plugins/bans/bans-plugin.js +428 -0
  71. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  72. package/dist/plugins/bans/index.d.ts +9 -0
  73. package/dist/plugins/bans/index.d.ts.map +1 -0
  74. package/dist/plugins/bans/index.js +10 -0
  75. package/dist/plugins/bans/index.js.map +1 -0
  76. package/dist/plugins/bans/stores/index.d.ts +7 -0
  77. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  78. package/dist/plugins/bans/stores/index.js +7 -0
  79. package/dist/plugins/bans/stores/index.js.map +1 -0
  80. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  81. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  82. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  83. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  84. package/dist/plugins/bans/types.d.ts +128 -0
  85. package/dist/plugins/bans/types.d.ts.map +1 -0
  86. package/dist/plugins/bans/types.js +11 -0
  87. package/dist/plugins/bans/types.js.map +1 -0
  88. package/dist/plugins/cache-plugin.d.ts +14 -3
  89. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  90. package/dist/plugins/cache-plugin.js +27 -7
  91. package/dist/plugins/cache-plugin.js.map +1 -1
  92. package/dist/plugins/cache-plugin.test.js +99 -32
  93. package/dist/plugins/cache-plugin.test.js.map +1 -1
  94. package/dist/plugins/config-plugin.d.ts +3 -2
  95. package/dist/plugins/config-plugin.d.ts.map +1 -1
  96. package/dist/plugins/config-plugin.js +17 -10
  97. package/dist/plugins/config-plugin.js.map +1 -1
  98. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  99. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  100. package/dist/plugins/diagnostics-plugin.js +17 -10
  101. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  102. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  103. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  105. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  106. package/dist/plugins/entitlements/index.d.ts +12 -0
  107. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/index.js +16 -0
  109. package/dist/plugins/entitlements/index.js.map +1 -0
  110. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  111. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  112. package/dist/plugins/entitlements/sources/index.js +9 -0
  113. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  114. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  115. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  116. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  117. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  118. package/dist/plugins/entitlements/types.d.ts +232 -0
  119. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  120. package/dist/plugins/entitlements/types.js +11 -0
  121. package/dist/plugins/entitlements/types.js.map +1 -0
  122. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  123. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  124. package/dist/plugins/frontend-app-plugin.js +14 -9
  125. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  126. package/dist/plugins/health-plugin.d.ts +5 -2
  127. package/dist/plugins/health-plugin.d.ts.map +1 -1
  128. package/dist/plugins/health-plugin.js +20 -5
  129. package/dist/plugins/health-plugin.js.map +1 -1
  130. package/dist/plugins/index.d.ts +10 -2
  131. package/dist/plugins/index.d.ts.map +1 -1
  132. package/dist/plugins/index.js +10 -2
  133. package/dist/plugins/index.js.map +1 -1
  134. package/dist/plugins/logs-plugin.d.ts +3 -2
  135. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  136. package/dist/plugins/logs-plugin.js +21 -12
  137. package/dist/plugins/logs-plugin.js.map +1 -1
  138. package/dist/plugins/postgres-plugin.d.ts +3 -3
  139. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  140. package/dist/plugins/postgres-plugin.js +9 -7
  141. package/dist/plugins/postgres-plugin.js.map +1 -1
  142. package/dist/plugins/postgres-plugin.test.js +50 -29
  143. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  144. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  145. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  146. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  147. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  148. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  149. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  150. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  151. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  152. package/dist/plugins/preferences/index.d.ts +12 -0
  153. package/dist/plugins/preferences/index.d.ts.map +1 -0
  154. package/dist/plugins/preferences/index.js +13 -0
  155. package/dist/plugins/preferences/index.js.map +1 -0
  156. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  157. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  158. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  159. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  160. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  161. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  162. package/dist/plugins/preferences/stores/index.js +9 -0
  163. package/dist/plugins/preferences/stores/index.js.map +1 -0
  164. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  165. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  166. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  167. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  168. package/dist/plugins/preferences/types.d.ts +91 -0
  169. package/dist/plugins/preferences/types.d.ts.map +1 -0
  170. package/dist/plugins/preferences/types.js +10 -0
  171. package/dist/plugins/preferences/types.js.map +1 -0
  172. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  173. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  174. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  175. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  176. package/dist/plugins/users/index.d.ts +12 -0
  177. package/dist/plugins/users/index.d.ts.map +1 -0
  178. package/dist/plugins/users/index.js +13 -0
  179. package/dist/plugins/users/index.js.map +1 -0
  180. package/dist/plugins/users/stores/index.d.ts +7 -0
  181. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  182. package/dist/plugins/users/stores/index.js +7 -0
  183. package/dist/plugins/users/stores/index.js.map +1 -0
  184. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  185. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  186. package/dist/plugins/users/stores/postgres-store.js +157 -0
  187. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  188. package/dist/plugins/users/types.d.ts +225 -0
  189. package/dist/plugins/users/types.d.ts.map +1 -0
  190. package/dist/plugins/users/types.js +12 -0
  191. package/dist/plugins/users/types.js.map +1 -0
  192. package/dist/plugins/users/users-plugin.d.ts +45 -0
  193. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  194. package/dist/plugins/users/users-plugin.js +359 -0
  195. package/dist/plugins/users/users-plugin.js.map +1 -0
  196. package/dist-ui/assets/index-BY8OxNgO.js +465 -0
  197. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  198. package/dist-ui/index.html +1 -1
  199. package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
  200. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  201. package/dist-ui-lib/components/index.d.ts +18 -0
  202. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  203. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  204. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  205. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  206. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
  207. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
  208. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  209. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  210. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  211. package/dist-ui-lib/index.js +5172 -0
  212. package/dist-ui-lib/index.js.map +1 -0
  213. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  214. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  215. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  216. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  217. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  218. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  219. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  220. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  221. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  222. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  223. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  224. package/package.json +24 -7
  225. package/src/core/control-panel.ts +145 -61
  226. package/src/core/gateway.ts +863 -403
  227. package/src/core/index.ts +21 -2
  228. package/src/core/plugin-registry.ts +716 -0
  229. package/src/core/types.ts +31 -37
  230. package/src/index.ts +125 -19
  231. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  232. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  233. package/src/plugins/auth/adapters/index.ts +10 -0
  234. package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
  235. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  236. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  237. package/src/plugins/auth/auth-plugin.ts +303 -0
  238. package/src/plugins/auth/env-config.ts +572 -0
  239. package/src/plugins/auth/index.ts +42 -0
  240. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  241. package/src/plugins/auth/types.ts +245 -0
  242. package/src/plugins/bans/bans-plugin.ts +485 -0
  243. package/src/plugins/bans/index.ts +31 -0
  244. package/src/plugins/bans/stores/index.ts +7 -0
  245. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  246. package/src/plugins/bans/types.ts +141 -0
  247. package/src/plugins/cache-plugin.test.ts +108 -32
  248. package/src/plugins/cache-plugin.ts +40 -9
  249. package/src/plugins/config-plugin.ts +23 -12
  250. package/src/plugins/diagnostics-plugin.ts +22 -12
  251. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  252. package/src/plugins/entitlements/index.ts +51 -0
  253. package/src/plugins/entitlements/sources/index.ts +9 -0
  254. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  255. package/src/plugins/entitlements/types.ts +256 -0
  256. package/src/plugins/frontend-app-plugin.ts +24 -12
  257. package/src/plugins/health-plugin.ts +27 -7
  258. package/src/plugins/index.ts +132 -4
  259. package/src/plugins/logs-plugin.ts +28 -14
  260. package/src/plugins/postgres-plugin.test.ts +52 -29
  261. package/src/plugins/postgres-plugin.ts +11 -9
  262. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  263. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  264. package/src/plugins/preferences/index.ts +30 -0
  265. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  266. package/src/plugins/preferences/stores/index.ts +9 -0
  267. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  268. package/src/plugins/preferences/types.ts +100 -0
  269. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  270. package/src/plugins/users/index.ts +38 -0
  271. package/src/plugins/users/stores/index.ts +7 -0
  272. package/src/plugins/users/stores/postgres-store.ts +225 -0
  273. package/src/plugins/users/types.ts +247 -0
  274. package/src/plugins/users/users-plugin.ts +418 -0
  275. package/ui/src/App.tsx +188 -31
  276. package/ui/src/api/controlPanelApi.ts +453 -1
  277. package/ui/src/components/ControlPanelApp.tsx +212 -0
  278. package/ui/src/components/index.ts +62 -0
  279. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  280. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  281. package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
  282. package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
  283. package/ui/src/dashboard/builtInWidgets.tsx +35 -0
  284. package/ui/src/dashboard/index.ts +35 -0
  285. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  286. package/ui/src/dashboard/widgets/index.ts +7 -0
  287. package/ui/src/pages/AuthPage.tsx +259 -0
  288. package/ui/src/pages/DashboardPage.tsx +28 -149
  289. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  290. package/ui/src/pages/LogsPage.tsx +174 -8
  291. package/ui/src/pages/PluginPage.tsx +148 -0
  292. package/ui/src/pages/PluginsPage.tsx +394 -0
  293. package/ui/src/pages/SystemPage.tsx +445 -0
  294. package/ui/src/pages/UsersPage.tsx +837 -0
  295. package/ui/tsconfig.lib.json +11 -0
  296. package/ui/vite.lib.config.ts +56 -0
  297. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  298. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  299. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Users Plugin
3
+ *
4
+ * User identity management plugin for @qwickapps/server.
5
+ * Provides CRUD operations, search, and user lookup functionality.
6
+ *
7
+ * Note: Ban management is handled by the separate Bans Plugin.
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ import type { Request, Response } from 'express';
13
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
14
+ import type {
15
+ UsersPluginConfig,
16
+ UserStore,
17
+ User,
18
+ CreateUserInput,
19
+ UpdateUserInput,
20
+ UserSearchParams,
21
+ UserInfo,
22
+ UserSyncInput,
23
+ } from './types.js';
24
+ // Import helpers from other plugins for buildUserInfo
25
+ // Note: These imports are used dynamically based on registry.hasPlugin() checks
26
+ import { getEntitlements } from '../entitlements/entitlements-plugin.js';
27
+ import { getPreferences } from '../preferences/preferences-plugin.js';
28
+ import { getActiveBan } from '../bans/bans-plugin.js';
29
+
30
+ // Store instance for helper access
31
+ let currentStore: UserStore | null = null;
32
+ let currentRegistry: PluginRegistry | null = null;
33
+
34
+ /**
35
+ * Create the Users plugin
36
+ */
37
+ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
38
+ const debug = config.debug || false;
39
+ // Routes are mounted under /api by the control panel, so don't include /api in prefix
40
+ const apiPrefix = config.api?.prefix || '/users';
41
+
42
+ function log(message: string, data?: Record<string, unknown>) {
43
+ if (debug) {
44
+ console.log(`[UsersPlugin] ${message}`, data || '');
45
+ }
46
+ }
47
+
48
+ return {
49
+ id: 'users',
50
+ name: 'Users',
51
+ version: '1.0.0',
52
+
53
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
54
+ log('Starting users plugin');
55
+
56
+ // Initialize the store (creates tables if needed)
57
+ await config.store.initialize();
58
+ log('Users plugin migrations complete');
59
+
60
+ // Store references for helper access
61
+ currentStore = config.store;
62
+ currentRegistry = registry;
63
+
64
+ // Register health check
65
+ registry.registerHealthCheck({
66
+ name: 'users-store',
67
+ type: 'custom',
68
+ check: async () => {
69
+ try {
70
+ // Simple health check - try to search with limit 1
71
+ await config.store.search({ limit: 1 });
72
+ return { healthy: true };
73
+ } catch {
74
+ return { healthy: false };
75
+ }
76
+ },
77
+ });
78
+
79
+ // Add API routes if enabled
80
+ if (config.api?.crud !== false) {
81
+ // List/Search users
82
+ registry.addRoute({
83
+ method: 'get',
84
+ path: apiPrefix,
85
+ pluginId: 'users',
86
+ handler: async (req: Request, res: Response) => {
87
+ try {
88
+ const params: UserSearchParams = {
89
+ query: req.query.q as string,
90
+ provider: req.query.provider as string,
91
+ page: parseInt(req.query.page as string) || 1,
92
+ limit: Math.min(parseInt(req.query.limit as string) || 20, 100),
93
+ sortBy: (req.query.sortBy as UserSearchParams['sortBy']) || 'created_at',
94
+ sortOrder: (req.query.sortOrder as UserSearchParams['sortOrder']) || 'desc',
95
+ };
96
+
97
+ const result = await config.store.search(params);
98
+ res.json(result);
99
+ } catch (error) {
100
+ console.error('[UsersPlugin] Search error:', error);
101
+ res.status(500).json({ error: 'Failed to search users' });
102
+ }
103
+ },
104
+ });
105
+
106
+ // Get user by ID
107
+ registry.addRoute({
108
+ method: 'get',
109
+ path: `${apiPrefix}/:id`,
110
+ pluginId: 'users',
111
+ handler: async (req: Request, res: Response) => {
112
+ try {
113
+ const user = await config.store.getById(req.params.id);
114
+ if (!user) {
115
+ return res.status(404).json({ error: 'User not found' });
116
+ }
117
+ res.json(user);
118
+ } catch (error) {
119
+ console.error('[UsersPlugin] Get user error:', error);
120
+ res.status(500).json({ error: 'Failed to get user' });
121
+ }
122
+ },
123
+ });
124
+
125
+ // Create user
126
+ registry.addRoute({
127
+ method: 'post',
128
+ path: apiPrefix,
129
+ pluginId: 'users',
130
+ handler: async (req: Request, res: Response) => {
131
+ try {
132
+ const input: CreateUserInput = {
133
+ email: req.body.email,
134
+ name: req.body.name,
135
+ external_id: req.body.external_id,
136
+ provider: req.body.provider,
137
+ picture: req.body.picture,
138
+ metadata: req.body.metadata,
139
+ };
140
+
141
+ if (!input.email) {
142
+ return res.status(400).json({ error: 'Email is required' });
143
+ }
144
+
145
+ // Check if user already exists
146
+ const existing = await config.store.getByEmail(input.email);
147
+ if (existing) {
148
+ return res.status(409).json({ error: 'User with this email already exists' });
149
+ }
150
+
151
+ const user = await config.store.create(input);
152
+ res.status(201).json(user);
153
+ } catch (error) {
154
+ console.error('[UsersPlugin] Create user error:', error);
155
+ res.status(500).json({ error: 'Failed to create user' });
156
+ }
157
+ },
158
+ });
159
+
160
+ // Update user
161
+ registry.addRoute({
162
+ method: 'put',
163
+ path: `${apiPrefix}/:id`,
164
+ pluginId: 'users',
165
+ handler: async (req: Request, res: Response) => {
166
+ try {
167
+ const input: UpdateUserInput = {
168
+ name: req.body.name,
169
+ picture: req.body.picture,
170
+ metadata: req.body.metadata,
171
+ };
172
+
173
+ const user = await config.store.update(req.params.id, input);
174
+ if (!user) {
175
+ return res.status(404).json({ error: 'User not found' });
176
+ }
177
+ res.json(user);
178
+ } catch (error) {
179
+ console.error('[UsersPlugin] Update user error:', error);
180
+ res.status(500).json({ error: 'Failed to update user' });
181
+ }
182
+ },
183
+ });
184
+
185
+ // Delete user
186
+ registry.addRoute({
187
+ method: 'delete',
188
+ path: `${apiPrefix}/:id`,
189
+ pluginId: 'users',
190
+ handler: async (req: Request, res: Response) => {
191
+ try {
192
+ const deleted = await config.store.delete(req.params.id);
193
+ if (!deleted) {
194
+ return res.status(404).json({ error: 'User not found' });
195
+ }
196
+ res.status(204).send();
197
+ } catch (error) {
198
+ console.error('[UsersPlugin] Delete user error:', error);
199
+ res.status(500).json({ error: 'Failed to delete user' });
200
+ }
201
+ },
202
+ });
203
+
204
+ // GET /users/:id/info - Get comprehensive user info
205
+ registry.addRoute({
206
+ method: 'get',
207
+ path: `${apiPrefix}/:id/info`,
208
+ pluginId: 'users',
209
+ handler: async (req: Request, res: Response) => {
210
+ try {
211
+ const user = await config.store.getById(req.params.id);
212
+ if (!user) {
213
+ return res.status(404).json({ error: 'User not found' });
214
+ }
215
+
216
+ const info = await buildUserInfo(user, registry);
217
+ res.json(info);
218
+ } catch (error) {
219
+ console.error('[UsersPlugin] Get user info error:', error);
220
+ res.status(500).json({ error: 'Failed to get user info' });
221
+ }
222
+ },
223
+ });
224
+
225
+ // POST /users/sync - Find or create user, return comprehensive info
226
+ registry.addRoute({
227
+ method: 'post',
228
+ path: `${apiPrefix}/sync`,
229
+ pluginId: 'users',
230
+ handler: async (req: Request, res: Response) => {
231
+ try {
232
+ const input = req.body as UserSyncInput;
233
+
234
+ // Normalize and validate email
235
+ const email = input.email?.trim().toLowerCase();
236
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
237
+ if (!email || !emailRegex.test(email)) {
238
+ return res.status(400).json({ error: 'Valid email is required' });
239
+ }
240
+
241
+ // Validate required fields
242
+ if (!input.external_id) {
243
+ return res.status(400).json({ error: 'external_id is required' });
244
+ }
245
+ if (!input.provider) {
246
+ return res.status(400).json({ error: 'provider is required' });
247
+ }
248
+
249
+ // Find or create user
250
+ const user = await findOrCreateUser({
251
+ email: email,
252
+ external_id: input.external_id,
253
+ provider: input.provider,
254
+ name: input.name?.trim(),
255
+ picture: input.picture?.trim(),
256
+ });
257
+
258
+ const info = await buildUserInfo(user, registry);
259
+ res.json(info);
260
+ } catch (error) {
261
+ console.error('[UsersPlugin] User sync error:', error);
262
+ res.status(500).json({ error: 'Failed to sync user' });
263
+ }
264
+ },
265
+ });
266
+ }
267
+
268
+ log('Users plugin started');
269
+ },
270
+
271
+ async onStop(): Promise<void> {
272
+ log('Stopping users plugin');
273
+ await config.store.shutdown();
274
+ currentStore = null;
275
+ currentRegistry = null;
276
+ log('Users plugin stopped');
277
+ },
278
+ };
279
+ }
280
+
281
+ // ========================================
282
+ // Helper Functions
283
+ // ========================================
284
+
285
+ /**
286
+ * Get the current user store instance
287
+ */
288
+ export function getUserStore(): UserStore | null {
289
+ return currentStore;
290
+ }
291
+
292
+ /**
293
+ * Get a user by ID
294
+ */
295
+ export async function getUserById(id: string): Promise<User | null> {
296
+ if (!currentStore) {
297
+ throw new Error('Users plugin not initialized');
298
+ }
299
+ return currentStore.getById(id);
300
+ }
301
+
302
+ /**
303
+ * Get a user by email
304
+ */
305
+ export async function getUserByEmail(email: string): Promise<User | null> {
306
+ if (!currentStore) {
307
+ throw new Error('Users plugin not initialized');
308
+ }
309
+ return currentStore.getByEmail(email);
310
+ }
311
+
312
+ /**
313
+ * Find or create a user from auth provider data
314
+ */
315
+ export async function findOrCreateUser(data: {
316
+ email: string;
317
+ name?: string;
318
+ external_id: string;
319
+ provider: string;
320
+ picture?: string;
321
+ }): Promise<User> {
322
+ if (!currentStore) {
323
+ throw new Error('Users plugin not initialized');
324
+ }
325
+
326
+ // Try to find by external ID first
327
+ let user = await currentStore.getByExternalId(data.external_id, data.provider);
328
+ if (user) {
329
+ await currentStore.updateLastLogin(user.id);
330
+ return user;
331
+ }
332
+
333
+ // Try to find by email
334
+ user = await currentStore.getByEmail(data.email);
335
+ if (user) {
336
+ // Note: external_id cannot be updated after user creation for security reasons
337
+ await currentStore.updateLastLogin(user.id);
338
+ return user;
339
+ }
340
+
341
+ // Create new user
342
+ user = await currentStore.create({
343
+ email: data.email,
344
+ name: data.name,
345
+ external_id: data.external_id,
346
+ provider: data.provider,
347
+ picture: data.picture,
348
+ });
349
+
350
+ return user;
351
+ }
352
+
353
+ /**
354
+ * Build comprehensive user info by aggregating data from all loaded plugins.
355
+ * This helper fetches data from entitlements, preferences, and bans plugins
356
+ * in parallel (if they are loaded) and returns a unified UserInfo object.
357
+ */
358
+ export async function buildUserInfo(user: User, registry: PluginRegistry): Promise<UserInfo> {
359
+ const info: UserInfo = { user };
360
+
361
+ // Fetch data from other plugins in parallel
362
+ const promises: Promise<void>[] = [];
363
+
364
+ if (registry.hasPlugin('entitlements')) {
365
+ promises.push(
366
+ getEntitlements(user.email)
367
+ .then((result) => {
368
+ info.entitlements = result.entitlements;
369
+ })
370
+ .catch((error) => {
371
+ console.error('[UsersPlugin] Failed to fetch entitlements:', error);
372
+ // Continue without entitlements - don't fail the whole request
373
+ })
374
+ );
375
+ }
376
+
377
+ if (registry.hasPlugin('preferences')) {
378
+ promises.push(
379
+ getPreferences(user.id)
380
+ .then((prefs) => {
381
+ info.preferences = prefs;
382
+ })
383
+ .catch((error) => {
384
+ console.error('[UsersPlugin] Failed to fetch preferences:', error);
385
+ // Continue without preferences - don't fail the whole request
386
+ })
387
+ );
388
+ }
389
+
390
+ if (registry.hasPlugin('bans')) {
391
+ promises.push(
392
+ getActiveBan(user.id)
393
+ .then((ban) => {
394
+ // Transform Ban to UserInfo.ban shape (only include relevant fields)
395
+ info.ban = ban
396
+ ? {
397
+ id: ban.id,
398
+ reason: ban.reason,
399
+ banned_at: ban.banned_at,
400
+ expires_at: ban.expires_at,
401
+ }
402
+ : null;
403
+ })
404
+ .catch((error) => {
405
+ console.error('[UsersPlugin] Failed to fetch ban status:', error);
406
+ // Continue without ban info - don't fail the whole request
407
+ })
408
+ );
409
+ }
410
+
411
+ // Future: roles plugin
412
+ // if (registry.hasPlugin('roles')) {
413
+ // promises.push(getUserRoles(user.id).then(roles => info.roles = roles));
414
+ // }
415
+
416
+ await Promise.all(promises);
417
+ return info;
418
+ }
package/ui/src/App.tsx CHANGED
@@ -1,21 +1,66 @@
1
+ import { useState, useEffect } from 'react';
1
2
  import { BrowserRouter, Routes, Route } from 'react-router-dom';
2
3
  import { QwickApp, ProductLogo, Text } from '@qwickapps/react-framework';
3
4
  import { Link, Box } from '@mui/material';
4
5
  import { defaultConfig } from './config/AppConfig';
6
+ import { DashboardWidgetProvider } from './dashboard';
5
7
  import { DashboardPage } from './pages/DashboardPage';
6
- import { HealthPage } from './pages/HealthPage';
7
8
  import { LogsPage } from './pages/LogsPage';
8
- import { ConfigPage } from './pages/ConfigPage';
9
- import { DiagnosticsPage } from './pages/DiagnosticsPage';
9
+ import { SystemPage } from './pages/SystemPage';
10
+ import { PluginsPage } from './pages/PluginsPage';
11
+ import { UsersPage } from './pages/UsersPage';
12
+ import { EntitlementsPage } from './pages/EntitlementsPage';
13
+ import { PluginPage } from './pages/PluginPage';
10
14
  import { NotFoundPage } from './pages/NotFoundPage';
15
+ import { api, type MenuContribution } from './api/controlPanelApi';
16
+
17
+ // Navigation item type
18
+ interface NavigationItem {
19
+ id: string;
20
+ label: string;
21
+ route: string;
22
+ icon: string;
23
+ }
24
+
25
+ // Core navigation items always shown
26
+ const coreNavigationItems: NavigationItem[] = [
27
+ { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
28
+ { id: 'plugins', label: 'Plugins', route: '/plugins', icon: 'extension' },
29
+ { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
30
+ { id: 'system', label: 'System', route: '/system', icon: 'settings' },
31
+ ];
32
+
33
+ // Built-in optional navigation items - shown if corresponding plugin is registered
34
+ const builtInPluginNavItems: Record<string, NavigationItem> = {
35
+ users: { id: 'users', label: 'Users', route: '/users', icon: 'people' },
36
+ };
37
+
38
+ // Routes that have dedicated page components
39
+ const dedicatedRoutes = new Set(['/', '/plugins', '/logs', '/system', '/users', '/entitlements']);
11
40
 
12
41
  // Package version - injected at build time or fallback
13
42
  const SERVER_VERSION = '1.0.0';
14
43
 
15
- // Default logo - consumers can customize
16
- const logo = <ProductLogo name="Control Panel" />;
44
+ // Declare global type for injected base path
45
+ declare global {
46
+ interface Window {
47
+ __APP_BASE_PATH__?: string;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Get the base path for the application.
53
+ *
54
+ * The server injects window.__APP_BASE_PATH__ at runtime based on
55
+ * either the configured mountPath or X-Forwarded-Prefix header.
56
+ * This is a simple, robust approach - no complex detection needed.
57
+ */
58
+ const basePath = window.__APP_BASE_PATH__ ?? '';
59
+
60
+ // Configure API with the detected base path
61
+ api.setBaseUrl(basePath);
17
62
 
18
- // Default footer content with QwickApps Server branding
63
+ // Footer content with QwickApps Server branding
19
64
  const footerContent = (
20
65
  <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, py: 2 }}>
21
66
  <Text variant="caption" customColor="var(--theme-text-secondary)">
@@ -34,32 +79,144 @@ const footerContent = (
34
79
  );
35
80
 
36
81
  export function App() {
82
+ const [navigationItems, setNavigationItems] = useState<NavigationItem[]>(coreNavigationItems);
83
+ const [registeredPlugins, setRegisteredPlugins] = useState<Set<string>>(new Set());
84
+ const [pluginMenuItems, setPluginMenuItems] = useState<MenuContribution[]>([]);
85
+ const [logoName, setLogoName] = useState<string>('Control Panel');
86
+ const [logoIconUrl, setLogoIconUrl] = useState<string | undefined>();
87
+ const [isLoading, setIsLoading] = useState(true);
88
+
89
+ // Fetch product info and UI contributions on mount
90
+ useEffect(() => {
91
+ const loadData = async () => {
92
+ try {
93
+ // Fetch both in parallel
94
+ const [infoResult, contributionsResult] = await Promise.allSettled([
95
+ api.getInfo(),
96
+ api.getUiContributions(),
97
+ ]);
98
+
99
+ // Update logo name and icon URL if info fetch succeeded
100
+ if (infoResult.status === 'fulfilled') {
101
+ setLogoName(infoResult.value.logoName);
102
+ setLogoIconUrl(infoResult.value.logoIconUrl);
103
+ } else {
104
+ console.warn('Failed to fetch product info:', infoResult.reason);
105
+ }
106
+
107
+ // Update navigation from UI contributions
108
+ if (contributionsResult.status === 'fulfilled') {
109
+ const { plugins, menuItems } = contributionsResult.value;
110
+ const pluginIds = new Set(plugins.map((p) => p.id));
111
+ setRegisteredPlugins(pluginIds);
112
+ setPluginMenuItems(menuItems);
113
+
114
+ // Build navigation: core items + built-in plugin items + dynamic menu items
115
+ const dynamicNav = [...coreNavigationItems];
116
+
117
+ // Add built-in plugin nav items (like Users)
118
+ for (const [pluginId, navItem] of Object.entries(builtInPluginNavItems)) {
119
+ if (pluginIds.has(pluginId)) {
120
+ dynamicNav.push(navItem);
121
+ }
122
+ }
123
+
124
+ // Add plugin-contributed menu items (sorted by order)
125
+ const sortedMenuItems = [...menuItems].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
126
+ for (const menuItem of sortedMenuItems) {
127
+ // Skip if we already have a nav item for this route
128
+ if (dynamicNav.some(nav => nav.route === menuItem.route)) {
129
+ continue;
130
+ }
131
+ dynamicNav.push({
132
+ id: menuItem.id,
133
+ label: menuItem.label,
134
+ route: menuItem.route,
135
+ icon: menuItem.icon || 'extension',
136
+ });
137
+ }
138
+
139
+ setNavigationItems(dynamicNav);
140
+ } else {
141
+ console.warn('Failed to fetch UI contributions:', contributionsResult.reason);
142
+ }
143
+ } finally {
144
+ setIsLoading(false);
145
+ }
146
+ };
147
+
148
+ loadData();
149
+ }, []);
150
+
151
+ // Dynamic logo based on logoName and logoIconUrl from API
152
+ // When logoIconUrl is provided, use it as a custom icon instead of the default QwickIcon
153
+ const logoIcon = logoIconUrl ? (
154
+ <img
155
+ src={logoIconUrl}
156
+ alt={logoName}
157
+ style={{ width: 32, height: 32, objectFit: 'contain' }}
158
+ />
159
+ ) : undefined;
160
+ const logo = <ProductLogo icon={logoIcon} name={logoName} />;
161
+
162
+ // Show loading state until plugins are loaded
163
+ // This ensures QwickApp receives the correct navigation on first render
164
+ if (isLoading) {
165
+ return (
166
+ <Box
167
+ sx={{
168
+ display: 'flex',
169
+ justifyContent: 'center',
170
+ alignItems: 'center',
171
+ minHeight: '100vh',
172
+ bgcolor: 'var(--theme-background, #1a1a2e)',
173
+ }}
174
+ />
175
+ );
176
+ }
177
+
37
178
  return (
38
- <BrowserRouter>
39
- <QwickApp
40
- config={defaultConfig}
41
- logo={logo}
42
- footerContent={footerContent}
43
- enableScaffolding={true}
44
- navigationItems={[
45
- { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
46
- { id: 'health', label: 'Health', route: '/health', icon: 'favorite' },
47
- { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
48
- { id: 'config', label: 'Config', route: '/config', icon: 'settings' },
49
- { id: 'diagnostics', label: 'Diagnostics', route: '/diagnostics', icon: 'bug_report' },
50
- ]}
51
- showThemeSwitcher={true}
52
- showPaletteSwitcher={true}
53
- >
54
- <Routes>
55
- <Route path="/" element={<DashboardPage />} />
56
- <Route path="/health" element={<HealthPage />} />
57
- <Route path="/logs" element={<LogsPage />} />
58
- <Route path="/config" element={<ConfigPage />} />
59
- <Route path="/diagnostics" element={<DiagnosticsPage />} />
60
- <Route path="*" element={<NotFoundPage />} />
61
- </Routes>
62
- </QwickApp>
179
+ <BrowserRouter basename={basePath || undefined}>
180
+ <DashboardWidgetProvider>
181
+ <QwickApp
182
+ config={defaultConfig}
183
+ logo={logo}
184
+ footerContent={footerContent}
185
+ enableScaffolding={true}
186
+ navigationItems={navigationItems}
187
+ showThemeSwitcher={true}
188
+ showPaletteSwitcher={true}
189
+ >
190
+ <Routes>
191
+ {/* Core routes */}
192
+ <Route path="/" element={<DashboardPage />} />
193
+ <Route path="/plugins" element={<PluginsPage />} />
194
+ <Route path="/logs" element={<LogsPage />} />
195
+ <Route path="/system" element={<SystemPage />} />
196
+
197
+ {/* Built-in plugin routes */}
198
+ {registeredPlugins.has('users') && (
199
+ <Route path="/users" element={<UsersPage />} />
200
+ )}
201
+ {registeredPlugins.has('entitlements') && (
202
+ <Route path="/entitlements" element={<EntitlementsPage />} />
203
+ )}
204
+
205
+ {/* Dynamic plugin routes - render generic PluginPage for non-dedicated routes */}
206
+ {pluginMenuItems
207
+ .filter(item => !dedicatedRoutes.has(item.route))
208
+ .map(item => (
209
+ <Route
210
+ key={item.id}
211
+ path={item.route}
212
+ element={<PluginPage pluginId={item.pluginId} title={item.label} route={item.route} />}
213
+ />
214
+ ))}
215
+
216
+ <Route path="*" element={<NotFoundPage />} />
217
+ </Routes>
218
+ </QwickApp>
219
+ </DashboardWidgetProvider>
63
220
  </BrowserRouter>
64
221
  );
65
222
  }