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.
- package/dist/index.cjs +145 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +5 -3
- package/template/host/e2e/auth.spec.ts +38 -0
- package/template/host/e2e/layout.spec.ts +38 -0
- package/template/host/modern.config.ts +19 -0
- package/template/host/package.json +71 -0
- package/template/host/playwright.config.ts +34 -0
- package/template/host/postcss.config.mjs +6 -0
- package/template/host/src/App.tsx +6 -0
- package/template/host/src/bootstrap.tsx +74 -0
- package/template/host/src/global.css +59 -0
- package/template/host/src/index.ts +2 -0
- package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
- package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
- package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
- package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
- package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
- package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
- package/template/host/src/kernel/auth/hooks.ts +174 -0
- package/template/host/src/kernel/auth/index.ts +5 -0
- package/template/host/src/kernel/auth/schemas.ts +27 -0
- package/template/host/src/kernel/auth/service.ts +197 -0
- package/template/host/src/kernel/auth/types.ts +36 -0
- package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
- package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
- package/template/host/src/kernel/channels/events.ts +27 -0
- package/template/host/src/kernel/channels/hooks.ts +168 -0
- package/template/host/src/kernel/channels/index.ts +6 -0
- package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
- package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
- package/template/host/src/kernel/channels/types.ts +112 -0
- package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
- package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
- package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
- package/template/host/src/kernel/components/index.ts +32 -0
- package/template/host/src/kernel/components/ui/alert.tsx +58 -0
- package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
- package/template/host/src/kernel/components/ui/badge.tsx +35 -0
- package/template/host/src/kernel/components/ui/button.tsx +50 -0
- package/template/host/src/kernel/components/ui/card.tsx +78 -0
- package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
- package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
- package/template/host/src/kernel/components/ui/index.ts +7 -0
- package/template/host/src/kernel/components/ui/input.tsx +24 -0
- package/template/host/src/kernel/components/ui/label.tsx +21 -0
- package/template/host/src/kernel/components/ui/popover.tsx +28 -0
- package/template/host/src/kernel/components/ui/progress.tsx +25 -0
- package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
- package/template/host/src/kernel/components/ui/select.tsx +155 -0
- package/template/host/src/kernel/components/ui/separator.tsx +28 -0
- package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
- package/template/host/src/kernel/components/ui/switch.tsx +26 -0
- package/template/host/src/kernel/components/ui/table.tsx +116 -0
- package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
- package/template/host/src/kernel/components/ui/toast.tsx +126 -0
- package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
- package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
- package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
- package/template/host/src/kernel/index.ts +48 -0
- package/template/host/src/kernel/lib/cn.ts +1 -0
- package/template/host/src/kernel/lib/utils.ts +36 -0
- package/template/host/src/kernel/plugins/Slot.tsx +41 -0
- package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
- package/template/host/src/kernel/plugins/index.ts +23 -0
- package/template/host/src/kernel/plugins/loader.ts +122 -0
- package/template/host/src/kernel/plugins/schemas.ts +54 -0
- package/template/host/src/kernel/plugins/store.ts +185 -0
- package/template/host/src/kernel/plugins/types.ts +103 -0
- package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
- package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
- package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
- package/template/host/src/kernel/providers/index.ts +3 -0
- package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
- package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
- package/template/host/src/kernel/rbac/hooks.ts +379 -0
- package/template/host/src/kernel/rbac/index.ts +6 -0
- package/template/host/src/kernel/rbac/service.ts +504 -0
- package/template/host/src/kernel/rbac/types.ts +164 -0
- package/template/host/src/kernel/rbac/utils.ts +34 -0
- package/template/host/src/kernel/shared-state/bridge.ts +31 -0
- package/template/host/src/kernel/shared-state/index.ts +3 -0
- package/template/host/src/kernel/shared-state/store.ts +62 -0
- package/template/host/src/kernel/shared-state/types.ts +60 -0
- package/template/host/src/kernel/use-migrations.ts +72 -0
- package/template/host/src/layout/MobileMenu.tsx +61 -0
- package/template/host/src/layout/Shell.tsx +42 -0
- package/template/host/src/layout/Sidebar.tsx +178 -0
- package/template/host/src/layout/Topbar.tsx +50 -0
- package/template/host/src/layout/index.ts +4 -0
- package/template/host/src/lib/pocketbase/client.ts +38 -0
- package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
- package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
- package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
- package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
- package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
- package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
- package/template/host/src/lib/pocketbase/index.ts +5 -0
- package/template/host/src/lib/pocketbase/migrations.ts +44 -0
- package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
- package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
- package/template/host/src/lib/pocketbase/seed.ts +113 -0
- package/template/host/src/lib/pocketbase/types.ts +102 -0
- package/template/host/src/modern.runtime.ts +26 -0
- package/template/host/src/plugins.d.ts +9 -0
- package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
- package/template/host/src/routes/_.tsx +6 -0
- package/template/host/src/routes/dashboard._.tsx +41 -0
- package/template/host/src/routes/index.tsx +93 -0
- package/template/host/src/routes/login.tsx +36 -0
- package/template/host/src/saas.config.ts +52 -0
- package/template/host/src/test/setup.ts +65 -0
- package/template/host/src/test/utils.tsx +69 -0
- package/template/host/src/test/vitest-globals.d.ts +19 -0
- package/template/host/src/vite-env.d.ts +16 -0
- package/template/host/tailwind.config.ts +77 -0
- package/template/host/tsconfig.json +19 -0
- package/template/host/vitest.config.ts +30 -0
- package/template/package.json +44 -0
- package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
- package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
- package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
- package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
- package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
- package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
- package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
- package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
- package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
- package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
- package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
- package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.json +8 -0
|
@@ -0,0 +1,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
|
+
}
|