create-lego-one 2.0.10 → 2.0.13

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 (242) hide show
  1. package/dist/index.cjs +179 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/package.json +5 -3
  4. package/template/.cursor/rules/rules.mdc +639 -0
  5. package/template/.dockerignore +58 -0
  6. package/template/.env.example +18 -0
  7. package/template/.eslintignore +5 -0
  8. package/template/.eslintrc.js +28 -0
  9. package/template/.prettierignore +6 -0
  10. package/template/.prettierrc +11 -0
  11. package/template/CLAUDE.md +634 -0
  12. package/template/Dockerfile +67 -0
  13. package/template/PROMPT.md +457 -0
  14. package/template/README.md +325 -0
  15. package/template/docker-compose.yml +48 -0
  16. package/template/docker-entrypoint.sh +23 -0
  17. package/template/docs/checkpoints/.template.md +64 -0
  18. package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
  19. package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
  20. package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
  21. package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
  22. package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
  23. package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
  24. package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
  25. package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
  26. package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
  27. package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
  28. package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
  29. package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
  30. package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
  31. package/template/docs/framework/plans/00-index.md +164 -0
  32. package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
  33. package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
  34. package/template/docs/framework/plans/03-host-kernel.md +1518 -0
  35. package/template/docs/framework/plans/04-auth-system.md +1466 -0
  36. package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
  37. package/template/docs/framework/plans/06-ui-components.md +1478 -0
  38. package/template/docs/framework/plans/07-communication-system.md +1106 -0
  39. package/template/docs/framework/plans/08-plugin-system.md +1179 -0
  40. package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
  41. package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
  42. package/template/docs/framework/plans/11-testing.md +935 -0
  43. package/template/docs/framework/plans/12-deployment.md +896 -0
  44. package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
  45. package/template/docs/framework/research/00-modernjs-audit.md +488 -0
  46. package/template/docs/framework/research/01-system-blueprint.md +721 -0
  47. package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
  48. package/template/docs/framework/research/03-host-setup.md +714 -0
  49. package/template/docs/framework/research/04-plugin-architecture.md +645 -0
  50. package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
  51. package/template/docs/framework/research/06-cli-strategy.md +615 -0
  52. package/template/docs/framework/research/07-deployment.md +629 -0
  53. package/template/docs/framework/research/README.md +282 -0
  54. package/template/docs/framework/setup/00-index.md +210 -0
  55. package/template/docs/framework/setup/01-framework-structure.md +308 -0
  56. package/template/docs/framework/setup/02-development-workflow.md +405 -0
  57. package/template/docs/framework/setup/03-environment-setup.md +215 -0
  58. package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
  59. package/template/docs/framework/setup/05-plugin-system.md +620 -0
  60. package/template/docs/framework/setup/06-communication-patterns.md +451 -0
  61. package/template/docs/framework/setup/07-plugin-development.md +582 -0
  62. package/template/docs/framework/setup/08-component-library.md +658 -0
  63. package/template/docs/framework/setup/09-data-integration.md +609 -0
  64. package/template/docs/framework/setup/10-auth-rbac.md +497 -0
  65. package/template/docs/framework/setup/11-hooks-api.md +393 -0
  66. package/template/docs/framework/setup/12-components-api.md +665 -0
  67. package/template/docs/framework/setup/13-deployment-guide.md +566 -0
  68. package/template/docs/framework/setup/README.md +548 -0
  69. package/template/host/e2e/auth.spec.ts +38 -0
  70. package/template/host/e2e/layout.spec.ts +38 -0
  71. package/template/host/modern.config.ts +19 -0
  72. package/template/host/package.json +71 -0
  73. package/template/host/playwright.config.ts +34 -0
  74. package/template/host/postcss.config.mjs +6 -0
  75. package/template/host/src/App.tsx +6 -0
  76. package/template/host/src/bootstrap.tsx +74 -0
  77. package/template/host/src/global.css +59 -0
  78. package/template/host/src/index.ts +2 -0
  79. package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
  80. package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
  81. package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
  82. package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
  83. package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
  84. package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
  85. package/template/host/src/kernel/auth/hooks.ts +174 -0
  86. package/template/host/src/kernel/auth/index.ts +5 -0
  87. package/template/host/src/kernel/auth/schemas.ts +27 -0
  88. package/template/host/src/kernel/auth/service.ts +197 -0
  89. package/template/host/src/kernel/auth/types.ts +36 -0
  90. package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
  91. package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
  92. package/template/host/src/kernel/channels/events.ts +27 -0
  93. package/template/host/src/kernel/channels/hooks.ts +168 -0
  94. package/template/host/src/kernel/channels/index.ts +6 -0
  95. package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
  96. package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
  97. package/template/host/src/kernel/channels/types.ts +112 -0
  98. package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
  99. package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
  100. package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
  101. package/template/host/src/kernel/components/index.ts +32 -0
  102. package/template/host/src/kernel/components/ui/alert.tsx +58 -0
  103. package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
  104. package/template/host/src/kernel/components/ui/badge.tsx +35 -0
  105. package/template/host/src/kernel/components/ui/button.tsx +50 -0
  106. package/template/host/src/kernel/components/ui/card.tsx +78 -0
  107. package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
  108. package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
  109. package/template/host/src/kernel/components/ui/index.ts +7 -0
  110. package/template/host/src/kernel/components/ui/input.tsx +24 -0
  111. package/template/host/src/kernel/components/ui/label.tsx +21 -0
  112. package/template/host/src/kernel/components/ui/popover.tsx +28 -0
  113. package/template/host/src/kernel/components/ui/progress.tsx +25 -0
  114. package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
  115. package/template/host/src/kernel/components/ui/select.tsx +155 -0
  116. package/template/host/src/kernel/components/ui/separator.tsx +28 -0
  117. package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
  118. package/template/host/src/kernel/components/ui/switch.tsx +26 -0
  119. package/template/host/src/kernel/components/ui/table.tsx +116 -0
  120. package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
  121. package/template/host/src/kernel/components/ui/toast.tsx +126 -0
  122. package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
  123. package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
  124. package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
  125. package/template/host/src/kernel/index.ts +48 -0
  126. package/template/host/src/kernel/lib/cn.ts +1 -0
  127. package/template/host/src/kernel/lib/utils.ts +36 -0
  128. package/template/host/src/kernel/plugins/Slot.tsx +41 -0
  129. package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
  130. package/template/host/src/kernel/plugins/index.ts +23 -0
  131. package/template/host/src/kernel/plugins/loader.ts +122 -0
  132. package/template/host/src/kernel/plugins/schemas.ts +54 -0
  133. package/template/host/src/kernel/plugins/store.ts +185 -0
  134. package/template/host/src/kernel/plugins/types.ts +103 -0
  135. package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
  136. package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
  137. package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
  138. package/template/host/src/kernel/providers/index.ts +3 -0
  139. package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
  140. package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
  141. package/template/host/src/kernel/rbac/hooks.ts +379 -0
  142. package/template/host/src/kernel/rbac/index.ts +6 -0
  143. package/template/host/src/kernel/rbac/service.ts +504 -0
  144. package/template/host/src/kernel/rbac/types.ts +164 -0
  145. package/template/host/src/kernel/rbac/utils.ts +34 -0
  146. package/template/host/src/kernel/shared-state/bridge.ts +31 -0
  147. package/template/host/src/kernel/shared-state/index.ts +3 -0
  148. package/template/host/src/kernel/shared-state/store.ts +62 -0
  149. package/template/host/src/kernel/shared-state/types.ts +60 -0
  150. package/template/host/src/kernel/use-migrations.ts +72 -0
  151. package/template/host/src/layout/MobileMenu.tsx +61 -0
  152. package/template/host/src/layout/Shell.tsx +42 -0
  153. package/template/host/src/layout/Sidebar.tsx +178 -0
  154. package/template/host/src/layout/Topbar.tsx +50 -0
  155. package/template/host/src/layout/index.ts +4 -0
  156. package/template/host/src/lib/pocketbase/client.ts +38 -0
  157. package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
  158. package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
  159. package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
  160. package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
  161. package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
  162. package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
  163. package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
  164. package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
  165. package/template/host/src/lib/pocketbase/index.ts +5 -0
  166. package/template/host/src/lib/pocketbase/migrations.ts +44 -0
  167. package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
  168. package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
  169. package/template/host/src/lib/pocketbase/seed.ts +113 -0
  170. package/template/host/src/lib/pocketbase/types.ts +102 -0
  171. package/template/host/src/modern.runtime.ts +26 -0
  172. package/template/host/src/plugins.d.ts +9 -0
  173. package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
  174. package/template/host/src/routes/_.tsx +6 -0
  175. package/template/host/src/routes/dashboard._.tsx +41 -0
  176. package/template/host/src/routes/index.tsx +93 -0
  177. package/template/host/src/routes/login.tsx +36 -0
  178. package/template/host/src/saas.config.ts +52 -0
  179. package/template/host/src/test/setup.ts +65 -0
  180. package/template/host/src/test/utils.tsx +69 -0
  181. package/template/host/src/test/vitest-globals.d.ts +19 -0
  182. package/template/host/src/vite-env.d.ts +16 -0
  183. package/template/host/tailwind.config.ts +77 -0
  184. package/template/host/tsconfig.json +19 -0
  185. package/template/host/vitest.config.ts +30 -0
  186. package/template/nginx.conf +72 -0
  187. package/template/package.json +44 -0
  188. package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
  189. package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
  190. package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
  191. package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
  192. package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
  193. package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
  194. package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
  195. package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
  196. package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
  197. package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
  198. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
  199. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
  200. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
  201. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
  202. package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
  203. package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
  204. package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
  205. package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
  206. package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
  207. package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
  208. package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
  209. package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
  210. package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
  211. package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
  212. package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
  213. package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
  214. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
  215. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
  216. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
  217. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
  218. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
  219. package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
  220. package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
  221. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
  222. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
  223. package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
  224. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
  225. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
  226. package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
  227. package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
  228. package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
  229. package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
  230. package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
  231. package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
  232. package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
  233. package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
  234. package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
  235. package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
  236. package/template/pnpm-workspace.yaml +4 -0
  237. package/template/pocketbase/CHANGELOG.md +911 -0
  238. package/template/pocketbase/LICENSE.md +17 -0
  239. package/template/scripts/create-plugin.js +221 -0
  240. package/template/scripts/deploy.sh +56 -0
  241. package/template/tsconfig.base.json +26 -0
  242. package/template/tsconfig.json +8 -0
