hazo_auth 4.4.1 → 4.5.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 (94) hide show
  1. package/README.md +207 -5
  2. package/SETUP_CHECKLIST.md +1 -1
  3. package/cli-src/lib/auth/auth_types.ts +22 -0
  4. package/cli-src/lib/auth/hazo_get_auth.server.ts +25 -1
  5. package/cli-src/lib/auth/session_token_validator.edge.ts +1 -0
  6. package/cli-src/lib/config/default_config.ts +36 -0
  7. package/cli-src/lib/navbar_config.server.ts +129 -0
  8. package/cli-src/lib/scope_hierarchy_config.server.ts +3 -14
  9. package/cli-src/lib/services/registration_service.ts +12 -0
  10. package/cli-src/lib/services/scope_labels_service.ts +21 -21
  11. package/cli-src/lib/services/scope_service.ts +15 -11
  12. package/cli-src/lib/services/session_token_service.ts +1 -0
  13. package/cli-src/lib/ui_shell_config.server.ts +15 -0
  14. package/cli-src/lib/user_types_config.server.ts +178 -0
  15. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
  16. package/dist/app/api/hazo_auth/me/route.js +17 -0
  17. package/dist/app/api/hazo_auth/org_management/orgs/route.d.ts +26 -0
  18. package/dist/app/api/hazo_auth/org_management/orgs/route.d.ts.map +1 -0
  19. package/dist/app/api/hazo_auth/org_management/orgs/route.js +315 -0
  20. package/dist/app/api/hazo_auth/user_management/users/route.d.ts +11 -1
  21. package/dist/app/api/hazo_auth/user_management/users/route.d.ts.map +1 -1
  22. package/dist/app/api/hazo_auth/user_management/users/route.js +121 -16
  23. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
  24. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +8 -14
  25. package/dist/components/layouts/rbac_test/index.d.ts +1 -3
  26. package/dist/components/layouts/rbac_test/index.d.ts.map +1 -1
  27. package/dist/components/layouts/rbac_test/index.js +2 -2
  28. package/dist/components/layouts/shared/components/auth_navbar.d.ts +26 -0
  29. package/dist/components/layouts/shared/components/auth_navbar.d.ts.map +1 -0
  30. package/dist/components/layouts/shared/components/auth_navbar.js +14 -0
  31. package/dist/components/layouts/shared/components/auth_page_shell.d.ts +3 -1
  32. package/dist/components/layouts/shared/components/auth_page_shell.d.ts.map +1 -1
  33. package/dist/components/layouts/shared/components/auth_page_shell.js +17 -2
  34. package/dist/components/layouts/shared/components/standalone_layout_wrapper.d.ts +6 -1
  35. package/dist/components/layouts/shared/components/standalone_layout_wrapper.d.ts.map +1 -1
  36. package/dist/components/layouts/shared/components/standalone_layout_wrapper.js +7 -2
  37. package/dist/components/layouts/shared/index.d.ts +2 -0
  38. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  39. package/dist/components/layouts/shared/index.js +1 -0
  40. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +3 -2
  41. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -1
  42. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +45 -18
  43. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +3 -2
  44. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -1
  45. package/dist/components/layouts/user_management/components/scope_labels_tab.js +48 -20
  46. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -1
  47. package/dist/components/layouts/user_management/components/user_scopes_tab.js +1 -1
  48. package/dist/components/layouts/user_management/index.d.ts +11 -3
  49. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  50. package/dist/components/layouts/user_management/index.js +52 -5
  51. package/dist/components/ui/user-type-badge.d.ts +23 -0
  52. package/dist/components/ui/user-type-badge.d.ts.map +1 -0
  53. package/dist/components/ui/user-type-badge.js +42 -0
  54. package/dist/lib/auth/auth_types.d.ts +17 -0
  55. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  56. package/dist/lib/auth/auth_types.js +11 -0
  57. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  58. package/dist/lib/auth/hazo_get_auth.server.js +21 -1
  59. package/dist/lib/config/default_config.d.ts +60 -0
  60. package/dist/lib/config/default_config.d.ts.map +1 -1
  61. package/dist/lib/config/default_config.js +34 -0
  62. package/dist/lib/navbar_config.server.d.ts +36 -0
  63. package/dist/lib/navbar_config.server.d.ts.map +1 -0
  64. package/dist/lib/navbar_config.server.js +45 -0
  65. package/dist/lib/scope_hierarchy_config.server.d.ts +3 -7
  66. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -1
  67. package/dist/lib/scope_hierarchy_config.server.js +1 -10
  68. package/dist/lib/services/registration_service.d.ts.map +1 -1
  69. package/dist/lib/services/registration_service.js +8 -0
  70. package/dist/lib/services/scope_labels_service.d.ts +7 -7
  71. package/dist/lib/services/scope_labels_service.d.ts.map +1 -1
  72. package/dist/lib/services/scope_labels_service.js +20 -20
  73. package/dist/lib/services/scope_service.d.ts +8 -5
  74. package/dist/lib/services/scope_service.d.ts.map +1 -1
  75. package/dist/lib/services/scope_service.js +9 -8
  76. package/dist/lib/ui_shell_config.server.d.ts +5 -0
  77. package/dist/lib/ui_shell_config.server.d.ts.map +1 -1
  78. package/dist/lib/ui_shell_config.server.js +5 -0
  79. package/dist/lib/user_types_config.server.d.ts +56 -0
  80. package/dist/lib/user_types_config.server.d.ts.map +1 -0
  81. package/dist/lib/user_types_config.server.js +100 -0
  82. package/dist/server/routes/index.d.ts +1 -0
  83. package/dist/server/routes/index.d.ts.map +1 -1
  84. package/dist/server/routes/index.js +2 -0
  85. package/dist/server/routes/org_management_orgs.d.ts +2 -0
  86. package/dist/server/routes/org_management_orgs.d.ts.map +1 -0
  87. package/dist/server/routes/org_management_orgs.js +2 -0
  88. package/hazo_auth_config.example.ini +9 -0
  89. package/package.json +1 -1
  90. package/cli-src/server/logging/logger_service.ts +0 -56
  91. /package/public/profile_pictures/library/Cars/{050 - citroe/314/210n_c3.jpeg" → 050 - citro/303/253n_c3.jpeg"} +0 -0
  92. /package/public/profile_pictures/library/Cars/{064 - lamborghini_huraca/314/201n.jpeg" → 064 - lamborghini_hurac/303/241n.jpeg"} +0 -0
  93. /package/public/profile_pictures/library/Cars/{099 - citroe/314/210n_2cv_(classic).jpeg" → 099 - citro/303/253n_2cv_(classic).jpeg"} +0 -0
  94. /package/public/profile_pictures/library/Cars/{131 - lamborghini_huraca/314/201n_sto.jpeg" → 131 - lamborghini_hurac/303/241n_sto.jpeg"} +0 -0
