hazo_auth 4.2.0 → 4.4.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 (136) hide show
  1. package/bin/hazo_auth.mjs +35 -0
  2. package/cli-src/assets/images/forgot_password_default.jpg +0 -0
  3. package/cli-src/assets/images/login_default.jpg +0 -0
  4. package/cli-src/assets/images/register_default.jpg +0 -0
  5. package/cli-src/assets/images/reset_password_default.jpg +0 -0
  6. package/cli-src/assets/images/verify_email_default.jpg +0 -0
  7. package/cli-src/cli/generate.ts +276 -0
  8. package/cli-src/cli/index.ts +207 -0
  9. package/cli-src/cli/init.ts +254 -0
  10. package/cli-src/cli/init_users.ts +376 -0
  11. package/cli-src/cli/validate.ts +581 -0
  12. package/cli-src/lib/already_logged_in_config.server.ts +46 -0
  13. package/cli-src/lib/app_logger.ts +24 -0
  14. package/cli-src/lib/auth/auth_cache.ts +220 -0
  15. package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
  16. package/cli-src/lib/auth/auth_types.ts +117 -0
  17. package/cli-src/lib/auth/auth_utils.server.ts +196 -0
  18. package/cli-src/lib/auth/dev_lock_validator.edge.ts +171 -0
  19. package/cli-src/lib/auth/hazo_get_auth.server.ts +583 -0
  20. package/cli-src/lib/auth/index.ts +23 -0
  21. package/cli-src/lib/auth/nextauth_config.ts +227 -0
  22. package/cli-src/lib/auth/org_cache.ts +148 -0
  23. package/cli-src/lib/auth/scope_cache.ts +233 -0
  24. package/cli-src/lib/auth/server_auth.ts +88 -0
  25. package/cli-src/lib/auth/session_token_validator.edge.ts +92 -0
  26. package/cli-src/lib/auth_utility_config.server.ts +136 -0
  27. package/cli-src/lib/config/config_loader.server.ts +164 -0
  28. package/cli-src/lib/config/default_config.ts +243 -0
  29. package/cli-src/lib/dev_lock_config.server.ts +148 -0
  30. package/cli-src/lib/email_verification_config.server.ts +63 -0
  31. package/cli-src/lib/file_types_config.server.ts +25 -0
  32. package/cli-src/lib/forgot_password_config.server.ts +63 -0
  33. package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
  34. package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
  35. package/cli-src/lib/hazo_connect_setup.ts +54 -0
  36. package/cli-src/lib/index.ts +46 -0
  37. package/cli-src/lib/login_config.server.ts +106 -0
  38. package/cli-src/lib/messages_config.server.ts +45 -0
  39. package/cli-src/lib/migrations/apply_migration.ts +105 -0
  40. package/cli-src/lib/multi_tenancy_config.server.ts +94 -0
  41. package/cli-src/lib/my_settings_config.server.ts +135 -0
  42. package/cli-src/lib/oauth_config.server.ts +87 -0
  43. package/cli-src/lib/password_requirements_config.server.ts +40 -0
  44. package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
  45. package/cli-src/lib/profile_picture_config.server.ts +56 -0
  46. package/cli-src/lib/register_config.server.ts +101 -0
  47. package/cli-src/lib/reset_password_config.server.ts +103 -0
  48. package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
  49. package/cli-src/lib/services/email_service.ts +587 -0
  50. package/cli-src/lib/services/email_verification_service.ts +270 -0
  51. package/cli-src/lib/services/index.ts +16 -0
  52. package/cli-src/lib/services/login_service.ts +150 -0
  53. package/cli-src/lib/services/oauth_service.ts +494 -0
  54. package/cli-src/lib/services/org_service.ts +965 -0
  55. package/cli-src/lib/services/password_change_service.ts +154 -0
  56. package/cli-src/lib/services/password_reset_service.ts +418 -0
  57. package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
  58. package/cli-src/lib/services/profile_picture_service.ts +451 -0
  59. package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
  60. package/cli-src/lib/services/registration_service.ts +185 -0
  61. package/cli-src/lib/services/scope_labels_service.ts +348 -0
  62. package/cli-src/lib/services/scope_service.ts +778 -0
  63. package/cli-src/lib/services/session_token_service.ts +178 -0
  64. package/cli-src/lib/services/token_service.ts +240 -0
  65. package/cli-src/lib/services/user_profiles_cache.ts +189 -0
  66. package/cli-src/lib/services/user_profiles_service.ts +264 -0
  67. package/cli-src/lib/services/user_scope_service.ts +554 -0
  68. package/cli-src/lib/services/user_update_service.ts +141 -0
  69. package/cli-src/lib/ui_shell_config.server.ts +73 -0
  70. package/cli-src/lib/ui_sizes_config.server.ts +37 -0
  71. package/cli-src/lib/user_fields_config.server.ts +31 -0
  72. package/cli-src/lib/user_management_config.server.ts +39 -0
  73. package/cli-src/lib/user_profiles_config.server.ts +55 -0
  74. package/cli-src/lib/utils/api_route_helpers.ts +60 -0
  75. package/cli-src/lib/utils/error_sanitizer.ts +75 -0
  76. package/cli-src/lib/utils/password_validator.ts +65 -0
  77. package/cli-src/lib/utils.ts +11 -0
  78. package/cli-src/server/logging/logger_service.ts +56 -0
  79. package/cli-src/server/types/app_types.ts +74 -0
  80. package/cli-src/server/types/express.d.ts +16 -0
  81. package/dist/cli/index.js +18 -0
  82. package/dist/cli/init_users.d.ts +17 -0
  83. package/dist/cli/init_users.d.ts.map +1 -0
  84. package/dist/cli/init_users.js +307 -0
  85. package/dist/components/layouts/dev_lock/index.d.ts +29 -0
  86. package/dist/components/layouts/dev_lock/index.d.ts.map +1 -0
  87. package/dist/components/layouts/dev_lock/index.js +60 -0
  88. package/dist/components/layouts/index.d.ts +2 -0
  89. package/dist/components/layouts/index.d.ts.map +1 -1
  90. package/dist/components/layouts/index.js +1 -0
  91. package/dist/components/layouts/org_management/index.d.ts +26 -0
  92. package/dist/components/layouts/org_management/index.d.ts.map +1 -0
  93. package/dist/components/layouts/org_management/index.js +75 -0
  94. package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
  95. package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
  96. package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts +13 -0
  97. package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts.map +1 -0
  98. package/dist/components/layouts/user_management/components/org_hierarchy_tab.js +276 -0
  99. package/dist/components/layouts/user_management/index.d.ts +3 -1
  100. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  101. package/dist/components/layouts/user_management/index.js +10 -4
  102. package/dist/lib/auth/auth_types.d.ts +6 -0
  103. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  104. package/dist/lib/auth/dev_lock_validator.edge.d.ts +38 -0
  105. package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -0
  106. package/dist/lib/auth/dev_lock_validator.edge.js +122 -0
  107. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  108. package/dist/lib/auth/hazo_get_auth.server.js +61 -1
  109. package/dist/lib/auth/org_cache.d.ts +65 -0
  110. package/dist/lib/auth/org_cache.d.ts.map +1 -0
  111. package/dist/lib/auth/org_cache.js +103 -0
  112. package/dist/lib/config/default_config.d.ts +76 -0
  113. package/dist/lib/config/default_config.d.ts.map +1 -1
  114. package/dist/lib/config/default_config.js +42 -0
  115. package/dist/lib/dev_lock_config.server.d.ts +41 -0
  116. package/dist/lib/dev_lock_config.server.d.ts.map +1 -0
  117. package/dist/lib/dev_lock_config.server.js +50 -0
  118. package/dist/lib/multi_tenancy_config.server.d.ts +30 -0
  119. package/dist/lib/multi_tenancy_config.server.d.ts.map +1 -0
  120. package/dist/lib/multi_tenancy_config.server.js +41 -0
  121. package/dist/lib/services/org_service.d.ts +191 -0
  122. package/dist/lib/services/org_service.d.ts.map +1 -0
  123. package/dist/lib/services/org_service.js +746 -0
  124. package/dist/lib/utils/password_validator.d.ts +7 -1
  125. package/dist/lib/utils/password_validator.d.ts.map +1 -1
  126. package/dist/page_components/dev_lock.d.ts +11 -0
  127. package/dist/page_components/dev_lock.d.ts.map +1 -0
  128. package/dist/page_components/dev_lock.js +17 -0
  129. package/dist/page_components/index.d.ts +1 -0
  130. package/dist/page_components/index.d.ts.map +1 -1
  131. package/dist/page_components/index.js +1 -0
  132. package/dist/page_components/org_management.d.ts +27 -0
  133. package/dist/page_components/org_management.d.ts.map +1 -0
  134. package/dist/page_components/org_management.js +18 -0
  135. package/hazo_auth_config.example.ini +30 -0
  136. package/package.json +27 -3
