@qwickapps/server 1.3.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 (132) hide show
  1. package/README.md +154 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +30 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  14. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  15. package/dist/plugins/auth/adapters/index.js +1 -0
  16. package/dist/plugins/auth/adapters/index.js.map +1 -1
  17. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  18. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  19. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  20. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  22. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  23. package/dist/plugins/auth/env-config.d.ts +88 -0
  24. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  25. package/dist/plugins/auth/env-config.js +489 -0
  26. package/dist/plugins/auth/env-config.js.map +1 -0
  27. package/dist/plugins/auth/index.d.ts +3 -1
  28. package/dist/plugins/auth/index.d.ts.map +1 -1
  29. package/dist/plugins/auth/index.js +3 -0
  30. package/dist/plugins/auth/index.js.map +1 -1
  31. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  32. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  33. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  34. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  35. package/dist/plugins/auth/types.d.ts +70 -0
  36. package/dist/plugins/auth/types.d.ts.map +1 -1
  37. package/dist/plugins/auth/types.js.map +1 -1
  38. package/dist/plugins/cache-plugin.test.js +3 -0
  39. package/dist/plugins/cache-plugin.test.js.map +1 -1
  40. package/dist/plugins/index.d.ts +4 -2
  41. package/dist/plugins/index.d.ts.map +1 -1
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/index.js.map +1 -1
  44. package/dist/plugins/postgres-plugin.test.js +3 -0
  45. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  46. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  47. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  48. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  49. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  50. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  51. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  52. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  53. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  54. package/dist/plugins/preferences/index.d.ts +12 -0
  55. package/dist/plugins/preferences/index.d.ts.map +1 -0
  56. package/dist/plugins/preferences/index.js +13 -0
  57. package/dist/plugins/preferences/index.js.map +1 -0
  58. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  59. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  60. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  61. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  62. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  63. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  64. package/dist/plugins/preferences/stores/index.js +9 -0
  65. package/dist/plugins/preferences/stores/index.js.map +1 -0
  66. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  67. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  68. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  69. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  70. package/dist/plugins/preferences/types.d.ts +91 -0
  71. package/dist/plugins/preferences/types.d.ts.map +1 -0
  72. package/dist/plugins/preferences/types.js +10 -0
  73. package/dist/plugins/preferences/types.js.map +1 -0
  74. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  75. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  76. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  77. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  78. package/dist/plugins/users/index.d.ts +2 -2
  79. package/dist/plugins/users/index.d.ts.map +1 -1
  80. package/dist/plugins/users/index.js +1 -1
  81. package/dist/plugins/users/index.js.map +1 -1
  82. package/dist/plugins/users/types.d.ts +36 -0
  83. package/dist/plugins/users/types.d.ts.map +1 -1
  84. package/dist/plugins/users/users-plugin.d.ts +8 -2
  85. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  86. package/dist/plugins/users/users-plugin.js +122 -5
  87. package/dist/plugins/users/users-plugin.js.map +1 -1
  88. package/dist-ui/assets/{index-Bsp2ntcw.js → index-BY8OxNgO.js} +112 -112
  89. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  90. package/dist-ui/index.html +1 -1
  91. package/dist-ui-lib/api/controlPanelApi.d.ts +53 -7
  92. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  93. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  94. package/dist-ui-lib/index.js +2382 -3651
  95. package/dist-ui-lib/index.js.map +1 -1
  96. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  97. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  98. package/package.json +7 -2
  99. package/src/core/control-panel.ts +33 -2
  100. package/src/core/plugin-registry.ts +63 -0
  101. package/src/index.ts +7 -0
  102. package/src/plugins/auth/adapters/index.ts +1 -0
  103. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  104. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  105. package/src/plugins/auth/env-config.ts +572 -0
  106. package/src/plugins/auth/index.ts +9 -0
  107. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  108. package/src/plugins/auth/types.ts +80 -0
  109. package/src/plugins/cache-plugin.test.ts +3 -0
  110. package/src/plugins/index.ts +26 -0
  111. package/src/plugins/postgres-plugin.test.ts +3 -0
  112. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  113. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  114. package/src/plugins/preferences/index.ts +30 -0
  115. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  116. package/src/plugins/preferences/stores/index.ts +9 -0
  117. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  118. package/src/plugins/preferences/types.ts +100 -0
  119. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  120. package/src/plugins/users/index.ts +3 -0
  121. package/src/plugins/users/types.ts +38 -0
  122. package/src/plugins/users/users-plugin.ts +142 -5
  123. package/ui/src/App.tsx +4 -1
  124. package/ui/src/api/controlPanelApi.ts +100 -1
  125. package/ui/src/components/ControlPanelApp.tsx +3 -0
  126. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  127. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  128. package/ui/src/dashboard/builtInWidgets.tsx +8 -2
  129. package/ui/src/pages/AuthPage.tsx +259 -0
  130. package/ui/src/pages/PluginsPage.tsx +394 -0
  131. package/ui/vite.lib.config.ts +5 -0
  132. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -207,3 +207,41 @@ export interface UsersPluginConfig {
207
207
  /** Enable debug logging */
208
208
  debug?: boolean;
209
209
  }
