hazo_auth 4.3.0 → 4.4.1

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 (119) hide show
  1. package/cli-src/lib/already_logged_in_config.server.ts +1 -1
  2. package/cli-src/lib/app_logger.ts +8 -18
  3. package/cli-src/lib/auth/auth_types.ts +7 -0
  4. package/cli-src/lib/auth/auth_utils.server.ts +2 -2
  5. package/cli-src/lib/auth/dev_lock_validator.edge.ts +171 -0
  6. package/cli-src/lib/auth/hazo_get_auth.server.ts +84 -13
  7. package/cli-src/lib/auth/index.ts +5 -5
  8. package/cli-src/lib/auth/nextauth_config.ts +4 -4
  9. package/cli-src/lib/auth/org_cache.ts +148 -0
  10. package/cli-src/lib/auth/server_auth.ts +2 -2
  11. package/cli-src/lib/auth/session_token_validator.edge.ts +4 -0
  12. package/cli-src/lib/auth_utility_config.server.ts +1 -1
  13. package/cli-src/lib/config/config_loader.server.ts +1 -1
  14. package/cli-src/lib/config/default_config.ts +44 -0
  15. package/cli-src/lib/dev_lock_config.server.ts +148 -0
  16. package/cli-src/lib/email_verification_config.server.ts +3 -3
  17. package/cli-src/lib/file_types_config.server.ts +1 -1
  18. package/cli-src/lib/forgot_password_config.server.ts +3 -3
  19. package/cli-src/lib/hazo_connect_instance.server.ts +2 -2
  20. package/cli-src/lib/hazo_connect_setup.server.ts +2 -2
  21. package/cli-src/lib/index.ts +24 -24
  22. package/cli-src/lib/login_config.server.ts +4 -4
  23. package/cli-src/lib/messages_config.server.ts +1 -1
  24. package/cli-src/lib/multi_tenancy_config.server.ts +94 -0
  25. package/cli-src/lib/my_settings_config.server.ts +7 -7
  26. package/cli-src/lib/oauth_config.server.ts +2 -2
  27. package/cli-src/lib/password_requirements_config.server.ts +2 -2
  28. package/cli-src/lib/profile_pic_menu_config.server.ts +1 -1
  29. package/cli-src/lib/profile_picture_config.server.ts +2 -2
  30. package/cli-src/lib/register_config.server.ts +5 -5
  31. package/cli-src/lib/reset_password_config.server.ts +4 -4
  32. package/cli-src/lib/scope_hierarchy_config.server.ts +2 -2
  33. package/cli-src/lib/services/email_service.ts +2 -2
  34. package/cli-src/lib/services/email_verification_service.ts +3 -3
  35. package/cli-src/lib/services/login_service.ts +3 -3
  36. package/cli-src/lib/services/oauth_service.ts +4 -4
  37. package/cli-src/lib/services/org_service.ts +965 -0
  38. package/cli-src/lib/services/password_change_service.ts +3 -3
  39. package/cli-src/lib/services/password_reset_service.ts +3 -3
  40. package/cli-src/lib/services/profile_picture_remove_service.ts +3 -3
  41. package/cli-src/lib/services/profile_picture_service.ts +5 -5
  42. package/cli-src/lib/services/registration_service.ts +8 -8
  43. package/cli-src/lib/services/scope_labels_service.ts +3 -3
  44. package/cli-src/lib/services/scope_service.ts +2 -2
  45. package/cli-src/lib/services/session_token_service.ts +6 -2
  46. package/cli-src/lib/services/token_service.ts +2 -2
  47. package/cli-src/lib/services/user_profiles_service.ts +4 -4
  48. package/cli-src/lib/services/user_scope_service.ts +3 -3
  49. package/cli-src/lib/services/user_update_service.ts +4 -4
  50. package/cli-src/lib/ui_shell_config.server.ts +1 -1
  51. package/cli-src/lib/ui_sizes_config.server.ts +1 -1
  52. package/cli-src/lib/user_fields_config.server.ts +1 -1
  53. package/cli-src/lib/user_management_config.server.ts +1 -1
  54. package/cli-src/lib/user_profiles_config.server.ts +1 -1
  55. package/cli-src/lib/utils/error_sanitizer.ts +1 -1
  56. package/cli-src/server/types/app_types.ts +72 -0
  57. package/cli-src/server/types/express.d.ts +16 -0
  58. package/dist/components/layouts/dev_lock/index.d.ts +29 -0
  59. package/dist/components/layouts/dev_lock/index.d.ts.map +1 -0
  60. package/dist/components/layouts/dev_lock/index.js +60 -0
  61. package/dist/components/layouts/index.d.ts +2 -0
  62. package/dist/components/layouts/index.d.ts.map +1 -1
  63. package/dist/components/layouts/index.js +1 -0
  64. package/dist/components/layouts/login/hooks/use_login_form.js +2 -2
  65. package/dist/components/layouts/org_management/index.d.ts +26 -0
  66. package/dist/components/layouts/org_management/index.d.ts.map +1 -0
  67. package/dist/components/layouts/org_management/index.js +75 -0
  68. package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts +13 -0
  69. package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts.map +1 -0
  70. package/dist/components/layouts/user_management/components/org_hierarchy_tab.js +276 -0
  71. package/dist/components/layouts/user_management/index.d.ts +3 -1
  72. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  73. package/dist/components/layouts/user_management/index.js +10 -4
  74. package/dist/components/ui/button.d.ts +1 -1
  75. package/dist/lib/app_logger.d.ts +3 -9
  76. package/dist/lib/app_logger.d.ts.map +1 -1
  77. package/dist/lib/app_logger.js +7 -10
  78. package/dist/lib/auth/auth_types.d.ts +6 -0
  79. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  80. package/dist/lib/auth/dev_lock_validator.edge.d.ts +38 -0
  81. package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -0
  82. package/dist/lib/auth/dev_lock_validator.edge.js +122 -0
  83. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  84. package/dist/lib/auth/hazo_get_auth.server.js +61 -1
  85. package/dist/lib/auth/org_cache.d.ts +65 -0
  86. package/dist/lib/auth/org_cache.d.ts.map +1 -0
  87. package/dist/lib/auth/org_cache.js +103 -0
  88. package/dist/lib/config/default_config.d.ts +76 -0
  89. package/dist/lib/config/default_config.d.ts.map +1 -1
  90. package/dist/lib/config/default_config.js +42 -0
  91. package/dist/lib/dev_lock_config.server.d.ts +41 -0
  92. package/dist/lib/dev_lock_config.server.d.ts.map +1 -0
  93. package/dist/lib/dev_lock_config.server.js +50 -0
  94. package/dist/lib/multi_tenancy_config.server.d.ts +30 -0
  95. package/dist/lib/multi_tenancy_config.server.d.ts.map +1 -0
  96. package/dist/lib/multi_tenancy_config.server.js +41 -0
  97. package/dist/lib/services/org_service.d.ts +191 -0
  98. package/dist/lib/services/org_service.d.ts.map +1 -0
  99. package/dist/lib/services/org_service.js +746 -0
  100. package/dist/page_components/dev_lock.d.ts +11 -0
  101. package/dist/page_components/dev_lock.d.ts.map +1 -0
  102. package/dist/page_components/dev_lock.js +17 -0
  103. package/dist/page_components/index.d.ts +1 -0
  104. package/dist/page_components/index.d.ts.map +1 -1
  105. package/dist/page_components/index.js +1 -0
  106. package/dist/page_components/login.d.ts.map +1 -1
  107. package/dist/page_components/login.js +3 -7
  108. package/dist/page_components/org_management.d.ts +27 -0
  109. package/dist/page_components/org_management.d.ts.map +1 -0
  110. package/dist/page_components/org_management.js +18 -0
  111. package/dist/server/config/config_loader.js +2 -2
  112. package/dist/server/index.d.ts.map +1 -1
  113. package/dist/server/index.js +2 -3
  114. package/dist/server/types/app_types.d.ts +3 -7
  115. package/dist/server/types/app_types.d.ts.map +1 -1
  116. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  117. package/dist/server_pages/login_client_wrapper.js +1 -3
  118. package/hazo_auth_config.example.ini +30 -0
  119. package/package.json +29 -2
