hazo_auth 3.0.4 → 4.0.0

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 (61) hide show
  1. package/README.md +146 -0
  2. package/SETUP_CHECKLIST.md +369 -0
  3. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
  4. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +2 -2
  5. package/dist/components/layouts/rbac_test/index.d.ts +15 -0
  6. package/dist/components/layouts/rbac_test/index.d.ts.map +1 -0
  7. package/dist/components/layouts/rbac_test/index.js +378 -0
  8. package/dist/components/layouts/shared/components/password_field.js +1 -1
  9. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  10. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  11. package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
  12. package/dist/components/layouts/user_management/components/roles_matrix.d.ts +2 -3
  13. package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
  14. package/dist/components/layouts/user_management/components/roles_matrix.js +133 -8
  15. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +12 -0
  16. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -0
  17. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +291 -0
  18. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +13 -0
  19. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -0
  20. package/dist/components/layouts/user_management/components/scope_labels_tab.js +158 -0
  21. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +11 -0
  22. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -0
  23. package/dist/components/layouts/user_management/components/user_scopes_tab.js +267 -0
  24. package/dist/components/layouts/user_management/index.d.ts +9 -2
  25. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  26. package/dist/components/layouts/user_management/index.js +22 -6
  27. package/dist/components/ui/select.d.ts +14 -0
  28. package/dist/components/ui/select.d.ts.map +1 -0
  29. package/dist/components/ui/select.js +59 -0
  30. package/dist/components/ui/tree-view.d.ts +108 -0
  31. package/dist/components/ui/tree-view.d.ts.map +1 -0
  32. package/dist/components/ui/tree-view.js +194 -0
  33. package/dist/lib/auth/auth_types.d.ts +45 -0
  34. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  35. package/dist/lib/auth/auth_types.js +13 -0
  36. package/dist/lib/auth/hazo_get_auth.server.d.ts +4 -2
  37. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  38. package/dist/lib/auth/hazo_get_auth.server.js +107 -3
  39. package/dist/lib/auth/scope_cache.d.ts +92 -0
  40. package/dist/lib/auth/scope_cache.d.ts.map +1 -0
  41. package/dist/lib/auth/scope_cache.js +171 -0
  42. package/dist/lib/scope_hierarchy_config.server.d.ts +39 -0
  43. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -0
  44. package/dist/lib/scope_hierarchy_config.server.js +96 -0
  45. package/dist/lib/services/email_service.d.ts.map +1 -1
  46. package/dist/lib/services/email_service.js +7 -2
  47. package/dist/lib/services/profile_picture_service.d.ts +1 -7
  48. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  49. package/dist/lib/services/profile_picture_service.js +77 -32
  50. package/dist/lib/services/registration_service.js +1 -1
  51. package/dist/lib/services/scope_labels_service.d.ts +48 -0
  52. package/dist/lib/services/scope_labels_service.d.ts.map +1 -0
  53. package/dist/lib/services/scope_labels_service.js +277 -0
  54. package/dist/lib/services/scope_service.d.ts +114 -0
  55. package/dist/lib/services/scope_service.d.ts.map +1 -0
  56. package/dist/lib/services/scope_service.js +582 -0
  57. package/dist/lib/services/user_scope_service.d.ts +74 -0
  58. package/dist/lib/services/user_scope_service.d.ts.map +1 -0
  59. package/dist/lib/services/user_scope_service.js +415 -0
  60. package/hazo_auth_config.example.ini +1 -1
  61. package/package.json +3 -1
