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,504 @@
1
+ import PocketBase from 'pocketbase';
2
+ import type {
3
+ Organization,
4
+ CreateOrganizationData,
5
+ UpdateOrganizationData,
6
+ Role,
7
+ CreateRoleData,
8
+ UpdateRoleData,
9
+ Permission,
10
+ UserRole,
11
+ AssignRoleData,
12
+ UserWithRoles,
13
+ CreateUserRequest,
14
+ UpdateUserRequest,
15
+ PermissionCheck,
16
+ ResourceType,
17
+ ActionType,
18
+ } from './types';
19
+ import { SYSTEM_ROLES, DEFAULT_ROLE_PERMISSIONS } from './types';
20
+ import type { ListResult } from 'pocketbase';
21
+
22
+ export class RBACService {
23
+ constructor(private pb: PocketBase) {}
24
+
25
+ // ==================== Organization Management ====================
26
+
27
+ /**
28
+ * Get all organizations for current user
29
+ */
30
+ async getUserOrganizations(): Promise<Organization[]> {
31
+ try {
32
+ // The user's organizations are available via the relation
33
+ const model = this.pb.authStore.model as any;
34
+ const userId = model?.id;
35
+ if (!userId) return [];
36
+
37
+ // Get user record with expanded organizations
38
+ const userRecord = await this.pb.collection('users').getOne(userId, {
39
+ expand: 'organizations',
40
+ });
41
+
42
+ return (userRecord.expand?.organizations || []) as Organization[];
43
+ } catch (error: any) {
44
+ throw this.handleError(error);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get organization by ID
50
+ */
51
+ async getOrganization(id: string): Promise<Organization> {
52
+ try {
53
+ return await this.pb.collection('organizations').getOne(id);
54
+ } catch (error: any) {
55
+ throw this.handleError(error);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get organization by slug
61
+ */
62
+ async getOrganizationBySlug(slug: string): Promise<Organization> {
63
+ try {
64
+ const result = await this.pb
65
+ .collection('organizations')
66
+ .getList(1, 1, { filter: `slug = "${slug}"` });
67
+ if (result.items.length === 0) {
68
+ throw new Error('Organization not found');
69
+ }
70
+ return result.items[0] as unknown as Organization;
71
+ } catch (error: any) {
72
+ throw this.handleError(error);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Create new organization
78
+ */
79
+ async createOrganization(data: CreateOrganizationData): Promise<Organization> {
80
+ try {
81
+ const model = this.pb.authStore.model as any;
82
+ const userId = model?.id;
83
+ if (!userId) throw new Error('Not authenticated');
84
+
85
+ const record = await this.pb.collection('organizations').create({
86
+ ...data,
87
+ ownerId: userId,
88
+ });
89
+
90
+ // Assign owner role to creator
91
+ const ownerRole = await this.getRoleByName(SYSTEM_ROLES.OWNER);
92
+ if (ownerRole) {
93
+ await this.assignRole({
94
+ userId,
95
+ roleId: ownerRole.id,
96
+ organizationId: record.id,
97
+ });
98
+ }
99
+
100
+ return record as unknown as Organization;
101
+ } catch (error: any) {
102
+ throw this.handleError(error);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Update organization
108
+ */
109
+ async updateOrganization(id: string, data: UpdateOrganizationData): Promise<Organization> {
110
+ try {
111
+ return await this.pb.collection('organizations').update(id, data);
112
+ } catch (error: any) {
113
+ throw this.handleError(error);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Delete organization
119
+ */
120
+ async deleteOrganization(id: string): Promise<void> {
121
+ try {
122
+ await this.pb.collection('organizations').delete(id);
123
+ } catch (error: any) {
124
+ throw this.handleError(error);
125
+ }
126
+ }
127
+
128
+ // ==================== Role Management ====================
129
+
130
+ /**
131
+ * Get all roles for an organization
132
+ */
133
+ async getOrganizationRoles(organizationId: string): Promise<Role[]> {
134
+ try {
135
+ const result = await this.pb.collection('roles').getList(1, 100, {
136
+ filter: `organizationId = "${organizationId}" || organizationId = ""`,
137
+ });
138
+ return result.items as unknown as Role[];
139
+ } catch (error: any) {
140
+ throw this.handleError(error);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Get role by ID
146
+ */
147
+ async getRole(id: string): Promise<Role> {
148
+ try {
149
+ return await this.pb.collection('roles').getOne(id);
150
+ } catch (error: any) {
151
+ throw this.handleError(error);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get role by name
157
+ */
158
+ async getRoleByName(name: string): Promise<Role | null> {
159
+ try {
160
+ const result = await this.pb
161
+ .collection('roles')
162
+ .getList(1, 1, { filter: `name = "${name}" && organizationId = ""` });
163
+ if (result.items.length === 0) return null;
164
+ return result.items[0] as unknown as Role;
165
+ } catch (error) {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Create custom role
172
+ */
173
+ async createRole(data: CreateRoleData): Promise<Role> {
174
+ try {
175
+ return await this.pb.collection('roles').create({
176
+ name: data.name,
177
+ description: data.description,
178
+ organizationId: data.organizationId || '',
179
+ isSystem: false,
180
+ });
181
+ } catch (error: any) {
182
+ throw this.handleError(error);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Update role
188
+ */
189
+ async updateRole(id: string, data: UpdateRoleData): Promise<Role> {
190
+ try {
191
+ return await this.pb.collection('roles').update(id, data);
192
+ } catch (error: any) {
193
+ throw this.handleError(error);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Delete role
199
+ */
200
+ async deleteRole(id: string): Promise<void> {
201
+ try {
202
+ await this.pb.collection('roles').delete(id);
203
+ } catch (error: any) {
204
+ throw this.handleError(error);
205
+ }
206
+ }
207
+
208
+ // ==================== Permission Management ====================
209
+
210
+ /**
211
+ * Get all permissions
212
+ */
213
+ async getAllPermissions(): Promise<Permission[]> {
214
+ try {
215
+ const result = await this.pb.collection('permissions').getList(1, 100, {
216
+ sort: 'resource,action',
217
+ });
218
+ return result.items as unknown as Permission[];
219
+ } catch (error: any) {
220
+ throw this.handleError(error);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Check if user has permission
226
+ */
227
+ async hasPermission(
228
+ userId: string,
229
+ organizationId: string,
230
+ resource: ResourceType,
231
+ action: ActionType
232
+ ): Promise<PermissionCheck> {
233
+ try {
234
+ // Get all user roles for this organization
235
+ const userRoles = await this.pb.collection('user_roles').getList(1, 50, {
236
+ filter: `userId = "${userId}" && organizationId = "${organizationId}"`,
237
+ });
238
+
239
+ if (userRoles.items.length === 0) {
240
+ return { allowed: false, reason: 'No roles assigned' };
241
+ }
242
+
243
+ const roleIds = userRoles.items.map((ur: any) => ur.roleId);
244
+
245
+ // Get all permissions for these roles
246
+ const permissions: Permission[] = [];
247
+
248
+ for (const roleId of roleIds) {
249
+ const role = await this.pb.collection('roles').getOne(roleId);
250
+
251
+ // If role has expand permissions
252
+ if (role.expand?.permissions) {
253
+ permissions.push(...role.expand.permissions);
254
+ }
255
+
256
+ // Check for system role defaults
257
+ if (role.isSystem) {
258
+ const defaults = DEFAULT_ROLE_PERMISSIONS[role.name];
259
+ if (defaults) {
260
+ // Convert defaults to Permission-like objects
261
+ for (const def of defaults) {
262
+ permissions.push({
263
+ id: `${def.resource}:${def.action}`,
264
+ resource: def.resource,
265
+ action: def.action,
266
+ } as Permission);
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ // Check for matching permission
273
+ const hasAll = permissions.some(p => p.resource === 'all' && p.action === 'all');
274
+ const hasResourceAll = permissions.some(p => p.resource === resource && p.action === 'all');
275
+ const hasActionAll = permissions.some(p => p.resource === 'all' && p.action === action);
276
+ const hasExact = permissions.some(p => p.resource === resource && p.action === action);
277
+
278
+ if (hasAll || hasResourceAll || hasActionAll || hasExact) {
279
+ return { allowed: true };
280
+ }
281
+
282
+ return { allowed: false, reason: 'Permission denied' };
283
+ } catch (error: any) {
284
+ return { allowed: false, reason: error.message };
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Get user permissions for an organization
290
+ */
291
+ async getUserPermissions(
292
+ userId: string,
293
+ organizationId: string
294
+ ): Promise<{ resource: ResourceType; action: ActionType }[]> {
295
+ try {
296
+ const userRoles = await this.pb.collection('user_roles').getList(1, 50, {
297
+ filter: `userId = "${userId}" && organizationId = "${organizationId}"`,
298
+ expand: 'roleId',
299
+ });
300
+
301
+ const permissions: { resource: ResourceType; action: ActionType }[] = [];
302
+
303
+ for (const item of userRoles.items) {
304
+ const role = (item as any).expand?.roleId;
305
+ if (!role) continue;
306
+
307
+ if (role.isSystem) {
308
+ const defaults = DEFAULT_ROLE_PERMISSIONS[role.name];
309
+ if (defaults) {
310
+ permissions.push(...defaults);
311
+ }
312
+ }
313
+
314
+ if (role.expand?.permissions) {
315
+ for (const perm of role.expand.permissions) {
316
+ permissions.push({
317
+ resource: perm.resource as ResourceType,
318
+ action: perm.action as ActionType,
319
+ });
320
+ }
321
+ }
322
+ }
323
+
324
+ return permissions;
325
+ } catch (error: any) {
326
+ throw this.handleError(error);
327
+ }
328
+ }
329
+
330
+ // ==================== User Management ====================
331
+
332
+ /**
333
+ * Get all users in an organization
334
+ */
335
+ async getOrganizationUsers(organizationId: string): Promise<UserWithRoles[]> {
336
+ try {
337
+ // Get users through the user_roles relation
338
+ const userRoles = await this.pb.collection('user_roles').getList(1, 100, {
339
+ filter: `organizationId = "${organizationId}"`,
340
+ expand: 'userId,roleId',
341
+ });
342
+
343
+ const usersMap = new Map<string, UserWithRoles>();
344
+
345
+ for (const item of userRoles.items) {
346
+ const ur = item as any;
347
+ const user = ur.expand?.userId;
348
+ const role = ur.expand?.roleId;
349
+
350
+ if (!user) continue;
351
+
352
+ if (!usersMap.has(user.id)) {
353
+ usersMap.set(user.id, {
354
+ id: user.id,
355
+ email: user.email,
356
+ name: user.name,
357
+ avatar: user.avatar,
358
+ roles: [],
359
+ organizations: [],
360
+ createdAt: user.created,
361
+ });
362
+ }
363
+
364
+ if (role) {
365
+ usersMap.get(user.id)!.roles.push({
366
+ id: role.id,
367
+ name: role.name,
368
+ organizationId: role.organizationId || organizationId,
369
+ });
370
+ }
371
+ }
372
+
373
+ return Array.from(usersMap.values());
374
+ } catch (error: any) {
375
+ throw this.handleError(error);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Assign role to user
381
+ */
382
+ async assignRole(data: AssignRoleData): Promise<UserRole> {
383
+ try {
384
+ return await this.pb.collection('user_roles').create(data);
385
+ } catch (error: any) {
386
+ throw this.handleError(error);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Remove role from user
392
+ */
393
+ async removeRole(userRoleId: string): Promise<void> {
394
+ try {
395
+ await this.pb.collection('user_roles').delete(userRoleId);
396
+ } catch (error: any) {
397
+ throw this.handleError(error);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Invite/create user in organization
403
+ */
404
+ async createUser(data: CreateUserRequest): Promise<void> {
405
+ try {
406
+ // Create user
407
+ const record = await this.pb.collection('users').create({
408
+ email: data.email,
409
+ password: data.password,
410
+ passwordConfirm: data.password,
411
+ name: data.name,
412
+ organizationId: data.organizationId,
413
+ });
414
+
415
+ // Assign roles if provided
416
+ if (data.roleIds && data.roleIds.length > 0) {
417
+ for (const roleId of data.roleIds) {
418
+ await this.assignRole({
419
+ userId: record.id,
420
+ roleId,
421
+ organizationId: data.organizationId,
422
+ });
423
+ }
424
+ }
425
+ } catch (error: any) {
426
+ throw this.handleError(error);
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Update user
432
+ */
433
+ async updateUser(userId: string, data: UpdateUserRequest): Promise<void> {
434
+ try {
435
+ await this.pb.collection('users').update(userId, data);
436
+ } catch (error: any) {
437
+ throw this.handleError(error);
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Delete user
443
+ */
444
+ async deleteUser(userId: string): Promise<void> {
445
+ try {
446
+ await this.pb.collection('users').delete(userId);
447
+ } catch (error: any) {
448
+ throw this.handleError(error);
449
+ }
450
+ }
451
+
452
+ // ==================== Audit Logging ====================
453
+
454
+ /**
455
+ * Log an action
456
+ */
457
+ async logAction(data: {
458
+ userId: string;
459
+ organizationId: string;
460
+ action: string;
461
+ resource: string;
462
+ resourceId?: string;
463
+ details?: Record<string, any>;
464
+ }): Promise<void> {
465
+ try {
466
+ await this.pb.collection('audit_logs').create(data);
467
+ } catch (error) {
468
+ // Log errors should not fail the main operation
469
+ console.error('Failed to log action:', error);
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Get audit logs for organization
475
+ */
476
+ async getAuditLogs(
477
+ organizationId: string,
478
+ page = 1,
479
+ perPage = 50
480
+ ): Promise<ListResult<any>> {
481
+ try {
482
+ return await this.pb.collection('audit_logs').getList(page, perPage, {
483
+ filter: `organizationId = "${organizationId}"`,
484
+ sort: '-created',
485
+ expand: 'userId',
486
+ });
487
+ } catch (error: any) {
488
+ throw this.handleError(error);
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Handle PocketBase errors
494
+ */
495
+ private handleError(error: any): Error {
496
+ if (error?.data?.message) {
497
+ return new Error(error.data.message);
498
+ }
499
+ if (error?.message) {
500
+ return new Error(error.message);
501
+ }
502
+ return new Error('An unexpected error occurred');
503
+ }
504
+ }
@@ -0,0 +1,164 @@
1
+ // Organization types
2
+ export interface Organization {
3
+ id: string;
4
+ name: string;
5
+ slug: string;
6
+ ownerId: string;
7
+ createdAt: string;
8
+ updated: string;
9
+ }
10
+
11
+ export interface CreateOrganizationData {
12
+ name: string;
13
+ slug: string;
14
+ }
15
+
16
+ export interface UpdateOrganizationData {
17
+ name?: string;
18
+ slug?: string;
19
+ }
20
+
21
+ // Role types
22
+ export interface Role {
23
+ id: string;
24
+ name: string;
25
+ description?: string;
26
+ isSystem: boolean;
27
+ organizationId?: string;
28
+ createdAt: string;
29
+ updated: string;
30
+ }
31
+
32
+ export interface CreateRoleData {
33
+ name: string;
34
+ description?: string;
35
+ organizationId?: string;
36
+ permissions?: string[];
37
+ }
38
+
39
+ export interface UpdateRoleData {
40
+ name?: string;
41
+ description?: string;
42
+ permissions?: string[];
43
+ }
44
+
45
+ // Permission types
46
+ export interface Permission {
47
+ id: string;
48
+ resource: string;
49
+ action: string;
50
+ description?: string;
51
+ createdAt: string;
52
+ }
53
+
54
+ // User role types
55
+ export interface UserRole {
56
+ id: string;
57
+ userId: string;
58
+ roleId: string;
59
+ organizationId: string;
60
+ createdAt: string;
61
+ }
62
+
63
+ export interface AssignRoleData {
64
+ userId: string;
65
+ roleId: string;
66
+ organizationId: string;
67
+ }
68
+
69
+ // User management types
70
+ export interface UserWithRoles {
71
+ id: string;
72
+ email: string;
73
+ name: string;
74
+ avatar?: string;
75
+ roles: Array<{
76
+ id: string;
77
+ name: string;
78
+ organizationId: string;
79
+ }>;
80
+ organizations: string[];
81
+ createdAt: string;
82
+ }
83
+
84
+ export interface CreateUserRequest {
85
+ email: string;
86
+ name: string;
87
+ password: string;
88
+ organizationId: string;
89
+ roleIds?: string[];
90
+ }
91
+
92
+ export interface UpdateUserRequest {
93
+ name?: string;
94
+ email?: string;
95
+ avatar?: string;
96
+ }
97
+
98
+ // Permission check result
99
+ export interface PermissionCheck {
100
+ allowed: boolean;
101
+ reason?: string;
102
+ }
103
+
104
+ // Resource types for permissions
105
+ export type ResourceType =
106
+ | 'organizations'
107
+ | 'users'
108
+ | 'roles'
109
+ | 'permissions'
110
+ | 'todos'
111
+ | 'settings'
112
+ | 'audit_logs'
113
+ | 'all';
114
+
115
+ // Action types for permissions
116
+ export type ActionType =
117
+ | 'create'
118
+ | 'read'
119
+ | 'update'
120
+ | 'delete'
121
+ | 'manage'
122
+ | 'all';
123
+
124
+ // System roles
125
+ export const SYSTEM_ROLES = {
126
+ OWNER: 'Owner',
127
+ ADMIN: 'Admin',
128
+ MEMBER: 'Member',
129
+ GUEST: 'Guest',
130
+ } as const;
131
+
132
+ // Default permissions for system roles
133
+ export const DEFAULT_ROLE_PERMISSIONS: Record<
134
+ string,
135
+ { resource: ResourceType; action: ActionType }[]
136
+ > = {
137
+ [SYSTEM_ROLES.OWNER]: [
138
+ { resource: 'all', action: 'all' },
139
+ ],
140
+ [SYSTEM_ROLES.ADMIN]: [
141
+ { resource: 'organizations', action: 'read' },
142
+ { resource: 'organizations', action: 'update' },
143
+ { resource: 'users', action: 'create' },
144
+ { resource: 'users', action: 'read' },
145
+ { resource: 'users', action: 'update' },
146
+ { resource: 'users', action: 'delete' },
147
+ { resource: 'roles', action: 'read' },
148
+ { resource: 'permissions', action: 'read' },
149
+ { resource: 'todos', action: 'manage' },
150
+ { resource: 'settings', action: 'read' },
151
+ { resource: 'settings', action: 'update' },
152
+ { resource: 'audit_logs', action: 'read' },
153
+ ],
154
+ [SYSTEM_ROLES.MEMBER]: [
155
+ { resource: 'organizations', action: 'read' },
156
+ { resource: 'users', action: 'read' },
157
+ { resource: 'todos', action: 'manage' },
158
+ { resource: 'settings', action: 'read' },
159
+ ],
160
+ [SYSTEM_ROLES.GUEST]: [
161
+ { resource: 'organizations', action: 'read' },
162
+ { resource: 'todos', action: 'read' },
163
+ ],
164
+ };
@@ -0,0 +1,34 @@
1
+ import type { Organization } from './types';
2
+
3
+ /**
4
+ * Initialize first organization for user if none exists
5
+ */
6
+ export async function initializeFirstOrganization(
7
+ pb: any,
8
+ userId: string
9
+ ): Promise<Organization | null> {
10
+ try {
11
+ // Check if user has any organizations
12
+ const userRecord = await pb.collection('users').getOne(userId, {
13
+ expand: 'organizations',
14
+ });
15
+
16
+ const organizations = userRecord.expand?.organizations || [];
17
+
18
+ if (organizations.length > 0) {
19
+ return organizations[0] as Organization;
20
+ }
21
+
22
+ // Create default organization
23
+ const org = await pb.collection('organizations').create({
24
+ name: `${userRecord.name || 'User'}'s Organization`,
25
+ slug: `${userRecord.email?.split('@')[0] || 'user'}-${Date.now()}`,
26
+ ownerId: userId,
27
+ });
28
+
29
+ return org as Organization;
30
+ } catch (error) {
31
+ console.error('Failed to initialize organization:', error);
32
+ return null;
33
+ }
34
+ }