@@ -0,0 +1,965 @@
1
+ // file_description: service for organization management in multi-tenancy 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
+
8
+ // section: types
9
+
10
+ /**
11
+ * Organization record from hazo_org table
12
+ */
13
+ export type OrgRecord = {
14
+ id: string;
15
+ name: string;
16
+ user_limit: number;
17
+ parent_org_id: string | null;
18
+ root_org_id: string | null;
19
+ active: boolean;
20
+ created_at: string;
21
+ created_by: string | null;
22
+ changed_at: string;
23
+ changed_by: string | null;
24
+ };
25
+
26
+ /**
27
+ * Organization record with computed user count
28
+ */
29
+ export type OrgWithUserCount = OrgRecord & {
30
+ current_user_count: number;
31
+ };
32
+
33
+ /**
34
+ * Result type for org service operations
35
+ */
36
+ export type OrgServiceResult = {
37
+ success: boolean;
38
+ org?: OrgRecord;
39
+ orgs?: OrgRecord[];
40
+ error?: string;
41
+ };
42
+
43
+ /**
44
+ * Result type for org service operations with user count
45
+ */
46
+ export type OrgServiceResultWithCount = {
47
+ success: boolean;
48
+ org?: OrgWithUserCount;
49
+ orgs?: OrgWithUserCount[];
50
+ error?: string;
51
+ };
52
+
53
+ /**
54
+ * Data for creating a new organization
55
+ */
56
+ export type CreateOrgData = {
57
+ name: string;
58
+ user_limit?: number;
59
+ parent_org_id?: string;
60
+ created_by: string;
61
+ };
62
+
63
+ /**
64
+ * Data for updating an organization
65
+ */
66
+ export type UpdateOrgData = {
67
+ name?: string;
68
+ user_limit?: number;
69
+ changed_by: string;
70
+ };
71
+
72
+ /**
73
+ * Organization tree node for hierarchy display
74
+ */
75
+ export type OrgTreeNode = OrgWithUserCount & {
76
+ children?: OrgTreeNode[];
77
+ };
78
+
79
+ /**
80
+ * Options for getting organizations
81
+ */
82
+ export type GetOrgsOptions = {
83
+ root_org_id?: string;
84
+ include_inactive?: boolean;
85
+ };
86
+
87
+ // section: constants
88
+
89
+ const TABLE_NAME = "hazo_org";
90
+ const USERS_TABLE_NAME = "hazo_users";
91
+
92
+ // section: helpers
93
+
94
+ /**
95
+ * Gets the user count for an organization
96
+ * @param adapter - HazoConnect adapter
97
+ * @param org_id - Organization ID
98
+ * @returns User count
99
+ */
100
+ export async function get_org_user_count(
101
+ adapter: HazoConnectAdapter,
102
+ org_id: string,
103
+ ): Promise<{ success: boolean; count?: number; error?: string }> {
104
+ try {
105
+ const users_service = createCrudService(adapter, USERS_TABLE_NAME);
106
+ const users = await users_service.findBy({ org_id });
107
+
108
+ const count = Array.isArray(users) ? users.length : 0;
109
+
110
+ return {
111
+ success: true,
112
+ count,
113
+ };
114
+ } catch (error) {
115
+ const logger = create_app_logger();
116
+ const error_message = sanitize_error_for_user(error, {
117
+ logToConsole: true,
118
+ logToLogger: true,
119
+ logger,
120
+ context: {
121
+ filename: "org_service.ts",
122
+ line_number: 0,
123
+ operation: "get_org_user_count",
124
+ org_id,
125
+ },
126
+ });
127
+
128
+ return {
129
+ success: false,
130
+ error: error_message,
131
+ };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Gets user count for the root organization (includes all child orgs)
137
+ * @param adapter - HazoConnect adapter
138
+ * @param root_org_id - Root organization ID
139
+ * @returns Total user count across org tree
140
+ */
141
+ export async function get_root_org_user_count(
142
+ adapter: HazoConnectAdapter,
143
+ root_org_id: string,
144
+ ): Promise<{ success: boolean; count?: number; error?: string }> {
145
+ try {
146
+ const users_service = createCrudService(adapter, USERS_TABLE_NAME);
147
+ const users = await users_service.findBy({ root_org_id });
148
+
149
+ const count = Array.isArray(users) ? users.length : 0;
150
+
151
+ return {
152
+ success: true,
153
+ count,
154
+ };
155
+ } catch (error) {
156
+ const logger = create_app_logger();
157
+ const error_message = sanitize_error_for_user(error, {
158
+ logToConsole: true,
159
+ logToLogger: true,
160
+ logger,
161
+ context: {
162
+ filename: "org_service.ts",
163
+ line_number: 0,
164
+ operation: "get_root_org_user_count",
165
+ root_org_id,
166
+ },
167
+ });
168
+
169
+ return {
170
+ success: false,
171
+ error: error_message,
172
+ };
173
+ }
174
+ }
175
+
176
+ // section: service_functions
177
+
178
+ /**
179
+ * Gets all organizations, optionally filtered by root_org_id
180
+ * @param adapter - HazoConnect adapter
181
+ * @param options - Filter options
182
+ * @returns List of organizations
183
+ */
184
+ export async function get_orgs(
185
+ adapter: HazoConnectAdapter,
186
+ options?: GetOrgsOptions,
187
+ ): Promise<OrgServiceResult> {
188
+ try {
189
+ const org_service = createCrudService(adapter, TABLE_NAME);
190
+
191
+ let orgs: unknown[];
192
+
193
+ if (options?.root_org_id) {
194
+ // Get orgs in this org tree (by root_org_id)
195
+ orgs = await org_service.findBy({ root_org_id: options.root_org_id });
196
+
197
+ // Also include the root org itself
198
+ const root_orgs = await org_service.findBy({ id: options.root_org_id });
199
+ if (Array.isArray(root_orgs) && root_orgs.length > 0) {
200
+ orgs = [...root_orgs, ...(Array.isArray(orgs) ? orgs : [])];
201
+ }
202
+ } else {
203
+ // Get all orgs (global admin view)
204
+ orgs = await org_service.findBy({});
205
+ }
206
+
207
+ if (!Array.isArray(orgs)) {
208
+ return {
209
+ success: true,
210
+ orgs: [],
211
+ };
212
+ }
213
+
214
+ // Filter inactive if not requested
215
+ let filtered_orgs = orgs as OrgRecord[];
216
+ if (!options?.include_inactive) {
217
+ filtered_orgs = filtered_orgs.filter((org) => org.active !== false);
218
+ }
219
+
220
+ return {
221
+ success: true,
222
+ orgs: filtered_orgs,
223
+ };
224
+ } catch (error) {
225
+ const logger = create_app_logger();
226
+ const error_message = sanitize_error_for_user(error, {
227
+ logToConsole: true,
228
+ logToLogger: true,
229
+ logger,
230
+ context: {
231
+ filename: "org_service.ts",
232
+ line_number: 0,
233
+ operation: "get_orgs",
234
+ options,
235
+ },
236
+ });
237
+
238
+ return {
239
+ success: false,
240
+ error: error_message,
241
+ };
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Gets a single organization by ID with computed user count
247
+ * @param adapter - HazoConnect adapter
248
+ * @param org_id - Organization ID
249
+ * @returns Organization with user count
250
+ */
251
+ export async function get_org_by_id(
252
+ adapter: HazoConnectAdapter,
253
+ org_id: string,
254
+ ): Promise<OrgServiceResultWithCount> {
255
+ try {
256
+ const org_service = createCrudService(adapter, TABLE_NAME);
257
+ const orgs = await org_service.findBy({ id: org_id });
258
+
259
+ if (!Array.isArray(orgs) || orgs.length === 0) {
260
+ return {
261
+ success: false,
262
+ error: "Organization not found",
263
+ };
264
+ }
265
+
266
+ const org = orgs[0] as OrgRecord;
267
+
268
+ // Get user count
269
+ const count_result = await get_org_user_count(adapter, org_id);
270
+ const current_user_count = count_result.success ? count_result.count! : 0;
271
+
272
+ return {
273
+ success: true,
274
+ org: {
275
+ ...org,
276
+ current_user_count,
277
+ },
278
+ };
279
+ } catch (error) {
280
+ const logger = create_app_logger();
281
+ const error_message = sanitize_error_for_user(error, {
282
+ logToConsole: true,
283
+ logToLogger: true,
284
+ logger,
285
+ context: {
286
+ filename: "org_service.ts",
287
+ line_number: 0,
288
+ operation: "get_org_by_id",
289
+ org_id,
290
+ },
291
+ });
292
+
293
+ return {
294
+ success: false,
295
+ error: error_message,
296
+ };
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Creates a new organization
302
+ * @param adapter - HazoConnect adapter
303
+ * @param data - Organization data
304
+ * @returns Created organization
305
+ */
306
+ export async function create_org(
307
+ adapter: HazoConnectAdapter,
308
+ data: CreateOrgData,
309
+ ): Promise<OrgServiceResult> {
310
+ try {
311
+ const org_service = createCrudService(adapter, TABLE_NAME);
312
+ const now = new Date().toISOString();
313
+
314
+ // Determine root_org_id
315
+ let root_org_id: string | null = null;
316
+
317
+ if (data.parent_org_id) {
318
+ // Validate parent exists
319
+ const parent_result = await get_org_by_id(adapter, data.parent_org_id);
320
+ if (!parent_result.success || !parent_result.org) {
321
+ return {
322
+ success: false,
323
+ error: "Parent organization not found",
324
+ };
325
+ }
326
+
327
+ // If parent has a root_org_id, use it; otherwise parent IS the root
328
+ root_org_id = parent_result.org.root_org_id || data.parent_org_id;
329
+ }
330
+
331
+ const insert_data: Record<string, unknown> = {
332
+ name: data.name,
333
+ user_limit: data.user_limit ?? 0,
334
+ parent_org_id: data.parent_org_id || null,
335
+ root_org_id: root_org_id,
336
+ active: true,
337
+ created_at: now,
338
+ created_by: data.created_by,
339
+ changed_at: now,
340
+ changed_by: data.created_by,
341
+ };
342
+
343
+ const inserted = await org_service.insert(insert_data);
344
+
345
+ if (!Array.isArray(inserted) || inserted.length === 0) {
346
+ return {
347
+ success: false,
348
+ error: "Failed to create organization",
349
+ };
350
+ }
351
+
352
+ return {
353
+ success: true,
354
+ org: inserted[0] as OrgRecord,
355
+ };
356
+ } catch (error) {
357
+ const logger = create_app_logger();
358
+ const error_message = sanitize_error_for_user(error, {
359
+ logToConsole: true,
360
+ logToLogger: true,
361
+ logger,
362
+ context: {
363
+ filename: "org_service.ts",
364
+ line_number: 0,
365
+ operation: "create_org",
366
+ data,
367
+ },
368
+ });
369
+
370
+ return {
371
+ success: false,
372
+ error: error_message,
373
+ };
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Updates an existing organization
379
+ * @param adapter - HazoConnect adapter
380
+ * @param org_id - Organization ID
381
+ * @param data - Update data
382
+ * @returns Updated organization
383
+ */
384
+ export async function update_org(
385
+ adapter: HazoConnectAdapter,
386
+ org_id: string,
387
+ data: UpdateOrgData,
388
+ ): Promise<OrgServiceResult> {
389
+ try {
390
+ const org_service = createCrudService(adapter, TABLE_NAME);
391
+ const now = new Date().toISOString();
392
+
393
+ // Check org exists
394
+ const existing = await get_org_by_id(adapter, org_id);
395
+ if (!existing.success) {
396
+ return {
397
+ success: false,
398
+ error: existing.error || "Organization not found",
399
+ };
400
+ }
401
+
402
+ const update_data: Record<string, unknown> = {
403
+ changed_at: now,
404
+ changed_by: data.changed_by,
405
+ };
406
+
407
+ if (data.name !== undefined) {
408
+ update_data.name = data.name;
409
+ }
410
+
411
+ if (data.user_limit !== undefined) {
412
+ update_data.user_limit = data.user_limit;
413
+ }
414
+
415
+ const updated = await org_service.updateById(org_id, update_data);
416
+
417
+ if (!Array.isArray(updated) || updated.length === 0) {
418
+ return {
419
+ success: false,
420
+ error: "Failed to update organization",
421
+ };
422
+ }
423
+
424
+ return {
425
+ success: true,
426
+ org: updated[0] as OrgRecord,
427
+ };
428
+ } catch (error) {
429
+ const logger = create_app_logger();
430
+ const error_message = sanitize_error_for_user(error, {
431
+ logToConsole: true,
432
+ logToLogger: true,
433
+ logger,
434
+ context: {
435
+ filename: "org_service.ts",
436
+ line_number: 0,
437
+ operation: "update_org",
438
+ org_id,
439
+ data,
440
+ },
441
+ });
442
+
443
+ return {
444
+ success: false,
445
+ error: error_message,
446
+ };
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Soft deletes an organization (sets active = false)
452
+ * @param adapter - HazoConnect adapter
453
+ * @param org_id - Organization ID
454
+ * @param changed_by - User ID making the change
455
+ * @returns Deactivated organization
456
+ */
457
+ export async function soft_delete_org(
458
+ adapter: HazoConnectAdapter,
459
+ org_id: string,
460
+ changed_by: string,
461
+ ): Promise<OrgServiceResult> {
462
+ try {
463
+ const org_service = createCrudService(adapter, TABLE_NAME);
464
+ const now = new Date().toISOString();
465
+
466
+ // Check org exists
467
+ const existing = await get_org_by_id(adapter, org_id);
468
+ if (!existing.success) {
469
+ return {
470
+ success: false,
471
+ error: existing.error || "Organization not found",
472
+ };
473
+ }
474
+
475
+ const update_data: Record<string, unknown> = {
476
+ active: false,
477
+ changed_at: now,
478
+ changed_by: changed_by,
479
+ };
480
+
481
+ const updated = await org_service.updateById(org_id, update_data);
482
+
483
+ if (!Array.isArray(updated) || updated.length === 0) {
484
+ return {
485
+ success: false,
486
+ error: "Failed to deactivate organization",
487
+ };
488
+ }
489
+
490
+ return {
491
+ success: true,
492
+ org: updated[0] as OrgRecord,
493
+ };
494
+ } catch (error) {
495
+ const logger = create_app_logger();
496
+ const error_message = sanitize_error_for_user(error, {
497
+ logToConsole: true,
498
+ logToLogger: true,
499
+ logger,
500
+ context: {
501
+ filename: "org_service.ts",
502
+ line_number: 0,
503
+ operation: "soft_delete_org",
504
+ org_id,
505
+ changed_by,
506
+ },
507
+ });
508
+
509
+ return {
510
+ success: false,
511
+ error: error_message,
512
+ };
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Gets immediate children of an organization
518
+ * @param adapter - HazoConnect adapter
519
+ * @param org_id - Parent organization ID
520
+ * @returns Child organizations
521
+ */
522
+ export async function get_org_children(
523
+ adapter: HazoConnectAdapter,
524
+ org_id: string,
525
+ include_inactive?: boolean,
526
+ ): Promise<OrgServiceResult> {
527
+ try {
528
+ const org_service = createCrudService(adapter, TABLE_NAME);
529
+ const children = await org_service.findBy({ parent_org_id: org_id });
530
+
531
+ if (!Array.isArray(children)) {
532
+ return {
533
+ success: true,
534
+ orgs: [],
535
+ };
536
+ }
537
+
538
+ // Filter inactive if not requested
539
+ let filtered = children as OrgRecord[];
540
+ if (!include_inactive) {
541
+ filtered = filtered.filter((org) => org.active !== false);
542
+ }
543
+
544
+ return {
545
+ success: true,
546
+ orgs: filtered,
547
+ };
548
+ } catch (error) {
549
+ const logger = create_app_logger();
550
+ const error_message = sanitize_error_for_user(error, {
551
+ logToConsole: true,
552
+ logToLogger: true,
553
+ logger,
554
+ context: {
555
+ filename: "org_service.ts",
556
+ line_number: 0,
557
+ operation: "get_org_children",
558
+ org_id,
559
+ },
560
+ });
561
+
562
+ return {
563
+ success: false,
564
+ error: error_message,
565
+ };
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Gets all ancestors of an organization up to root
571
+ * Returns array ordered from immediate parent to root
572
+ * @param adapter - HazoConnect adapter
573
+ * @param org_id - Organization ID
574
+ * @returns Ancestor organizations
575
+ */
576
+ export async function get_org_ancestors(
577
+ adapter: HazoConnectAdapter,
578
+ org_id: string,
579
+ ): Promise<OrgServiceResult> {
580
+ try {
581
+ const ancestors: OrgRecord[] = [];
582
+
583
+ // Get the org first
584
+ const org_result = await get_org_by_id(adapter, org_id);
585
+ if (!org_result.success || !org_result.org) {
586
+ return {
587
+ success: false,
588
+ error: org_result.error || "Organization not found",
589
+ };
590
+ }
591
+
592
+ let current_org = org_result.org;
593
+
594
+ // Walk up the hierarchy
595
+ while (current_org.parent_org_id) {
596
+ const parent_result = await get_org_by_id(adapter, current_org.parent_org_id);
597
+
598
+ if (!parent_result.success || !parent_result.org) break;
599
+
600
+ ancestors.push(parent_result.org);
601
+ current_org = parent_result.org;
602
+ }
603
+
604
+ return {
605
+ success: true,
606
+ orgs: ancestors,
607
+ };
608
+ } catch (error) {
609
+ const logger = create_app_logger();
610
+ const error_message = sanitize_error_for_user(error, {
611
+ logToConsole: true,
612
+ logToLogger: true,
613
+ logger,
614
+ context: {
615
+ filename: "org_service.ts",
616
+ line_number: 0,
617
+ operation: "get_org_ancestors",
618
+ org_id,
619
+ },
620
+ });
621
+
622
+ return {
623
+ success: false,
624
+ error: error_message,
625
+ };
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Gets all descendants of an organization
631
+ * Returns flat array of all descendant orgs
632
+ * @param adapter - HazoConnect adapter
633
+ * @param org_id - Organization ID
634
+ * @returns Descendant organizations
635
+ */
636
+ export async function get_org_descendants(
637
+ adapter: HazoConnectAdapter,
638
+ org_id: string,
639
+ include_inactive?: boolean,
640
+ ): Promise<OrgServiceResult> {
641
+ try {
642
+ const descendants: OrgRecord[] = [];
643
+
644
+ // Recursive function to collect all children
645
+ async function collect_descendants(current_id: string): Promise<void> {
646
+ const children_result = await get_org_children(adapter, current_id, include_inactive);
647
+
648
+ if (children_result.success && children_result.orgs) {
649
+ for (const child of children_result.orgs) {
650
+ descendants.push(child);
651
+ await collect_descendants(child.id);
652
+ }
653
+ }
654
+ }
655
+
656
+ await collect_descendants(org_id);
657
+
658
+ return {
659
+ success: true,
660
+ orgs: descendants,
661
+ };
662
+ } catch (error) {
663
+ const logger = create_app_logger();
664
+ const error_message = sanitize_error_for_user(error, {
665
+ logToConsole: true,
666
+ logToLogger: true,
667
+ logger,
668
+ context: {
669
+ filename: "org_service.ts",
670
+ line_number: 0,
671
+ operation: "get_org_descendants",
672
+ org_id,
673
+ },
674
+ });
675
+
676
+ return {
677
+ success: false,
678
+ error: error_message,
679
+ };
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Gets organization hierarchy tree
685
+ * @param adapter - HazoConnect adapter
686
+ * @param root_org_id - Optional root org ID to start from (global admin: no filter)
687
+ * @param include_inactive - Include inactive orgs in tree
688
+ * @returns Nested organization tree
689
+ */
690
+ export async function get_org_tree(
691
+ adapter: HazoConnectAdapter,
692
+ root_org_id?: string,
693
+ include_inactive?: boolean,
694
+ ): Promise<{ success: boolean; tree?: OrgTreeNode[]; error?: string }> {
695
+ try {
696
+ const org_service = createCrudService(adapter, TABLE_NAME);
697
+
698
+ // Get root-level orgs (those without parent_org_id)
699
+ let root_orgs: unknown[];
700
+
701
+ if (root_org_id) {
702
+ // Get specific root org
703
+ root_orgs = await org_service.findBy({ id: root_org_id });
704
+ } else {
705
+ // Get all root orgs (no parent)
706
+ const all_orgs = await org_service.findBy({});
707
+ if (Array.isArray(all_orgs)) {
708
+ root_orgs = all_orgs.filter(
709
+ (org: unknown) => (org as OrgRecord).parent_org_id === null
710
+ );
711
+ } else {
712
+ root_orgs = [];
713
+ }
714
+ }
715
+
716
+ if (!Array.isArray(root_orgs)) {
717
+ return {
718
+ success: true,
719
+ tree: [],
720
+ };
721
+ }
722
+
723
+ // Filter inactive if not requested
724
+ let filtered_roots = root_orgs as OrgRecord[];
725
+ if (!include_inactive) {
726
+ filtered_roots = filtered_roots.filter((org) => org.active !== false);
727
+ }
728
+
729
+ // Build tree recursively
730
+ async function build_tree(org: OrgRecord): Promise<OrgTreeNode> {
731
+ // Get user count
732
+ const count_result = await get_org_user_count(adapter, org.id);
733
+ const current_user_count = count_result.success ? count_result.count! : 0;
734
+
735
+ const node: OrgTreeNode = {
736
+ ...org,
737
+ current_user_count,
738
+ children: [],
739
+ };
740
+
741
+ const children_result = await get_org_children(adapter, org.id, include_inactive);
742
+ if (children_result.success && children_result.orgs) {
743
+ for (const child of children_result.orgs) {
744
+ const child_node = await build_tree(child);
745
+ node.children!.push(child_node);
746
+ }
747
+ }
748
+
749
+ return node;
750
+ }
751
+
752
+ const tree: OrgTreeNode[] = [];
753
+ for (const root_org of filtered_roots) {
754
+ const node = await build_tree(root_org);
755
+ tree.push(node);
756
+ }
757
+
758
+ return {
759
+ success: true,
760
+ tree,
761
+ };
762
+ } catch (error) {
763
+ const logger = create_app_logger();
764
+ const error_message = sanitize_error_for_user(error, {
765
+ logToConsole: true,
766
+ logToLogger: true,
767
+ logger,
768
+ context: {
769
+ filename: "org_service.ts",
770
+ line_number: 0,
771
+ operation: "get_org_tree",
772
+ root_org_id,
773
+ },
774
+ });
775
+
776
+ return {
777
+ success: false,
778
+ error: error_message,
779
+ };
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Checks if a user can be added to an organization (user_limit check)
785
+ * Only applies to root-level orgs (checks root_org's user_limit)
786
+ * @param adapter - HazoConnect adapter
787
+ * @param org_id - Organization ID
788
+ * @returns Whether user can be added and reason if not
789
+ */
790
+ export async function can_add_user_to_org(
791
+ adapter: HazoConnectAdapter,
792
+ org_id: string,
793
+ ): Promise<{ success: boolean; can_add: boolean; reason?: string; error?: string }> {
794
+ try {
795
+ // Get the org
796
+ const org_result = await get_org_by_id(adapter, org_id);
797
+ if (!org_result.success || !org_result.org) {
798
+ return {
799
+ success: false,
800
+ can_add: false,
801
+ error: org_result.error || "Organization not found",
802
+ };
803
+ }
804
+
805
+ const org = org_result.org;
806
+
807
+ // If org is inactive, can't add users
808
+ if (org.active === false) {
809
+ return {
810
+ success: true,
811
+ can_add: false,
812
+ reason: "Organization is inactive",
813
+ };
814
+ }
815
+
816
+ // Determine which user_limit to check (root org's limit)
817
+ let root_org_id = org.root_org_id || org.id;
818
+ let root_org: OrgWithUserCount;
819
+
820
+ if (root_org_id === org.id) {
821
+ root_org = org;
822
+ } else {
823
+ const root_result = await get_org_by_id(adapter, root_org_id);
824
+ if (!root_result.success || !root_result.org) {
825
+ // If can't find root, assume no limit
826
+ return {
827
+ success: true,
828
+ can_add: true,
829
+ };
830
+ }
831
+ root_org = root_result.org;
832
+ }
833
+
834
+ // If user_limit is 0, unlimited
835
+ if (root_org.user_limit === 0) {
836
+ return {
837
+ success: true,
838
+ can_add: true,
839
+ };
840
+ }
841
+
842
+ // Get total user count for root org tree
843
+ const count_result = await get_root_org_user_count(adapter, root_org.id);
844
+ if (!count_result.success) {
845
+ return {
846
+ success: true,
847
+ can_add: true, // Assume can add if can't check
848
+ };
849
+ }
850
+
851
+ const current_count = count_result.count!;
852
+
853
+ if (current_count >= root_org.user_limit) {
854
+ return {
855
+ success: true,
856
+ can_add: false,
857
+ reason: `Organization user limit reached (${current_count}/${root_org.user_limit})`,
858
+ };
859
+ }
860
+
861
+ return {
862
+ success: true,
863
+ can_add: true,
864
+ };
865
+ } catch (error) {
866
+ const logger = create_app_logger();
867
+ const error_message = sanitize_error_for_user(error, {
868
+ logToConsole: true,
869
+ logToLogger: true,
870
+ logger,
871
+ context: {
872
+ filename: "org_service.ts",
873
+ line_number: 0,
874
+ operation: "can_add_user_to_org",
875
+ org_id,
876
+ },
877
+ });
878
+
879
+ return {
880
+ success: false,
881
+ can_add: false,
882
+ error: error_message,
883
+ };
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Checks if user has access to an organization (is in org's hierarchy)
889
+ * @param adapter - HazoConnect adapter
890
+ * @param user_org_id - User's org_id
891
+ * @param user_root_org_id - User's root_org_id
892
+ * @param target_org_id - Target org to check access to
893
+ * @returns Whether user has access
894
+ */
895
+ export async function check_user_org_access(
896
+ adapter: HazoConnectAdapter,
897
+ user_org_id: string | null,
898
+ user_root_org_id: string | null,
899
+ target_org_id: string,
900
+ ): Promise<{ success: boolean; has_access: boolean; error?: string }> {
901
+ try {
902
+ // If user has no org, no access
903
+ if (!user_org_id && !user_root_org_id) {
904
+ return {
905
+ success: true,
906
+ has_access: false,
907
+ };
908
+ }
909
+
910
+ // Get target org
911
+ const target_result = await get_org_by_id(adapter, target_org_id);
912
+ if (!target_result.success || !target_result.org) {
913
+ return {
914
+ success: true,
915
+ has_access: false,
916
+ };
917
+ }
918
+
919
+ const target_org = target_result.org;
920
+
921
+ // Check if target is in user's org tree
922
+ const target_root = target_org.root_org_id || target_org.id;
923
+
924
+ // User has access if they share the same root org
925
+ if (user_root_org_id === target_root || user_org_id === target_root) {
926
+ return {
927
+ success: true,
928
+ has_access: true,
929
+ };
930
+ }
931
+
932
+ // Or if user's org_id matches the target
933
+ if (user_org_id === target_org_id) {
934
+ return {
935
+ success: true,
936
+ has_access: true,
937
+ };
938
+ }
939
+
940
+ return {
941
+ success: true,
942
+ has_access: false,
943
+ };
944
+ } catch (error) {
945
+ const logger = create_app_logger();
946
+ const error_message = sanitize_error_for_user(error, {
947
+ logToConsole: true,
948
+ logToLogger: true,
949
+ logger,
950
+ context: {
951
+ filename: "org_service.ts",
952
+ line_number: 0,
953
+ operation: "check_user_org_access",
954
+ user_org_id,
955
+ target_org_id,
956
+ },
957
+ });
958
+
959
+ return {
960
+ success: false,
961
+ has_access: false,
962
+ error: error_message,
963
+ };
964
+ }
965
+ }