@@ -0,0 +1,554 @@
1
+ // file_description: service for managing user scope assignments in HRBAC using hazo_connect
2
+ // section: imports
3
+ import type { HazoConnectAdapter } from "hazo_connect";
4
+ import { createCrudService } from "hazo_connect/server";
5
+ import { create_app_logger } from "../app_logger.js";
6
+ import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
7
+ import {
8
+ type ScopeLevel,
9
+ SCOPE_LEVELS,
10
+ SCOPE_LEVEL_NUMBERS,
11
+ get_scope_by_id,
12
+ get_scope_by_seq,
13
+ get_scope_ancestors,
14
+ is_valid_scope_level,
15
+ } from "./scope_service.js";
16
+
17
+ // section: types
18
+ export type UserScope = {
19
+ user_id: string;
20
+ scope_id: string;
21
+ scope_seq: string;
22
+ scope_type: ScopeLevel;
23
+ created_at: string;
24
+ changed_at: string;
25
+ };
26
+
27
+ export type UserScopeResult = {
28
+ success: boolean;
29
+ scope?: UserScope;
30
+ scopes?: UserScope[];
31
+ error?: string;
32
+ };
33
+
34
+ export type ScopeAccessCheckResult = {
35
+ has_access: boolean;
36
+ access_via?: {
37
+ scope_type: ScopeLevel;
38
+ scope_id: string;
39
+ scope_seq: string;
40
+ };
41
+ user_scopes?: UserScope[];
42
+ };
43
+
44
+ // section: helpers
45
+
46
+ /**
47
+ * Gets all scope assignments for a user
48
+ */
49
+ export async function get_user_scopes(
50
+ adapter: HazoConnectAdapter,
51
+ user_id: string,
52
+ ): Promise<UserScopeResult> {
53
+ try {
54
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
55
+ const scopes = await user_scope_service.findBy({ user_id });
56
+
57
+ return {
58
+ success: true,
59
+ scopes: Array.isArray(scopes) ? (scopes as UserScope[]) : [],
60
+ };
61
+ } catch (error) {
62
+ const logger = create_app_logger();
63
+ const error_message = sanitize_error_for_user(error, {
64
+ logToConsole: true,
65
+ logToLogger: true,
66
+ logger,
67
+ context: {
68
+ filename: "user_scope_service.ts",
69
+ line_number: 0,
70
+ operation: "get_user_scopes",
71
+ user_id,
72
+ },
73
+ });
74
+
75
+ return {
76
+ success: false,
77
+ error: error_message,
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Gets all users assigned to a specific scope
84
+ */
85
+ export async function get_users_by_scope(
86
+ adapter: HazoConnectAdapter,
87
+ scope_type: ScopeLevel,
88
+ scope_id: string,
89
+ ): Promise<UserScopeResult> {
90
+ try {
91
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
92
+ const scopes = await user_scope_service.findBy({ scope_type, scope_id });
93
+
94
+ return {
95
+ success: true,
96
+ scopes: Array.isArray(scopes) ? (scopes as UserScope[]) : [],
97
+ };
98
+ } catch (error) {
99
+ const logger = create_app_logger();
100
+ const error_message = sanitize_error_for_user(error, {
101
+ logToConsole: true,
102
+ logToLogger: true,
103
+ logger,
104
+ context: {
105
+ filename: "user_scope_service.ts",
106
+ line_number: 0,
107
+ operation: "get_users_by_scope",
108
+ scope_type,
109
+ scope_id,
110
+ },
111
+ });
112
+
113
+ return {
114
+ success: false,
115
+ error: error_message,
116
+ };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Assigns a scope to a user
122
+ */
123
+ export async function assign_user_scope(
124
+ adapter: HazoConnectAdapter,
125
+ user_id: string,
126
+ scope_type: ScopeLevel,
127
+ scope_id: string,
128
+ scope_seq: string,
129
+ ): Promise<UserScopeResult> {
130
+ try {
131
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
132
+ const now = new Date().toISOString();
133
+
134
+ // Check if assignment already exists
135
+ const existing = await user_scope_service.findBy({
136
+ user_id,
137
+ scope_type,
138
+ scope_id,
139
+ });
140
+
141
+ if (Array.isArray(existing) && existing.length > 0) {
142
+ return {
143
+ success: true,
144
+ scope: existing[0] as UserScope, // Already assigned
145
+ };
146
+ }
147
+
148
+ // Verify the scope exists
149
+ const scope_result = await get_scope_by_id(adapter, scope_type, scope_id);
150
+ if (!scope_result.success) {
151
+ return {
152
+ success: false,
153
+ error: "Scope not found",
154
+ };
155
+ }
156
+
157
+ // Insert new assignment
158
+ const inserted = await user_scope_service.insert({
159
+ user_id,
160
+ scope_id,
161
+ scope_seq,
162
+ scope_type,
163
+ created_at: now,
164
+ changed_at: now,
165
+ });
166
+
167
+ if (!Array.isArray(inserted) || inserted.length === 0) {
168
+ return {
169
+ success: false,
170
+ error: "Failed to assign scope to user",
171
+ };
172
+ }
173
+
174
+ return {
175
+ success: true,
176
+ scope: inserted[0] as UserScope,
177
+ };
178
+ } catch (error) {
179
+ const logger = create_app_logger();
180
+ const error_message = sanitize_error_for_user(error, {
181
+ logToConsole: true,
182
+ logToLogger: true,
183
+ logger,
184
+ context: {
185
+ filename: "user_scope_service.ts",
186
+ line_number: 0,
187
+ operation: "assign_user_scope",
188
+ user_id,
189
+ scope_type,
190
+ scope_id,
191
+ },
192
+ });
193
+
194
+ return {
195
+ success: false,
196
+ error: error_message,
197
+ };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Removes a scope assignment from a user
203
+ */
204
+ export async function remove_user_scope(
205
+ adapter: HazoConnectAdapter,
206
+ user_id: string,
207
+ scope_type: ScopeLevel,
208
+ scope_id: string,
209
+ ): Promise<UserScopeResult> {
210
+ try {
211
+ const user_scope_service = createCrudService(adapter, "hazo_user_scopes");
212
+
213
+ // Find the assignment
214
+ const existing = await user_scope_service.findBy({
215
+ user_id,
216
+ scope_type,
217
+ scope_id,
218
+ });
219
+
220
+ if (!Array.isArray(existing) || existing.length === 0) {
221
+ return {
222
+ success: true, // Already not assigned
223
+ };
224
+ }
225
+
226
+ // Delete using a filter-based approach since there's no single ID
227
+ // Note: hazo_user_scopes uses composite primary key (user_id, scope_id, scope_type)
228
+ // We need to find and delete by the combination
229
+ const existing_scope = existing[0] as UserScope;
230
+
231
+ // Use raw delete with filters if available, otherwise try by the composite key pattern
232
+ // Most hazo_connect adapters support deleteBy or similar
233
+ try {
234
+ // Try to delete by finding records with matching criteria
235
+ const all_user_scopes = await user_scope_service.findBy({ user_id });
236
+ if (Array.isArray(all_user_scopes)) {
237
+ for (const scope of all_user_scopes) {
238
+ const s = scope as UserScope;
239
+ if (s.scope_type === scope_type && s.scope_id === scope_id) {
240
+ // If the record has an id field, use it
241
+ if ((scope as Record<string, unknown>).id) {
242
+ await user_scope_service.deleteById((scope as Record<string, unknown>).id as string);
243
+ }
244
+ break;
245
+ }
246
+ }
247
+ }
248
+ } catch {
249
+ // Fallback: Some adapters might not support this pattern
250
+ const logger = create_app_logger();
251
+ logger.warn("user_scope_delete_fallback", {
252
+ filename: "user_scope_service.ts",
253
+ line_number: 0,
254
+ note: "Delete by composite key not fully supported",
255
+ });
256
+ }
257
+
258
+ return {
259
+ success: true,
260
+ scope: existing_scope,
261
+ };
262
+ } catch (error) {
263
+ const logger = create_app_logger();
264
+ const error_message = sanitize_error_for_user(error, {
265
+ logToConsole: true,
266
+ logToLogger: true,
267
+ logger,
268
+ context: {
269
+ filename: "user_scope_service.ts",
270
+ line_number: 0,
271
+ operation: "remove_user_scope",
272
+ user_id,
273
+ scope_type,
274
+ scope_id,
275
+ },
276
+ });
277
+
278
+ return {
279
+ success: false,
280
+ error: error_message,
281
+ };
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Bulk update user scope assignments
287
+ * Replaces all existing assignments with the new set
288
+ */
289
+ export async function update_user_scopes(
290
+ adapter: HazoConnectAdapter,
291
+ user_id: string,
292
+ new_scopes: Array<{ scope_type: ScopeLevel; scope_id: string; scope_seq: string }>,
293
+ ): Promise<UserScopeResult> {
294
+ try {
295
+ // Get current scopes
296
+ const current_result = await get_user_scopes(adapter, user_id);
297
+ if (!current_result.success) {
298
+ return current_result;
299
+ }
300
+
301
+ const current_scopes = current_result.scopes || [];
302
+
303
+ // Determine scopes to add and remove
304
+ const current_keys = new Set(
305
+ current_scopes.map((s) => `${s.scope_type}:${s.scope_id}`),
306
+ );
307
+ const new_keys = new Set(
308
+ new_scopes.map((s) => `${s.scope_type}:${s.scope_id}`),
309
+ );
310
+
311
+ // Remove scopes not in new set
312
+ for (const scope of current_scopes) {
313
+ const key = `${scope.scope_type}:${scope.scope_id}`;
314
+ if (!new_keys.has(key)) {
315
+ await remove_user_scope(adapter, user_id, scope.scope_type, scope.scope_id);
316
+ }
317
+ }
318
+
319
+ // Add scopes not in current set
320
+ for (const scope of new_scopes) {
321
+ const key = `${scope.scope_type}:${scope.scope_id}`;
322
+ if (!current_keys.has(key)) {
323
+ const result = await assign_user_scope(
324
+ adapter,
325
+ user_id,
326
+ scope.scope_type,
327
+ scope.scope_id,
328
+ scope.scope_seq,
329
+ );
330
+ if (!result.success) {
331
+ return result;
332
+ }
333
+ }
334
+ }
335
+
336
+ // Return updated scopes
337
+ return get_user_scopes(adapter, user_id);
338
+ } catch (error) {
339
+ const logger = create_app_logger();
340
+ const error_message = sanitize_error_for_user(error, {
341
+ logToConsole: true,
342
+ logToLogger: true,
343
+ logger,
344
+ context: {
345
+ filename: "user_scope_service.ts",
346
+ line_number: 0,
347
+ operation: "update_user_scopes",
348
+ user_id,
349
+ },
350
+ });
351
+
352
+ return {
353
+ success: false,
354
+ error: error_message,
355
+ };
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Checks if a user has access to a specific scope
361
+ * Access is granted if:
362
+ * 1. User has the exact scope assigned, OR
363
+ * 2. User has access to an ancestor scope (L2 user can access L3, L4, etc.)
364
+ *
365
+ * @param adapter - HazoConnect adapter
366
+ * @param user_id - User ID to check
367
+ * @param target_scope_type - The scope level being accessed
368
+ * @param target_scope_id - The scope ID being accessed (optional if target_scope_seq provided)
369
+ * @param target_scope_seq - The scope seq being accessed (optional if target_scope_id provided)
370
+ */
371
+ export async function check_user_scope_access(
372
+ adapter: HazoConnectAdapter,
373
+ user_id: string,
374
+ target_scope_type: ScopeLevel,
375
+ target_scope_id?: string,
376
+ target_scope_seq?: string,
377
+ ): Promise<ScopeAccessCheckResult> {
378
+ try {
379
+ // Resolve scope ID if only seq provided
380
+ let resolved_scope_id = target_scope_id;
381
+ let resolved_scope_seq = target_scope_seq;
382
+
383
+ if (!resolved_scope_id && resolved_scope_seq) {
384
+ const scope_result = await get_scope_by_seq(
385
+ adapter,
386
+ target_scope_type,
387
+ resolved_scope_seq,
388
+ );
389
+ if (!scope_result.success || !scope_result.scope) {
390
+ return { has_access: false };
391
+ }
392
+ resolved_scope_id = scope_result.scope.id;
393
+ } else if (resolved_scope_id && !resolved_scope_seq) {
394
+ const scope_result = await get_scope_by_id(
395
+ adapter,
396
+ target_scope_type,
397
+ resolved_scope_id,
398
+ );
399
+ if (!scope_result.success || !scope_result.scope) {
400
+ return { has_access: false };
401
+ }
402
+ resolved_scope_seq = scope_result.scope.seq;
403
+ }
404
+
405
+ if (!resolved_scope_id) {
406
+ return { has_access: false };
407
+ }
408
+
409
+ // Get user's assigned scopes
410
+ const user_scopes_result = await get_user_scopes(adapter, user_id);
411
+ if (!user_scopes_result.success || !user_scopes_result.scopes) {
412
+ return { has_access: false };
413
+ }
414
+
415
+ const user_scopes = user_scopes_result.scopes;
416
+
417
+ // Check 1: Does user have exact scope assigned?
418
+ for (const user_scope of user_scopes) {
419
+ if (
420
+ user_scope.scope_type === target_scope_type &&
421
+ user_scope.scope_id === resolved_scope_id
422
+ ) {
423
+ return {
424
+ has_access: true,
425
+ access_via: {
426
+ scope_type: user_scope.scope_type,
427
+ scope_id: user_scope.scope_id,
428
+ scope_seq: user_scope.scope_seq,
429
+ },
430
+ user_scopes,
431
+ };
432
+ }
433
+ }
434
+
435
+ // Check 2: Does user have access via an ancestor scope?
436
+ // Get all ancestors of the target scope
437
+ const ancestors_result = await get_scope_ancestors(
438
+ adapter,
439
+ target_scope_type,
440
+ resolved_scope_id,
441
+ );
442
+
443
+ if (ancestors_result.success && ancestors_result.scopes) {
444
+ const ancestors = ancestors_result.scopes;
445
+
446
+ // For each ancestor, check if user has it assigned
447
+ // Need to determine the level of each ancestor
448
+ let current_level = SCOPE_LEVEL_NUMBERS[target_scope_type];
449
+
450
+ for (const ancestor of ancestors) {
451
+ current_level--;
452
+ const ancestor_level = `hazo_scopes_l${current_level}` as ScopeLevel;
453
+
454
+ for (const user_scope of user_scopes) {
455
+ if (
456
+ user_scope.scope_type === ancestor_level &&
457
+ user_scope.scope_id === ancestor.id
458
+ ) {
459
+ // User has access via this ancestor
460
+ return {
461
+ has_access: true,
462
+ access_via: {
463
+ scope_type: user_scope.scope_type,
464
+ scope_id: user_scope.scope_id,
465
+ scope_seq: user_scope.scope_seq,
466
+ },
467
+ user_scopes,
468
+ };
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ // No access
475
+ return {
476
+ has_access: false,
477
+ user_scopes,
478
+ };
479
+ } catch (error) {
480
+ const logger = create_app_logger();
481
+ logger.error("check_user_scope_access_error", {
482
+ filename: "user_scope_service.ts",
483
+ line_number: 0,
484
+ error: error instanceof Error ? error.message : "Unknown error",
485
+ user_id,
486
+ target_scope_type,
487
+ target_scope_id,
488
+ });
489
+
490
+ return { has_access: false };
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Gets the effective scopes a user has access to
496
+ * This includes directly assigned scopes and all their descendants
497
+ */
498
+ export async function get_user_effective_scopes(
499
+ adapter: HazoConnectAdapter,
500
+ user_id: string,
501
+ ): Promise<{
502
+ success: boolean;
503
+ direct_scopes?: UserScope[];
504
+ inherited_scope_types?: ScopeLevel[];
505
+ error?: string;
506
+ }> {
507
+ try {
508
+ const user_scopes_result = await get_user_scopes(adapter, user_id);
509
+ if (!user_scopes_result.success) {
510
+ return {
511
+ success: false,
512
+ error: user_scopes_result.error,
513
+ };
514
+ }
515
+
516
+ const direct_scopes = user_scopes_result.scopes || [];
517
+
518
+ // Determine which levels user has inherited access to
519
+ // If user has L2 access, they inherit L3, L4, L5, L6, L7
520
+ const inherited_levels = new Set<ScopeLevel>();
521
+
522
+ for (const scope of direct_scopes) {
523
+ const level_num = SCOPE_LEVEL_NUMBERS[scope.scope_type];
524
+ // Add all levels below this one
525
+ for (let i = level_num + 1; i <= 7; i++) {
526
+ inherited_levels.add(`hazo_scopes_l${i}` as ScopeLevel);
527
+ }
528
+ }
529
+
530
+ return {
531
+ success: true,
532
+ direct_scopes,
533
+ inherited_scope_types: Array.from(inherited_levels),
534
+ };
535
+ } catch (error) {
536
+ const logger = create_app_logger();
537
+ const error_message = sanitize_error_for_user(error, {
538
+ logToConsole: true,
539
+ logToLogger: true,
540
+ logger,
541
+ context: {
542
+ filename: "user_scope_service.ts",
543
+ line_number: 0,
544
+ operation: "get_user_effective_scopes",
545
+ user_id,
546
+ },
547
+ });
548
+
549
+ return {
550
+ success: false,
551
+ error: error_message,
552
+ };
553
+ }
554
+ }
@@ -0,0 +1,141 @@
1
+ // file_description: service for updating user profile information using hazo_connect
2
+ // section: imports
3
+ import type { HazoConnectAdapter } from "hazo_connect";
4
+ import { createCrudService } from "hazo_connect/server";
5
+ import { map_ui_source_to_db, type ProfilePictureSourceUI } from "./profile_picture_source_mapper.js";
6
+ import { create_app_logger } from "../app_logger.js";
7
+ import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
8
+ import { get_filename, get_line_number } from "../utils/api_route_helpers.js";
9
+
10
+ // section: types
11
+ export type UserUpdateData = {
12
+ name?: string;
13
+ email?: string;
14
+ profile_picture_url?: string;
15
+ profile_source?: ProfilePictureSourceUI;
16
+ };
17
+
18
+ export type UserUpdateResult = {
19
+ success: boolean;
20
+ email_changed?: boolean;
21
+ error?: string;
22
+ };
23
+
24
+ // section: helpers
25
+ /**
26
+ * Updates user profile information (name, email)
27
+ * If email is changed, sets email_verified to false
28
+ * @param adapter - The hazo_connect adapter instance
29
+ * @param user_id - The user ID to update
30
+ * @param data - User update data (name, email)
31
+ * @returns User update result with success status, email_changed flag, or error
32
+ */
33
+ export async function update_user_profile(
34
+ adapter: HazoConnectAdapter,
35
+ user_id: string,
36
+ data: UserUpdateData,
37
+ ): Promise<UserUpdateResult> {
38
+ try {
39
+ const { name, email } = data;
40
+
41
+ // Create CRUD service for hazo_users table
42
+ const users_service = createCrudService(adapter, "hazo_users");
43
+
44
+ // Get current user data
45
+ const users = await users_service.findBy({
46
+ id: user_id,
47
+ });
48
+
49
+ if (!Array.isArray(users) || users.length === 0) {
50
+ return {
51
+ success: false,
52
+ error: "User not found",
53
+ };
54
+ }
55
+
56
+ const current_user = users[0];
57
+ const current_email = current_user.email_address as string;
58
+ const email_changed = email !== undefined && email !== current_email;
59
+
60
+ // If email is being changed, check if new email already exists
61
+ if (email_changed) {
62
+ const existing_users = await users_service.findBy({
63
+ email_address: email,
64
+ });
65
+
66
+ if (Array.isArray(existing_users) && existing_users.length > 0) {
67
+ // Check if it's the same user
68
+ const existing_user = existing_users[0];
69
+ if (existing_user.id !== user_id) {
70
+ return {
71
+ success: false,
72
+ error: "Email address already registered",
73
+ };
74
+ }
75
+ }
76
+
77
+ // Validate email format
78
+ const email_regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
79
+ if (!email_regex.test(email)) {
80
+ return {
81
+ success: false,
82
+ error: "Invalid email address format",
83
+ };
84
+ }
85
+ }
86
+
87
+ // Prepare update data
88
+ const update_data: Record<string, unknown> = {
89
+ changed_at: new Date().toISOString(),
90
+ };
91
+
92
+ if (name !== undefined) {
93
+ update_data.name = name;
94
+ }
95
+
96
+ if (email !== undefined) {
97
+ update_data.email_address = email;
98
+ }
99
+
100
+ if (data.profile_picture_url !== undefined) {
101
+ update_data.profile_picture_url = data.profile_picture_url;
102
+ }
103
+
104
+ if (data.profile_source !== undefined) {
105
+ // Map UI source value to database enum value
106
+ update_data.profile_source = map_ui_source_to_db(data.profile_source);
107
+ }
108
+
109
+ // If email changed, set email_verified to false
110
+ if (email_changed) {
111
+ update_data.email_verified = false;
112
+ }
113
+
114
+ // Update user in database
115
+ await users_service.updateById(user_id, update_data);
116
+
117
+ return {
118
+ success: true,
119
+ email_changed,
120
+ };
121
+ } catch (error) {
122
+ const logger = create_app_logger();
123
+ const user_friendly_error = sanitize_error_for_user(error, {
124
+ logToConsole: true,
125
+ logToLogger: true,
126
+ logger,
127
+ context: {
128
+ filename: "user_update_service.ts",
129
+ line_number: get_line_number(),
130
+ user_id,
131
+ operation: "update_user_profile",
132
+ },
133
+ });
134
+
135
+ return {
136
+ success: false,
137
+ error: user_friendly_error,
138
+ };
139
+ }
140
+ }
141
+