@@ -0,0 +1,315 @@
1
+ // file_description: API route for organization management (list, create, update, soft delete)
2
+ // section: imports
3
+ import { NextResponse } from "next/server";
4
+ import { get_hazo_connect_instance } from "../../../../../lib/hazo_connect_instance.server";
5
+ import { hazo_get_auth } from "../../../../../lib/auth/hazo_get_auth.server";
6
+ import { create_app_logger } from "../../../../../lib/app_logger";
7
+ import { get_filename, get_line_number } from "../../../../../lib/utils/api_route_helpers";
8
+ import { is_multi_tenancy_enabled } from "../../../../../lib/multi_tenancy_config.server";
9
+ import { get_orgs, create_org, update_org, soft_delete_org, get_org_tree, check_user_org_access, } from "../../../../../lib/services/org_service";
10
+ // section: route_config
11
+ export const dynamic = "force-dynamic";
12
+ // section: constants
13
+ const PERMISSION_ORG_MANAGEMENT = "hazo_perm_org_management";
14
+ const PERMISSION_GLOBAL_ADMIN = "hazo_org_global_admin";
15
+ // section: helpers
16
+ async function check_permission(request) {
17
+ const auth_result = await hazo_get_auth(request, {
18
+ required_permissions: [PERMISSION_ORG_MANAGEMENT],
19
+ strict: false,
20
+ });
21
+ if (!auth_result.authenticated) {
22
+ return {
23
+ authorized: false,
24
+ error: NextResponse.json({ error: "Authentication required" }, { status: 401 }),
25
+ };
26
+ }
27
+ if (!auth_result.permission_ok) {
28
+ return {
29
+ authorized: false,
30
+ error: NextResponse.json({ error: "Permission denied", missing_permissions: auth_result.missing_permissions }, { status: 403 }),
31
+ };
32
+ }
33
+ // Check if user has global admin permission
34
+ const is_global_admin = auth_result.permissions.includes(PERMISSION_GLOBAL_ADMIN);
35
+ return {
36
+ authorized: true,
37
+ user_id: auth_result.user.id,
38
+ user_org_id: auth_result.user.org_id || null,
39
+ user_root_org_id: auth_result.user.root_org_id || null,
40
+ is_global_admin,
41
+ };
42
+ }
43
+ // section: api_handler
44
+ /**
45
+ * GET - Fetch organizations
46
+ * Query params:
47
+ * - action: 'list' | 'tree' (default: 'list')
48
+ * - include_inactive: boolean (default: false)
49
+ * - root_org_id: string (optional, filter by root org - required unless global admin)
50
+ */
51
+ export async function GET(request) {
52
+ var _a;
53
+ const logger = create_app_logger();
54
+ try {
55
+ // Check if multi-tenancy is enabled
56
+ if (!is_multi_tenancy_enabled()) {
57
+ return NextResponse.json({ error: "Multi-tenancy is not enabled", code: "MULTI_TENANCY_DISABLED" }, { status: 400 });
58
+ }
59
+ // Check permission
60
+ const perm_check = await check_permission(request);
61
+ if (!perm_check.authorized) {
62
+ return perm_check.error;
63
+ }
64
+ const { searchParams } = new URL(request.url);
65
+ const action = searchParams.get("action") || "list";
66
+ const include_inactive = searchParams.get("include_inactive") === "true";
67
+ let root_org_id = searchParams.get("root_org_id") || undefined;
68
+ const adapter = get_hazo_connect_instance();
69
+ // If not global admin, restrict to user's org tree
70
+ if (!perm_check.is_global_admin) {
71
+ // User must have an org to view orgs
72
+ if (!perm_check.user_root_org_id && !perm_check.user_org_id) {
73
+ return NextResponse.json({ error: "You are not assigned to any organization" }, { status: 403 });
74
+ }
75
+ // Force filter to user's root org
76
+ root_org_id = perm_check.user_root_org_id || perm_check.user_org_id || undefined;
77
+ }
78
+ if (action === "tree") {
79
+ // Return hierarchical tree structure
80
+ const result = await get_org_tree(adapter, root_org_id, include_inactive);
81
+ if (!result.success) {
82
+ return NextResponse.json({ error: result.error }, { status: 500 });
83
+ }
84
+ return NextResponse.json({
85
+ success: true,
86
+ tree: result.tree,
87
+ });
88
+ }
89
+ // List organizations
90
+ const result = await get_orgs(adapter, {
91
+ root_org_id,
92
+ include_inactive,
93
+ });
94
+ if (!result.success) {
95
+ return NextResponse.json({ error: result.error }, { status: 500 });
96
+ }
97
+ logger.info("org_management_orgs_fetched", {
98
+ filename: get_filename(),
99
+ line_number: get_line_number(),
100
+ root_org_id,
101
+ count: ((_a = result.orgs) === null || _a === void 0 ? void 0 : _a.length) || 0,
102
+ is_global_admin: perm_check.is_global_admin,
103
+ });
104
+ return NextResponse.json({
105
+ success: true,
106
+ orgs: result.orgs,
107
+ });
108
+ }
109
+ catch (error) {
110
+ const error_message = error instanceof Error ? error.message : "Unknown error";
111
+ logger.error("org_management_orgs_fetch_error", {
112
+ filename: get_filename(),
113
+ line_number: get_line_number(),
114
+ error_message,
115
+ });
116
+ return NextResponse.json({ error: "Failed to fetch organizations" }, { status: 500 });
117
+ }
118
+ }
119
+ /**
120
+ * POST - Create a new organization
121
+ * Body: { name: string, user_limit?: number, parent_org_id?: string }
122
+ */
123
+ export async function POST(request) {
124
+ var _a;
125
+ const logger = create_app_logger();
126
+ try {
127
+ // Check if multi-tenancy is enabled
128
+ if (!is_multi_tenancy_enabled()) {
129
+ return NextResponse.json({ error: "Multi-tenancy is not enabled", code: "MULTI_TENANCY_DISABLED" }, { status: 400 });
130
+ }
131
+ // Check permission
132
+ const perm_check = await check_permission(request);
133
+ if (!perm_check.authorized) {
134
+ return perm_check.error;
135
+ }
136
+ const body = await request.json();
137
+ const { name, user_limit, parent_org_id } = body;
138
+ // Validate required fields
139
+ if (!name || typeof name !== "string" || name.trim().length === 0) {
140
+ return NextResponse.json({ error: "name is required and must be a non-empty string" }, { status: 400 });
141
+ }
142
+ const adapter = get_hazo_connect_instance();
143
+ // If not global admin, verify user has access to parent org
144
+ if (!perm_check.is_global_admin && parent_org_id) {
145
+ const access_check = await check_user_org_access(adapter, perm_check.user_org_id || null, perm_check.user_root_org_id || null, parent_org_id);
146
+ if (!access_check.has_access) {
147
+ return NextResponse.json({ error: "You do not have access to the parent organization" }, { status: 403 });
148
+ }
149
+ }
150
+ // If not global admin and no parent_org_id, they can't create root orgs
151
+ if (!perm_check.is_global_admin && !parent_org_id) {
152
+ return NextResponse.json({ error: "Only global admins can create root organizations" }, { status: 403 });
153
+ }
154
+ const result = await create_org(adapter, {
155
+ name: name.trim(),
156
+ user_limit: typeof user_limit === "number" ? user_limit : 0,
157
+ parent_org_id,
158
+ created_by: perm_check.user_id,
159
+ });
160
+ if (!result.success) {
161
+ return NextResponse.json({ error: result.error }, { status: 400 });
162
+ }
163
+ logger.info("org_management_org_created", {
164
+ filename: get_filename(),
165
+ line_number: get_line_number(),
166
+ org_id: (_a = result.org) === null || _a === void 0 ? void 0 : _a.id,
167
+ name: name.trim(),
168
+ parent_org_id,
169
+ created_by: perm_check.user_id,
170
+ });
171
+ return NextResponse.json({
172
+ success: true,
173
+ org: result.org,
174
+ }, { status: 201 });
175
+ }
176
+ catch (error) {
177
+ const error_message = error instanceof Error ? error.message : "Unknown error";
178
+ logger.error("org_management_org_create_error", {
179
+ filename: get_filename(),
180
+ line_number: get_line_number(),
181
+ error_message,
182
+ });
183
+ return NextResponse.json({ error: "Failed to create organization" }, { status: 500 });
184
+ }
185
+ }
186
+ /**
187
+ * PATCH - Update an existing organization
188
+ * Body: { org_id: string, name?: string, user_limit?: number }
189
+ */
190
+ export async function PATCH(request) {
191
+ const logger = create_app_logger();
192
+ try {
193
+ // Check if multi-tenancy is enabled
194
+ if (!is_multi_tenancy_enabled()) {
195
+ return NextResponse.json({ error: "Multi-tenancy is not enabled", code: "MULTI_TENANCY_DISABLED" }, { status: 400 });
196
+ }
197
+ // Check permission
198
+ const perm_check = await check_permission(request);
199
+ if (!perm_check.authorized) {
200
+ return perm_check.error;
201
+ }
202
+ const body = await request.json();
203
+ const { org_id, name, user_limit } = body;
204
+ // Validate required fields
205
+ if (!org_id || typeof org_id !== "string") {
206
+ return NextResponse.json({ error: "org_id is required" }, { status: 400 });
207
+ }
208
+ const adapter = get_hazo_connect_instance();
209
+ // If not global admin, verify user has access to this org
210
+ if (!perm_check.is_global_admin) {
211
+ const access_check = await check_user_org_access(adapter, perm_check.user_org_id || null, perm_check.user_root_org_id || null, org_id);
212
+ if (!access_check.has_access) {
213
+ return NextResponse.json({ error: "You do not have access to this organization" }, { status: 403 });
214
+ }
215
+ }
216
+ // Build update data
217
+ const update_data = {
218
+ changed_by: perm_check.user_id,
219
+ };
220
+ if (name !== undefined) {
221
+ if (typeof name !== "string" || name.trim().length === 0) {
222
+ return NextResponse.json({ error: "name must be a non-empty string" }, { status: 400 });
223
+ }
224
+ update_data.name = name.trim();
225
+ }
226
+ if (user_limit !== undefined) {
227
+ if (typeof user_limit !== "number" || user_limit < 0) {
228
+ return NextResponse.json({ error: "user_limit must be a non-negative number" }, { status: 400 });
229
+ }
230
+ update_data.user_limit = user_limit;
231
+ }
232
+ if (update_data.name === undefined && update_data.user_limit === undefined) {
233
+ return NextResponse.json({ error: "No update data provided" }, { status: 400 });
234
+ }
235
+ const result = await update_org(adapter, org_id, update_data);
236
+ if (!result.success) {
237
+ return NextResponse.json({ error: result.error }, { status: 400 });
238
+ }
239
+ logger.info("org_management_org_updated", {
240
+ filename: get_filename(),
241
+ line_number: get_line_number(),
242
+ org_id,
243
+ changed_by: perm_check.user_id,
244
+ });
245
+ return NextResponse.json({
246
+ success: true,
247
+ org: result.org,
248
+ });
249
+ }
250
+ catch (error) {
251
+ const error_message = error instanceof Error ? error.message : "Unknown error";
252
+ logger.error("org_management_org_update_error", {
253
+ filename: get_filename(),
254
+ line_number: get_line_number(),
255
+ error_message,
256
+ });
257
+ return NextResponse.json({ error: "Failed to update organization" }, { status: 500 });
258
+ }
259
+ }
260
+ /**
261
+ * DELETE - Soft delete an organization (sets active = false)
262
+ * Query params: org_id
263
+ */
264
+ export async function DELETE(request) {
265
+ const logger = create_app_logger();
266
+ try {
267
+ // Check if multi-tenancy is enabled
268
+ if (!is_multi_tenancy_enabled()) {
269
+ return NextResponse.json({ error: "Multi-tenancy is not enabled", code: "MULTI_TENANCY_DISABLED" }, { status: 400 });
270
+ }
271
+ // Check permission
272
+ const perm_check = await check_permission(request);
273
+ if (!perm_check.authorized) {
274
+ return perm_check.error;
275
+ }
276
+ const { searchParams } = new URL(request.url);
277
+ const org_id = searchParams.get("org_id");
278
+ if (!org_id) {
279
+ return NextResponse.json({ error: "org_id query parameter is required" }, { status: 400 });
280
+ }
281
+ const adapter = get_hazo_connect_instance();
282
+ // If not global admin, verify user has access to this org
283
+ if (!perm_check.is_global_admin) {
284
+ const access_check = await check_user_org_access(adapter, perm_check.user_org_id || null, perm_check.user_root_org_id || null, org_id);
285
+ if (!access_check.has_access) {
286
+ return NextResponse.json({ error: "You do not have access to this organization" }, { status: 403 });
287
+ }
288
+ }
289
+ // Soft delete (set active = false)
290
+ const result = await soft_delete_org(adapter, org_id, perm_check.user_id);
291
+ if (!result.success) {
292
+ return NextResponse.json({ error: result.error }, { status: 400 });
293
+ }
294
+ logger.info("org_management_org_deactivated", {
295
+ filename: get_filename(),
296
+ line_number: get_line_number(),
297
+ org_id,
298
+ deactivated_by: perm_check.user_id,
299
+ });
300
+ return NextResponse.json({
301
+ success: true,
302
+ org: result.org,
303
+ message: "Organization deactivated successfully",
304
+ });
305
+ }
306
+ catch (error) {
307
+ const error_message = error instanceof Error ? error.message : "Unknown error";
308
+ logger.error("org_management_org_deactivate_error", {
309
+ filename: get_filename(),
310
+ line_number: get_line_number(),
311
+ error_message,
312
+ });
313
+ return NextResponse.json({ error: "Failed to deactivate organization" }, { status: 500 });
314
+ }
315
+ }
@@ -8,6 +8,13 @@ export declare function GET(request: NextRequest): Promise<NextResponse<{
8
8
  error: string;
9
9
  }> | NextResponse<{
10
10
  success: boolean;
11
+ user_types_enabled: boolean;
12
+ available_user_types: {
13
+ key: string;
14
+ label: string;
15
+ badge_color: string;
16
+ }[];
17
+ multi_tenancy_enabled: boolean;
11
18
  users: {
12
19
  id: unknown;
13
20
  name: {} | null;
@@ -18,10 +25,13 @@ export declare function GET(request: NextRequest): Promise<NextResponse<{
18
25
  created_at: {} | null;
19
26
  profile_picture_url: {} | null;
20
27
  profile_source: {} | null;
28
+ user_type: string | null;
29
+ org_id: string | null | undefined;
30
+ root_org_id: string | null | undefined;
21
31
  }[];
22
32
  }>>;
23
33
  /**
24
- * PATCH - Update user (deactivate: set is_active to false)
34
+ * PATCH - Update user (deactivate: set is_active to false, assign org, etc.)
25
35
  */
26
36
  export declare function PATCH(request: NextRequest): Promise<NextResponse<{
27
37
  error: string;
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../src/app/api/hazo_auth/user_management/users/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUxD,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAGvC;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;IA2D7C;AAED;;GAEG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,WAAW;;;;IA0E/C;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;IA2E9C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../src/app/api/hazo_auth/user_management/users/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsBxD,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAGvC;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;IA+E7C;AAED;;GAEG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,WAAW;;;;IA2L/C;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;IA2E9C"}
@@ -8,6 +8,11 @@ import { get_filename, get_line_number } from "../../../../../lib/utils/api_rout
8
8
  import { request_password_reset } from "../../../../../lib/services/password_reset_service";
9
9
  import { get_auth_cache } from "../../../../../lib/auth/auth_cache";
10
10
  import { get_auth_utility_config } from "../../../../../lib/auth_utility_config.server";
11
+ import { is_user_types_enabled, get_all_user_types, get_user_types_config, } from "../../../../../lib/user_types_config.server";
12
+ import { is_multi_tenancy_enabled } from "../../../../../lib/multi_tenancy_config.server";
13
+ import { get_org_by_id, can_add_user_to_org, } from "../../../../../lib/services/org_service";
14
+ import { get_org_cache } from "../../../../../lib/auth/org_cache";
15
+ import { get_multi_tenancy_config } from "../../../../../lib/multi_tenancy_config.server";
11
16
  // section: route_config
12
17
  export const dynamic = 'force-dynamic';
13
18
  // section: api_handler
@@ -32,8 +37,22 @@ export async function GET(request) {
32
37
  line_number: get_line_number(),
33
38
  user_count: users.length,
34
39
  });
40
+ // Check if user types feature is enabled
41
+ const user_types_enabled = is_user_types_enabled();
42
+ const available_user_types = user_types_enabled
43
+ ? get_all_user_types().map((t) => ({
44
+ key: t.key,
45
+ label: t.label,
46
+ badge_color: t.badge_color,
47
+ }))
48
+ : [];
49
+ // Check if multi-tenancy is enabled
50
+ const multi_tenancy_enabled = is_multi_tenancy_enabled();
35
51
  return NextResponse.json({
36
52
  success: true,
53
+ user_types_enabled,
54
+ available_user_types,
55
+ multi_tenancy_enabled,
37
56
  users: users.map((user) => ({
38
57
  id: user.id,
39
58
  name: user.name || null,
@@ -44,6 +63,10 @@ export async function GET(request) {
44
63
  created_at: user.created_at || null,
45
64
  profile_picture_url: user.profile_picture_url || null,
46
65
  profile_source: user.profile_source || null,
66
+ user_type: user.user_type || null,
67
+ // Include org info when multi-tenancy is enabled
68
+ org_id: multi_tenancy_enabled ? user.org_id || null : undefined,
69
+ root_org_id: multi_tenancy_enabled ? user.root_org_id || null : undefined,
47
70
  })),
48
71
  }, { status: 200 });
49
72
  }
@@ -60,30 +83,89 @@ export async function GET(request) {
60
83
  }
61
84
  }
