create-lego-one 2.0.9 → 2.0.12
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/dist/index.cjs +145 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +5 -3
- package/template/host/e2e/auth.spec.ts +38 -0
- package/template/host/e2e/layout.spec.ts +38 -0
- package/template/host/modern.config.ts +19 -0
- package/template/host/package.json +71 -0
- package/template/host/playwright.config.ts +34 -0
- package/template/host/postcss.config.mjs +6 -0
- package/template/host/src/App.tsx +6 -0
- package/template/host/src/bootstrap.tsx +74 -0
- package/template/host/src/global.css +59 -0
- package/template/host/src/index.ts +2 -0
- package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
- package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
- package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
- package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
- package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
- package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
- package/template/host/src/kernel/auth/hooks.ts +174 -0
- package/template/host/src/kernel/auth/index.ts +5 -0
- package/template/host/src/kernel/auth/schemas.ts +27 -0
- package/template/host/src/kernel/auth/service.ts +197 -0
- package/template/host/src/kernel/auth/types.ts +36 -0
- package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
- package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
- package/template/host/src/kernel/channels/events.ts +27 -0
- package/template/host/src/kernel/channels/hooks.ts +168 -0
- package/template/host/src/kernel/channels/index.ts +6 -0
- package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
- package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
- package/template/host/src/kernel/channels/types.ts +112 -0
- package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
- package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
- package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
- package/template/host/src/kernel/components/index.ts +32 -0
- package/template/host/src/kernel/components/ui/alert.tsx +58 -0
- package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
- package/template/host/src/kernel/components/ui/badge.tsx +35 -0
- package/template/host/src/kernel/components/ui/button.tsx +50 -0
- package/template/host/src/kernel/components/ui/card.tsx +78 -0
- package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
- package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
- package/template/host/src/kernel/components/ui/index.ts +7 -0
- package/template/host/src/kernel/components/ui/input.tsx +24 -0
- package/template/host/src/kernel/components/ui/label.tsx +21 -0
- package/template/host/src/kernel/components/ui/popover.tsx +28 -0
- package/template/host/src/kernel/components/ui/progress.tsx +25 -0
- package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
- package/template/host/src/kernel/components/ui/select.tsx +155 -0
- package/template/host/src/kernel/components/ui/separator.tsx +28 -0
- package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
- package/template/host/src/kernel/components/ui/switch.tsx +26 -0
- package/template/host/src/kernel/components/ui/table.tsx +116 -0
- package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
- package/template/host/src/kernel/components/ui/toast.tsx +126 -0
- package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
- package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
- package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
- package/template/host/src/kernel/index.ts +48 -0
- package/template/host/src/kernel/lib/cn.ts +1 -0
- package/template/host/src/kernel/lib/utils.ts +36 -0
- package/template/host/src/kernel/plugins/Slot.tsx +41 -0
- package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
- package/template/host/src/kernel/plugins/index.ts +23 -0
- package/template/host/src/kernel/plugins/loader.ts +122 -0
- package/template/host/src/kernel/plugins/schemas.ts +54 -0
- package/template/host/src/kernel/plugins/store.ts +185 -0
- package/template/host/src/kernel/plugins/types.ts +103 -0
- package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
- package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
- package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
- package/template/host/src/kernel/providers/index.ts +3 -0
- package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
- package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
- package/template/host/src/kernel/rbac/hooks.ts +379 -0
- package/template/host/src/kernel/rbac/index.ts +6 -0
- package/template/host/src/kernel/rbac/service.ts +504 -0
- package/template/host/src/kernel/rbac/types.ts +164 -0
- package/template/host/src/kernel/rbac/utils.ts +34 -0
- package/template/host/src/kernel/shared-state/bridge.ts +31 -0
- package/template/host/src/kernel/shared-state/index.ts +3 -0
- package/template/host/src/kernel/shared-state/store.ts +62 -0
- package/template/host/src/kernel/shared-state/types.ts +60 -0
- package/template/host/src/kernel/use-migrations.ts +72 -0
- package/template/host/src/layout/MobileMenu.tsx +61 -0
- package/template/host/src/layout/Shell.tsx +42 -0
- package/template/host/src/layout/Sidebar.tsx +178 -0
- package/template/host/src/layout/Topbar.tsx +50 -0
- package/template/host/src/layout/index.ts +4 -0
- package/template/host/src/lib/pocketbase/client.ts +38 -0
- package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
- package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
- package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
- package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
- package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
- package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
- package/template/host/src/lib/pocketbase/index.ts +5 -0
- package/template/host/src/lib/pocketbase/migrations.ts +44 -0
- package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
- package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
- package/template/host/src/lib/pocketbase/seed.ts +113 -0
- package/template/host/src/lib/pocketbase/types.ts +102 -0
- package/template/host/src/modern.runtime.ts +26 -0
- package/template/host/src/plugins.d.ts +9 -0
- package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
- package/template/host/src/routes/_.tsx +6 -0
- package/template/host/src/routes/dashboard._.tsx +41 -0
- package/template/host/src/routes/index.tsx +93 -0
- package/template/host/src/routes/login.tsx +36 -0
- package/template/host/src/saas.config.ts +52 -0
- package/template/host/src/test/setup.ts +65 -0
- package/template/host/src/test/utils.tsx +69 -0
- package/template/host/src/test/vitest-globals.d.ts +19 -0
- package/template/host/src/vite-env.d.ts +16 -0
- package/template/host/tailwind.config.ts +77 -0
- package/template/host/tsconfig.json +19 -0
- package/template/host/vitest.config.ts +30 -0
- package/template/package.json +44 -0
- package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
- package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
- package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
- package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
- package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
- package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
- package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
- package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
- package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
- package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
- package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
- package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.json +8 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { usePocketBase } from '../providers';
|
|
4
|
+
import { AuthService } from './service';
|
|
5
|
+
import type { LoginCredentials, RegisterData, User } from './types';
|
|
6
|
+
import { useGlobalKernelState } from '../shared-state';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get auth service instance
|
|
10
|
+
*/
|
|
11
|
+
function useAuthService(): AuthService | null {
|
|
12
|
+
const pb = usePocketBase();
|
|
13
|
+
if (!pb) return null;
|
|
14
|
+
return new AuthService(pb);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main auth hook - provides auth state and actions
|
|
19
|
+
*/
|
|
20
|
+
export function useAuth() {
|
|
21
|
+
const pb = usePocketBase();
|
|
22
|
+
const queryClient = useQueryClient();
|
|
23
|
+
const { setUser, setToken, clearAuth, setIsLoading } = useGlobalKernelState();
|
|
24
|
+
const [authService, setAuthService] = useState<AuthService | null>(null);
|
|
25
|
+
|
|
26
|
+
// Initialize auth service
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (pb) {
|
|
29
|
+
setAuthService(new AuthService(pb));
|
|
30
|
+
|
|
31
|
+
// Restore session from PocketBase auth store
|
|
32
|
+
if (pb.authStore.isValid && pb.authStore.model) {
|
|
33
|
+
setUser({
|
|
34
|
+
id: pb.authStore.model.id,
|
|
35
|
+
email: pb.authStore.model.email,
|
|
36
|
+
name: pb.authStore.model.name || pb.authStore.model.email,
|
|
37
|
+
avatar: pb.authStore.model.avatar,
|
|
38
|
+
role: pb.authStore.model.role,
|
|
39
|
+
organizationId: pb.authStore.model.organizationId,
|
|
40
|
+
});
|
|
41
|
+
setToken(pb.authStore.token);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [pb, setUser, setToken, setIsLoading]);
|
|
47
|
+
|
|
48
|
+
// Listen to auth state changes
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!pb) return;
|
|
51
|
+
|
|
52
|
+
const unsubscribe = pb.authStore.onChange((token, model) => {
|
|
53
|
+
if (model && token) {
|
|
54
|
+
setUser({
|
|
55
|
+
id: model.id,
|
|
56
|
+
email: model.email,
|
|
57
|
+
name: model.name || model.email,
|
|
58
|
+
avatar: model.avatar,
|
|
59
|
+
role: model.role,
|
|
60
|
+
organizationId: model.organizationId,
|
|
61
|
+
});
|
|
62
|
+
setToken(token);
|
|
63
|
+
} else {
|
|
64
|
+
clearAuth();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return unsubscribe;
|
|
69
|
+
}, [pb, setUser, setToken, clearAuth]);
|
|
70
|
+
|
|
71
|
+
const loginMutation = useMutation({
|
|
72
|
+
mutationFn: async (credentials: LoginCredentials) => {
|
|
73
|
+
if (!authService) throw new Error('Auth service not initialized');
|
|
74
|
+
return authService.login(credentials);
|
|
75
|
+
},
|
|
76
|
+
onSuccess: (data) => {
|
|
77
|
+
setUser(data.user);
|
|
78
|
+
setToken(data.token);
|
|
79
|
+
queryClient.invalidateQueries({ queryKey: ['auth', 'user'] });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const registerMutation = useMutation({
|
|
84
|
+
mutationFn: async (data: { registerData: RegisterData; organizationId?: string }) => {
|
|
85
|
+
if (!authService) throw new Error('Auth service not initialized');
|
|
86
|
+
return authService.register(data.registerData, data.organizationId);
|
|
87
|
+
},
|
|
88
|
+
onSuccess: (data) => {
|
|
89
|
+
setUser(data.user);
|
|
90
|
+
setToken(data.token);
|
|
91
|
+
queryClient.invalidateQueries({ queryKey: ['auth', 'user'] });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const logoutMutation = useMutation({
|
|
96
|
+
mutationFn: async () => {
|
|
97
|
+
if (!authService) throw new Error('Auth service not initialized');
|
|
98
|
+
return authService.logout();
|
|
99
|
+
},
|
|
100
|
+
onSuccess: () => {
|
|
101
|
+
clearAuth();
|
|
102
|
+
queryClient.clear();
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const login = useCallback(
|
|
107
|
+
(credentials: LoginCredentials) => {
|
|
108
|
+
return loginMutation.mutateAsync(credentials);
|
|
109
|
+
},
|
|
110
|
+
[loginMutation]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const register = useCallback(
|
|
114
|
+
(registerData: RegisterData, organizationId?: string) => {
|
|
115
|
+
return registerMutation.mutateAsync({ registerData: registerData, organizationId });
|
|
116
|
+
},
|
|
117
|
+
[registerMutation]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const logout = useCallback(() => {
|
|
121
|
+
return logoutMutation.mutateAsync();
|
|
122
|
+
}, [logoutMutation]);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
user: authService?.getCurrentUser() || null,
|
|
126
|
+
isAuthenticated: !!pb?.authStore.isValid,
|
|
127
|
+
isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending,
|
|
128
|
+
login,
|
|
129
|
+
register,
|
|
130
|
+
logout,
|
|
131
|
+
error: loginMutation.error || registerMutation.error || logoutMutation.error,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get current user data
|
|
137
|
+
*/
|
|
138
|
+
export function useCurrentUser() {
|
|
139
|
+
const { user, isAuthenticated } = useAuth();
|
|
140
|
+
|
|
141
|
+
const { data: currentUser, isLoading } = useQuery({
|
|
142
|
+
queryKey: ['auth', 'user'],
|
|
143
|
+
queryFn: () => Promise.resolve(user),
|
|
144
|
+
enabled: isAuthenticated,
|
|
145
|
+
staleTime: Infinity,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
user: currentUser,
|
|
150
|
+
isLoading,
|
|
151
|
+
isAuthenticated,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Require authentication - redirect if not logged in
|
|
157
|
+
*/
|
|
158
|
+
export function useRequireAuth() {
|
|
159
|
+
const { isAuthenticated, isLoading, user } = useAuth();
|
|
160
|
+
const [shouldRedirect, setShouldRedirect] = useState(false);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!isLoading && !isAuthenticated) {
|
|
164
|
+
setShouldRedirect(true);
|
|
165
|
+
}
|
|
166
|
+
}, [isLoading, isAuthenticated]);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
user,
|
|
170
|
+
isAuthenticated,
|
|
171
|
+
isLoading,
|
|
172
|
+
shouldRedirect,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const loginSchema = z.object({
|
|
4
|
+
email: z.string().email('Invalid email address'),
|
|
5
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type LoginFormData = z.infer<typeof loginSchema>;
|
|
9
|
+
|
|
10
|
+
export const registerSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
email: z.string().email('Invalid email address'),
|
|
13
|
+
password: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(8, 'Password must be at least 8 characters')
|
|
16
|
+
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
|
17
|
+
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
|
18
|
+
.regex(/[0-9]/, 'Password must contain at least one number'),
|
|
19
|
+
passwordConfirm: z.string(),
|
|
20
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
21
|
+
})
|
|
22
|
+
.refine((data) => data.password === data.passwordConfirm, {
|
|
23
|
+
message: 'Passwords do not match',
|
|
24
|
+
path: ['passwordConfirm'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type RegisterFormData = z.infer<typeof registerSchema>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import PocketBase from 'pocketbase';
|
|
2
|
+
import type { LoginCredentials, RegisterData, User, AuthResponse, AuthError } from './types';
|
|
3
|
+
|
|
4
|
+
// Map PocketBase auth record to our User type
|
|
5
|
+
function mapAuthRecordToUser(record: any): User {
|
|
6
|
+
return {
|
|
7
|
+
id: record.id,
|
|
8
|
+
email: record.email,
|
|
9
|
+
name: record.name || record.email,
|
|
10
|
+
avatar: record.avatar,
|
|
11
|
+
role: record.role,
|
|
12
|
+
organizationId: record.organizationId,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AuthService {
|
|
17
|
+
constructor(private pb: PocketBase) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Login with email and password
|
|
21
|
+
*/
|
|
22
|
+
async login(credentials: LoginCredentials): Promise<AuthResponse> {
|
|
23
|
+
try {
|
|
24
|
+
const authData = await this.pb.collection('users').authWithPassword(
|
|
25
|
+
credentials.email,
|
|
26
|
+
credentials.password
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
token: authData.token,
|
|
31
|
+
user: mapAuthRecordToUser(authData.record),
|
|
32
|
+
};
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
throw this.handleError(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register new user (admin only in our system)
|
|
40
|
+
*/
|
|
41
|
+
async register(data: RegisterData, organizationId?: string): Promise<AuthResponse> {
|
|
42
|
+
try {
|
|
43
|
+
const record = await this.pb.collection('users').create({
|
|
44
|
+
email: data.email,
|
|
45
|
+
password: data.password,
|
|
46
|
+
passwordConfirm: data.passwordConfirm,
|
|
47
|
+
name: data.name,
|
|
48
|
+
organizationId,
|
|
49
|
+
role: 'member', // Default role
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Auto-login after registration
|
|
53
|
+
const authData = await this.pb.collection('users').authWithPassword(
|
|
54
|
+
data.email,
|
|
55
|
+
data.password
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
token: authData.token,
|
|
60
|
+
user: mapAuthRecordToUser(authData.record),
|
|
61
|
+
};
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
throw this.handleError(error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Logout current user
|
|
69
|
+
*/
|
|
70
|
+
async logout(): Promise<void> {
|
|
71
|
+
this.pb.authStore.clear();
|
|
72
|
+
localStorage.removeItem('pocketbase_auth');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get current authenticated user
|
|
77
|
+
*/
|
|
78
|
+
getCurrentUser(): User | null {
|
|
79
|
+
if (!this.pb.authStore.isValid) return null;
|
|
80
|
+
|
|
81
|
+
const model = this.pb.authStore.model;
|
|
82
|
+
if (!model) return null;
|
|
83
|
+
|
|
84
|
+
return mapAuthRecordToUser(model);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get current auth token
|
|
89
|
+
*/
|
|
90
|
+
getToken(): string | null {
|
|
91
|
+
return this.pb.authStore.token;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Refresh auth token
|
|
96
|
+
*/
|
|
97
|
+
async refreshToken(): Promise<string> {
|
|
98
|
+
try {
|
|
99
|
+
// PocketBase handles token refresh automatically
|
|
100
|
+
// This is a no-op but kept for interface consistency
|
|
101
|
+
return this.pb.authStore.token;
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
throw this.handleError(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Update user profile
|
|
109
|
+
*/
|
|
110
|
+
async updateProfile(userId: string, data: Partial<User>): Promise<User> {
|
|
111
|
+
try {
|
|
112
|
+
const record = await this.pb.collection('users').update(userId, {
|
|
113
|
+
name: data.name,
|
|
114
|
+
avatar: data.avatar,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return mapAuthRecordToUser(record);
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
throw this.handleError(error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Change password
|
|
125
|
+
*/
|
|
126
|
+
async changePassword(oldPassword: string, newPassword: string): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
const model = this.pb.authStore.model as any;
|
|
129
|
+
await this.pb.collection('users').update(model?.id, {
|
|
130
|
+
oldPassword,
|
|
131
|
+
password: newPassword,
|
|
132
|
+
passwordConfirm: newPassword,
|
|
133
|
+
});
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
throw this.handleError(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Request password reset
|
|
141
|
+
*/
|
|
142
|
+
async requestPasswordReset(email: string): Promise<void> {
|
|
143
|
+
try {
|
|
144
|
+
await this.pb.collection('users').requestPasswordReset(email);
|
|
145
|
+
} catch (error: any) {
|
|
146
|
+
throw this.handleError(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Confirm password reset
|
|
152
|
+
*/
|
|
153
|
+
async confirmPasswordReset(token: string, password: string): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
await this.pb.collection('users').confirmPasswordReset(token, password, password);
|
|
156
|
+
} catch (error: any) {
|
|
157
|
+
throw this.handleError(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle PocketBase errors
|
|
163
|
+
*/
|
|
164
|
+
private handleError(error: any): AuthError {
|
|
165
|
+
// PocketBase error format
|
|
166
|
+
if (error?.data?.message) {
|
|
167
|
+
return {
|
|
168
|
+
message: error.data.message,
|
|
169
|
+
code: error.status,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (error?.message) {
|
|
174
|
+
return {
|
|
175
|
+
message: error.message,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
message: 'An unexpected error occurred',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if user is authenticated
|
|
186
|
+
*/
|
|
187
|
+
isAuthenticated(): boolean {
|
|
188
|
+
return this.pb.authStore.isValid;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Listen to auth state changes
|
|
193
|
+
*/
|
|
194
|
+
onAuthChange(callback: (token: string, record: any | null) => void): () => void {
|
|
195
|
+
return this.pb.authStore.onChange(callback);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface LoginCredentials {
|
|
2
|
+
email: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RegisterData {
|
|
7
|
+
email: string;
|
|
8
|
+
password: string;
|
|
9
|
+
passwordConfirm: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface User {
|
|
14
|
+
id: string;
|
|
15
|
+
email: string;
|
|
16
|
+
name: string;
|
|
17
|
+
avatar?: string;
|
|
18
|
+
role?: string;
|
|
19
|
+
organizationId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AuthResponse {
|
|
23
|
+
token: string;
|
|
24
|
+
user: User;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuthError {
|
|
28
|
+
message: string;
|
|
29
|
+
code?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SessionData {
|
|
33
|
+
token: string;
|
|
34
|
+
user: User;
|
|
35
|
+
organizationId?: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import Garfish from 'garfish';
|
|
2
|
+
import type {
|
|
3
|
+
ChannelName,
|
|
4
|
+
ChannelMessage,
|
|
5
|
+
ChannelSubscriber,
|
|
6
|
+
ChannelAPI,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ChannelBus - Singleton service for inter-plugin communication using Garfish channels
|
|
11
|
+
*/
|
|
12
|
+
class ChannelBus implements ChannelAPI {
|
|
13
|
+
private garfishInstance: typeof Garfish | null = null;
|
|
14
|
+
private subscribers: Map<ChannelName, Set<ChannelSubscriber>> = new Map();
|
|
15
|
+
private initialized = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the channel bus with Garfish instance
|
|
19
|
+
*/
|
|
20
|
+
initialize(garfish: typeof Garfish): void {
|
|
21
|
+
if (this.initialized) {
|
|
22
|
+
console.warn('[ChannelBus] Already initialized');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.garfishInstance = garfish;
|
|
27
|
+
this.initialized = true;
|
|
28
|
+
|
|
29
|
+
console.log('[ChannelBus] Initialized');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Publish a message to a channel
|
|
34
|
+
*/
|
|
35
|
+
publish<T extends ChannelMessage>(message: T): void {
|
|
36
|
+
if (!this.initialized || !this.garfishInstance) {
|
|
37
|
+
console.warn('[ChannelBus] Not initialized, cannot publish message');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { channel, data } = message;
|
|
42
|
+
|
|
43
|
+
// Add metadata
|
|
44
|
+
const enrichedData = {
|
|
45
|
+
...data,
|
|
46
|
+
id: data.id || this.generateId(),
|
|
47
|
+
timestamp: data.timestamp || Date.now(),
|
|
48
|
+
source: data.source || 'host',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Publish via Garfish channel
|
|
52
|
+
try {
|
|
53
|
+
this.garfishInstance.channel.emit(channel, enrichedData);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('[ChannelBus] Failed to publish message:', error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Also notify local subscribers (in same app context)
|
|
59
|
+
const channelSubscribers = this.subscribers.get(channel);
|
|
60
|
+
if (channelSubscribers) {
|
|
61
|
+
channelSubscribers.forEach((callback) => {
|
|
62
|
+
try {
|
|
63
|
+
callback({ channel, data: enrichedData } as T);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('[ChannelBus] Subscriber callback error:', error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to a channel
|
|
73
|
+
* Returns unsubscribe function
|
|
74
|
+
*/
|
|
75
|
+
subscribe<T extends ChannelMessage>(
|
|
76
|
+
channel: ChannelName,
|
|
77
|
+
callback: ChannelSubscriber<T>
|
|
78
|
+
): () => void {
|
|
79
|
+
if (!this.initialized || !this.garfishInstance) {
|
|
80
|
+
console.warn('[ChannelBus] Not initialized, subscription may not work');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add to local subscribers
|
|
84
|
+
if (!this.subscribers.has(channel)) {
|
|
85
|
+
this.subscribers.set(channel, new Set());
|
|
86
|
+
}
|
|
87
|
+
this.subscribers.get(channel)!.add(callback as ChannelSubscriber);
|
|
88
|
+
|
|
89
|
+
// Also subscribe to Garfish channel for cross-app communication
|
|
90
|
+
if (this.garfishInstance) {
|
|
91
|
+
try {
|
|
92
|
+
this.garfishInstance.channel.on(channel, callback as any);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('[ChannelBus] Failed to subscribe to Garfish channel:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`[ChannelBus] Subscribed to channel: ${channel}`);
|
|
99
|
+
|
|
100
|
+
// Return unsubscribe function
|
|
101
|
+
return () => this.unsubscribe(channel, callback as ChannelSubscriber);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Unsubscribe from a channel
|
|
106
|
+
*/
|
|
107
|
+
unsubscribe(channel: ChannelName, callback: ChannelSubscriber): void {
|
|
108
|
+
// Remove from local subscribers
|
|
109
|
+
const channelSubscribers = this.subscribers.get(channel);
|
|
110
|
+
if (channelSubscribers) {
|
|
111
|
+
channelSubscribers.delete(callback);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Also unsubscribe from Garfish channel
|
|
115
|
+
if (this.garfishInstance) {
|
|
116
|
+
try {
|
|
117
|
+
this.garfishInstance.channel.off(channel, callback as any);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('[ChannelBus] Failed to unsubscribe from Garfish channel:', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`[ChannelBus] Unsubscribed from channel: ${channel}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Unsubscribe all from a channel
|
|
128
|
+
*/
|
|
129
|
+
unsubscribeAll(channel: ChannelName): void {
|
|
130
|
+
const channelSubscribers = this.subscribers.get(channel);
|
|
131
|
+
if (channelSubscribers) {
|
|
132
|
+
channelSubscribers.forEach((callback) => {
|
|
133
|
+
if (this.garfishInstance) {
|
|
134
|
+
this.garfishInstance.channel.off(channel, callback as any);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
channelSubscribers.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clear all subscribers
|
|
143
|
+
*/
|
|
144
|
+
clear(): void {
|
|
145
|
+
this.subscribers.forEach((_, channel) => {
|
|
146
|
+
this.unsubscribeAll(channel);
|
|
147
|
+
});
|
|
148
|
+
this.subscribers.clear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate unique ID for events
|
|
153
|
+
*/
|
|
154
|
+
private generateId(): string {
|
|
155
|
+
return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get list of active channels
|
|
160
|
+
*/
|
|
161
|
+
getActiveChannels(): ChannelName[] {
|
|
162
|
+
return Array.from(this.subscribers.keys());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get subscriber count for a channel
|
|
167
|
+
*/
|
|
168
|
+
getSubscriberCount(channel: ChannelName): number {
|
|
169
|
+
return this.subscribers.get(channel)?.size || 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Export singleton instance
|
|
174
|
+
export const channelBus = new ChannelBus();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Initialize channel bus (call from host bootstrap)
|
|
178
|
+
*/
|
|
179
|
+
export function initializeChannelBus(garfish: typeof Garfish): void {
|
|
180
|
+
channelBus.initialize(garfish);
|
|
181
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { ToastIntegration } from './integrations/ToastIntegration';
|
|
3
|
+
import { useGlobalKernelState } from '../shared-state';
|
|
4
|
+
import { useAuth } from '../auth/hooks';
|
|
5
|
+
import { usePublish } from './hooks';
|
|
6
|
+
import { ChannelName } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ChannelProvider - Sets up all channel integrations
|
|
10
|
+
*
|
|
11
|
+
* This component:
|
|
12
|
+
* 1. Renders the ToastIntegration component
|
|
13
|
+
* 2. Publishes auth changes when authentication state changes
|
|
14
|
+
* 3. Publishes organization changes when organization changes
|
|
15
|
+
*/
|
|
16
|
+
export function ChannelProvider() {
|
|
17
|
+
const { user, isAuthenticated } = useAuth();
|
|
18
|
+
const { organization } = useGlobalKernelState();
|
|
19
|
+
const publish = usePublish();
|
|
20
|
+
|
|
21
|
+
// Publish auth changes
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
publish({
|
|
24
|
+
channel: ChannelName.AUTH_CHANGE,
|
|
25
|
+
data: {
|
|
26
|
+
isAuthenticated,
|
|
27
|
+
userId: user?.id,
|
|
28
|
+
organizationId: organization?.id,
|
|
29
|
+
id: `auth_${Date.now()}`,
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
source: 'host',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}, [isAuthenticated, user?.id, organization?.id, publish]);
|
|
35
|
+
|
|
36
|
+
// Publish organization changes
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (organization) {
|
|
39
|
+
publish({
|
|
40
|
+
channel: ChannelName.ORGANIZATION_CHANGE,
|
|
41
|
+
data: {
|
|
42
|
+
organizationId: organization.id,
|
|
43
|
+
organizationName: organization.name,
|
|
44
|
+
id: `org_${Date.now()}`,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
source: 'host',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [organization, publish]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<ToastIntegration />
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event constants for type-safe event publishing
|
|
3
|
+
*/
|
|
4
|
+
export const Events = {
|
|
5
|
+
TOAST: {
|
|
6
|
+
SUCCESS: (title: string, description?: string) => ({
|
|
7
|
+
type: 'success' as const,
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
}),
|
|
11
|
+
ERROR: (title: string, description?: string) => ({
|
|
12
|
+
type: 'error' as const,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
}),
|
|
16
|
+
INFO: (title: string, description?: string) => ({
|
|
17
|
+
type: 'info' as const,
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
}),
|
|
21
|
+
WARNING: (title: string, description?: string) => ({
|
|
22
|
+
type: 'warning' as const,
|
|
23
|
+
title,
|
|
24
|
+
description,
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
} as const;
|