@qwickapps/server 1.1.9 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -0
- package/dist/core/control-panel.d.ts +7 -2
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +99 -60
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +159 -79
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +683 -315
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +271 -0
- package/dist/core/plugin-registry.d.ts.map +1 -0
- package/dist/core/plugin-registry.js +326 -0
- package/dist/core/plugin-registry.js.map +1 -0
- package/dist/core/types.d.ts +16 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
- package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +9 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/index.js +9 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts +40 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +255 -0
- package/dist/plugins/auth/auth-plugin.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.js +147 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +12 -0
- package/dist/plugins/auth/index.d.ts.map +1 -0
- package/dist/plugins/auth/index.js +13 -0
- package/dist/plugins/auth/index.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +148 -0
- package/dist/plugins/auth/types.d.ts.map +1 -0
- package/dist/plugins/auth/types.js +14 -0
- package/dist/plugins/auth/types.js.map +1 -0
- package/dist/plugins/bans/bans-plugin.d.ts +59 -0
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
- package/dist/plugins/bans/bans-plugin.js +428 -0
- package/dist/plugins/bans/bans-plugin.js.map +1 -0
- package/dist/plugins/bans/index.d.ts +9 -0
- package/dist/plugins/bans/index.d.ts.map +1 -0
- package/dist/plugins/bans/index.js +10 -0
- package/dist/plugins/bans/index.js.map +1 -0
- package/dist/plugins/bans/stores/index.d.ts +7 -0
- package/dist/plugins/bans/stores/index.d.ts.map +1 -0
- package/dist/plugins/bans/stores/index.js +7 -0
- package/dist/plugins/bans/stores/index.js.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.js +132 -0
- package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
- package/dist/plugins/bans/types.d.ts +128 -0
- package/dist/plugins/bans/types.d.ts.map +1 -0
- package/dist/plugins/bans/types.js +11 -0
- package/dist/plugins/bans/types.js.map +1 -0
- package/dist/plugins/cache-plugin.d.ts +14 -3
- package/dist/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/plugins/cache-plugin.js +27 -7
- package/dist/plugins/cache-plugin.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +96 -32
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +3 -2
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +17 -10
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.d.ts +2 -2
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +17 -10
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
- package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
- package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
- package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
- package/dist/plugins/entitlements/index.d.ts +12 -0
- package/dist/plugins/entitlements/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/index.js +16 -0
- package/dist/plugins/entitlements/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/index.d.ts +9 -0
- package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/index.js +9 -0
- package/dist/plugins/entitlements/sources/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
- package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
- package/dist/plugins/entitlements/types.d.ts +232 -0
- package/dist/plugins/entitlements/types.d.ts.map +1 -0
- package/dist/plugins/entitlements/types.js +11 -0
- package/dist/plugins/entitlements/types.js.map +1 -0
- package/dist/plugins/frontend-app-plugin.d.ts +9 -3
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +14 -9
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.d.ts +5 -2
- package/dist/plugins/health-plugin.d.ts.map +1 -1
- package/dist/plugins/health-plugin.js +20 -5
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +8 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts +3 -2
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +21 -12
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +3 -3
- package/dist/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/plugins/postgres-plugin.js +9 -7
- package/dist/plugins/postgres-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +47 -29
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +12 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +13 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/stores/index.d.ts +7 -0
- package/dist/plugins/users/stores/index.d.ts.map +1 -0
- package/dist/plugins/users/stores/index.js +7 -0
- package/dist/plugins/users/stores/index.js.map +1 -0
- package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/users/stores/postgres-store.js +157 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -0
- package/dist/plugins/users/types.d.ts +189 -0
- package/dist/plugins/users/types.d.ts.map +1 -0
- package/dist/plugins/users/types.js +12 -0
- package/dist/plugins/users/types.js.map +1 -0
- package/dist/plugins/users/users-plugin.d.ts +39 -0
- package/dist/plugins/users/users-plugin.d.ts.map +1 -0
- package/dist/plugins/users/users-plugin.js +242 -0
- package/dist/plugins/users/users-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
- package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
- package/dist-ui-lib/components/index.d.ts +18 -0
- package/dist-ui-lib/config/AppConfig.d.ts +7 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
- package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
- package/dist-ui-lib/dashboard/index.d.ts +13 -0
- package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
- package/dist-ui-lib/index.js +6441 -0
- package/dist-ui-lib/index.js.map +1 -0
- package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
- package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
- package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
- package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
- package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
- package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
- package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
- package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
- package/package.json +18 -6
- package/src/core/control-panel.ts +122 -68
- package/src/core/gateway.ts +870 -399
- package/src/core/index.ts +21 -2
- package/src/core/plugin-registry.ts +653 -0
- package/src/core/types.ts +31 -37
- package/src/index.ts +118 -19
- package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
- package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
- package/src/plugins/auth/adapters/index.ts +9 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
- package/src/plugins/auth/auth-plugin.test.ts +176 -0
- package/src/plugins/auth/auth-plugin.ts +303 -0
- package/src/plugins/auth/index.ts +33 -0
- package/src/plugins/auth/types.ts +165 -0
- package/src/plugins/bans/bans-plugin.ts +485 -0
- package/src/plugins/bans/index.ts +31 -0
- package/src/plugins/bans/stores/index.ts +7 -0
- package/src/plugins/bans/stores/postgres-store.ts +195 -0
- package/src/plugins/bans/types.ts +141 -0
- package/src/plugins/cache-plugin.test.ts +105 -32
- package/src/plugins/cache-plugin.ts +40 -9
- package/src/plugins/config-plugin.ts +23 -12
- package/src/plugins/diagnostics-plugin.ts +22 -12
- package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
- package/src/plugins/entitlements/index.ts +51 -0
- package/src/plugins/entitlements/sources/index.ts +9 -0
- package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
- package/src/plugins/entitlements/types.ts +256 -0
- package/src/plugins/frontend-app-plugin.ts +24 -12
- package/src/plugins/health-plugin.ts +27 -7
- package/src/plugins/index.ts +106 -4
- package/src/plugins/logs-plugin.ts +28 -14
- package/src/plugins/postgres-plugin.test.ts +49 -29
- package/src/plugins/postgres-plugin.ts +11 -9
- package/src/plugins/users/index.ts +35 -0
- package/src/plugins/users/stores/index.ts +7 -0
- package/src/plugins/users/stores/postgres-store.ts +225 -0
- package/src/plugins/users/types.ts +209 -0
- package/src/plugins/users/users-plugin.ts +281 -0
- package/ui/src/App.tsx +185 -31
- package/ui/src/api/controlPanelApi.ts +354 -1
- package/ui/src/components/ControlPanelApp.tsx +209 -0
- package/ui/src/components/index.ts +62 -0
- package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
- package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
- package/ui/src/dashboard/builtInWidgets.tsx +29 -0
- package/ui/src/dashboard/index.ts +35 -0
- package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
- package/ui/src/dashboard/widgets/index.ts +7 -0
- package/ui/src/pages/DashboardPage.tsx +28 -149
- package/ui/src/pages/EntitlementsPage.tsx +557 -0
- package/ui/src/pages/LogsPage.tsx +174 -8
- package/ui/src/pages/PluginPage.tsx +148 -0
- package/ui/src/pages/SystemPage.tsx +445 -0
- package/ui/src/pages/UsersPage.tsx +837 -0
- package/ui/tsconfig.lib.json +11 -0
- package/ui/vite.lib.config.ts +51 -0
- package/dist-ui/assets/index-CW1BviRn.js +0 -465
- package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
- package/ui/src/pages/HealthPage.tsx +0 -204
package/ui/src/App.tsx
CHANGED
|
@@ -1,21 +1,64 @@
|
|
|
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 {
|
|
9
|
-
import {
|
|
9
|
+
import { SystemPage } from './pages/SystemPage';
|
|
10
|
+
import { UsersPage } from './pages/UsersPage';
|
|
11
|
+
import { EntitlementsPage } from './pages/EntitlementsPage';
|
|
12
|
+
import { PluginPage } from './pages/PluginPage';
|
|
10
13
|
import { NotFoundPage } from './pages/NotFoundPage';
|
|
14
|
+
import { api, type MenuContribution } from './api/controlPanelApi';
|
|
15
|
+
|
|
16
|
+
// Navigation item type
|
|
17
|
+
interface NavigationItem {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
route: string;
|
|
21
|
+
icon: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Core navigation items always shown
|
|
25
|
+
const coreNavigationItems: NavigationItem[] = [
|
|
26
|
+
{ id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
|
|
27
|
+
{ id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
|
|
28
|
+
{ id: 'system', label: 'System', route: '/system', icon: 'settings' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Built-in optional navigation items - shown if corresponding plugin is registered
|
|
32
|
+
const builtInPluginNavItems: Record<string, NavigationItem> = {
|
|
33
|
+
users: { id: 'users', label: 'Users', route: '/users', icon: 'people' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Routes that have dedicated page components
|
|
37
|
+
const dedicatedRoutes = new Set(['/', '/logs', '/system', '/users', '/entitlements']);
|
|
11
38
|
|
|
12
39
|
// Package version - injected at build time or fallback
|
|
13
40
|
const SERVER_VERSION = '1.0.0';
|
|
14
41
|
|
|
15
|
-
//
|
|
16
|
-
|
|
42
|
+
// Declare global type for injected base path
|
|
43
|
+
declare global {
|
|
44
|
+
interface Window {
|
|
45
|
+
__APP_BASE_PATH__?: string;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the base path for the application.
|
|
51
|
+
*
|
|
52
|
+
* The server injects window.__APP_BASE_PATH__ at runtime based on
|
|
53
|
+
* either the configured mountPath or X-Forwarded-Prefix header.
|
|
54
|
+
* This is a simple, robust approach - no complex detection needed.
|
|
55
|
+
*/
|
|
56
|
+
const basePath = window.__APP_BASE_PATH__ ?? '';
|
|
57
|
+
|
|
58
|
+
// Configure API with the detected base path
|
|
59
|
+
api.setBaseUrl(basePath);
|
|
17
60
|
|
|
18
|
-
//
|
|
61
|
+
// Footer content with QwickApps Server branding
|
|
19
62
|
const footerContent = (
|
|
20
63
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, py: 2 }}>
|
|
21
64
|
<Text variant="caption" customColor="var(--theme-text-secondary)">
|
|
@@ -34,32 +77,143 @@ const footerContent = (
|
|
|
34
77
|
);
|
|
35
78
|
|
|
36
79
|
export function App() {
|
|
80
|
+
const [navigationItems, setNavigationItems] = useState<NavigationItem[]>(coreNavigationItems);
|
|
81
|
+
const [registeredPlugins, setRegisteredPlugins] = useState<Set<string>>(new Set());
|
|
82
|
+
const [pluginMenuItems, setPluginMenuItems] = useState<MenuContribution[]>([]);
|
|
83
|
+
const [logoName, setLogoName] = useState<string>('Control Panel');
|
|
84
|
+
const [logoIconUrl, setLogoIconUrl] = useState<string | undefined>();
|
|
85
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
86
|
+
|
|
87
|
+
// Fetch product info and UI contributions on mount
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const loadData = async () => {
|
|
90
|
+
try {
|
|
91
|
+
// Fetch both in parallel
|
|
92
|
+
const [infoResult, contributionsResult] = await Promise.allSettled([
|
|
93
|
+
api.getInfo(),
|
|
94
|
+
api.getUiContributions(),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Update logo name and icon URL if info fetch succeeded
|
|
98
|
+
if (infoResult.status === 'fulfilled') {
|
|
99
|
+
setLogoName(infoResult.value.logoName);
|
|
100
|
+
setLogoIconUrl(infoResult.value.logoIconUrl);
|
|
101
|
+
} else {
|
|
102
|
+
console.warn('Failed to fetch product info:', infoResult.reason);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update navigation from UI contributions
|
|
106
|
+
if (contributionsResult.status === 'fulfilled') {
|
|
107
|
+
const { plugins, menuItems } = contributionsResult.value;
|
|
108
|
+
const pluginIds = new Set(plugins.map((p) => p.id));
|
|
109
|
+
setRegisteredPlugins(pluginIds);
|
|
110
|
+
setPluginMenuItems(menuItems);
|
|
111
|
+
|
|
112
|
+
// Build navigation: core items + built-in plugin items + dynamic menu items
|
|
113
|
+
const dynamicNav = [...coreNavigationItems];
|
|
114
|
+
|
|
115
|
+
// Add built-in plugin nav items (like Users)
|
|
116
|
+
for (const [pluginId, navItem] of Object.entries(builtInPluginNavItems)) {
|
|
117
|
+
if (pluginIds.has(pluginId)) {
|
|
118
|
+
dynamicNav.push(navItem);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add plugin-contributed menu items (sorted by order)
|
|
123
|
+
const sortedMenuItems = [...menuItems].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
124
|
+
for (const menuItem of sortedMenuItems) {
|
|
125
|
+
// Skip if we already have a nav item for this route
|
|
126
|
+
if (dynamicNav.some(nav => nav.route === menuItem.route)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
dynamicNav.push({
|
|
130
|
+
id: menuItem.id,
|
|
131
|
+
label: menuItem.label,
|
|
132
|
+
route: menuItem.route,
|
|
133
|
+
icon: menuItem.icon || 'extension',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setNavigationItems(dynamicNav);
|
|
138
|
+
} else {
|
|
139
|
+
console.warn('Failed to fetch UI contributions:', contributionsResult.reason);
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
setIsLoading(false);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
loadData();
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
// Dynamic logo based on logoName and logoIconUrl from API
|
|
150
|
+
// When logoIconUrl is provided, use it as a custom icon instead of the default QwickIcon
|
|
151
|
+
const logoIcon = logoIconUrl ? (
|
|
152
|
+
<img
|
|
153
|
+
src={logoIconUrl}
|
|
154
|
+
alt={logoName}
|
|
155
|
+
style={{ width: 32, height: 32, objectFit: 'contain' }}
|
|
156
|
+
/>
|
|
157
|
+
) : undefined;
|
|
158
|
+
const logo = <ProductLogo icon={logoIcon} name={logoName} />;
|
|
159
|
+
|
|
160
|
+
// Show loading state until plugins are loaded
|
|
161
|
+
// This ensures QwickApp receives the correct navigation on first render
|
|
162
|
+
if (isLoading) {
|
|
163
|
+
return (
|
|
164
|
+
<Box
|
|
165
|
+
sx={{
|
|
166
|
+
display: 'flex',
|
|
167
|
+
justifyContent: 'center',
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
minHeight: '100vh',
|
|
170
|
+
bgcolor: 'var(--theme-background, #1a1a2e)',
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
37
176
|
return (
|
|
38
|
-
<BrowserRouter>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
{
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
177
|
+
<BrowserRouter basename={basePath || undefined}>
|
|
178
|
+
<DashboardWidgetProvider>
|
|
179
|
+
<QwickApp
|
|
180
|
+
config={defaultConfig}
|
|
181
|
+
logo={logo}
|
|
182
|
+
footerContent={footerContent}
|
|
183
|
+
enableScaffolding={true}
|
|
184
|
+
navigationItems={navigationItems}
|
|
185
|
+
showThemeSwitcher={true}
|
|
186
|
+
showPaletteSwitcher={true}
|
|
187
|
+
>
|
|
188
|
+
<Routes>
|
|
189
|
+
{/* Core routes */}
|
|
190
|
+
<Route path="/" element={<DashboardPage />} />
|
|
191
|
+
<Route path="/logs" element={<LogsPage />} />
|
|
192
|
+
<Route path="/system" element={<SystemPage />} />
|
|
193
|
+
|
|
194
|
+
{/* Built-in plugin routes */}
|
|
195
|
+
{registeredPlugins.has('users') && (
|
|
196
|
+
<Route path="/users" element={<UsersPage />} />
|
|
197
|
+
)}
|
|
198
|
+
{registeredPlugins.has('entitlements') && (
|
|
199
|
+
<Route path="/entitlements" element={<EntitlementsPage />} />
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Dynamic plugin routes - render generic PluginPage for non-dedicated routes */}
|
|
203
|
+
{pluginMenuItems
|
|
204
|
+
.filter(item => !dedicatedRoutes.has(item.route))
|
|
205
|
+
.map(item => (
|
|
206
|
+
<Route
|
|
207
|
+
key={item.id}
|
|
208
|
+
path={item.route}
|
|
209
|
+
element={<PluginPage pluginId={item.pluginId} title={item.label} route={item.route} />}
|
|
210
|
+
/>
|
|
211
|
+
))}
|
|
212
|
+
|
|
213
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
214
|
+
</Routes>
|
|
215
|
+
</QwickApp>
|
|
216
|
+
</DashboardWidgetProvider>
|
|
63
217
|
</BrowserRouter>
|
|
64
218
|
);
|
|
65
219
|
}
|
|
@@ -20,12 +20,13 @@ export interface HealthResponse {
|
|
|
20
20
|
|
|
21
21
|
export interface InfoResponse {
|
|
22
22
|
product: string;
|
|
23
|
+
logoName: string;
|
|
24
|
+
logoIconUrl?: string;
|
|
23
25
|
version: string;
|
|
24
26
|
uptime: number;
|
|
25
27
|
links: Array<{ label: string; url: string; external?: boolean }>;
|
|
26
28
|
branding?: {
|
|
27
29
|
primaryColor?: string;
|
|
28
|
-
logo?: string;
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -33,6 +34,8 @@ export interface DiagnosticsResponse {
|
|
|
33
34
|
timestamp: string;
|
|
34
35
|
product: string;
|
|
35
36
|
version?: string;
|
|
37
|
+
/** @qwickapps/server framework version */
|
|
38
|
+
frameworkVersion?: string;
|
|
36
39
|
uptime: number;
|
|
37
40
|
health: Record<string, HealthCheck>;
|
|
38
41
|
system: {
|
|
@@ -75,6 +78,131 @@ export interface LogSource {
|
|
|
75
78
|
available: boolean;
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
// ==================
|
|
82
|
+
// Users API Types
|
|
83
|
+
// ==================
|
|
84
|
+
export interface User {
|
|
85
|
+
id: string;
|
|
86
|
+
email: string;
|
|
87
|
+
name?: string;
|
|
88
|
+
created_at?: string;
|
|
89
|
+
updated_at?: string;
|
|
90
|
+
last_login?: string;
|
|
91
|
+
metadata?: Record<string, unknown>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface UsersResponse {
|
|
95
|
+
users: User[];
|
|
96
|
+
total: number;
|
|
97
|
+
page: number;
|
|
98
|
+
limit: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ==================
|
|
102
|
+
// Bans API Types
|
|
103
|
+
// ==================
|
|
104
|
+
export interface Ban {
|
|
105
|
+
id: string;
|
|
106
|
+
user_id?: string;
|
|
107
|
+
email: string;
|
|
108
|
+
reason: string;
|
|
109
|
+
banned_at: string;
|
|
110
|
+
banned_by: string;
|
|
111
|
+
expires_at?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface BansResponse {
|
|
115
|
+
bans: Ban[];
|
|
116
|
+
total: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==================
|
|
120
|
+
// Entitlements API Types
|
|
121
|
+
// ==================
|
|
122
|
+
export interface EntitlementDefinition {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
category?: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface EntitlementResult {
|
|
130
|
+
identifier: string;
|
|
131
|
+
entitlements: string[];
|
|
132
|
+
source: string;
|
|
133
|
+
cached?: boolean;
|
|
134
|
+
cachedAt?: string;
|
|
135
|
+
expiresAt?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ==================
|
|
139
|
+
// Entitlements Status
|
|
140
|
+
// ==================
|
|
141
|
+
export interface EntitlementSourceInfo {
|
|
142
|
+
name: string;
|
|
143
|
+
description?: string;
|
|
144
|
+
readonly: boolean;
|
|
145
|
+
primary: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface EntitlementsStatus {
|
|
149
|
+
readonly: boolean;
|
|
150
|
+
writeEnabled: boolean;
|
|
151
|
+
cacheEnabled: boolean;
|
|
152
|
+
cacheTtl: number;
|
|
153
|
+
sources: EntitlementSourceInfo[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ==================
|
|
157
|
+
// Plugin Feature Detection
|
|
158
|
+
// ==================
|
|
159
|
+
export interface PluginFeatures {
|
|
160
|
+
users: boolean;
|
|
161
|
+
bans: boolean;
|
|
162
|
+
entitlements: boolean;
|
|
163
|
+
entitlementsReadonly?: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ==================
|
|
167
|
+
// UI Contributions Types
|
|
168
|
+
// ==================
|
|
169
|
+
|
|
170
|
+
export interface MenuContribution {
|
|
171
|
+
id: string;
|
|
172
|
+
label: string;
|
|
173
|
+
icon?: string;
|
|
174
|
+
route: string;
|
|
175
|
+
order?: number;
|
|
176
|
+
pluginId: string;
|
|
177
|
+
parent?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface PageContribution {
|
|
181
|
+
id: string;
|
|
182
|
+
route: string;
|
|
183
|
+
title: string;
|
|
184
|
+
pluginId: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface WidgetContribution {
|
|
188
|
+
id: string;
|
|
189
|
+
title: string;
|
|
190
|
+
/** Component name to render (matched by frontend widget registry) */
|
|
191
|
+
component: string;
|
|
192
|
+
/** Priority for ordering (lower = first, default: 100) */
|
|
193
|
+
priority?: number;
|
|
194
|
+
/** Whether this widget is shown by default */
|
|
195
|
+
showByDefault?: boolean;
|
|
196
|
+
pluginId: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface UiContributionsResponse {
|
|
200
|
+
menuItems: MenuContribution[];
|
|
201
|
+
pages: PageContribution[];
|
|
202
|
+
widgets: WidgetContribution[];
|
|
203
|
+
plugins: Array<{ id: string; name: string; version?: string; status: string }>;
|
|
204
|
+
}
|
|
205
|
+
|
|
78
206
|
class ControlPanelApi {
|
|
79
207
|
private baseUrl: string;
|
|
80
208
|
|
|
@@ -82,6 +210,207 @@ class ControlPanelApi {
|
|
|
82
210
|
this.baseUrl = baseUrl;
|
|
83
211
|
}
|
|
84
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Set the base URL for API requests.
|
|
215
|
+
* Call this when the control panel is mounted at a custom path.
|
|
216
|
+
*/
|
|
217
|
+
setBaseUrl(baseUrl: string): void {
|
|
218
|
+
this.baseUrl = baseUrl;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ==================
|
|
222
|
+
// Plugin Feature Detection
|
|
223
|
+
// ==================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Detect which user management plugins are available by probing their endpoints
|
|
227
|
+
*/
|
|
228
|
+
async detectFeatures(): Promise<PluginFeatures> {
|
|
229
|
+
const [users, bans, entitlements] = await Promise.all([
|
|
230
|
+
this.checkEndpoint('/api/users'),
|
|
231
|
+
this.checkEndpoint('/api/bans'),
|
|
232
|
+
this.checkEndpoint('/api/entitlements/available'),
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
// If entitlements is available, get readonly status
|
|
236
|
+
let entitlementsReadonly = true;
|
|
237
|
+
if (entitlements) {
|
|
238
|
+
try {
|
|
239
|
+
const status = await this.getEntitlementsStatus();
|
|
240
|
+
entitlementsReadonly = status.readonly;
|
|
241
|
+
} catch {
|
|
242
|
+
// Default to readonly if we can't get status
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { users, bans, entitlements, entitlementsReadonly };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async checkEndpoint(path: string): Promise<boolean> {
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(`${this.baseUrl}${path}`, { method: 'HEAD' });
|
|
252
|
+
// 200, 401, 403 mean the endpoint exists (might need auth)
|
|
253
|
+
// 404 means it doesn't exist
|
|
254
|
+
return response.status !== 404;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ==================
|
|
261
|
+
// Users API
|
|
262
|
+
// ==================
|
|
263
|
+
|
|
264
|
+
async getUsers(options: {
|
|
265
|
+
limit?: number;
|
|
266
|
+
page?: number;
|
|
267
|
+
search?: string;
|
|
268
|
+
} = {}): Promise<UsersResponse> {
|
|
269
|
+
const params = new URLSearchParams();
|
|
270
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
271
|
+
if (options.page) params.set('page', options.page.toString());
|
|
272
|
+
if (options.search) params.set('search', options.search);
|
|
273
|
+
|
|
274
|
+
const response = await fetch(`${this.baseUrl}/api/users?${params}`);
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error(`Users request failed: ${response.statusText}`);
|
|
277
|
+
}
|
|
278
|
+
return response.json();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getUserById(id: string): Promise<User> {
|
|
282
|
+
const response = await fetch(`${this.baseUrl}/api/users/${id}`);
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
throw new Error(`User request failed: ${response.statusText}`);
|
|
285
|
+
}
|
|
286
|
+
return response.json();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ==================
|
|
290
|
+
// Bans API
|
|
291
|
+
// ==================
|
|
292
|
+
|
|
293
|
+
async getBans(): Promise<BansResponse> {
|
|
294
|
+
const response = await fetch(`${this.baseUrl}/api/bans`);
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
throw new Error(`Bans request failed: ${response.statusText}`);
|
|
297
|
+
}
|
|
298
|
+
return response.json();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async banUser(email: string, reason: string, expiresAt?: string): Promise<void> {
|
|
302
|
+
const response = await fetch(`${this.baseUrl}/api/bans`, {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: { 'Content-Type': 'application/json' },
|
|
305
|
+
body: JSON.stringify({ email, reason, expiresAt }),
|
|
306
|
+
});
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
const error = await response.json().catch(() => ({}));
|
|
309
|
+
throw new Error(error.error || `Ban request failed: ${response.statusText}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async unbanUser(email: string): Promise<void> {
|
|
314
|
+
const response = await fetch(`${this.baseUrl}/api/bans/${encodeURIComponent(email)}`, {
|
|
315
|
+
method: 'DELETE',
|
|
316
|
+
});
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
throw new Error(`Unban request failed: ${response.statusText}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async checkBan(email: string): Promise<{ banned: boolean; ban?: Ban }> {
|
|
323
|
+
const response = await fetch(`${this.baseUrl}/api/bans/check/${encodeURIComponent(email)}`);
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
throw new Error(`Ban check failed: ${response.statusText}`);
|
|
326
|
+
}
|
|
327
|
+
return response.json();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ==================
|
|
331
|
+
// Entitlements API
|
|
332
|
+
// ==================
|
|
333
|
+
|
|
334
|
+
async getEntitlements(email: string): Promise<EntitlementResult> {
|
|
335
|
+
const response = await fetch(`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}`);
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
throw new Error(`Entitlements request failed: ${response.statusText}`);
|
|
338
|
+
}
|
|
339
|
+
return response.json();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async refreshEntitlements(email: string): Promise<EntitlementResult> {
|
|
343
|
+
const response = await fetch(`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}/refresh`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(`Entitlements refresh failed: ${response.statusText}`);
|
|
348
|
+
}
|
|
349
|
+
return response.json();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async checkEntitlement(email: string, entitlement: string): Promise<{ has: boolean }> {
|
|
353
|
+
const response = await fetch(
|
|
354
|
+
`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}/check/${encodeURIComponent(entitlement)}`
|
|
355
|
+
);
|
|
356
|
+
if (!response.ok) {
|
|
357
|
+
throw new Error(`Entitlement check failed: ${response.statusText}`);
|
|
358
|
+
}
|
|
359
|
+
return response.json();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async getAvailableEntitlements(): Promise<EntitlementDefinition[]> {
|
|
363
|
+
const response = await fetch(`${this.baseUrl}/api/entitlements/available`);
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
throw new Error(`Available entitlements request failed: ${response.statusText}`);
|
|
366
|
+
}
|
|
367
|
+
const data = await response.json();
|
|
368
|
+
return data.entitlements;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async grantEntitlement(email: string, entitlement: string): Promise<void> {
|
|
372
|
+
const response = await fetch(`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}`, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
body: JSON.stringify({ entitlement }),
|
|
376
|
+
});
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
const error = await response.json().catch(() => ({}));
|
|
379
|
+
throw new Error(error.error || `Grant entitlement failed: ${response.statusText}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async revokeEntitlement(email: string, entitlement: string): Promise<void> {
|
|
384
|
+
const response = await fetch(
|
|
385
|
+
`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}/${encodeURIComponent(entitlement)}`,
|
|
386
|
+
{ method: 'DELETE' }
|
|
387
|
+
);
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
throw new Error(`Revoke entitlement failed: ${response.statusText}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async invalidateEntitlementCache(email: string): Promise<void> {
|
|
394
|
+
const response = await fetch(`${this.baseUrl}/api/entitlements/cache/${encodeURIComponent(email)}`, {
|
|
395
|
+
method: 'DELETE',
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(`Cache invalidation failed: ${response.statusText}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async getEntitlementsStatus(): Promise<EntitlementsStatus> {
|
|
403
|
+
const response = await fetch(`${this.baseUrl}/api/entitlements/status`);
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
throw new Error(`Entitlements status request failed: ${response.statusText}`);
|
|
406
|
+
}
|
|
407
|
+
return response.json();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ==================
|
|
411
|
+
// Health API
|
|
412
|
+
// ==================
|
|
413
|
+
|
|
85
414
|
async getHealth(): Promise<HealthResponse> {
|
|
86
415
|
const response = await fetch(`${this.baseUrl}/api/health`);
|
|
87
416
|
if (!response.ok) {
|
|
@@ -143,6 +472,30 @@ class ControlPanelApi {
|
|
|
143
472
|
const data = await response.json();
|
|
144
473
|
return data.sources;
|
|
145
474
|
}
|
|
475
|
+
|
|
476
|
+
// ==================
|
|
477
|
+
// Plugins API
|
|
478
|
+
// ==================
|
|
479
|
+
|
|
480
|
+
async getPlugins(): Promise<{ plugins: Array<{ id: string; name: string; version?: string }> }> {
|
|
481
|
+
const response = await fetch(`${this.baseUrl}/api/plugins`);
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
throw new Error(`Plugins request failed: ${response.statusText}`);
|
|
484
|
+
}
|
|
485
|
+
return response.json();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ==================
|
|
489
|
+
// UI Contributions API
|
|
490
|
+
// ==================
|
|
491
|
+
|
|
492
|
+
async getUiContributions(): Promise<UiContributionsResponse> {
|
|
493
|
+
const response = await fetch(`${this.baseUrl}/api/ui-contributions`);
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
throw new Error(`UI contributions request failed: ${response.statusText}`);
|
|
496
|
+
}
|
|
497
|
+
return response.json();
|
|
498
|
+
}
|
|
146
499
|
}
|
|
147
500
|
|
|
148
501
|
export const api = new ControlPanelApi();
|