210
+
211
+ /**
212
+ * Comprehensive user information aggregated from multiple plugins.
213
+ * Used by /users/:id/info and /users/sync endpoints.
214
+ */
215
+ export interface UserInfo {
216
+ /** Core user data from users plugin */
217
+ user: User;
218
+ /** User's entitlements (if entitlements plugin loaded) */
219
+ entitlements?: string[];
220
+ /** User's preferences (if preferences plugin loaded) */
221
+ preferences?: Record<string, unknown>;
222
+ /** Active ban info (if bans plugin loaded, null if not banned) */
223
+ ban?: {
224
+ id: string;
225
+ reason: string;
226
+ banned_at: Date;
227
+ expires_at?: Date;
228
+ } | null;
229
+ /** User's roles (if roles plugin loaded - future) */
230
+ roles?: string[];
231
+ }
232
+
233
+ /**
234
+ * Input for POST /users/sync endpoint
235
+ */
236
+ export interface UserSyncInput {
237
+ /** User's email address */
238
+ email: string;
239
+ /** External provider ID (e.g., Auth0 user_id) */
240
+ external_id: string;
241
+ /** Provider name (e.g., 'auth0', 'google') */
242
+ provider: string;
243
+ /** User's display name (optional) */
244
+ name?: string;
245
+ /** Profile picture URL (optional) */
246
+ picture?: string;
247
+ }
@@ -18,10 +18,18 @@ import type {
18
18
  CreateUserInput,
19
19
  UpdateUserInput,
20
20
  UserSearchParams,
21
+ UserInfo,
22
+ UserSyncInput,
21
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';
22
29
 
23
30
  // Store instance for helper access
24
31
  let currentStore: UserStore | null = null;
32
+ let currentRegistry: PluginRegistry | null = null;
25
33
 
26
34
  /**
27
35
  * Create the Users plugin
@@ -49,8 +57,9 @@ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
49
57
  await config.store.initialize();
50
58
  log('Users plugin migrations complete');
51
59
 
52
- // Store reference for helper access
60
+ // Store references for helper access
53
61
  currentStore = config.store;
62
+ currentRegistry = registry;
54
63
 
55
64
  // Register health check
56
65
  registry.registerHealthCheck({
@@ -191,6 +200,69 @@ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
191
200
  }
192
201
  },
193
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
+ });
194
266
  }
195
267
 
196
268
  log('Users plugin started');
@@ -200,6 +272,7 @@ export function createUsersPlugin(config: UsersPluginConfig): Plugin {
200
272
  log('Stopping users plugin');
201
273
  await config.store.shutdown();
202
274
  currentStore = null;
275
+ currentRegistry = null;
203
276
  log('Users plugin stopped');
204
277
  },
205
278
  };
@@ -260,10 +333,7 @@ export async function findOrCreateUser(data: {
260
333
  // Try to find by email
261
334
  user = await currentStore.getByEmail(data.email);
262
335
  if (user) {
263
- // Update with external ID if not set
264
- if (!user.external_id) {
265
- await currentStore.update(user.id, {});
266
- }
336
+ // Note: external_id cannot be updated after user creation for security reasons
267
337
  await currentStore.updateLastLogin(user.id);
268
338
  return user;
269
339
  }
@@ -279,3 +349,70 @@ export async function findOrCreateUser(data: {
279
349
 
280
350
  return user;
281
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
@@ -7,6 +7,7 @@ import { DashboardWidgetProvider } from './dashboard';
7
7
  import { DashboardPage } from './pages/DashboardPage';
8
8
  import { LogsPage } from './pages/LogsPage';
9
9
  import { SystemPage } from './pages/SystemPage';
10
+ import { PluginsPage } from './pages/PluginsPage';
10
11
  import { UsersPage } from './pages/UsersPage';
11
12
  import { EntitlementsPage } from './pages/EntitlementsPage';
12
13
  import { PluginPage } from './pages/PluginPage';
@@ -24,6 +25,7 @@ interface NavigationItem {
24
25
  // Core navigation items always shown
25
26
  const coreNavigationItems: NavigationItem[] = [
26
27
  { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
28
+ { id: 'plugins', label: 'Plugins', route: '/plugins', icon: 'extension' },
27
29
  { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
28
30
  { id: 'system', label: 'System', route: '/system', icon: 'settings' },
29
31
  ];
@@ -34,7 +36,7 @@ const builtInPluginNavItems: Record<string, NavigationItem> = {
34
36
  };
35
37
 
36
38
  // Routes that have dedicated page components
37
- const dedicatedRoutes = new Set(['/', '/logs', '/system', '/users', '/entitlements']);
39
+ const dedicatedRoutes = new Set(['/', '/plugins', '/logs', '/system', '/users', '/entitlements']);
38
40
 
39
41
  // Package version - injected at build time or fallback
40
42
  const SERVER_VERSION = '1.0.0';
@@ -188,6 +190,7 @@ export function App() {
188
190
  <Routes>
189
191
  {/* Core routes */}