@@ -0,0 +1,1527 @@
1
+ # Multitenancy and RBAC Implementation Plan
2
+
3
+ > **For AI Implementing This Plan:** This is document 05 of 13. Complete documents 01-04 first.
4
+
5
+ **Goal:** Implement complete multi-tenancy with organizations and full role-based access control (RBAC) system including roles, permissions, and user management.
6
+
7
+ **Architecture:** Organization-based multi-tenancy with data isolation via PocketBase API rules. RBAC system with roles (Owner, Admin, Member, Guest) and granular permissions. Admin manages users and roles through settings UI.
8
+
9
+ **Tech Stack:** PocketBase collections, React hooks, TanStack Query, Zustand, Zod validation
10
+
11
+ ---
12
+
13
+ ## Prerequisites
14
+
15
+ - ✅ Completed `01-infrastructure-setup.md`
16
+ - ✅ Completed `02-pocketbase-setup.md` (organizations, roles, permissions, user_roles collections)
17
+ - ✅ Completed `03-host-kernel.md` (host app, shared state)
18
+ - ✅ Completed `04-auth-system.md` (authentication, protected routes)
19
+
20
+ ---
21
+
22
+ ## Task 1: Create Multitenancy Types and Interfaces
23
+
24
+ **Files:**
25
+ - Create: `host/src/kernel/rbac/types.ts`
26
+
27
+ ### Step 1: Create RBAC types
28
+
29
+ **File:** `host/src/kernel/rbac/types.ts`
30
+
31
+ ```typescript
32
+ import type { Record } from 'pocketbase';
33
+
34
+ // Organization types
35
+ export interface Organization {
36
+ id: string;
37
+ name: string;
38
+ slug: string;
39
+ ownerId: string;
40
+ createdAt: string;
41
+ updated: string;
42
+ }
43
+
44
+ export interface CreateOrganizationData {
45
+ name: string;
46
+ slug: string;
47
+ }
48
+
49
+ export interface UpdateOrganizationData {
50
+ name?: string;
51
+ slug?: string;
52
+ }
53
+
54
+ // Role types
55
+ export interface Role {
56
+ id: string;
57
+ name: string;
58
+ description?: string;
59
+ isSystem: boolean;
60
+ organizationId?: string;
61
+ createdAt: string;
62
+ updated: string;
63
+ }
64
+
65
+ export interface CreateRoleData {
66
+ name: string;
67
+ description?: string;
68
+ organizationId?: string;
69
+ permissions?: string[];
70
+ }
71
+
72
+ export interface UpdateRoleData {
73
+ name?: string;
74
+ description?: string;
75
+ permissions?: string[];
76
+ }
77
+
78
+ // Permission types
79
+ export interface Permission {
80
+ id: string;
81
+ resource: string;
82
+ action: string;
83
+ description?: string;
84
+ createdAt: string;
85
+ }
86
+
87
+ // User role types
88
+ export interface UserRole {
89
+ id: string;
90
+ userId: string;
91
+ roleId: string;
92
+ organizationId: string;
93
+ createdAt: string;
94
+ }
95
+
96
+ export interface AssignRoleData {
97
+ userId: string;
98
+ roleId: string;
99
+ organizationId: string;
100
+ }
101
+
102
+ // User management types
103
+ export interface UserWithRoles {
104
+ id: string;
105
+ email: string;
106
+ name: string;
107
+ avatar?: string;
108
+ roles: Array<{
109
+ id: string;
110
+ name: string;
111
+ organizationId: string;
112
+ }>;
113
+ organizations: string[];
114
+ createdAt: string;
115
+ }
116
+
117
+ export interface CreateUserRequest {
118
+ email: string;
119
+ name: string;
120
+ password: string;
121
+ organizationId: string;
122
+ roleIds?: string[];
123
+ }
124
+
125
+ export interface UpdateUserRequest {
126
+ name?: string;
127
+ email?: string;
128
+ avatar?: string;
129
+ }
130
+
131
+ // Permission check result
132
+ export interface PermissionCheck {
133
+ allowed: boolean;
134
+ reason?: string;
135
+ }
136
+
137
+ // Resource types for permissions
138
+ export type ResourceType =
139
+ | 'organizations'
140
+ | 'users'
141
+ | 'roles'
142
+ | 'permissions'
143
+ | 'todos'
144
+ | 'settings'
145
+ | 'audit_logs'
146
+ | 'all';
147
+
148
+ // Action types for permissions
149
+ export type ActionType =
150
+ | 'create'
151
+ | 'read'
152
+ | 'update'
153
+ | 'delete'
154
+ | 'manage'
155
+ | 'all';
156
+
157
+ // System roles
158
+ export const SYSTEM_ROLES = {
159
+ OWNER: 'Owner',
160
+ ADMIN: 'Admin',
161
+ MEMBER: 'Member',
162
+ GUEST: 'Guest',
163
+ } as const;
164
+
165
+ // Default permissions for system roles
166
+ export const DEFAULT_ROLE_PERMISSIONS: Record<
167
+ string,
168
+ { resource: ResourceType; action: ActionType }[]
169
+ > = {
170
+ [SYSTEM_ROLES.OWNER]: [
171
+ { resource: 'all', action: 'all' },
172
+ ],
173
+ [SYSTEM_ROLES.ADMIN]: [
174
+ { resource: 'organizations', action: 'read' },
175
+ { resource: 'organizations', action: 'update' },
176
+ { resource: 'users', action: 'create' },
177
+ { resource: 'users', action: 'read' },
178
+ { resource: 'users', action: 'update' },
179
+ { resource: 'users', action: 'delete' },
180
+ { resource: 'roles', action: 'read' },
181
+ { resource: 'permissions', action: 'read' },
182
+ { resource: 'todos', action: 'manage' },
183
+ { resource: 'settings', action: 'read' },
184
+ { resource: 'settings', action: 'update' },
185
+ { resource: 'audit_logs', action: 'read' },
186
+ ],
187
+ [SYSTEM_ROLES.MEMBER]: [
188
+ { resource: 'organizations', action: 'read' },
189
+ { resource: 'users', action: 'read' },
190
+ { resource: 'todos', action: 'manage' },
191
+ { resource: 'settings', action: 'read' },
192
+ ],
193
+ [SYSTEM_ROLES.GUEST]: [
194
+ { resource: 'organizations', action: 'read' },
195
+ { resource: 'todos', action: 'read' },
196
+ ],
197
+ };
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Task 2: Create RBAC Service
203
+
204
+ **Files:**
205
+ - Create: `host/src/kernel/rbac/service.ts`
206
+
207
+ ### Step 1: Create RBAC service
208
+
209
+ **File:** `host/src/kernel/rbac/service.ts`
210
+
211
+ ```typescript
212
+ import PocketBase from 'pocketbase';
213
+ import type {
214
+ Organization,
215
+ CreateOrganizationData,
216
+ UpdateOrganizationData,
217
+ Role,
218
+ CreateRoleData,
219
+ UpdateRoleData,
220
+ Permission,
221
+ UserRole,
222
+ AssignRoleData,
223
+ UserWithRoles,
224
+ CreateUserRequest,
225
+ UpdateUserRequest,
226
+ PermissionCheck,
227
+ ResourceType,
228
+ ActionType,
229
+ } from './types';
230
+ import { SYSTEM_ROLES, DEFAULT_ROLE_PERMISSIONS } from './types';
231
+ import type { ListResult } from 'pocketbase';
232
+
233
+ export class RBACService {
234
+ constructor(private pb: PocketBase) {}
235
+
236
+ // ==================== Organization Management ====================
237
+
238
+ /**
239
+ * Get all organizations for current user
240
+ */
241
+ async getUserOrganizations(): Promise<Organization[]> {
242
+ try {
243
+ // The user's organizations are available via the relation
244
+ const userId = this.pb.authStore.record?.id;
245
+ if (!userId) return [];
246
+
247
+ // Get user record with expanded organizations
248
+ const userRecord = await this.pb.collection('users').getOne(userId, {
249
+ expand: 'organizations',
250
+ });
251
+
252
+ return (userRecord.expand?.organizations || []) as Organization[];
253
+ } catch (error: any) {
254
+ throw this.handleError(error);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get organization by ID
260
+ */
261
+ async getOrganization(id: string): Promise<Organization> {
262
+ try {
263
+ return await this.pb.collection('organizations').getOne(id);
264
+ } catch (error: any) {
265
+ throw this.handleError(error);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Get organization by slug
271
+ */
272
+ async getOrganizationBySlug(slug: string): Promise<Organization> {
273
+ try {
274
+ const result = await this.pb
275
+ .collection('organizations')
276
+ .getList(1, 1, { filter: `slug = "${slug}"` });
277
+ if (result.items.length === 0) {
278
+ throw new Error('Organization not found');
279
+ }
280
+ return result.items[0] as Organization;
281
+ } catch (error: any) {
282
+ throw this.handleError(error);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Create new organization
288
+ */
289
+ async createOrganization(data: CreateOrganizationData): Promise<Organization> {
290
+ try {
291
+ const userId = this.pb.authStore.record?.id;
292
+ if (!userId) throw new Error('Not authenticated');
293
+
294
+ const record = await this.pb.collection('organizations').create({
295
+ ...data,
296
+ ownerId: userId,
297
+ });
298
+
299
+ // Assign owner role to creator
300
+ const ownerRole = await this.getRoleByName(SYSTEM_ROLES.OWNER);
301
+ if (ownerRole) {
302
+ await this.assignRole({
303
+ userId,
304
+ roleId: ownerRole.id,
305
+ organizationId: record.id,
306
+ });
307
+ }
308
+
309
+ return record as Organization;
310
+ } catch (error: any) {
311
+ throw this.handleError(error);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Update organization
317
+ */
318
+ async updateOrganization(id: string, data: UpdateOrganizationData): Promise<Organization> {
319
+ try {
320
+ return await this.pb.collection('organizations').update(id, data);
321
+ } catch (error: any) {
322
+ throw this.handleError(error);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Delete organization
328
+ */
329
+ async deleteOrganization(id: string): Promise<void> {
330
+ try {
331
+ await this.pb.collection('organizations').delete(id);
332
+ } catch (error: any) {
333
+ throw this.handleError(error);
334
+ }
335
+ }
336
+
337
+ // ==================== Role Management ====================
338
+
339
+ /**
340
+ * Get all roles for an organization
341
+ */
342
+ async getOrganizationRoles(organizationId: string): Promise<Role[]> {
343
+ try {
344
+ const result = await this.pb.collection('roles').getList(1, 100, {
345
+ filter: `organizationId = "${organizationId}" || organizationId = ""`,
346
+ });
347
+ return result.items as Role[];
348
+ } catch (error: any) {
349
+ throw this.handleError(error);
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Get role by ID
355
+ */
356
+ async getRole(id: string): Promise<Role> {
357
+ try {
358
+ return await this.pb.collection('roles').getOne(id);
359
+ } catch (error: any) {
360
+ throw this.handleError(error);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Get role by name
366
+ */
367
+ async getRoleByName(name: string): Promise<Role | null> {
368
+ try {
369
+ const result = await this.pb
370
+ .collection('roles')
371
+ .getList(1, 1, { filter: `name = "${name}" && organizationId = ""` });
372
+ if (result.items.length === 0) return null;
373
+ return result.items[0] as Role;
374
+ } catch (error) {
375
+ return null;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Create custom role
381
+ */
382
+ async createRole(data: CreateRoleData): Promise<Role> {
383
+ try {
384
+ return await this.pb.collection('roles').create({
385
+ name: data.name,
386
+ description: data.description,
387
+ organizationId: data.organizationId || '',
388
+ isSystem: false,
389
+ });
390
+ } catch (error: any) {
391
+ throw this.handleError(error);
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Update role
397
+ */
398
+ async updateRole(id: string, data: UpdateRoleData): Promise<Role> {
399
+ try {
400
+ return await this.pb.collection('roles').update(id, data);
401
+ } catch (error: any) {
402
+ throw this.handleError(error);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Delete role
408
+ */
409
+ async deleteRole(id: string): Promise<void> {
410
+ try {
411
+ await this.pb.collection('roles').delete(id);
412
+ } catch (error: any) {
413
+ throw this.handleError(error);
414
+ }
415
+ }
416
+
417
+ // ==================== Permission Management ====================
418
+
419
+ /**
420
+ * Get all permissions
421
+ */
422
+ async getAllPermissions(): Promise<Permission[]> {
423
+ try {
424
+ const result = await this.pb.collection('permissions').getList(1, 100, {
425
+ sort: 'resource,action',
426
+ });
427
+ return result.items as Permission[];
428
+ } catch (error: any) {
429
+ throw this.handleError(error);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Check if user has permission
435
+ */
436
+ async hasPermission(
437
+ userId: string,
438
+ organizationId: string,
439
+ resource: ResourceType,
440
+ action: ActionType
441
+ ): Promise<PermissionCheck> {
442
+ try {
443
+ // Get all user roles for this organization
444
+ const userRoles = await this.pb.collection('user_roles').getList(1, 50, {
445
+ filter: `userId = "${userId}" && organizationId = "${organizationId}"`,
446
+ });
447
+
448
+ if (userRoles.items.length === 0) {
449
+ return { allowed: false, reason: 'No roles assigned' };
450
+ }
451
+
452
+ const roleIds = userRoles.items.map((ur: any) => ur.roleId);
453
+
454
+ // Get all permissions for these roles
455
+ const permissions: Permission[] = [];
456
+
457
+ for (const roleId of roleIds) {
458
+ const role = await this.pb.collection('roles').getOne(roleId);
459
+
460
+ // If role has expand permissions
461
+ if (role.expand?.permissions) {
462
+ permissions.push(...role.expand.permissions);
463
+ }
464
+
465
+ // Check for system role defaults
466
+ if (role.isSystem) {
467
+ const defaults = DEFAULT_ROLE_PERMISSIONS[role.name];
468
+ if (defaults) {
469
+ // Convert defaults to Permission-like objects
470
+ for (const def of defaults) {
471
+ permissions.push({
472
+ id: `${def.resource}:${def.action}`,
473
+ resource: def.resource,
474
+ action: def.action,
475
+ } as Permission);
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ // Check for matching permission
482
+ const hasAll = permissions.some(p => p.resource === 'all' && p.action === 'all');
483
+ const hasResourceAll = permissions.some(p => p.resource === resource && p.action === 'all');
484
+ const hasActionAll = permissions.some(p => p.resource === 'all' && p.action === action);
485
+ const hasExact = permissions.some(p => p.resource === resource && p.action === action);
486
+
487
+ if (hasAll || hasResourceAll || hasActionAll || hasExact) {
488
+ return { allowed: true };
489
+ }
490
+
491
+ return { allowed: false, reason: 'Permission denied' };
492
+ } catch (error: any) {
493
+ return { allowed: false, reason: error.message };
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Get user permissions for an organization
499
+ */
500
+ async getUserPermissions(
501
+ userId: string,
502
+ organizationId: string
503
+ ): Promise<{ resource: ResourceType; action: ActionType }[]> {
504
+ try {
505
+ const userRoles = await this.pb.collection('user_roles').getList(1, 50, {
506
+ filter: `userId = "${userId}" && organizationId = "${organizationId}"`,
507
+ expand: 'roleId',
508
+ });
509
+
510
+ const permissions: { resource: ResourceType; action: ActionType }[] = [];
511
+
512
+ for (const item of userRoles.items) {
513
+ const role = (item as any).expand?.roleId;
514
+ if (!role) continue;
515
+
516
+ if (role.isSystem) {
517
+ const defaults = DEFAULT_ROLE_PERMISSIONS[role.name];
518
+ if (defaults) {
519
+ permissions.push(...defaults);
520
+ }
521
+ }
522
+
523
+ if (role.expand?.permissions) {
524
+ for (const perm of role.expand.permissions) {
525
+ permissions.push({
526
+ resource: perm.resource as ResourceType,
527
+ action: perm.action as ActionType,
528
+ });
529
+ }
530
+ }
531
+ }
532
+
533
+ return permissions;
534
+ } catch (error: any) {
535
+ throw this.handleError(error);
536
+ }
537
+ }
538
+
539
+ // ==================== User Management ====================
540
+
541
+ /**
542
+ * Get all users in an organization
543
+ */
544
+ async getOrganizationUsers(organizationId: string): Promise<UserWithRoles[]> {
545
+ try {
546
+ // Get users through the user_roles relation
547
+ const userRoles = await this.pb.collection('user_roles').getList(1, 100, {
548
+ filter: `organizationId = "${organizationId}"`,
549
+ expand: 'userId,roleId',
550
+ });
551
+
552
+ const usersMap = new Map<string, UserWithRoles>();
553
+
554
+ for (const item of userRoles.items) {
555
+ const ur = item as any;
556
+ const user = ur.expand?.userId;
557
+ const role = ur.expand?.roleId;
558
+
559
+ if (!user) continue;
560
+
561
+ if (!usersMap.has(user.id)) {
562
+ usersMap.set(user.id, {
563
+ id: user.id,
564
+ email: user.email,
565
+ name: user.name,
566
+ avatar: user.avatar,
567
+ roles: [],
568
+ organizations: [],
569
+ createdAt: user.created,
570
+ });
571
+ }
572
+
573
+ if (role) {
574
+ usersMap.get(user.id)!.roles.push({
575
+ id: role.id,
576
+ name: role.name,
577
+ organizationId: role.organizationId || organizationId,
578
+ });
579
+ }
580
+ }
581
+
582
+ return Array.from(usersMap.values());
583
+ } catch (error: any) {
584
+ throw this.handleError(error);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Assign role to user
590
+ */
591
+ async assignRole(data: AssignRoleData): Promise<UserRole> {
592
+ try {
593
+ return await this.pb.collection('user_roles').create(data);
594
+ } catch (error: any) {
595
+ throw this.handleError(error);
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Remove role from user
601
+ */
602
+ async removeRole(userRoleId: string): Promise<void> {
603
+ try {
604
+ await this.pb.collection('user_roles').delete(userRoleId);
605
+ } catch (error: any) {
606
+ throw this.handleError(error);
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Invite/create user in organization
612
+ */
613
+ async createUser(data: CreateUserRequest): Promise<void> {
614
+ try {
615
+ // Create user
616
+ const record = await this.pb.collection('users').create({
617
+ email: data.email,
618
+ password: data.password,
619
+ passwordConfirm: data.password,
620
+ name: data.name,
621
+ organizationId: data.organizationId,
622
+ });
623
+
624
+ // Assign roles if provided
625
+ if (data.roleIds && data.roleIds.length > 0) {
626
+ for (const roleId of data.roleIds) {
627
+ await this.assignRole({
628
+ userId: record.id,
629
+ roleId,
630
+ organizationId: data.organizationId,
631
+ });
632
+ }
633
+ }
634
+ } catch (error: any) {
635
+ throw this.handleError(error);
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Update user
641
+ */
642
+ async updateUser(userId: string, data: UpdateUserRequest): Promise<void> {
643
+ try {
644
+ await this.pb.collection('users').update(userId, data);
645
+ } catch (error: any) {
646
+ throw this.handleError(error);
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Delete user
652
+ */
653
+ async deleteUser(userId: string): Promise<void> {
654
+ try {
655
+ await this.pb.collection('users').delete(userId);
656
+ } catch (error: any) {
657
+ throw this.handleError(error);
658
+ }
659
+ }
660
+
661
+ // ==================== Audit Logging ====================
662
+
663
+ /**
664
+ * Log an action
665
+ */
666
+ async logAction(data: {
667
+ userId: string;
668
+ organizationId: string;
669
+ action: string;
670
+ resource: string;
671
+ resourceId?: string;
672
+ details?: Record<string, any>;
673
+ }): Promise<void> {
674
+ try {
675
+ await this.pb.collection('audit_logs').create(data);
676
+ } catch (error) {
677
+ // Log errors should not fail the main operation
678
+ console.error('Failed to log action:', error);
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Get audit logs for organization
684
+ */
685
+ async getAuditLogs(
686
+ organizationId: string,
687
+ page = 1,
688
+ perPage = 50
689
+ ): Promise<ListResult> {
690
+ try {
691
+ return await this.pb.collection('audit_logs').getList(page, perPage, {
692
+ filter: `organizationId = "${organizationId}"`,
693
+ sort: '-created',
694
+ expand: 'userId',
695
+ });
696
+ } catch (error: any) {
697
+ throw this.handleError(error);
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Handle PocketBase errors
703
+ */
704
+ private handleError(error: any): Error {
705
+ if (error?.data?.message) {
706
+ return new Error(error.data.message);
707
+ }
708
+ if (error?.message) {
709
+ return new Error(error.message);
710
+ }
711
+ return new Error('An unexpected error occurred');
712
+ }
713
+ }
714
+ ```
715
+
716
+ ---
717
+
718
+ ## Task 3: Create RBAC Hooks
719
+
720
+ **Files:**
721
+ - Create: `host/src/kernel/rbac/hooks.ts`
722
+
723
+ ### Step 1: Create RBAC hooks
724
+
725
+ **File:** `host/src/kernel/rbac/hooks.ts`
726
+
727
+ ```typescript
728
+ import { useCallback, useEffect, useState } from 'react';
729
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
730
+ import { usePocketBase } from '../providers';
731
+ import { RBACService } from './service';
732
+ import type {
733
+ Organization,
734
+ CreateOrganizationData,
735
+ UpdateOrganizationData,
736
+ Role,
737
+ CreateRoleData,
738
+ UpdateRoleData,
739
+ Permission,
740
+ UserRole,
741
+ AssignRoleData,
742
+ UserWithRoles,
743
+ CreateUserRequest,
744
+ UpdateUserRequest,
745
+ ResourceType,
746
+ ActionType,
747
+ } from './types';
748
+ import { useGlobalKernelState } from '../shared-state';
749
+
750
+ /**
751
+ * Get RBAC service instance
752
+ */
753
+ function useRBACService(): RBACService | null {
754
+ const pb = usePocketBase();
755
+ if (!pb) return null;
756
+ return new RBACService(pb);
757
+ }
758
+
759
+ /**
760
+ * Manage current organization
761
+ */
762
+ export function useCurrentOrganization() {
763
+ const { organization, setOrganization } = useGlobalKernelState();
764
+ const queryClient = useQueryClient();
765
+
766
+ const setCurrentOrganization = useCallback((org: Organization | null) => {
767
+ setOrganization(org);
768
+ // Clear related queries when switching organizations
769
+ queryClient.clear();
770
+ }, [setOrganization, queryClient]);
771
+
772
+ return {
773
+ organization,
774
+ setCurrentOrganization,
775
+ };
776
+ }
777
+
778
+ /**
779
+ * Get user's organizations
780
+ */
781
+ export function useOrganizations() {
782
+ const service = useRBACService();
783
+
784
+ return useQuery({
785
+ queryKey: ['organizations'],
786
+ queryFn: () => service?.getUserOrganizations() || [],
787
+ enabled: !!service,
788
+ });
789
+ }
790
+
791
+ /**
792
+ * Get single organization
793
+ */
794
+ export function useOrganization(id: string) {
795
+ const service = useRBACService();
796
+
797
+ return useQuery({
798
+ queryKey: ['organization', id],
799
+ queryFn: () => service?.getOrganization(id),
800
+ enabled: !!service && !!id,
801
+ });
802
+ }
803
+
804
+ /**
805
+ * Create organization
806
+ */
807
+ export function useCreateOrganization() {
808
+ const service = useRBACService();
809
+ const queryClient = useQueryClient();
810
+ const { setCurrentOrganization } = useCurrentOrganization();
811
+
812
+ return useMutation({
813
+ mutationFn: (data: CreateOrganizationData) => {
814
+ if (!service) throw new Error('Service not available');
815
+ return service.createOrganization(data);
816
+ },
817
+ onSuccess: (org) => {
818
+ queryClient.invalidateQueries({ queryKey: ['organizations'] });
819
+ setCurrentOrganization(org);
820
+ },
821
+ });
822
+ }
823
+
824
+ /**
825
+ * Update organization
826
+ */
827
+ export function useUpdateOrganization() {
828
+ const service = useRBACService();
829
+ const queryClient = useQueryClient();
830
+
831
+ return useMutation({
832
+ mutationFn: ({ id, data }: { id: string; data: UpdateOrganizationData }) => {
833
+ if (!service) throw new Error('Service not available');
834
+ return service.updateOrganization(id, data);
835
+ },
836
+ onSuccess: (_, { id }) => {
837
+ queryClient.invalidateQueries({ queryKey: ['organization', id] });
838
+ queryClient.invalidateQueries({ queryKey: ['organizations'] });
839
+ },
840
+ });
841
+ }
842
+
843
+ /**
844
+ * Get organization roles
845
+ */
846
+ export function useOrganizationRoles(organizationId: string | null) {
847
+ const service = useRBACService();
848
+
849
+ return useQuery({
850
+ queryKey: ['roles', organizationId],
851
+ queryFn: () => {
852
+ if (!organizationId || !service) return [];
853
+ return service.getOrganizationRoles(organizationId);
854
+ },
855
+ enabled: !!service && !!organizationId,
856
+ });
857
+ }
858
+
859
+ /**
860
+ * Create custom role
861
+ */
862
+ export function useCreateRole() {
863
+ const service = useRBACService();
864
+ const queryClient = useQueryClient();
865
+
866
+ return useMutation({
867
+ mutationFn: (data: CreateRoleData) => {
868
+ if (!service) throw new Error('Service not available');
869
+ return service.createRole(data);
870
+ },
871
+ onSuccess: (_, { organizationId }) => {
872
+ queryClient.invalidateQueries({ queryKey: ['roles', organizationId] });
873
+ },
874
+ });
875
+ }
876
+
877
+ /**
878
+ * Update role
879
+ */
880
+ export function useUpdateRole() {
881
+ const service = useRBACService();
882
+ const queryClient = useQueryClient();
883
+
884
+ return useMutation({
885
+ mutationFn: ({ id, data }: { id: string; data: UpdateRoleData }) => {
886
+ if (!service) throw new Error('Service not available');
887
+ return service.updateRole(id, data);
888
+ },
889
+ onSuccess: () => {
890
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
891
+ },
892
+ });
893
+ }
894
+
895
+ /**
896
+ * Delete role
897
+ */
898
+ export function useDeleteRole() {
899
+ const service = useRBACService();
900
+ const queryClient = useQueryClient();
901
+
902
+ return useMutation({
903
+ mutationFn: (id: string) => {
904
+ if (!service) throw new Error('Service not available');
905
+ return service.deleteRole(id);
906
+ },
907
+ onSuccess: () => {
908
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
909
+ },
910
+ });
911
+ }
912
+
913
+ /**
914
+ * Get all permissions
915
+ */
916
+ export function usePermissions() {
917
+ const service = useRBACService();
918
+
919
+ return useQuery({
920
+ queryKey: ['permissions'],
921
+ queryFn: () => service?.getAllPermissions() || [],
922
+ enabled: !!service,
923
+ });
924
+ }
925
+
926
+ /**
927
+ * Get organization users
928
+ */
929
+ export function useOrganizationUsers(organizationId: string | null) {
930
+ const service = useRBACService();
931
+
932
+ return useQuery({
933
+ queryKey: ['users', organizationId],
934
+ queryFn: () => {
935
+ if (!organizationId || !service) return [];
936
+ return service.getOrganizationUsers(organizationId);
937
+ },
938
+ enabled: !!service && !!organizationId,
939
+ });
940
+ }
941
+
942
+ /**
943
+ * Assign role to user
944
+ */
945
+ export function useAssignRole() {
946
+ const service = useRBACService();
947
+ const queryClient = useQueryClient();
948
+
949
+ return useMutation({
950
+ mutationFn: (data: AssignRoleData) => {
951
+ if (!service) throw new Error('Service not available');
952
+ return service.assignRole(data);
953
+ },
954
+ onSuccess: (_, { organizationId }) => {
955
+ queryClient.invalidateQueries({ queryKey: ['users', organizationId] });
956
+ },
957
+ });
958
+ }
959
+
960
+ /**
961
+ * Remove role from user
962
+ */
963
+ export function useRemoveRole() {
964
+ const service = useRBACService();
965
+ const queryClient = useQueryClient();
966
+
967
+ return useMutation({
968
+ mutationFn: ({ userRoleId, organizationId }: { userRoleId: string; organizationId: string }) => {
969
+ if (!service) throw new Error('Service not available');
970
+ return service.removeRole(userRoleId);
971
+ },
972
+ onSuccess: (_, { organizationId }) => {
973
+ queryClient.invalidateQueries({ queryKey: ['users', organizationId] });
974
+ },
975
+ });
976
+ }
977
+
978
+ /**
979
+ * Create user
980
+ */
981
+ export function useCreateUser() {
982
+ const service = useRBACService();
983
+ const queryClient = useQueryClient();
984
+
985
+ return useMutation({
986
+ mutationFn: (data: CreateUserRequest) => {
987
+ if (!service) throw new Error('Service not available');
988
+ return service.createUser(data);
989
+ },
990
+ onSuccess: (_, { organizationId }) => {
991
+ queryClient.invalidateQueries({ queryKey: ['users', organizationId] });
992
+ },
993
+ });
994
+ }
995
+
996
+ /**
997
+ * Update user
998
+ */
999
+ export function useUpdateUser() {
1000
+ const service = useRBACService();
1001
+ const queryClient = useQueryClient();
1002
+
1003
+ return useMutation({
1004
+ mutationFn: ({ userId, data }: { userId: string; data: UpdateUserRequest }) => {
1005
+ if (!service) throw new Error('Service not available');
1006
+ return service.updateUser(userId, data);
1007
+ },
1008
+ onSuccess: (_, { userId }) => {
1009
+ queryClient.invalidateQueries({ queryKey: ['users'] });
1010
+ queryClient.invalidateQueries({ queryKey: ['auth', 'user'] });
1011
+ },
1012
+ });
1013
+ }
1014
+
1015
+ /**
1016
+ * Delete user
1017
+ */
1018
+ export function useDeleteUser() {
1019
+ const service = useRBACService();
1020
+ const queryClient = useQueryClient();
1021
+
1022
+ return useMutation({
1023
+ mutationFn: ({ userId, organizationId }: { userId: string; organizationId: string }) => {
1024
+ if (!service) throw new Error('Service not available');
1025
+ return service.deleteUser(userId);
1026
+ },
1027
+ onSuccess: (_, { organizationId }) => {
1028
+ queryClient.invalidateQueries({ queryKey: ['users', organizationId] });
1029
+ },
1030
+ });
1031
+ }
1032
+
1033
+ /**
1034
+ * Check permissions
1035
+ */
1036
+ export function useHasPermission() {
1037
+ const service = useRBACService();
1038
+ const { user, organization } = useGlobalKernelState();
1039
+
1040
+ return useCallback(
1041
+ async (resource: ResourceType, action: ActionType): Promise<boolean> => {
1042
+ if (!service || !user?.id || !organization?.id) return false;
1043
+ const result = await service.hasPermission(user.id, organization.id, resource, action);
1044
+ return result.allowed;
1045
+ },
1046
+ [service, user, organization]
1047
+ );
1048
+ }
1049
+
1050
+ /**
1051
+ * Get user permissions for current organization
1052
+ */
1053
+ export function useUserPermissions() {
1054
+ const service = useRBACService();
1055
+ const { user, organization } = useGlobalKernelState();
1056
+
1057
+ return useQuery({
1058
+ queryKey: ['permissions', 'user', user?.id, organization?.id],
1059
+ queryFn: () => {
1060
+ if (!service || !user?.id || !organization?.id) return [];
1061
+ return service.getUserPermissions(user.id, organization.id);
1062
+ },
1063
+ enabled: !!service && !!user?.id && !!organization?.id,
1064
+ });
1065
+ }
1066
+
1067
+ /**
1068
+ * Require permission - returns whether user has permission
1069
+ */
1070
+ export function useRequirePermission(resource: ResourceType, action: ActionType) {
1071
+ const permissions = useUserPermissions();
1072
+ const [hasPermission, setHasPermission] = useState(false);
1073
+
1074
+ useEffect(() => {
1075
+ const perms = permissions.data || [];
1076
+
1077
+ const allowed =
1078
+ perms.some(p => p.resource === 'all' && p.action === 'all') ||
1079
+ perms.some(p => p.resource === resource && p.action === 'all') ||
1080
+ perms.some(p => p.resource === 'all' && p.action === action) ||
1081
+ perms.some(p => p.resource === resource && p.action === action);
1082
+
1083
+ setHasPermission(allowed);
1084
+ }, [permissions.data, resource, action]);
1085
+
1086
+ return {
1087
+ hasPermission,
1088
+ isLoading: permissions.isLoading,
1089
+ };
1090
+ }
1091
+
1092
+ /**
1093
+ * Get audit logs
1094
+ */
1095
+ export function useAuditLogs(organizationId: string | null, page = 1) {
1096
+ const service = useRBACService();
1097
+
1098
+ return useQuery({
1099
+ queryKey: ['audit-logs', organizationId, page],
1100
+ queryFn: () => {
1101
+ if (!organizationId || !service) return null;
1102
+ return service.getAuditLogs(organizationId, page);
1103
+ },
1104
+ enabled: !!service && !!organizationId,
1105
+ });
1106
+ }
1107
+ ```
1108
+
1109
+ ---
1110
+
1111
+ ## Task 4: Create Organization Selector Component
1112
+
1113
+ **Files:**
1114
+ - Create: `host/src/kernel/rbac/components/OrganizationSelector.tsx`
1115
+
1116
+ ### Step 1: Create organization selector
1117
+
1118
+ **File:** `host/src/kernel/rbac/components/OrganizationSelector.tsx`
1119
+
1120
+ ```typescript
1121
+ import { useCurrentOrganization, useOrganizations } from '../hooks';
1122
+ import {
1123
+ DropdownMenu,
1124
+ DropdownMenuContent,
1125
+ DropdownMenuItem,
1126
+ DropdownMenuLabel,
1127
+ DropdownMenuSeparator,
1128
+ DropdownMenuTrigger,
1129
+ } from '../../components/ui/dropdown-menu';
1130
+ import { Button } from '../../components/ui/button';
1131
+ import { Building2, ChevronDown, Loader2, Plus } from 'lucide-react';
1132
+ import { useNavigate } from '@modern-js/runtime/router';
1133
+
1134
+ export function OrganizationSelector() {
1135
+ const { organization, setCurrentOrganization } = useCurrentOrganization();
1136
+ const { data: organizations, isLoading } = useOrganizations();
1137
+ const navigate = useNavigate();
1138
+
1139
+ const handleSwitch = (orgId: string) => {
1140
+ const org = organizations?.find(o => o.id === orgId);
1141
+ if (org) {
1142
+ setCurrentOrganization(org);
1143
+ }
1144
+ };
1145
+
1146
+ const handleCreate = () => {
1147
+ navigate('/settings/organizations/new');
1148
+ };
1149
+
1150
+ return (
1151
+ <DropdownMenu>
1152
+ <DropdownMenuTrigger asChild>
1153
+ <Button variant="outline" className="gap-2">
1154
+ <Building2 className="h-4 w-4" />
1155
+ {isLoading ? (
1156
+ <Loader2 className="h-4 w-4 animate-spin" />
1157
+ ) : organization ? (
1158
+ <span className="max-w-[150px] truncate">{organization.name}</span>
1159
+ ) : (
1160
+ <span>Select Organization</span>
1161
+ )}
1162
+ <ChevronDown className="h-4 w-4" />
1163
+ </Button>
1164
+ </DropdownMenuTrigger>
1165
+ <DropdownMenuContent className="w-56" align="start">
1166
+ <DropdownMenuLabel>Organizations</DropdownMenuLabel>
1167
+ <DropdownMenuSeparator />
1168
+ {organizations?.map((org) => (
1169
+ <DropdownMenuItem
1170
+ key={org.id}
1171
+ onClick={() => handleSwitch(org.id)}
1172
+ className={organization?.id === org.id ? 'bg-accent' : ''}
1173
+ >
1174
+ <Building2 className="mr-2 h-4 w-4" />
1175
+ <span className="flex-1 truncate">{org.name}</span>
1176
+ {organization?.id === org.id && (
1177
+ <span className="text-xs text-muted-foreground">Current</span>
1178
+ )}
1179
+ </DropdownMenuItem>
1180
+ ))}
1181
+ <DropdownMenuSeparator />
1182
+ <DropdownMenuItem onClick={handleCreate}>
1183
+ <Plus className="mr-2 h-4 w-4" />
1184
+ Create Organization
1185
+ </DropdownMenuItem>
1186
+ </DropdownMenuContent>
1187
+ </DropdownMenu>
1188
+ );
1189
+ }
1190
+ ```
1191
+
1192
+ ---
1193
+
1194
+ ## Task 5: Create Permission Gate Component
1195
+
1196
+ **Files:**
1197
+ - Create: `host/src/kernel/rbac/components/PermissionGate.tsx`
1198
+
1199
+ ### Step 1: Create permission gate component
1200
+
1201
+ **File:** `host/src/kernel/rbac/components/PermissionGate.tsx`
1202
+
1203
+ ```typescript
1204
+ import { useRequirePermission } from '../hooks';
1205
+ import type { ResourceType, ActionType } from '../types';
1206
+ import { Alert, AlertDescription } from '../../components/ui/alert';
1207
+ import { Lock } from 'lucide-react';
1208
+
1209
+ interface PermissionGateProps {
1210
+ resource: ResourceType;
1211
+ action: ActionType;
1212
+ children: React.ReactNode;
1213
+ fallback?: React.ReactNode;
1214
+ }
1215
+
1216
+ export function PermissionGate({
1217
+ resource,
1218
+ action,
1219
+ children,
1220
+ fallback,
1221
+ }: PermissionGateProps) {
1222
+ const { hasPermission, isLoading } = useRequirePermission(resource, action);
1223
+
1224
+ if (isLoading) {
1225
+ return (
1226
+ <div className="flex min-h-[200px] items-center justify-center">
1227
+ <div className="text-sm text-muted-foreground">Checking permissions...</div>
1228
+ </div>
1229
+ );
1230
+ }
1231
+
1232
+ if (!hasPermission) {
1233
+ return (
1234
+ fallback || (
1235
+ <Alert variant="destructive">
1236
+ <Lock className="h-4 w-4" />
1237
+ <AlertDescription>
1238
+ You don't have permission to {action} {resource}.
1239
+ </AlertDescription>
1240
+ </Alert>
1241
+ )
1242
+ );
1243
+ }
1244
+
1245
+ return <>{children}</>;
1246
+ }
1247
+ ```
1248
+
1249
+ ---
1250
+
1251
+ ## Task 6: Update Topbar with Organization Selector
1252
+
1253
+ **Files:**
1254
+ - Modify: `host/src/layout/Topbar.tsx`
1255
+
1256
+ ### Step 1: Add organization selector to topbar
1257
+
1258
+ **File:** `host/src/layout/Topbar.tsx`
1259
+
1260
+ ```typescript
1261
+ import { useGlobalKernelState } from '../kernel/shared-state';
1262
+ import { Menu, Bell } from 'lucide-react';
1263
+ import { LogoutButton } from '../kernel/auth/components/LogoutButton';
1264
+ import { useAuth } from '../kernel/auth/hooks';
1265
+ import { OrganizationSelector } from '../kernel/rbac/components/OrganizationSelector';
1266
+
1267
+ export function Topbar() {
1268
+ const { toggleMobileMenu } = useGlobalKernelState();
1269
+ const { isAuthenticated } = useAuth();
1270
+
1271
+ return (
1272
+ <header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-6">
1273
+ {/* Mobile menu button */}
1274
+ <button
1275
+ onClick={toggleMobileMenu}
1276
+ className="lg:hidden rounded-lg p-2 text-muted-foreground hover:bg-muted"
1277
+ >
1278
+ <Menu className="h-5 w-5" />
1279
+ </button>
1280
+
1281
+ {/* Organization selector */}
1282
+ {isAuthenticated && <OrganizationSelector />}
1283
+
1284
+ {/* Breadcrumb/spacer */}
1285
+ <div className="flex-1" />
1286
+
1287
+ {/* Actions */}
1288
+ <div className="flex items-center gap-2">
1289
+ {isAuthenticated && (
1290
+ <button className="rounded-lg p-2 text-muted-foreground hover:bg-muted">
1291
+ <Bell className="h-5 w-5" />
1292
+ </button>
1293
+ )}
1294
+
1295
+ {isAuthenticated && <LogoutButton />}
1296
+ </div>
1297
+ </header>
1298
+ );
1299
+ }
1300
+ ```
1301
+
1302
+ ---
1303
+
1304
+ ## Task 7: Create RBAC Context and Initial Organization Setup
1305
+
1306
+ **Files:**
1307
+ - Create: `host/src/kernel/rbac/utils.ts`
1308
+ - Modify: `host/src/kernel/providers/PocketBaseProvider.tsx`
1309
+
1310
+ ### Step 1: Create RBAC utilities
1311
+
1312
+ **File:** `host/src/kernel/rbac/utils.ts`
1313
+
1314
+ ```typescript
1315
+ import type { Organization } from './types';
1316
+
1317
+ /**
1318
+ * Initialize first organization for user if none exists
1319
+ */
1320
+ export async function initializeFirstOrganization(
1321
+ pb: any,
1322
+ userId: string
1323
+ ): Promise<Organization | null> {
1324
+ try {
1325
+ // Check if user has any organizations
1326
+ const userRecord = await pb.collection('users').getOne(userId, {
1327
+ expand: 'organizations',
1328
+ });
1329
+
1330
+ const organizations = userRecord.expand?.organizations || [];
1331
+
1332
+ if (organizations.length > 0) {
1333
+ return organizations[0] as Organization;
1334
+ }
1335
+
1336
+ // Create default organization
1337
+ const org = await pb.collection('organizations').create({
1338
+ name: `${userRecord.name || 'User'}'s Organization`,
1339
+ slug: `${userRecord.email?.split('@')[0] || 'user'}-${Date.now()}`,
1340
+ ownerId: userId,
1341
+ });
1342
+
1343
+ return org as Organization;
1344
+ } catch (error) {
1345
+ console.error('Failed to initialize organization:', error);
1346
+ return null;
1347
+ }
1348
+ }
1349
+ ```
1350
+
1351
+ ### Step 2: Update PocketBaseProvider to initialize organization
1352
+
1353
+ **File:** `host/src/kernel/providers/PocketBaseProvider.tsx`
1354
+
1355
+ Add organization initialization after auth:
1356
+
1357
+ ```typescript
1358
+ import { createContext, useContext, useEffect, useState } from 'react';
1359
+ import PocketBase from 'pocketbase';
1360
+ import { useGlobalKernelState } from '../shared-state';
1361
+ import { initializeFirstOrganization } from '../rbac/utils';
1362
+
1363
+ const PocketBaseContext = createContext<PocketBase | null>(null);
1364
+
1365
+ export function PocketBaseProvider({ children }: { children: React.ReactNode }) {
1366
+ const [pb, setPb] = useState<PocketBase | null>(null);
1367
+ const { token, clearAuth, setOrganization } = useGlobalKernelState();
1368
+
1369
+ useEffect(() => {
1370
+ // Initialize PocketBase client
1371
+ const client = new PocketBase(import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090');
1372
+
1373
+ // Load stored token
1374
+ const storedToken = localStorage.getItem('pocketbase_auth');
1375
+ if (storedToken) {
1376
+ client.authStore.save(storedToken, null);
1377
+ }
1378
+
1379
+ setPb(client);
1380
+
1381
+ // Set up auth cleanup on token invalidation
1382
+ client.authStore.onChange(async (token, model) => {
1383
+ if (!token) {
1384
+ clearAuth();
1385
+ localStorage.removeItem('pocketbase_auth');
1386
+ setOrganization(null);
1387
+ } else {
1388
+ localStorage.setItem('pocketbase_auth', token);
1389
+
1390
+ // Initialize organization on login
1391
+ if (model && !pb.authStore.record?.organizationId) {
1392
+ const org = await initializeFirstOrganization(client, model.id);
1393
+ if (org) {
1394
+ setOrganization(org);
1395
+ }
1396
+ }
1397
+ }
1398
+ });
1399
+
1400
+ return () => {
1401
+ // Cleanup on unmount
1402
+ };
1403
+ }, [clearAuth, setOrganization]);
1404
+
1405
+ // Update auth store when token changes in state
1406
+ useEffect(() => {
1407
+ if (pb && token && pb.authStore.token !== token) {
1408
+ pb.authStore.save(token, null);
1409
+ }
1410
+ }, [pb, token]);
1411
+
1412
+ if (!pb) {
1413
+ return null; // or a loading spinner
1414
+ }
1415
+
1416
+ return (
1417
+ <PocketBaseContext.Provider value={pb}>
1418
+ {children}
1419
+ </PocketBaseContext.Provider>
1420
+ );
1421
+ }
1422
+
1423
+ export function usePocketBase() {
1424
+ const context = useContext(PocketBaseContext);
1425
+ if (!context) {
1426
+ throw new Error('usePocketBase must be used within PocketBaseProvider');
1427
+ }
1428
+ return context;
1429
+ }
1430
+ ```
1431
+
1432
+ ---
1433
+
1434
+ ## Task 8: Create RBAC Module Barrel Export
1435
+
1436
+ **Files:**
1437
+ - Create: `host/src/kernel/rbac/index.ts`
1438
+
1439
+ ### Step 1: Create barrel export
1440
+
1441
+ **File:** `host/src/kernel/rbac/index.ts`
1442
+
1443
+ ```typescript
1444
+ export * from './types';
1445
+ export * from './service';
1446
+ export * from './hooks';
1447
+ export * from './components/PermissionGate';
1448
+ export * from './components/OrganizationSelector';
1449
+ export * from './utils';
1450
+ ```
1451
+
1452
+ ---
1453
+
1454
+ ## Verification
1455
+
1456
+ ### Step 1: Build the host
1457
+
1458
+ **Run:**
1459
+
1460
+ ```bash
1461
+ cd host
1462
+ pnpm run build
1463
+ ```
1464
+
1465
+ Expected: Build completes without errors.
1466
+
1467
+ ### Step 2: Start development server
1468
+
1469
+ **Run:**
1470
+
1471
+ ```bash
1472
+ cd host
1473
+ pnpm run dev
1474
+ ```
1475
+
1476
+ Expected: Server starts on http://localhost:8080
1477
+
1478
+ ### Step 3: Test organization initialization
1479
+
1480
+ 1. Login with admin credentials
1481
+ 2. After login, organization selector should appear in topbar
1482
+ 3. First organization should be auto-created
1483
+ 4. Should see organization name in dropdown
1484
+
1485
+ ### Step 4: Test organization switching (after implementing settings pages)
1486
+
1487
+ 1. Open organization selector dropdown
1488
+ 2. Should list all user's organizations
1489
+ 3. Switching should update current organization context
1490
+
1491
+ ---
1492
+
1493
+ ## Summary
1494
+
1495
+ After completing this document, you will have:
1496
+
1497
+ 1. ✅ Complete RBAC service with organizations, roles, permissions, and users
1498
+ 2. ✅ Organization selector component in topbar
1499
+ 3. ✅ Permission gate component for protecting UI elements
1500
+ 4. ✅ Hooks for managing organizations, roles, users, and permissions
1501
+ 5. ✅ Auto-initialization of first organization for new users
1502
+ 6. ✅ Organization context in global state
1503
+ 7. ✅ System roles (Owner, Admin, Member, Guest) with default permissions
1504
+ 8. ✅ Support for custom roles and permissions
1505
+
1506
+ **Next:** `06-ui-components.md` - Complete Shadcn UI component library with all components.
1507
+
1508
+ ---
1509
+
1510
+ ## Files Created
1511
+
1512
+ ```
1513
+ host/
1514
+ └── src/
1515
+ └── kernel/
1516
+ ├── rbac/
1517
+ │ ├── types.ts
1518
+ │ ├── service.ts
1519
+ │ ├── hooks.ts
1520
+ │ ├── utils.ts
1521
+ │ ├── components/
1522
+ │ │ ├── OrganizationSelector.tsx
1523
+ │ │ └── PermissionGate.tsx
1524
+ │ └── index.ts
1525
+ └── providers/
1526
+ └── PocketBaseProvider.tsx (modified)
1527
+ ```