@lego-box/shell 1.0.5 → 1.0.7
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/.krasrc +13 -0
- package/dist/emulator/lego-box-shell-1.0.7.tgz +0 -0
- package/package.json +6 -3
- package/postcss.config.js +6 -0
- package/src/auth/auth-store.ts +33 -0
- package/src/auth/auth.ts +176 -0
- package/src/components/ProtectedPage.tsx +48 -0
- package/src/config/env.node.ts +38 -0
- package/src/config/env.ts +105 -0
- package/src/context/AbilityContext.tsx +213 -0
- package/src/context/PiralInstanceContext.tsx +17 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/useAuditLogs.ts +190 -0
- package/src/hooks/useDebounce.ts +34 -0
- package/src/hooks/usePermissionGuard.tsx +39 -0
- package/src/hooks/usePermissions.ts +190 -0
- package/src/hooks/useRoles.ts +233 -0
- package/src/hooks/useTickets.ts +214 -0
- package/src/hooks/useUserLogins.ts +39 -0
- package/src/hooks/useUsers.ts +252 -0
- package/src/index.html +16 -0
- package/src/index.tsx +296 -0
- package/src/layout.tsx +246 -0
- package/src/migrations/config.ts +62 -0
- package/src/migrations/dev-migrations.ts +75 -0
- package/src/migrations/index.ts +13 -0
- package/src/migrations/run-migrations.ts +187 -0
- package/src/migrations/runner.ts +925 -0
- package/src/migrations/types.ts +207 -0
- package/src/migrations/utils.ts +264 -0
- package/src/pages/AuditLogsPage.tsx +378 -0
- package/src/pages/ContactSupportPage.tsx +610 -0
- package/src/pages/LandingPage.tsx +221 -0
- package/src/pages/LoginPage.tsx +217 -0
- package/src/pages/MigrationsPage.tsx +1364 -0
- package/src/pages/ProfilePage.tsx +335 -0
- package/src/pages/SettingsPage.tsx +101 -0
- package/src/pages/SystemHealthCheckPage.tsx +144 -0
- package/src/pages/UserManagementPage.tsx +1010 -0
- package/src/piral/api.ts +39 -0
- package/src/piral/auth-casl.ts +56 -0
- package/src/piral/menu.ts +102 -0
- package/src/piral/piral.json +4 -0
- package/src/services/telemetry.ts +84 -0
- package/src/styles/globals.css +1351 -0
- package/src/utils/auditLogger.ts +68 -0
- package/tailwind.config.js +86 -0
- package/webpack.config.js +89 -0
- package/dist/emulator/lego-box-shell-1.0.5.tgz +0 -0
package/src/piral/api.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PiralPlugin } from 'piral';
|
|
2
|
+
import PocketBase from 'pocketbase';
|
|
3
|
+
import type { PiletPocketBaseApi } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface PocketBaseConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PocketBasePluginOptions {
|
|
10
|
+
/** PocketBase server URL (used to create a new instance if pb is not provided). */
|
|
11
|
+
url?: string;
|
|
12
|
+
/** Existing PocketBase instance (e.g. shared with auth plugin). */
|
|
13
|
+
pb?: PocketBase;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Piral plugin that exposes a single PocketBase instance to all pilets.
|
|
18
|
+
* Pilets use app.pocketbase for data and app.test.getTesting() for connectivity checks.
|
|
19
|
+
* Pass an existing pb instance to share it with the auth plugin.
|
|
20
|
+
*/
|
|
21
|
+
export function createPocketBasePlugin(
|
|
22
|
+
config: PocketBaseConfig | PocketBasePluginOptions
|
|
23
|
+
): PiralPlugin<PiletPocketBaseApi> {
|
|
24
|
+
const pb =
|
|
25
|
+
'pb' in config && config.pb
|
|
26
|
+
? config.pb
|
|
27
|
+
: new PocketBase((config as PocketBaseConfig).url);
|
|
28
|
+
|
|
29
|
+
// When creating our own pb we don't call loadFromCookie; default store uses localStorage.
|
|
30
|
+
// When pb is passed from shell, the shell owns the instance and does not load from cookie.
|
|
31
|
+
|
|
32
|
+
return () => (_api: unknown, _target: unknown) => ({
|
|
33
|
+
pocketbase: pb,
|
|
34
|
+
test: {
|
|
35
|
+
getTesting: () =>
|
|
36
|
+
'Hello from PocketBase plugin! This validates the wrapper is working.',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { PiralPlugin } from 'piral';
|
|
3
|
+
import type { PiletAuthObject, PiletCaslApi } from '../types';
|
|
4
|
+
import { getRoleName, getPermissions, can } from '../auth/auth-store';
|
|
5
|
+
import { ProtectedPage } from '../components/ProtectedPage';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a Piral plugin that exposes auth, casl, and registerProtectedPage to pilets.
|
|
9
|
+
* Pilets use app.auth.getId(), app.auth.getRole(), app.auth.can(), app.casl.can(), etc.
|
|
10
|
+
* Use registerProtectedPage for route-level permission guards (same as core ProtectedPage).
|
|
11
|
+
*
|
|
12
|
+
* Requires: auth plugin (for getUser, login, logout) and AbilityProvider (for CASL data).
|
|
13
|
+
*/
|
|
14
|
+
export function createAuthCaslPlugin(): PiralPlugin<{
|
|
15
|
+
auth: PiletAuthObject;
|
|
16
|
+
casl: PiletCaslApi;
|
|
17
|
+
registerProtectedPage: (
|
|
18
|
+
route: string,
|
|
19
|
+
Component: React.ComponentType,
|
|
20
|
+
options: { action: string; subject: string }
|
|
21
|
+
) => void;
|
|
22
|
+
}> {
|
|
23
|
+
return () => (api) => ({
|
|
24
|
+
auth: {
|
|
25
|
+
getId: () => api.getUserId?.() ?? null,
|
|
26
|
+
getEmail: () => api.getUser?.()?.email ?? null,
|
|
27
|
+
getRole: () => getRoleName(),
|
|
28
|
+
getPermissions: () => getPermissions(),
|
|
29
|
+
can: (action: string, subject: string) => can(action, subject),
|
|
30
|
+
getUser: () => api.getUser?.(),
|
|
31
|
+
isAuthenticated: () => api.isAuthenticated?.() ?? false,
|
|
32
|
+
login: (email: string, password: string) =>
|
|
33
|
+
api.login?.(email, password) ?? Promise.reject(new Error('Login not available')),
|
|
34
|
+
logout: () => {
|
|
35
|
+
if (typeof api.logout === 'function') api.logout();
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
casl: {
|
|
39
|
+
can: (action: string, subject: string) => can(action, subject),
|
|
40
|
+
},
|
|
41
|
+
registerProtectedPage(
|
|
42
|
+
route: string,
|
|
43
|
+
Component: React.ComponentType,
|
|
44
|
+
options: { action: string; subject: string }
|
|
45
|
+
) {
|
|
46
|
+
const { action, subject } = options;
|
|
47
|
+
const Wrapped = () =>
|
|
48
|
+
React.createElement(
|
|
49
|
+
ProtectedPage,
|
|
50
|
+
{ action, subject },
|
|
51
|
+
React.createElement(Component, null)
|
|
52
|
+
);
|
|
53
|
+
api.registerPage(route, Wrapped);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { PiralPlugin } from 'piral';
|
|
3
|
+
import type { PiletMenuItem, PiletMenuApi } from '../types';
|
|
4
|
+
|
|
5
|
+
// Module-level store for menu items
|
|
6
|
+
let menuItems: PiletMenuItem[] = [];
|
|
7
|
+
const listeners = new Set<() => void>();
|
|
8
|
+
|
|
9
|
+
// Notify all subscribers of changes
|
|
10
|
+
function notifyListeners() {
|
|
11
|
+
listeners.forEach((listener) => listener());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get current menu items
|
|
16
|
+
*/
|
|
17
|
+
export function getPiletMenus(): PiletMenuItem[] {
|
|
18
|
+
return menuItems;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Subscribe to menu changes
|
|
23
|
+
* Returns unsubscribe function
|
|
24
|
+
*/
|
|
25
|
+
export function subscribeToMenus(callback: () => void): () => void {
|
|
26
|
+
listeners.add(callback);
|
|
27
|
+
return () => listeners.delete(callback);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a Piral plugin that exposes menu registration API to all pilets.
|
|
32
|
+
* Pilets use app.registerMenu() to add sidebar menu items and app.unregisterMenu() to remove them.
|
|
33
|
+
*
|
|
34
|
+
* Example usage in pilet:
|
|
35
|
+
* ```tsx
|
|
36
|
+
* export function setup(context: PiletContext) {
|
|
37
|
+
* // Register a single menu item
|
|
38
|
+
* context.registerSidebarMenu({
|
|
39
|
+
* id: 'my-feature',
|
|
40
|
+
* label: 'My Feature',
|
|
41
|
+
* href: '/my-feature',
|
|
42
|
+
* icon: <Icon />
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Register a menu with children
|
|
46
|
+
* context.registerSidebarMenu({
|
|
47
|
+
* id: 'parent-menu',
|
|
48
|
+
* label: 'Parent Menu',
|
|
49
|
+
* icon: <Icon />,
|
|
50
|
+
* children: [
|
|
51
|
+
* { id: 'child-1', label: 'Child 1', href: '/child-1' },
|
|
52
|
+
* { id: 'child-2', label: 'Child 2', href: '/child-2' }
|
|
53
|
+
* ]
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // Unregister when no longer needed
|
|
57
|
+
* context.unregisterSidebarMenu('my-feature');
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function createMenuPlugin(): PiralPlugin<PiletMenuApi> {
|
|
62
|
+
return () => {
|
|
63
|
+
return {
|
|
64
|
+
registerSidebarMenu(menuItem: PiletMenuItem) {
|
|
65
|
+
// Check if menu already exists
|
|
66
|
+
const existingIndex = menuItems.findIndex((m) => m.id === menuItem.id);
|
|
67
|
+
|
|
68
|
+
if (existingIndex >= 0) {
|
|
69
|
+
// Update existing menu
|
|
70
|
+
menuItems = [...menuItems];
|
|
71
|
+
menuItems[existingIndex] = menuItem;
|
|
72
|
+
} else {
|
|
73
|
+
// Add new menu
|
|
74
|
+
menuItems = [...menuItems, menuItem];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
notifyListeners();
|
|
78
|
+
console.log('[MenuPlugin] Registered menu:', menuItem.id);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
unregisterSidebarMenu(id: string) {
|
|
82
|
+
menuItems = menuItems.filter((m) => m.id !== id);
|
|
83
|
+
notifyListeners();
|
|
84
|
+
console.log('[MenuPlugin] Unregistered menu:', id);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Hook to get pilet menus with subscription
|
|
92
|
+
* This ensures the component re-renders when menus change
|
|
93
|
+
*/
|
|
94
|
+
export function usePiletMenus(): PiletMenuItem[] {
|
|
95
|
+
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
96
|
+
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
return subscribeToMenus(forceUpdate);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
return menuItems;
|
|
102
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Service
|
|
3
|
+
*
|
|
4
|
+
* Handles tracking of user activities.
|
|
5
|
+
* Integrates with PocketBase for data storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import PocketBase from 'pocketbase';
|
|
9
|
+
|
|
10
|
+
/** Generate a unique session ID */
|
|
11
|
+
export function generateSessionId(): string {
|
|
12
|
+
return `session_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Start a user session (track session start in PocketBase if collection exists)
|
|
17
|
+
*/
|
|
18
|
+
export async function startUserSession(
|
|
19
|
+
pocketbase: PocketBase,
|
|
20
|
+
userId: string,
|
|
21
|
+
sessionId: string,
|
|
22
|
+
meta?: { userAgent?: string }
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
await pocketbase.collection('user_sessions').create({
|
|
26
|
+
user_id: userId,
|
|
27
|
+
session_id: sessionId,
|
|
28
|
+
started_at: new Date().toISOString(),
|
|
29
|
+
user_agent: meta?.userAgent,
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
// Collection may not exist yet - ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* End a user session (update session end time in PocketBase if collection exists)
|
|
38
|
+
*/
|
|
39
|
+
export async function endUserSession(
|
|
40
|
+
pocketbase: PocketBase,
|
|
41
|
+
sessionId: string
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
const records = await pocketbase.collection('user_sessions').getList(1, 1, {
|
|
45
|
+
filter: `session_id="${sessionId}"`,
|
|
46
|
+
});
|
|
47
|
+
if (records.items.length > 0) {
|
|
48
|
+
await pocketbase.collection('user_sessions').update(records.items[0].id, {
|
|
49
|
+
ended_at: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Collection may not exist yet - ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Track a user login event
|
|
59
|
+
*/
|
|
60
|
+
export async function trackUserLogin(
|
|
61
|
+
pocketbase: PocketBase,
|
|
62
|
+
data: {
|
|
63
|
+
userId?: string;
|
|
64
|
+
emailAttempted?: string;
|
|
65
|
+
success: boolean;
|
|
66
|
+
failureReason?: string;
|
|
67
|
+
ipAddress?: string;
|
|
68
|
+
userAgent?: string;
|
|
69
|
+
}
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
await pocketbase.collection('user_logins').create({
|
|
73
|
+
user_id: data.userId,
|
|
74
|
+
email_attempted: data.emailAttempted,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
success: data.success,
|
|
77
|
+
failure_reason: data.failureReason,
|
|
78
|
+
ip_address: data.ipAddress,
|
|
79
|
+
user_agent: data.userAgent,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[Telemetry] Failed to track login:', error);
|
|
83
|
+
}
|
|
84
|
+
}
|