@@ -0,0 +1,415 @@
1
+ import { createCrudService } from "hazo_connect/server";
2
+ import { create_app_logger } from "../app_logger";
3
+ import { sanitize_error_for_user } from "../utils/error_sanitizer";
4
+ import { SCOPE_LEVEL_NUMBERS, get_scope_by_id, get_scope_by_seq, get_scope_ancestors, } from "./scope_service";
5
+ // section: helpers
6
+ /**
7
+ * Gets all scope assignments for a user
8
+ */
9
+ export async function get_user_scopes(adapter, user_id) {
10
+ try {
11
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
12
+ const scopes = await user_scope_service.findBy({ user_id });
13
+ return {
14
+ success: true,
15
+ scopes: Array.isArray(scopes) ? scopes : [],
16
+ };
17
+ }
18
+ catch (error) {
19
+ const logger = create_app_logger();
20
+ const error_message = sanitize_error_for_user(error, {
21
+ logToConsole: true,
22
+ logToLogger: true,
23
+ logger,
24
+ context: {
25
+ filename: "user_scope_service.ts",
26
+ line_number: 0,
27
+ operation: "get_user_scopes",
28
+ user_id,
29
+ },
30
+ });
31
+ return {
32
+ success: false,
33
+ error: error_message,
34
+ };
35
+ }
36
+ }
37
+ /**
38
+ * Gets all users assigned to a specific scope
39
+ */
40
+ export async function get_users_by_scope(adapter, scope_type, scope_id) {
41
+ try {
42
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
43
+ const scopes = await user_scope_service.findBy({ scope_type, scope_id });
44
+ return {
45
+ success: true,
46
+ scopes: Array.isArray(scopes) ? scopes : [],
47
+ };
48
+ }
49
+ catch (error) {
50
+ const logger = create_app_logger();
51
+ const error_message = sanitize_error_for_user(error, {
52
+ logToConsole: true,
53
+ logToLogger: true,
54
+ logger,
55
+ context: {
56
+ filename: "user_scope_service.ts",
57
+ line_number: 0,
58
+ operation: "get_users_by_scope",
59
+ scope_type,
60
+ scope_id,
61
+ },
62
+ });
63
+ return {
64
+ success: false,
65
+ error: error_message,
66
+ };
67
+ }
68
+ }
69
+ /**
70
+ * Assigns a scope to a user
71
+ */
72
+ export async function assign_user_scope(adapter, user_id, scope_type, scope_id, scope_seq) {
73
+ try {
74
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
75
+ const now = new Date().toISOString();
76
+ // Check if assignment already exists
77
+ const existing = await user_scope_service.findBy({
78
+ user_id,
79
+ scope_type,
80
+ scope_id,
81
+ });
82
+ if (Array.isArray(existing) && existing.length > 0) {
83
+ return {
84
+ success: true,
85
+ scope: existing[0], // Already assigned
86
+ };
87
+ }
88
+ // Verify the scope exists
89
+ const scope_result = await get_scope_by_id(adapter, scope_type, scope_id);
90
+ if (!scope_result.success) {
91
+ return {
92
+ success: false,
93
+ error: "Scope not found",
94
+ };
95
+ }
96
+ // Insert new assignment
97
+ const inserted = await user_scope_service.insert({
98
+ user_id,
99
+ scope_id,
100
+ scope_seq,
101
+ scope_type,
102
+ created_at: now,
103
+ changed_at: now,
104
+ });
105
+ if (!Array.isArray(inserted) || inserted.length === 0) {
106
+ return {
107
+ success: false,
108
+ error: "Failed to assign scope to user",
109
+ };
110
+ }
111
+ return {
112
+ success: true,
113
+ scope: inserted[0],
114
+ };
115
+ }
116
+ catch (error) {
117
+ const logger = create_app_logger();
118
+ const error_message = sanitize_error_for_user(error, {
119
+ logToConsole: true,
120
+ logToLogger: true,
121
+ logger,
122
+ context: {
123
+ filename: "user_scope_service.ts",
124
+ line_number: 0,
125
+ operation: "assign_user_scope",
126
+ user_id,
127
+ scope_type,
128
+ scope_id,
129
+ },
130
+ });
131
+ return {
132
+ success: false,
133
+ error: error_message,
134
+ };
135
+ }
136
+ }
137
+ /**
138
+ * Removes a scope assignment from a user
139
+ */
140
+ export async function remove_user_scope(adapter, user_id, scope_type, scope_id) {
141
+ try {
142
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
143
+ // Find the assignment
144
+ const existing = await user_scope_service.findBy({
145
+ user_id,
146
+ scope_type,
147
+ scope_id,
148
+ });
149
+ if (!Array.isArray(existing) || existing.length === 0) {
150
+ return {
151
+ success: true, // Already not assigned
152
+ };
153
+ }
154
+ // Delete using a filter-based approach since there's no single ID
155
+ // Note: hazo_user_scopes uses composite primary key (user_id, scope_id, scope_type)
156
+ // We need to find and delete by the combination
157
+ const existing_scope = existing[0];
158
+ // Use raw delete with filters if available, otherwise try by the composite key pattern
159
+ // Most hazo_connect adapters support deleteBy or similar
160
+ try {
161
+ // Try to delete by finding records with matching criteria
162
+ const all_user_scopes = await user_scope_service.findBy({ user_id });
163
+ if (Array.isArray(all_user_scopes)) {
164
+ for (const scope of all_user_scopes) {
165
+ const s = scope;
166
+ if (s.scope_type === scope_type && s.scope_id === scope_id) {
167
+ // If the record has an id field, use it
168
+ if (scope.id) {
169
+ await user_scope_service.deleteById(scope.id);
170
+ }
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ }
176
+ catch (_a) {
177
+ // Fallback: Some adapters might not support this pattern
178
+ const logger = create_app_logger();
179
+ logger.warn("user_scope_delete_fallback", {
180
+ filename: "user_scope_service.ts",
181
+ line_number: 0,
182
+ note: "Delete by composite key not fully supported",
183
+ });
184
+ }
185
+ return {
186
+ success: true,
187
+ scope: existing_scope,
188
+ };
189
+ }
190
+ catch (error) {
191
+ const logger = create_app_logger();
192
+ const error_message = sanitize_error_for_user(error, {
193
+ logToConsole: true,
194
+ logToLogger: true,
195
+ logger,
196
+ context: {
197
+ filename: "user_scope_service.ts",
198
+ line_number: 0,
199
+ operation: "remove_user_scope",
200
+ user_id,
201
+ scope_type,
202
+ scope_id,
203
+ },
204
+ });
205
+ return {
206
+ success: false,
207
+ error: error_message,
208
+ };
209
+ }
210
+ }
211
+ /**
212
+ * Bulk update user scope assignments
213
+ * Replaces all existing assignments with the new set
214
+ */
215
+ export async function update_user_scopes(adapter, user_id, new_scopes) {
216
+ try {
217
+ // Get current scopes
218
+ const current_result = await get_user_scopes(adapter, user_id);
219
+ if (!current_result.success) {
220
+ return current_result;
221
+ }
222
+ const current_scopes = current_result.scopes || [];
223
+ // Determine scopes to add and remove
224
+ const current_keys = new Set(current_scopes.map((s) => `${s.scope_type}:${s.scope_id}`));
225
+ const new_keys = new Set(new_scopes.map((s) => `${s.scope_type}:${s.scope_id}`));
226
+ // Remove scopes not in new set
227
+ for (const scope of current_scopes) {
228
+ const key = `${scope.scope_type}:${scope.scope_id}`;
229
+ if (!new_keys.has(key)) {
230
+ await remove_user_scope(adapter, user_id, scope.scope_type, scope.scope_id);
231
+ }
232
+ }
233
+ // Add scopes not in current set
234
+ for (const scope of new_scopes) {
235
+ const key = `${scope.scope_type}:${scope.scope_id}`;
236
+ if (!current_keys.has(key)) {
237
+ const result = await assign_user_scope(adapter, user_id, scope.scope_type, scope.scope_id, scope.scope_seq);
238
+ if (!result.success) {
239
+ return result;
240
+ }
241
+ }
242
+ }
243
+ // Return updated scopes
244
+ return get_user_scopes(adapter, user_id);
245
+ }
246
+ catch (error) {
247
+ const logger = create_app_logger();
248
+ const error_message = sanitize_error_for_user(error, {
249
+ logToConsole: true,
250
+ logToLogger: true,
251
+ logger,
252
+ context: {
253
+ filename: "user_scope_service.ts",
254
+ line_number: 0,
255
+ operation: "update_user_scopes",
256
+ user_id,
257
+ },
258
+ });
259
+ return {
260
+ success: false,
261
+ error: error_message,
262
+ };
263
+ }
264
+ }
265
+ /**
266
+ * Checks if a user has access to a specific scope
267
+ * Access is granted if:
268
+ * 1. User has the exact scope assigned, OR
269
+ * 2. User has access to an ancestor scope (L2 user can access L3, L4, etc.)
270
+ *
271
+ * @param adapter - HazoConnect adapter
272
+ * @param user_id - User ID to check
273
+ * @param target_scope_type - The scope level being accessed
274
+ * @param target_scope_id - The scope ID being accessed (optional if target_scope_seq provided)
275
+ * @param target_scope_seq - The scope seq being accessed (optional if target_scope_id provided)
276
+ */
277
+ export async function check_user_scope_access(adapter, user_id, target_scope_type, target_scope_id, target_scope_seq) {
278
+ try {
279
+ // Resolve scope ID if only seq provided
280
+ let resolved_scope_id = target_scope_id;
281
+ let resolved_scope_seq = target_scope_seq;
282
+ if (!resolved_scope_id && resolved_scope_seq) {
283
+ const scope_result = await get_scope_by_seq(adapter, target_scope_type, resolved_scope_seq);
284
+ if (!scope_result.success || !scope_result.scope) {
285
+ return { has_access: false };
286
+ }
287
+ resolved_scope_id = scope_result.scope.id;
288
+ }
289
+ else if (resolved_scope_id && !resolved_scope_seq) {
290
+ const scope_result = await get_scope_by_id(adapter, target_scope_type, resolved_scope_id);
291
+ if (!scope_result.success || !scope_result.scope) {
292
+ return { has_access: false };
293
+ }
294
+ resolved_scope_seq = scope_result.scope.seq;
295
+ }
296
+ if (!resolved_scope_id) {
297
+ return { has_access: false };
298
+ }
299
+ // Get user's assigned scopes
300
+ const user_scopes_result = await get_user_scopes(adapter, user_id);
301
+ if (!user_scopes_result.success || !user_scopes_result.scopes) {
302
+ return { has_access: false };
303
+ }
304
+ const user_scopes = user_scopes_result.scopes;
305
+ // Check 1: Does user have exact scope assigned?
306
+ for (const user_scope of user_scopes) {
307
+ if (user_scope.scope_type === target_scope_type &&
308
+ user_scope.scope_id === resolved_scope_id) {
309
+ return {
310
+ has_access: true,
311
+ access_via: {
312
+ scope_type: user_scope.scope_type,
313
+ scope_id: user_scope.scope_id,
314
+ scope_seq: user_scope.scope_seq,
315
+ },
316
+ user_scopes,
317
+ };
318
+ }
319
+ }
320
+ // Check 2: Does user have access via an ancestor scope?
321
+ // Get all ancestors of the target scope
322
+ const ancestors_result = await get_scope_ancestors(adapter, target_scope_type, resolved_scope_id);
323
+ if (ancestors_result.success && ancestors_result.scopes) {
324
+ const ancestors = ancestors_result.scopes;
325
+ // For each ancestor, check if user has it assigned
326
+ // Need to determine the level of each ancestor
327
+ let current_level = SCOPE_LEVEL_NUMBERS[target_scope_type];
328
+ for (const ancestor of ancestors) {
329
+ current_level--;
330
+ const ancestor_level = `hazo_scopes_l${current_level}`;
331
+ for (const user_scope of user_scopes) {
332
+ if (user_scope.scope_type === ancestor_level &&
333
+ user_scope.scope_id === ancestor.id) {
334
+ // User has access via this ancestor
335
+ return {
336
+ has_access: true,
337
+ access_via: {
338
+ scope_type: user_scope.scope_type,
339
+ scope_id: user_scope.scope_id,
340
+ scope_seq: user_scope.scope_seq,
341
+ },
342
+ user_scopes,
343
+ };
344
+ }
345
+ }
346
+ }
347
+ }
348
+ // No access
349
+ return {
350
+ has_access: false,
351
+ user_scopes,
352
+ };
353
+ }
354
+ catch (error) {
355
+ const logger = create_app_logger();
356
+ logger.error("check_user_scope_access_error", {
357
+ filename: "user_scope_service.ts",
358
+ line_number: 0,
359
+ error: error instanceof Error ? error.message : "Unknown error",
360
+ user_id,
361
+ target_scope_type,
362
+ target_scope_id,
363
+ });
364
+ return { has_access: false };
365
+ }
366
+ }
367
+ /**
368
+ * Gets the effective scopes a user has access to
369
+ * This includes directly assigned scopes and all their descendants
370
+ */
371
+ export async function get_user_effective_scopes(adapter, user_id) {
372
+ try {
373
+ const user_scopes_result = await get_user_scopes(adapter, user_id);
374
+ if (!user_scopes_result.success) {
375
+ return {
376
+ success: false,
377
+ error: user_scopes_result.error,
378
+ };
379
+ }
380
+ const direct_scopes = user_scopes_result.scopes || [];
381
+ // Determine which levels user has inherited access to
382
+ // If user has L2 access, they inherit L3, L4, L5, L6, L7
383
+ const inherited_levels = new Set();
384
+ for (const scope of direct_scopes) {
385
+ const level_num = SCOPE_LEVEL_NUMBERS[scope.scope_type];
386
+ // Add all levels below this one
387
+ for (let i = level_num + 1; i <= 7; i++) {
388
+ inherited_levels.add(`hazo_scopes_l${i}`);
389
+ }
390
+ }
391
+ return {
392
+ success: true,
393
+ direct_scopes,
394
+ inherited_scope_types: Array.from(inherited_levels),
395
+ };
396
+ }
397
+ catch (error) {
398
+ const logger = create_app_logger();
399
+ const error_message = sanitize_error_for_user(error, {
400
+ logToConsole: true,
401
+ logToLogger: true,
402
+ logger,
403
+ context: {
404
+ filename: "user_scope_service.ts",
405
+ line_number: 0,
406
+ operation: "get_user_effective_scopes",
407
+ user_id,
408
+ },
409
+ });
410
+ return {
411
+ success: false,
412
+ error: error_message,
413
+ };
414
+ }
415
+ }
@@ -296,7 +296,7 @@ enable_admin_ui = true
296
296
  # Application permission list defaults (comma-separated)
297
297
  # These permissions will be shown in the Permissions tab and can be migrated to the database
298
298
  # Example: application_permission_list_defaults = PERM_ONE,PERM_TWO,PERM_THREE
299
- application_permission_list_defaults = admin_user_management,admin_role_management,admin_permission_management
299
+ application_permission_list_defaults = admin_user_management,admin_role_management,admin_permission_management,admin_scope_hierarchy_management,admin_user_scope_assignment,admin_test_access
300
300
 
301
301
  [hazo_auth__initial_setup]
302
302
  # Initial setup configuration for initializing users, roles, and permissions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "3.0.4",
3
+ "version": "4.0.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -144,12 +144,14 @@
144
144
  "test:watch": "cross-env NODE_ENV=test POSTGREST_URL=http://209.38.26.241:4402 POSTGREST_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBpX3VzZXIifQ.zBoUGymrxTUk1DNYIGUCtQU4HFaWEHlbE9_8Y3hUaTw jest --watch"
145
145
  },
146
146
  "dependencies": {
147
+ "@radix-ui/react-accordion": "^1.2.12",
147
148
  "@radix-ui/react-alert-dialog": "^1.1.15",
148
149
  "@radix-ui/react-avatar": "^1.1.11",
149
150
  "@radix-ui/react-checkbox": "^1.3.3",
150
151
  "@radix-ui/react-dialog": "^1.1.15",
151
152
  "@radix-ui/react-dropdown-menu": "^2.1.16",
152
153
  "@radix-ui/react-label": "^2.1.8",
154
+ "@radix-ui/react-select": "^2.2.6",
153
155
  "@radix-ui/react-separator": "^1.1.8",
154
156
  "@radix-ui/react-slot": "^1.2.4",
155
157
  "@radix-ui/react-switch": "^1.2.6",