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.
Files changed (171) hide show
  1. package/dist/index.cjs +145 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/package.json +5 -3
  4. package/template/host/e2e/auth.spec.ts +38 -0
  5. package/template/host/e2e/layout.spec.ts +38 -0
  6. package/template/host/modern.config.ts +19 -0
  7. package/template/host/package.json +71 -0
  8. package/template/host/playwright.config.ts +34 -0
  9. package/template/host/postcss.config.mjs +6 -0
  10. package/template/host/src/App.tsx +6 -0
  11. package/template/host/src/bootstrap.tsx +74 -0
  12. package/template/host/src/global.css +59 -0
  13. package/template/host/src/index.ts +2 -0
  14. package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
  15. package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
  16. package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
  17. package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
  18. package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
  19. package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
  20. package/template/host/src/kernel/auth/hooks.ts +174 -0
  21. package/template/host/src/kernel/auth/index.ts +5 -0
  22. package/template/host/src/kernel/auth/schemas.ts +27 -0
  23. package/template/host/src/kernel/auth/service.ts +197 -0
  24. package/template/host/src/kernel/auth/types.ts +36 -0
  25. package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
  26. package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
  27. package/template/host/src/kernel/channels/events.ts +27 -0
  28. package/template/host/src/kernel/channels/hooks.ts +168 -0
  29. package/template/host/src/kernel/channels/index.ts +6 -0
  30. package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
  31. package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
  32. package/template/host/src/kernel/channels/types.ts +112 -0
  33. package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
  34. package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
  35. package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
  36. package/template/host/src/kernel/components/index.ts +32 -0
  37. package/template/host/src/kernel/components/ui/alert.tsx +58 -0
  38. package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
  39. package/template/host/src/kernel/components/ui/badge.tsx +35 -0
  40. package/template/host/src/kernel/components/ui/button.tsx +50 -0
  41. package/template/host/src/kernel/components/ui/card.tsx +78 -0
  42. package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
  43. package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
  44. package/template/host/src/kernel/components/ui/index.ts +7 -0
  45. package/template/host/src/kernel/components/ui/input.tsx +24 -0
  46. package/template/host/src/kernel/components/ui/label.tsx +21 -0
  47. package/template/host/src/kernel/components/ui/popover.tsx +28 -0
  48. package/template/host/src/kernel/components/ui/progress.tsx +25 -0
  49. package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
  50. package/template/host/src/kernel/components/ui/select.tsx +155 -0
  51. package/template/host/src/kernel/components/ui/separator.tsx +28 -0
  52. package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
  53. package/template/host/src/kernel/components/ui/switch.tsx +26 -0
  54. package/template/host/src/kernel/components/ui/table.tsx +116 -0
  55. package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
  56. package/template/host/src/kernel/components/ui/toast.tsx +126 -0
  57. package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
  58. package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
  59. package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
  60. package/template/host/src/kernel/index.ts +48 -0
  61. package/template/host/src/kernel/lib/cn.ts +1 -0
  62. package/template/host/src/kernel/lib/utils.ts +36 -0
  63. package/template/host/src/kernel/plugins/Slot.tsx +41 -0
  64. package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
  65. package/template/host/src/kernel/plugins/index.ts +23 -0
  66. package/template/host/src/kernel/plugins/loader.ts +122 -0
  67. package/template/host/src/kernel/plugins/schemas.ts +54 -0
  68. package/template/host/src/kernel/plugins/store.ts +185 -0
  69. package/template/host/src/kernel/plugins/types.ts +103 -0
  70. package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
  71. package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
  72. package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
  73. package/template/host/src/kernel/providers/index.ts +3 -0
  74. package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
  75. package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
  76. package/template/host/src/kernel/rbac/hooks.ts +379 -0
  77. package/template/host/src/kernel/rbac/index.ts +6 -0
  78. package/template/host/src/kernel/rbac/service.ts +504 -0
  79. package/template/host/src/kernel/rbac/types.ts +164 -0
  80. package/template/host/src/kernel/rbac/utils.ts +34 -0
  81. package/template/host/src/kernel/shared-state/bridge.ts +31 -0
  82. package/template/host/src/kernel/shared-state/index.ts +3 -0
  83. package/template/host/src/kernel/shared-state/store.ts +62 -0
  84. package/template/host/src/kernel/shared-state/types.ts +60 -0
  85. package/template/host/src/kernel/use-migrations.ts +72 -0
  86. package/template/host/src/layout/MobileMenu.tsx +61 -0
  87. package/template/host/src/layout/Shell.tsx +42 -0
  88. package/template/host/src/layout/Sidebar.tsx +178 -0
  89. package/template/host/src/layout/Topbar.tsx +50 -0
  90. package/template/host/src/layout/index.ts +4 -0
  91. package/template/host/src/lib/pocketbase/client.ts +38 -0
  92. package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
  93. package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
  94. package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
  95. package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
  96. package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
  97. package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
  98. package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
  99. package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
  100. package/template/host/src/lib/pocketbase/index.ts +5 -0
  101. package/template/host/src/lib/pocketbase/migrations.ts +44 -0
  102. package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
  103. package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
  104. package/template/host/src/lib/pocketbase/seed.ts +113 -0
  105. package/template/host/src/lib/pocketbase/types.ts +102 -0
  106. package/template/host/src/modern.runtime.ts +26 -0
  107. package/template/host/src/plugins.d.ts +9 -0
  108. package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
  109. package/template/host/src/routes/_.tsx +6 -0
  110. package/template/host/src/routes/dashboard._.tsx +41 -0
  111. package/template/host/src/routes/index.tsx +93 -0
  112. package/template/host/src/routes/login.tsx +36 -0
  113. package/template/host/src/saas.config.ts +52 -0
  114. package/template/host/src/test/setup.ts +65 -0
  115. package/template/host/src/test/utils.tsx +69 -0
  116. package/template/host/src/test/vitest-globals.d.ts +19 -0
  117. package/template/host/src/vite-env.d.ts +16 -0
  118. package/template/host/tailwind.config.ts +77 -0
  119. package/template/host/tsconfig.json +19 -0
  120. package/template/host/vitest.config.ts +30 -0
  121. package/template/package.json +44 -0
  122. package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
  123. package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
  124. package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
  125. package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
  126. package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
  127. package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
  128. package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
  129. package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
  130. package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
  131. package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
  132. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
  133. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
  134. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
  135. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
  136. package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
  137. package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
  138. package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
  139. package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
  140. package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
  141. package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
  142. package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
  143. package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
  144. package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
  145. package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
  146. package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
  147. package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
  148. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
  149. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
  150. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
  151. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
  152. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
  153. package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
  154. package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
  155. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
  156. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
  157. package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
  158. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
  159. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
  160. package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
  161. package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
  162. package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
  163. package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
  164. package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
  165. package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
  166. package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
  167. package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
  168. package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
  169. package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
  170. package/template/pnpm-workspace.yaml +4 -0
  171. 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,5 @@
1
+ export * from './types';
2
+ export * from './schemas';
3
+ export * from './service';
4
+ export * from './hooks';
5
+ export * from './ProtectedRoute';
@@ -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;