@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.
- package/README.md +154 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +30 -2
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +36 -0
- package/dist/core/plugin-registry.d.ts.map +1 -1
- package/dist/core/plugin-registry.js +26 -0
- package/dist/core/plugin-registry.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/index.d.ts +1 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/index.js +1 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +88 -0
- package/dist/plugins/auth/env-config.d.ts.map +1 -0
- package/dist/plugins/auth/env-config.js +489 -0
- package/dist/plugins/auth/env-config.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +3 -1
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +3 -0
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
- package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
- package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
- package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +70 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/auth/types.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +3 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +3 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
- package/dist/plugins/preferences/index.d.ts +12 -0
- package/dist/plugins/preferences/index.d.ts.map +1 -0
- package/dist/plugins/preferences/index.js +13 -0
- package/dist/plugins/preferences/index.js.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.js +226 -0
- package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
- package/dist/plugins/preferences/stores/index.d.ts +9 -0
- package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/index.js +9 -0
- package/dist/plugins/preferences/stores/index.js.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.js +181 -0
- package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
- package/dist/plugins/preferences/types.d.ts +91 -0
- package/dist/plugins/preferences/types.d.ts.map +1 -0
- package/dist/plugins/preferences/types.js +10 -0
- package/dist/plugins/preferences/types.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
- package/dist/plugins/users/index.d.ts +2 -2
- package/dist/plugins/users/index.d.ts.map +1 -1
- package/dist/plugins/users/index.js +1 -1
- package/dist/plugins/users/index.js.map +1 -1
- package/dist/plugins/users/types.d.ts +36 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +8 -2
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +122 -5
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/{index-Bsp2ntcw.js → index-BY8OxNgO.js} +112 -112
- package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +53 -7
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
- package/dist-ui-lib/index.js +2382 -3651
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
- package/package.json +7 -2
- package/src/core/control-panel.ts +33 -2
- package/src/core/plugin-registry.ts +63 -0
- package/src/index.ts +7 -0
- package/src/plugins/auth/adapters/index.ts +1 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
- package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
- package/src/plugins/auth/env-config.ts +572 -0
- package/src/plugins/auth/index.ts +9 -0
- package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
- package/src/plugins/auth/types.ts +80 -0
- package/src/plugins/cache-plugin.test.ts +3 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/postgres-plugin.test.ts +3 -0
- package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
- package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
- package/src/plugins/preferences/index.ts +30 -0
- package/src/plugins/preferences/preferences-plugin.ts +270 -0
- package/src/plugins/preferences/stores/index.ts +9 -0
- package/src/plugins/preferences/stores/postgres-store.ts +252 -0
- package/src/plugins/preferences/types.ts +100 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
- package/src/plugins/users/index.ts +3 -0
- package/src/plugins/users/types.ts +38 -0
- package/src/plugins/users/users-plugin.ts +142 -5
- package/ui/src/App.tsx +4 -1
- package/ui/src/api/controlPanelApi.ts +100 -1
- package/ui/src/components/ControlPanelApp.tsx +3 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
- package/ui/src/dashboard/builtInWidgets.tsx +8 -2
- package/ui/src/pages/AuthPage.tsx +259 -0
- package/ui/src/pages/PluginsPage.tsx +394 -0
- package/ui/vite.lib.config.ts +5 -0
- 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
|
|
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
|
-
//
|
|
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<
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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) =>
|
|
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,
|
|
51
|
-
const map = new Map<string,
|
|
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:
|
|
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):
|
|
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
|
|
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:
|
|
33
|
+
{ name: 'ServiceHealthWidget', component: ServiceHealthWidget },
|
|
28
34
|
];
|
|
29
35
|
}
|