62
85
  /**
63
- * PATCH - Update user (deactivate: set is_active to false)
86
+ * PATCH - Update user (deactivate: set is_active to false, assign org, etc.)
64
87
  */
65
88
  export async function PATCH(request) {
66
89
  const logger = create_app_logger();
67
90
  try {
68
91
  const body = await request.json();
69
- const { user_id, is_active } = body;
70
- if (!user_id || typeof is_active !== "boolean") {
71
- return NextResponse.json({ error: "user_id and is_active (boolean) are required" }, { status: 400 });
92
+ const { user_id, is_active, user_type, org_id } = body;
93
+ // user_id is always required
94
+ if (!user_id) {
95
+ return NextResponse.json({ error: "user_id is required" }, { status: 400 });
72
96
  }
97
+ // Build update object based on what's provided
98
+ const update_data = {
99
+ changed_at: new Date().toISOString(),
100
+ };
73
101
  const hazoConnect = get_hazo_connect_instance();
102
+ // Handle is_active if provided
103
+ if (typeof is_active === "boolean") {
104
+ update_data.is_active = is_active;
105
+ }
106
+ // Handle user_type if provided (only when feature is enabled)
107
+ if (user_type !== undefined) {
108
+ const config = get_user_types_config();
109
+ if (config.enable_user_types) {
110
+ // Allow null to clear the type, or validate type exists
111
+ if (user_type === null || user_type === "") {
112
+ update_data.user_type = null;
113
+ }
114
+ else if (config.user_types.has(user_type)) {
115
+ update_data.user_type = user_type;
116
+ }
117
+ else {
118
+ return NextResponse.json({ error: "Invalid user type" }, { status: 400 });
119
+ }
120
+ }
121
+ }
122
+ // Handle org_id if provided (only when multi-tenancy is enabled)
123
+ if (org_id !== undefined) {
124
+ if (!is_multi_tenancy_enabled()) {
125
+ return NextResponse.json({ error: "Multi-tenancy is not enabled" }, { status: 400 });
126
+ }
127
+ // Allow null to clear the org assignment
128
+ if (org_id === null || org_id === "") {
129
+ update_data.org_id = null;
130
+ update_data.root_org_id = null;
131
+ }
132
+ else {
133
+ // Validate org exists
134
+ const org_result = await get_org_by_id(hazoConnect, org_id);
135
+ if (!org_result.success || !org_result.org) {
136
+ return NextResponse.json({ error: "Organization not found" }, { status: 400 });
137
+ }
138
+ const org = org_result.org;
139
+ // Check if org is active
140
+ if (org.active === false) {
141
+ return NextResponse.json({ error: "Cannot assign user to inactive organization" }, { status: 400 });
142
+ }
143
+ // Check user limit
144
+ const limit_check = await can_add_user_to_org(hazoConnect, org_id);
145
+ if (limit_check.success && !limit_check.can_add) {
146
+ return NextResponse.json({ error: limit_check.reason || "Organization user limit reached" }, { status: 400 });
147
+ }
148
+ // Set org_id and calculate root_org_id
149
+ update_data.org_id = org_id;
150
+ update_data.root_org_id = org.root_org_id || org_id;
151
+ }
152
+ }
153
+ // Ensure there's something to update besides changed_at
154
+ if (Object.keys(update_data).length === 1) {
155
+ return NextResponse.json({ error: "No valid fields to update" }, { status: 400 });
156
+ }
74
157
  const users_service = createCrudService(hazoConnect, "hazo_users");
75
- // Update user with changed_at timestamp
76
- const now = new Date().toISOString();
77
- await users_service.updateById(user_id, {
78
- is_active,
79
- changed_at: now,
80
- });
81
- // Invalidate user cache after deactivation
82
- if (is_active === false) {
158
+ // Update user
159
+ await users_service.updateById(user_id, update_data);
160
+ // Invalidate caches
161
+ let cache_invalidated = false;
162
+ // Invalidate user auth cache if user deactivated or org changed
163
+ if (is_active === false || org_id !== undefined) {
83
164
  try {
84
- const config = get_auth_utility_config();
85
- const cache = get_auth_cache(config.cache_max_users, config.cache_ttl_minutes, config.cache_max_age_minutes);
86
- cache.invalidate_user(user_id);
165
+ const auth_config = get_auth_utility_config();
166
+ const auth_cache = get_auth_cache(auth_config.cache_max_users, auth_config.cache_ttl_minutes, auth_config.cache_max_age_minutes);
167
+ auth_cache.invalidate_user(user_id);
168
+ cache_invalidated = true;
87
169
  }
88
170
  catch (cache_error) {
89
171
  // Log but don't fail user update if cache invalidation fails
@@ -96,11 +178,34 @@ export async function PATCH(request) {
96
178
  });
97
179
  }
98
180
  }
181
+ // If org changed, also invalidate org cache for old and new orgs
182
+ if (org_id !== undefined && is_multi_tenancy_enabled()) {
183
+ try {
184
+ const mt_config = get_multi_tenancy_config();
185
+ const org_cache = get_org_cache(mt_config.org_cache_max_entries, mt_config.org_cache_ttl_minutes);
186
+ // Invalidate for the new org if set
187
+ if (org_id && typeof org_id === "string") {
188
+ org_cache.invalidate(org_id);
189
+ }
190
+ }
191
+ catch (cache_error) {
192
+ // Log but don't fail user update if cache invalidation fails
193
+ const cache_error_message = cache_error instanceof Error ? cache_error.message : "Unknown error";
194
+ logger.warn("user_management_org_cache_invalidation_failed", {
195
+ filename: get_filename(),
196
+ line_number: get_line_number(),
197
+ user_id,
198
+ org_id,
199
+ error: cache_error_message,
200
+ });
201
+ }
202
+ }
99
203
  logger.info("user_management_user_updated", {
100
204
  filename: get_filename(),
101
205
  line_number: get_line_number(),
102
206
  user_id,
103
- is_active,
207
+ updated_fields: Object.keys(update_data).filter((k) => k !== "changed_at"),
208
+ cache_invalidated,
104
209
  });
105
210
  return NextResponse.json({ success: true }, { status: 200 });
106
211
  }
@@ -1 +1 @@
1
- {"version":3,"file":"profile_picture_library_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_library_tab.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAGF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,QAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,EAAE,6BAA6B,2CAyQ/B"}
1
+ {"version":3,"file":"profile_picture_library_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_library_tab.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAGF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,QAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,EAAE,6BAA6B,2CAsQ/B"}
@@ -112,25 +112,19 @@ export function ProfilePictureLibraryTab({ useLibrary, onUseLibraryChange, onPho
112
112
  const getInitials = () => {
113
113
  return "L";
114
114
  };
115
- // Map column count to Tailwind grid class
116
- const getGridColumnsClass = (columns) => {
117
- const columnMap = {
118
- 1: "grid-cols-1",
119
- 2: "grid-cols-2",
120
- 3: "grid-cols-3",
121
- 4: "grid-cols-4",
122
- 5: "grid-cols-5",
123
- 6: "grid-cols-6",
124
- 7: "grid-cols-7",
125
- 8: "grid-cols-8",
115
+ // Get inline style for grid columns (using inline styles instead of Tailwind classes
116
+ // because dynamic classes like grid-cols-4 may not be included in consuming app's CSS bundle)
117
+ const getGridColumnsStyle = (columns) => {
118
+ const validColumns = Math.min(Math.max(columns, 1), 8);
119
+ return {
120
+ gridTemplateColumns: `repeat(${validColumns}, minmax(0, 1fr))`,
126
121
  };
127
- return columnMap[columns] || "grid-cols-4";
128
122
  };
129
- return (_jsxs("div", { className: "cls_profile_picture_library_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-library", checked: useLibrary, onCheckedChange: onUseLibraryChange, disabled: disabled, className: "cls_profile_picture_library_tab_switch_input", "aria-label": "Use library photo" }), _jsxs(Label, { htmlFor: "use-library", className: "cls_profile_picture_library_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: ["Use library photo", _jsx(HazoUITooltip, { message: libraryTooltipMessage, iconSize: tooltipIconSizeSmall, side: "top" })] })] }), _jsxs("div", { className: "cls_profile_picture_library_tab_content grid grid-cols-12 gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_categories_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_categories_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Categories" }), loadingCategories ? (_jsx("div", { className: "cls_profile_picture_library_tab_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : categories.length > 0 ? (_jsx(VerticalTabs, { value: selectedCategory || categories[0], onValueChange: setSelectedCategory, className: "cls_profile_picture_library_tab_vertical_tabs", children: _jsx(VerticalTabsList, { className: "cls_profile_picture_library_tab_vertical_tabs_list w-full", children: categories.map((category) => (_jsx(VerticalTabsTrigger, { value: category, className: "cls_profile_picture_library_tab_vertical_tabs_trigger w-full justify-start", children: category }, category))) }) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_categories flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_categories_text text-sm text-[var(--hazo-text-muted)]", children: "No categories available" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_photos_container flex flex-col gap-2 col-span-6", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_photos_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Photos" }), loadingPhotos ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : photos.length > 0 ? (_jsx("div", { className: `cls_profile_picture_library_tab_photos_grid grid ${getGridColumnsClass(libraryPhotoGridColumns)} gap-3 overflow-y-auto p-4 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px] max-h-[400px]`, children: photos.map((photoUrl) => (_jsx("button", { type: "button", onClick: () => handlePhotoClick(photoUrl), className: `
123
+ return (_jsxs("div", { className: "cls_profile_picture_library_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-library", checked: useLibrary, onCheckedChange: onUseLibraryChange, disabled: disabled, className: "cls_profile_picture_library_tab_switch_input", "aria-label": "Use library photo" }), _jsxs(Label, { htmlFor: "use-library", className: "cls_profile_picture_library_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: ["Use library photo", _jsx(HazoUITooltip, { message: libraryTooltipMessage, iconSize: tooltipIconSizeSmall, side: "top" })] })] }), _jsxs("div", { className: "cls_profile_picture_library_tab_content grid grid-cols-12 gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_categories_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_categories_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Categories" }), loadingCategories ? (_jsx("div", { className: "cls_profile_picture_library_tab_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : categories.length > 0 ? (_jsx(VerticalTabs, { value: selectedCategory || categories[0], onValueChange: setSelectedCategory, className: "cls_profile_picture_library_tab_vertical_tabs", children: _jsx(VerticalTabsList, { className: "cls_profile_picture_library_tab_vertical_tabs_list w-full", children: categories.map((category) => (_jsx(VerticalTabsTrigger, { value: category, className: "cls_profile_picture_library_tab_vertical_tabs_trigger w-full justify-start", children: category }, category))) }) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_categories flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_categories_text text-sm text-[var(--hazo-text-muted)]", children: "No categories available" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_photos_container flex flex-col gap-2 col-span-6", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_photos_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Photos" }), loadingPhotos ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : photos.length > 0 ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_grid grid gap-3 overflow-y-auto p-4 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px] max-h-[400px]", style: getGridColumnsStyle(libraryPhotoGridColumns), children: photos.map((photoUrl) => (_jsx("button", { type: "button", onClick: () => handlePhotoClick(photoUrl), className: `
130
124
  cls_profile_picture_library_tab_photo_thumbnail
131
125
  w-full aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
132
126
  ${selectedPhoto === photoUrl ? "border-blue-500 ring-2 ring-blue-200" : "border-[var(--hazo-border)] hover:border-[var(--hazo-border-emphasis)]"}
133
- `, style: { minHeight: '80px', minWidth: '80px' }, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
127
+ `, style: { minHeight: '80px', minWidth: '80px', aspectRatio: '1/1' }, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
134
128
  // Fallback if image fails to load
135
129
  const target = e.target;
136
130
  target.style.display = 'none';
@@ -2,8 +2,6 @@ export type RbacTestLayoutProps = {
2
2
  className?: string;
3
3
  /** Whether HRBAC is enabled (passed from server) */
4
4
  hrbacEnabled?: boolean;
5
- /** Default organization for HRBAC scopes */
6
- defaultOrg?: string;
7
5
  };
8
6
  /**
9
7
  * RBAC/HRBAC Test layout component
@@ -11,5 +9,5 @@ export type RbacTestLayoutProps = {
11
9
  * @param props - Component props
12
10
  * @returns RBAC test layout component
13
11
  */
14
- export declare function RbacTestLayout({ className, hrbacEnabled, defaultOrg, }: RbacTestLayoutProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function RbacTestLayout({ className, hrbacEnabled, }: RbacTestLayoutProps): import("react/jsx-runtime").JSX.Element;
15
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/rbac_test/index.tsx"],"names":[],"mappings":"AA2CA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA+GF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,YAAoB,EACpB,UAAe,GAChB,EAAE,mBAAmB,2CAw3BrB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/rbac_test/index.tsx"],"names":[],"mappings":"AA2CA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAgHF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,YAAoB,GACrB,EAAE,mBAAmB,2CAw3BrB"}