create-lego-one 2.0.10 → 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,59 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 221.2 83.2% 53.3%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 221.2 83.2% 53.3%;
26
+ --radius: 0.5rem;
27
+ }
28
+
29
+ .dark {
30
+ --background: 222.2 84% 4.9%;
31
+ --foreground: 210 40% 98%;
32
+ --card: 222.2 84% 4.9%;
33
+ --card-foreground: 210 40% 98%;
34
+ --popover: 222.2 84% 4.9%;
35
+ --popover-foreground: 210 40% 98%;
36
+ --primary: 217.2 91.2% 59.8%;
37
+ --primary-foreground: 222.2 47.4% 11.2%;
38
+ --secondary: 217.2 32.6% 17.5%;
39
+ --secondary-foreground: 210 40% 98%;
40
+ --muted: 217.2 32.6% 17.5%;
41
+ --muted-foreground: 215 20.2% 65.1%;
42
+ --accent: 217.2 32.6% 17.5%;
43
+ --accent-foreground: 210 40% 98%;
44
+ --destructive: 0 62.8% 30.6%;
45
+ --destructive-foreground: 210 40% 98%;
46
+ --border: 217.2 32.6% 17.5%;
47
+ --input: 217.2 32.6% 17.5%;
48
+ --ring: 224.3 76.3% 48%;
49
+ }
50
+ }
51
+
52
+ @layer base {
53
+ * {
54
+ @apply border-border;
55
+ }
56
+ body {
57
+ @apply bg-background text-foreground;
58
+ }
59
+ }
@@ -0,0 +1,2 @@
1
+ import '@modern-js/runtime/garfish';
2
+ import './bootstrap';
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { cn } from '../lib/utils';
3
+
4
+ describe('cn utility function', () => {
5
+ it('should merge class names correctly', () => {
6
+ expect(cn('foo', 'bar')).toBe('foo bar');
7
+ });
8
+
9
+ it('should handle conditional classes', () => {
10
+ expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz');
11
+ });
12
+
13
+ it('should handle undefined and null values', () => {
14
+ expect(cn('foo', undefined, null, 'bar')).toBe('foo bar');
15
+ });
16
+
17
+ it('should handle Tailwind conflict resolution', () => {
18
+ expect(cn('p-4', 'p-2')).toBe('p-2');
19
+ });
20
+
21
+ it('should handle empty input', () => {
22
+ expect(cn()).toBe('');
23
+ });
24
+
25
+ it('should handle arrays of classes', () => {
26
+ expect(cn(['foo', 'bar'], 'baz')).toBe('foo bar baz');
27
+ });
28
+
29
+ it('should handle objects with boolean values', () => {
30
+ expect(cn({ foo: true, bar: false, baz: true })).toBe('foo baz');
31
+ });
32
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { renderHook, waitFor, act } from '@testing-library/react';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import {
5
+ useCurrentOrganization,
6
+ useRequirePermission,
7
+ useUserPermissions,
8
+ } from '../rbac/hooks';
9
+ import * as types from '../rbac/types';
10
+ import { useGlobalKernelState } from '../shared-state';
11
+
12
+ // Mock PocketBase
13
+ vi.mock('pocketbase', () => ({
14
+ default: vi.fn().mockImplementation(() => ({
15
+ collection: vi.fn(),
16
+ })),
17
+ }));
18
+
19
+ // Mock PocketBase provider
20
+ vi.mock('../providers/PocketBaseProvider', () => ({
21
+ PocketBaseProvider: ({ children }: { children: React.ReactNode }) => children,
22
+ usePocketBase: () => null,
23
+ }));
24
+
25
+ const mockPermissions: types.Permission[] = [
26
+ { id: '1', resource: 'users' as const, action: 'read' as const, description: 'Read users' },
27
+ { id: '2', resource: 'users' as const, action: 'write' as const, description: 'Write users' },
28
+ ];
29
+
30
+ const createWrapper = (queryClient: QueryClient) => {
31
+ return ({ children }: { children: React.ReactNode }) => (
32
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
33
+ );
34
+ };
35
+
36
+ describe('RBAC Hooks', () => {
37
+ let queryClient: QueryClient;
38
+
39
+ beforeEach(() => {
40
+ queryClient = new QueryClient({
41
+ defaultOptions: {
42
+ queries: { retry: false },
43
+ mutations: { retry: false },
44
+ },
45
+ });
46
+ });
47
+
48
+ describe('useCurrentOrganization', () => {
49
+ it('should return current organization from state', () => {
50
+ const mockOrg: types.Organization = {
51
+ id: 'test-org',
52
+ name: 'Test Organization',
53
+ slug: 'test-org',
54
+ created: '2024-01-01',
55
+ updated: '2024-01-01',
56
+ };
57
+
58
+ const wrapper = createWrapper(queryClient);
59
+ const { result } = renderHook(() => useCurrentOrganization(), { wrapper });
60
+
61
+ // Initially null
62
+ expect(result.current.organization).toBeNull();
63
+
64
+ // Set organization via Zustand
65
+ act(() => {
66
+ useGlobalKernelState.getState().setOrganization(mockOrg);
67
+ });
68
+
69
+ // Check that hook returns the organization
70
+ const { result: newResult } = renderHook(() => useCurrentOrganization(), { wrapper });
71
+ expect(newResult.current.organization).toEqual(mockOrg);
72
+ });
73
+
74
+ it('should clear query client when setting organization', () => {
75
+ const mockOrg: types.Organization = {
76
+ id: 'test-org',
77
+ name: 'Test Organization',
78
+ slug: 'test-org',
79
+ created: '2024-01-01',
80
+ updated: '2024-01-01',
81
+ };
82
+
83
+ const clearSpy = vi.spyOn(queryClient, 'clear');
84
+
85
+ const wrapper = createWrapper(queryClient);
86
+ const { result } = renderHook(() => useCurrentOrganization(), { wrapper });
87
+
88
+ act(() => {
89
+ result.current.setCurrentOrganization(mockOrg);
90
+ });
91
+
92
+ expect(clearSpy).toHaveBeenCalled();
93
+ });
94
+ });
95
+
96
+ describe('useUserPermissions', () => {
97
+ it('should return undefined when not authenticated', () => {
98
+ const wrapper = createWrapper(queryClient);
99
+ const { result } = renderHook(() => useUserPermissions(), { wrapper });
100
+
101
+ // Service is not available (usePocketBase returns null)
102
+ // Query is disabled, so data is undefined
103
+ expect(result.current.data).toBeUndefined();
104
+ });
105
+
106
+ it('should not be enabled when user is null', () => {
107
+ const wrapper = createWrapper(queryClient);
108
+ const { result } = renderHook(() => useUserPermissions(), { wrapper });
109
+
110
+ // With no user, the hook should not be enabled
111
+ expect(result.current.isLoading).toBe(false);
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { initializeFirstOrganization } from '../rbac/utils';
3
+
4
+ describe('RBAC Utils', () => {
5
+ describe('initializeFirstOrganization', () => {
6
+ it('should return existing organization if user has one', async () => {
7
+ const mockPb = {
8
+ collection: vi.fn().mockReturnValue({
9
+ getOne: vi.fn().mockResolvedValue({
10
+ expand: {
11
+ organizations: [
12
+ { id: 'existing-org', name: 'Existing Org', slug: 'existing' },
13
+ ],
14
+ },
15
+ }),
16
+ }),
17
+ };
18
+
19
+ const result = await initializeFirstOrganization(mockPb, 'user-123');
20
+
21
+ expect(result).toEqual({
22
+ id: 'existing-org',
23
+ name: 'Existing Org',
24
+ slug: 'existing',
25
+ });
26
+ });
27
+
28
+ it('should create new organization if user has none', async () => {
29
+ const mockPb = {
30
+ collection: vi.fn((name: string) => {
31
+ if (name === 'users') {
32
+ return {
33
+ getOne: vi.fn().mockResolvedValue({
34
+ name: 'Test User',
35
+ email: 'test@example.com',
36
+ expand: { organizations: [] },
37
+ }),
38
+ };
39
+ }
40
+ if (name === 'organizations') {
41
+ return {
42
+ create: vi.fn().mockResolvedValue({
43
+ id: 'new-org',
44
+ name: "Test User's Organization",
45
+ slug: 'test-123456',
46
+ }),
47
+ };
48
+ }
49
+ return {};
50
+ }),
51
+ };
52
+
53
+ const result = await initializeFirstOrganization(mockPb, 'user-123');
54
+
55
+ expect(result).toEqual({
56
+ id: 'new-org',
57
+ name: "Test User's Organization",
58
+ slug: 'test-123456',
59
+ });
60
+ });
61
+
62
+ it('should return null on error', async () => {
63
+ const mockPb = {
64
+ collection: vi.fn().mockReturnValue({
65
+ getOne: vi.fn().mockRejectedValue(new Error('DB error')),
66
+ }),
67
+ };
68
+
69
+ const result = await initializeFirstOrganization(mockPb, 'user-123');
70
+
71
+ expect(result).toBeNull();
72
+ });
73
+
74
+ it('should handle user without email gracefully', async () => {
75
+ const mockPb = {
76
+ collection: vi.fn((name: string) => {
77
+ if (name === 'users') {
78
+ return {
79
+ getOne: vi.fn().mockResolvedValue({
80
+ name: null,
81
+ email: null,
82
+ expand: { organizations: [] },
83
+ }),
84
+ };
85
+ }
86
+ if (name === 'organizations') {
87
+ return {
88
+ create: vi.fn().mockResolvedValue({
89
+ id: 'new-org',
90
+ name: "User's Organization",
91
+ slug: 'user-123456',
92
+ }),
93
+ };
94
+ }
95
+ return {};
96
+ }),
97
+ };
98
+
99
+ const result = await initializeFirstOrganization(mockPb, 'user-123');
100
+
101
+ expect(result).toEqual({
102
+ id: 'new-org',
103
+ name: "User's Organization",
104
+ slug: 'user-123456',
105
+ });
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,41 @@
1
+ import { useEffect } from 'react';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import { useRequireAuth } from './hooks';
4
+ import { Skeleton } from '../components/ui/skeleton';
5
+
6
+ interface ProtectedRouteProps {
7
+ children: React.ReactNode;
8
+ redirectTo?: string;
9
+ }
10
+
11
+ export function ProtectedRoute({
12
+ children,
13
+ redirectTo = '/login',
14
+ }: ProtectedRouteProps) {
15
+ const { isAuthenticated, isLoading, shouldRedirect } = useRequireAuth();
16
+ const navigate = useNavigate();
17
+
18
+ useEffect(() => {
19
+ if (shouldRedirect) {
20
+ navigate(redirectTo);
21
+ }
22
+ }, [shouldRedirect, navigate, redirectTo]);
23
+
24
+ if (isLoading) {
25
+ return (
26
+ <div className="flex min-h-[400px] items-center justify-center">
27
+ <div className="space-y-4 text-center">
28
+ <Skeleton className="mx-auto h-12 w-12 rounded-full" />
29
+ <Skeleton className="mx-auto h-4 w-48" />
30
+ <Skeleton className="mx-auto h-4 w-32" />
31
+ </div>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (!isAuthenticated) {
37
+ return null; // Will redirect
38
+ }
39
+
40
+ return <>{children}</>;
41
+ }
@@ -0,0 +1,97 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import { useAuth } from '../hooks';
4
+ import { loginSchema, type LoginFormData } from '../schemas';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { useForm } from 'react-hook-form';
7
+ import { Button } from '../../components/ui/button';
8
+ import { Input } from '../../components/ui/input';
9
+ import { Label } from '../../components/ui/label';
10
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../../components/ui/card';
11
+ import { Alert, AlertDescription } from '../../components/ui/alert';
12
+ import { Loader2 } from 'lucide-react';
13
+
14
+ export function LoginForm() {
15
+ const navigate = useNavigate();
16
+ const { login, isLoading, error } = useAuth();
17
+ const [submitError, setSubmitError] = useState<string | null>(null);
18
+
19
+ const {
20
+ register,
21
+ handleSubmit,
22
+ formState: { errors },
23
+ } = useForm<LoginFormData>({
24
+ resolver: zodResolver(loginSchema),
25
+ });
26
+
27
+ const onSubmit = async (data: LoginFormData) => {
28
+ setSubmitError(null);
29
+ try {
30
+ await login(data);
31
+ navigate('/dashboard');
32
+ } catch (err: any) {
33
+ setSubmitError(err.message || 'Login failed. Please try again.');
34
+ }
35
+ };
36
+
37
+ return (
38
+ <Card className="w-full max-w-md">
39
+ <CardHeader>
40
+ <CardTitle className="text-2xl">Sign In</CardTitle>
41
+ <CardDescription>
42
+ Enter your credentials to access your account
43
+ </CardDescription>
44
+ </CardHeader>
45
+ <form onSubmit={handleSubmit(onSubmit)}>
46
+ <CardContent className="space-y-4">
47
+ {(submitError || error) && (
48
+ <Alert variant="destructive">
49
+ <AlertDescription>
50
+ {submitError || (error as any)?.message}
51
+ </AlertDescription>
52
+ </Alert>
53
+ )}
54
+
55
+ <div className="space-y-2">
56
+ <Label htmlFor="email">Email</Label>
57
+ <Input
58
+ id="email"
59
+ type="email"
60
+ placeholder="admin@example.com"
61
+ {...register('email')}
62
+ />
63
+ {errors.email && (
64
+ <p className="text-sm text-destructive">{errors.email.message}</p>
65
+ )}
66
+ </div>
67
+
68
+ <div className="space-y-2">
69
+ <Label htmlFor="password">Password</Label>
70
+ <Input
71
+ id="password"
72
+ type="password"
73
+ placeholder="••••••••"
74
+ {...register('password')}
75
+ />
76
+ {errors.password && (
77
+ <p className="text-sm text-destructive">{errors.password.message}</p>
78
+ )}
79
+ </div>
80
+ </CardContent>
81
+
82
+ <CardFooter>
83
+ <Button type="submit" className="w-full" disabled={isLoading}>
84
+ {isLoading ? (
85
+ <>
86
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
87
+ Signing in...
88
+ </>
89
+ ) : (
90
+ 'Sign In'
91
+ )}
92
+ </Button>
93
+ </CardFooter>
94
+ </form>
95
+ </Card>
96
+ );
97
+ }
@@ -0,0 +1,79 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from '@modern-js/runtime/router';
3
+ import { useAuth } from '../hooks';
4
+ import { Button } from '../../components/ui/button';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from '../../components/ui/dropdown-menu';
13
+ import { Loader2, LogOut, Settings, User } from 'lucide-react';
14
+ import { getInitials } from '../../lib/utils';
15
+ import { useGlobalKernelState } from '../../shared-state';
16
+
17
+ export function LogoutButton() {
18
+ const { logout, isLoading } = useAuth();
19
+ const navigate = useNavigate();
20
+ const { user } = useGlobalKernelState();
21
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
22
+
23
+ const handleLogout = async () => {
24
+ setIsLoggingOut(true);
25
+ try {
26
+ await logout();
27
+ navigate('/login');
28
+ } finally {
29
+ setIsLoggingOut(false);
30
+ }
31
+ };
32
+
33
+ return (
34
+ <DropdownMenu>
35
+ <DropdownMenuTrigger asChild>
36
+ <Button variant="ghost" className="relative h-9 w-9 rounded-full">
37
+ {user?.name ? (
38
+ <span className="flex h-full w-full items-center justify-center bg-primary text-primary-foreground text-sm font-medium">
39
+ {getInitials(user.name)}
40
+ </span>
41
+ ) : (
42
+ <User className="h-5 w-5" />
43
+ )}
44
+ </Button>
45
+ </DropdownMenuTrigger>
46
+ <DropdownMenuContent className="w-56" align="end" forceMount>
47
+ <DropdownMenuLabel className="font-normal">
48
+ <div className="flex flex-col space-y-1">
49
+ <p className="text-sm font-medium leading-none">{user?.name}</p>
50
+ <p className="text-xs leading-none text-muted-foreground">
51
+ {user?.email}
52
+ </p>
53
+ </div>
54
+ </DropdownMenuLabel>
55
+ <DropdownMenuSeparator />
56
+ <DropdownMenuItem onClick={() => navigate('/settings/profile')}>
57
+ <User className="mr-2 h-4 w-4" />
58
+ <span>Profile</span>
59
+ </DropdownMenuItem>
60
+ <DropdownMenuItem onClick={() => navigate('/settings')}>
61
+ <Settings className="mr-2 h-4 w-4" />
62
+ <span>Settings</span>
63
+ </DropdownMenuItem>
64
+ <DropdownMenuSeparator />
65
+ <DropdownMenuItem
66
+ onClick={handleLogout}
67
+ disabled={isLoading || isLoggingOut}
68
+ >
69
+ {isLoggingOut ? (
70
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
71
+ ) : (
72
+ <LogOut className="mr-2 h-4 w-4" />
73
+ )}
74
+ <span>Log out</span>
75
+ </DropdownMenuItem>
76
+ </DropdownMenuContent>
77
+ </DropdownMenu>
78
+ );
79
+ }