190
192
  <Route path="/" element={<DashboardPage />} />
193
+ <Route path="/plugins" element={<PluginsPage />} />
191
194
  <Route path="/logs" element={<LogsPage />} />
192
195
  <Route path="/system" element={<SystemPage />} />
193
196
 
@@ -203,6 +203,67 @@ export interface UiContributionsResponse {
203
203
  plugins: Array<{ id: string; name: string; version?: string; status: string }>;
204
204
  }
205
205
 
206
+ // ==================
207
+ // Plugin Detail Types
208
+ // ==================
209
+
210
+ export interface ConfigContribution {
211
+ id: string;
212
+ component: string;
213
+ title?: string;
214
+ pluginId: string;
215
+ }
216
+
217
+ export interface PluginContributions {
218
+ routes: Array<{ method: string; path: string }>;
219
+ menuItems: MenuContribution[];
220
+ pages: PageContribution[];
221
+ widgets: WidgetContribution[];
222
+ config?: ConfigContribution;
223
+ }
224
+
225
+ export interface PluginInfo {
226
+ id: string;
227
+ name: string;
228
+ version?: string;
229
+ status: 'starting' | 'active' | 'stopped' | 'error';
230
+ error?: string;
231
+ contributionCounts: {
232
+ routes: number;
233
+ menuItems: number;
234
+ pages: number;
235
+ widgets: number;
236
+ hasConfig: boolean;
237
+ };
238
+ }
239
+
240
+ export interface PluginsResponse {
241
+ plugins: PluginInfo[];
242
+ }
243
+
244
+ export interface PluginDetailResponse {
245
+ id: string;
246
+ name: string;
247
+ version?: string;
248
+ status: 'starting' | 'active' | 'stopped' | 'error';
249
+ error?: string;
250
+ contributions: PluginContributions;
251
+ }
252
+
253
+ // ==================
254
+ // Auth Config Types
255
+ // ==================
256
+
257
+ export type AuthPluginState = 'disabled' | 'enabled' | 'error';
258
+
259
+ export interface AuthConfigStatus {
260
+ state: AuthPluginState;
261
+ adapter: string | null;
262
+ error?: string;
263
+ missingVars?: string[];
264
+ config?: Record<string, string>;
265
+ }
266
+
206
267
  class ControlPanelApi {
207
268
  private baseUrl: string;
208
269
 
@@ -477,7 +538,7 @@ class ControlPanelApi {
477
538
  // Plugins API
478
539
  // ==================
479
540
 
480
- async getPlugins(): Promise<{ plugins: Array<{ id: string; name: string; version?: string }> }> {
541
+ async getPlugins(): Promise<PluginsResponse> {
481
542
  const response = await fetch(`${this.baseUrl}/api/plugins`);
482
543
  if (!response.ok) {
483
544
  throw new Error(`Plugins request failed: ${response.statusText}`);
@@ -485,6 +546,17 @@ class ControlPanelApi {
485
546
  return response.json();
486
547
  }
487
548
 
549
+ async getPluginDetail(id: string): Promise<PluginDetailResponse> {
550
+ const response = await fetch(`${this.baseUrl}/api/plugins/${encodeURIComponent(id)}`);
551
+ if (!response.ok) {
552
+ if (response.status === 404) {
553
+ throw new Error(`Plugin not found: ${id}`);
554
+ }
555
+ throw new Error(`Plugin detail request failed: ${response.statusText}`);
556
+ }
557
+ return response.json();
558
+ }
559
+
488
560
  // ==================
489
561
  // UI Contributions API
490
562
  // ==================
@@ -496,6 +568,33 @@ class ControlPanelApi {
496
568
  }
497
569
  return response.json();
498
570
  }
571
+
572
+ // ==================
573
+ // Auth Config API
574
+ // ==================
575
+
576
+ async getAuthConfigStatus(): Promise<AuthConfigStatus> {
577
+ const response = await fetch(`${this.baseUrl}/api/auth/config/status`);
578
+ if (!response.ok) {
579
+ // Return disabled state if endpoint not available
580
+ if (response.status === 404) {
581
+ return { state: 'disabled', adapter: null };
582
+ }
583
+ throw new Error(`Auth config status request failed: ${response.statusText}`);
584
+ }
585
+ return response.json();
586
+ }
587
+
588
+ async getAuthConfig(): Promise<AuthConfigStatus> {
589
+ const response = await fetch(`${this.baseUrl}/api/auth/config`);
590
+ if (!response.ok) {
591
+ if (response.status === 404) {
592
+ return { state: 'disabled', adapter: null };
593
+ }
594
+ throw new Error(`Auth config request failed: ${response.statusText}`);
595
+ }
596
+ return response.json();
597
+ }
499
598
  }
500
599
 
501
600
  export const api = new ControlPanelApi();
@@ -36,6 +36,7 @@ import { defaultConfig } from '../config/AppConfig';
36
36
  import { DashboardPage } from '../pages/DashboardPage';
37
37
  import { LogsPage } from '../pages/LogsPage';
38
38
  import { SystemPage } from '../pages/SystemPage';
39
+ import { AuthPage } from '../pages/AuthPage';
39
40
  import { NotFoundPage } from '../pages/NotFoundPage';
40
41
 
41
42
  // Dashboard widget system
@@ -122,6 +123,7 @@ function getBaseNavigationItems(): MenuItem[] {
122
123
  return [
123
124
  { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
124
125
  { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
126
+ { id: 'auth', label: 'Auth', route: '/auth', icon: 'lock' },
125
127
  { id: 'system', label: 'System', route: '/system', icon: 'settings' },
126
128
  ];
127
129
  }
@@ -192,6 +194,7 @@ export function ControlPanelApp({
192
194
  <>
193
195
  {!hideBaseNavItems.includes('dashboard') && <Route path="/" element={<DashboardPage />} />}
194
196
  {!hideBaseNavItems.includes('logs') && <Route path="/logs" element={<LogsPage />} />}
197
+ {!hideBaseNavItems.includes('auth') && <Route path="/auth" element={<AuthPage />} />}
195
198
  {!hideBaseNavItems.includes('system') && <Route path="/system" element={<SystemPage />} />}
196
199
  </>
197
200
  )}
@@ -100,16 +100,19 @@ export function PluginWidgetRenderer({
100
100
 
101
101
  return (
102
102
  <>
103
- {visibleWidgets.map(widget => (
104
- <Box key={widget.id} sx={{ mt: 4 }}>
105
- {widget.title && (
106
- <Typography variant="h6" sx={{ mb: 2, color: 'var(--theme-text-primary)' }}>
107
- {widget.title}
108
- </Typography>
109
- )}
110
- {getComponent(widget.component)}
111
- </Box>
112
- ))}
103
+ {visibleWidgets.map(widget => {
104
+ const Component = getComponent(widget.component);
105
+ return (
106
+ <Box key={widget.id} sx={{ mt: 4 }}>
107
+ {widget.title && (
108
+ <Typography variant="h6" sx={{ mb: 2, color: 'var(--theme-text-primary)' }}>
109
+ {widget.title}
110
+ </Typography>
111
+ )}
112
+ {Component && <Component />}
113
+ </Box>
114
+ );
115
+ })}
113
116
  </>
114
117
  );
115
118
  }
@@ -7,25 +7,29 @@
7
7
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
8
  */
9
9
 
10
- import { ReactNode, createContext, useContext, useState, useCallback, useMemo } from 'react';
10
+ import React, { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react';
11
11
 
12
12
  /**
13
13
  * Widget component definition
14
+ *
15
+ * IMPORTANT: We store component functions (ComponentType), not JSX instances (ReactNode).
16
+ * This ensures cross-React-version compatibility when the library is used in apps
17
+ * with different React versions.
14
18
  */
15
19
  export interface WidgetComponent {
16
20
  /** Component name (must match server-side WidgetContribution.component) */
17
21
  name: string;
18
- /** The React component to render */
19
- component: ReactNode;
22
+ /** The React component function to render */
23
+ component: React.ComponentType;
20
24
  }
21
25
 
22
26
  interface WidgetComponentRegistryContextValue {
23
27
  /** Register a widget component */
24
- registerComponent: (name: string, component: ReactNode) => void;
28
+ registerComponent: (name: string, component: React.ComponentType) => void;
25
29
  /** Register multiple widget components */
26
30
  registerComponents: (components: WidgetComponent[]) => void;
27
31
  /** Get a component by name */
28
- getComponent: (name: string) => ReactNode | null;
32
+ getComponent: (name: string) => React.ComponentType | null;
29
33
  /** Check if a component is registered */
30
34
  hasComponent: (name: string) => boolean;
31
35
  /** Get all registered component names */
@@ -47,15 +51,15 @@ export function WidgetComponentRegistryProvider({
47
51
  initialComponents = [],
48
52
  children,
49
53
  }: WidgetComponentRegistryProviderProps) {
50
- const [components, setComponents] = useState<Map<string, ReactNode>>(() => {
51
- const map = new Map<string, ReactNode>();
54
+ const [components, setComponents] = useState<Map<string, React.ComponentType>>(() => {
55
+ const map = new Map<string, React.ComponentType>();
52
56
  for (const comp of initialComponents) {
53
57
  map.set(comp.name, comp.component);
54
58
  }
55
59
  return map;
56
60
  });
57
61
 
58
- const registerComponent = useCallback((name: string, component: ReactNode) => {
62
+ const registerComponent = useCallback((name: string, component: React.ComponentType) => {
59
63
  setComponents(prev => {
60
64
  const next = new Map(prev);
61
65
  next.set(name, component);
@@ -73,7 +77,7 @@ export function WidgetComponentRegistryProvider({
73
77
  });
74
78
  }, []);
75
79
 
76
- const getComponent = useCallback((name: string): ReactNode | null => {
80
+ const getComponent = useCallback((name: string): React.ComponentType | null => {
77
81
  return components.get(name) ?? null;
78
82
  }, [components]);
79
83
 
@@ -4,6 +4,9 @@
4
4
  * Maps built-in widget component names to their React components.
5
5
  * These are the widgets that qwickapps-server provides out of the box.
6
6
  *
7
+ * IMPORTANT: We export component functions, not JSX instances.
8
+ * This ensures cross-React-version compatibility.
9
+ *
7
10
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
11
  */
9
12
 
@@ -19,11 +22,14 @@ export const builtInWidgetComponents: Record<string, React.ComponentType> = {
19
22
  };
20
23
 
21
24
  /**
22
- * Get built-in widget components as WidgetComponent array with JSX elements.
25
+ * Get built-in widget components as WidgetComponent array.
23
26
  * Use this when registering with WidgetComponentRegistryProvider.
27
+ *
28
+ * Returns component functions (not JSX instances) to ensure compatibility
29
+ * across different React versions.
24
30
  */
25
31
  export function getBuiltInWidgetComponents(): WidgetComponent[] {
26
32
  return [
27
- { name: 'ServiceHealthWidget', component: <ServiceHealthWidget /> },
33
+ { name: 'ServiceHealthWidget', component: ServiceHealthWidget },
28
34
  ];
29
35
  }