hazo_auth 1.6.4 → 1.6.5

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 (35) hide show
  1. package/README.md +46 -1
  2. package/SETUP_CHECKLIST.md +70 -4
  3. package/dist/app/api/hazo_auth/user_management/permissions/route.d.ts +50 -0
  4. package/dist/app/api/hazo_auth/user_management/permissions/route.d.ts.map +1 -0
  5. package/dist/app/api/hazo_auth/user_management/permissions/route.js +257 -0
  6. package/dist/app/api/hazo_auth/user_management/roles/route.d.ts +40 -0
  7. package/dist/app/api/hazo_auth/user_management/roles/route.d.ts.map +1 -0
  8. package/dist/app/api/hazo_auth/user_management/roles/route.js +352 -0
  9. package/dist/app/api/hazo_auth/user_management/users/roles/route.d.ts +37 -0
  10. package/dist/app/api/hazo_auth/user_management/users/roles/route.d.ts.map +1 -0
  11. package/dist/app/api/hazo_auth/user_management/users/roles/route.js +276 -0
  12. package/dist/app/api/hazo_auth/user_management/users/route.d.ts +39 -0
  13. package/dist/app/api/hazo_auth/user_management/users/route.d.ts.map +1 -0
  14. package/dist/app/api/hazo_auth/user_management/users/route.js +170 -0
  15. package/dist/cli/generate.d.ts.map +1 -1
  16. package/dist/cli/generate.js +38 -5
  17. package/dist/cli/validate.d.ts.map +1 -1
  18. package/dist/cli/validate.js +14 -0
  19. package/dist/server/routes/index.d.ts +4 -0
  20. package/dist/server/routes/index.d.ts.map +1 -1
  21. package/dist/server/routes/index.js +5 -0
  22. package/dist/server/routes/user_management_permissions.d.ts +2 -0
  23. package/dist/server/routes/user_management_permissions.d.ts.map +1 -0
  24. package/dist/server/routes/user_management_permissions.js +2 -0
  25. package/dist/server/routes/user_management_roles.d.ts +2 -0
  26. package/dist/server/routes/user_management_roles.d.ts.map +1 -0
  27. package/dist/server/routes/user_management_roles.js +2 -0
  28. package/dist/server/routes/user_management_users.d.ts +2 -0
  29. package/dist/server/routes/user_management_users.d.ts.map +1 -0
  30. package/dist/server/routes/user_management_users.js +2 -0
  31. package/dist/server/routes/user_management_users_roles.d.ts +2 -0
  32. package/dist/server/routes/user_management_users_roles.d.ts.map +1 -0
  33. package/dist/server/routes/user_management_users_roles.js +2 -0
  34. package/hazo_auth_config.example.ini +1 -1
  35. package/package.json +1 -1
@@ -0,0 +1,352 @@
1
+ // file_description: API route for roles management operations (list roles with permissions, create role, update role permissions)
2
+ // section: imports
3
+ import { NextResponse } from "next/server";
4
+ import { get_hazo_connect_instance } from "../../../../../lib/hazo_connect_instance.server";
5
+ import { createCrudService, getSqliteAdminService } from "hazo_connect/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 { get_auth_cache } from "../../../../../lib/auth/auth_cache";
9
+ import { get_auth_utility_config } from "../../../../../lib/auth_utility_config.server";
10
+ // section: route_config
11
+ export const dynamic = 'force-dynamic';
12
+ // section: api_handler
13
+ /**
14
+ * GET - Fetch all roles with their permissions
15
+ */
16
+ export async function GET(request) {
17
+ const logger = create_app_logger();
18
+ try {
19
+ const hazoConnect = get_hazo_connect_instance();
20
+ const roles_service = createCrudService(hazoConnect, "hazo_roles");
21
+ const permissions_service = createCrudService(hazoConnect, "hazo_permissions");
22
+ const role_permissions_service = createCrudService(hazoConnect, "hazo_role_permissions");
23
+ // Fetch all roles (empty object means no filter - get all records)
24
+ const roles = await roles_service.findBy({});
25
+ const permissions = await permissions_service.findBy({});
26
+ const role_permissions = await role_permissions_service.findBy({});
27
+ if (!Array.isArray(roles) || !Array.isArray(permissions) || !Array.isArray(role_permissions)) {
28
+ return NextResponse.json({ error: "Failed to fetch roles data" }, { status: 500 });
29
+ }
30
+ // Build role-permission mapping
31
+ const role_permission_map = {};
32
+ role_permissions.forEach((rp) => {
33
+ const role_id = rp.role_id;
34
+ const permission_id = rp.permission_id;
35
+ if (!role_permission_map[role_id]) {
36
+ role_permission_map[role_id] = [];
37
+ }
38
+ role_permission_map[role_id].push(permission_id);
39
+ });
40
+ // Build permission name map
41
+ const permission_name_map = {};
42
+ permissions.forEach((perm) => {
43
+ permission_name_map[perm.id] = perm.permission_name;
44
+ });
45
+ // Format response
46
+ const roles_with_permissions = roles.map((role) => {
47
+ const role_id = role.id;
48
+ const permission_ids = role_permission_map[role_id] || [];
49
+ const permission_names = permission_ids.map((pid) => permission_name_map[pid]).filter(Boolean);
50
+ return {
51
+ role_id: role.id,
52
+ role_name: role.role_name,
53
+ permissions: permission_names,
54
+ };
55
+ });
56
+ logger.info("user_management_roles_fetched", {
57
+ filename: get_filename(),
58
+ line_number: get_line_number(),
59
+ role_count: roles.length,
60
+ permission_count: permissions.length,
61
+ });
62
+ return NextResponse.json({
63
+ success: true,
64
+ roles: roles_with_permissions,
65
+ permissions: permissions.map((p) => ({
66
+ id: p.id,
67
+ permission_name: p.permission_name,
68
+ })),
69
+ }, { status: 200 });
70
+ }
71
+ catch (error) {
72
+ const error_message = error instanceof Error ? error.message : "Unknown error";
73
+ const error_stack = error instanceof Error ? error.stack : undefined;
74
+ logger.error("user_management_roles_fetch_error", {
75
+ filename: get_filename(),
76
+ line_number: get_line_number(),
77
+ error_message,
78
+ error_stack,
79
+ });
80
+ return NextResponse.json({ error: "Failed to fetch roles" }, { status: 500 });
81
+ }
82
+ }
83
+ /**
84
+ * POST - Create new role
85
+ */
86
+ export async function POST(request) {
87
+ const logger = create_app_logger();
88
+ try {
89
+ const body = await request.json();
90
+ const { role_name } = body;
91
+ if (!role_name || typeof role_name !== "string" || role_name.trim().length === 0) {
92
+ return NextResponse.json({ error: "role_name is required and must be a non-empty string" }, { status: 400 });
93
+ }
94
+ const hazoConnect = get_hazo_connect_instance();
95
+ const roles_service = createCrudService(hazoConnect, "hazo_roles");
96
+ // Check if role already exists
97
+ const existing_roles = await roles_service.findBy({
98
+ role_name: role_name.trim(),
99
+ });
100
+ if (Array.isArray(existing_roles) && existing_roles.length > 0) {
101
+ return NextResponse.json({ error: "Role with this name already exists" }, { status: 409 });
102
+ }
103
+ // Create new role
104
+ const now = new Date().toISOString();
105
+ const new_role_result = await roles_service.insert({
106
+ role_name: role_name.trim(),
107
+ created_at: now,
108
+ changed_at: now,
109
+ });
110
+ // insert() returns an array, get the first element
111
+ if (!Array.isArray(new_role_result) || new_role_result.length === 0) {
112
+ return NextResponse.json({ error: "Failed to create role - no record returned" }, { status: 500 });
113
+ }
114
+ const new_role = new_role_result[0];
115
+ logger.info("user_management_role_created", {
116
+ filename: get_filename(),
117
+ line_number: get_line_number(),
118
+ role_id: new_role.id,
119
+ role_name: role_name.trim(),
120
+ });
121
+ return NextResponse.json({
122
+ success: true,
123
+ role: {
124
+ role_id: new_role.id,
125
+ role_name: role_name.trim(),
126
+ },
127
+ }, { status: 201 });
128
+ }
129
+ catch (error) {
130
+ const error_message = error instanceof Error ? error.message : "Unknown error";
131
+ const error_stack = error instanceof Error ? error.stack : undefined;
132
+ logger.error("user_management_role_create_error", {
133
+ filename: get_filename(),
134
+ line_number: get_line_number(),
135
+ error_message,
136
+ error_stack,
137
+ });
138
+ return NextResponse.json({ error: "Failed to create role" }, { status: 500 });
139
+ }
140
+ }
141
+ /**
142
+ * PUT - Update role permissions (save role-permission matrix)
143
+ */
144
+ export async function PUT(request) {
145
+ const logger = create_app_logger();
146
+ try {
147
+ const body = await request.json();
148
+ const { roles } = body;
149
+ if (!Array.isArray(roles)) {
150
+ return NextResponse.json({ error: "roles array is required" }, { status: 400 });
151
+ }
152
+ const hazoConnect = get_hazo_connect_instance();
153
+ const roles_service = createCrudService(hazoConnect, "hazo_roles");
154
+ const permissions_service = createCrudService(hazoConnect, "hazo_permissions");
155
+ const role_permissions_service = createCrudService(hazoConnect, "hazo_role_permissions");
156
+ // Get all permissions to build name-to-id map (empty object means no filter - get all records)
157
+ const all_permissions = await permissions_service.findBy({});
158
+ if (!Array.isArray(all_permissions)) {
159
+ return NextResponse.json({ error: "Failed to fetch permissions" }, { status: 500 });
160
+ }
161
+ const permission_name_to_id = {};
162
+ all_permissions.forEach((perm) => {
163
+ permission_name_to_id[perm.permission_name] = perm.id;
164
+ });
165
+ const now = new Date().toISOString();
166
+ const modified_role_ids = []; // Track all role IDs that were modified
167
+ // Process each role
168
+ for (const role_data of roles) {
169
+ const { role_id, role_name, permissions } = role_data;
170
+ if (!role_name || !Array.isArray(permissions)) {
171
+ continue; // Skip invalid entries
172
+ }
173
+ let current_role_id;
174
+ if (role_id) {
175
+ // Update existing role
176
+ current_role_id = role_id;
177
+ await roles_service.updateById(role_id, {
178
+ role_name: role_name.trim(),
179
+ changed_at: now,
180
+ });
181
+ }
182
+ else {
183
+ // Create new role
184
+ const existing_roles = await roles_service.findBy({
185
+ role_name: role_name.trim(),
186
+ });
187
+ if (Array.isArray(existing_roles) && existing_roles.length > 0) {
188
+ current_role_id = existing_roles[0].id;
189
+ }
190
+ else {
191
+ const new_role = await roles_service.insert({
192
+ role_name: role_name.trim(),
193
+ created_at: now,
194
+ changed_at: now,
195
+ });
196
+ // Handle both single object and array responses from insert
197
+ if (Array.isArray(new_role) && new_role.length > 0) {
198
+ current_role_id = new_role[0].id;
199
+ }
200
+ else if (!Array.isArray(new_role) && new_role.id !== undefined) {
201
+ current_role_id = new_role.id;
202
+ }
203
+ else {
204
+ // If insert didn't return an id, try to find the role by name
205
+ const inserted_roles = await roles_service.findBy({
206
+ role_name: role_name.trim(),
207
+ });
208
+ if (Array.isArray(inserted_roles) && inserted_roles.length > 0) {
209
+ current_role_id = inserted_roles[0].id;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ // Skip if we couldn't determine the role ID
215
+ if (!current_role_id) {
216
+ logger.warn("user_management_role_id_not_found", {
217
+ filename: get_filename(),
218
+ line_number: get_line_number(),
219
+ role_name: role_name.trim(),
220
+ role_id,
221
+ });
222
+ continue;
223
+ }
224
+ // Track this role ID for cache invalidation
225
+ modified_role_ids.push(current_role_id);
226
+ // Get current role-permission mappings
227
+ const current_mappings = await role_permissions_service.findBy({
228
+ role_id: current_role_id,
229
+ });
230
+ const current_permission_ids = Array.isArray(current_mappings)
231
+ ? current_mappings.map((m) => m.permission_id)
232
+ : [];
233
+ // Get target permission IDs
234
+ const target_permission_ids = permissions
235
+ .map((perm_name) => permission_name_to_id[perm_name])
236
+ .filter((id) => id !== undefined);
237
+ // Delete removed permissions
238
+ // Note: hazo_role_permissions is a junction table without an id column
239
+ // We need to use SQLite admin service to delete by composite key (role_id, permission_id)
240
+ const to_delete = current_permission_ids.filter((id) => !target_permission_ids.includes(id));
241
+ if (to_delete.length > 0) {
242
+ try {
243
+ const admin_service = getSqliteAdminService();
244
+ for (const perm_id of to_delete) {
245
+ // Delete using SQLite admin service with criteria (role_id and permission_id)
246
+ await admin_service.deleteRows("hazo_role_permissions", {
247
+ role_id: current_role_id,
248
+ permission_id: perm_id,
249
+ });
250
+ }
251
+ }
252
+ catch (adminError) {
253
+ // Fallback: try using createCrudService deleteById if rowid exists
254
+ // SQLite tables have a hidden rowid column that can be used
255
+ const error_message = adminError instanceof Error ? adminError.message : "Unknown error";
256
+ logger.warn("user_management_role_permission_delete_admin_failed", {
257
+ filename: get_filename(),
258
+ line_number: get_line_number(),
259
+ error: error_message,
260
+ note: "Trying fallback method",
261
+ });
262
+ // Fallback: try to find and delete using rowid if available
263
+ for (const perm_id of to_delete) {
264
+ const mappings_to_delete = await role_permissions_service.findBy({
265
+ role_id: current_role_id,
266
+ permission_id: perm_id,
267
+ });
268
+ if (Array.isArray(mappings_to_delete) && mappings_to_delete.length > 0) {
269
+ for (const mapping of mappings_to_delete) {
270
+ // Try deleteById with rowid (SQLite has hidden rowid)
271
+ try {
272
+ // Check if mapping has an id field (could be rowid)
273
+ if (mapping.id !== undefined) {
274
+ await role_permissions_service.deleteById(mapping.id);
275
+ }
276
+ else if (mapping.rowid !== undefined) {
277
+ await role_permissions_service.deleteById(mapping.rowid);
278
+ }
279
+ else {
280
+ // Last resort: log error
281
+ logger.error("user_management_role_permission_delete_no_id", {
282
+ filename: get_filename(),
283
+ line_number: get_line_number(),
284
+ role_id: current_role_id,
285
+ permission_id: perm_id,
286
+ mapping,
287
+ });
288
+ }
289
+ }
290
+ catch (deleteError) {
291
+ const delete_error_message = deleteError instanceof Error ? deleteError.message : "Unknown error";
292
+ logger.error("user_management_role_permission_delete_failed", {
293
+ filename: get_filename(),
294
+ line_number: get_line_number(),
295
+ role_id: current_role_id,
296
+ permission_id: perm_id,
297
+ error: delete_error_message,
298
+ });
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ // Add new permissions
306
+ const to_add = target_permission_ids.filter((id) => !current_permission_ids.includes(id));
307
+ for (const perm_id of to_add) {
308
+ await role_permissions_service.insert({
309
+ role_id: current_role_id,
310
+ permission_id: perm_id,
311
+ created_at: now,
312
+ changed_at: now,
313
+ });
314
+ }
315
+ }
316
+ // Invalidate cache for all affected roles
317
+ try {
318
+ const config = get_auth_utility_config();
319
+ const cache = get_auth_cache(config.cache_max_users, config.cache_ttl_minutes, config.cache_max_age_minutes);
320
+ // Invalidate by all role IDs that were modified (including newly created ones)
321
+ if (modified_role_ids.length > 0) {
322
+ cache.invalidate_by_roles(modified_role_ids);
323
+ }
324
+ }
325
+ catch (cache_error) {
326
+ // Log but don't fail role update if cache invalidation fails
327
+ const cache_error_message = cache_error instanceof Error ? cache_error.message : "Unknown error";
328
+ logger.warn("user_management_roles_cache_invalidation_failed", {
329
+ filename: get_filename(),
330
+ line_number: get_line_number(),
331
+ error: cache_error_message,
332
+ });
333
+ }
334
+ logger.info("user_management_roles_updated", {
335
+ filename: get_filename(),
336
+ line_number: get_line_number(),
337
+ role_count: roles.length,
338
+ });
339
+ return NextResponse.json({ success: true }, { status: 200 });
340
+ }
341
+ catch (error) {
342
+ const error_message = error instanceof Error ? error.message : "Unknown error";
343
+ const error_stack = error instanceof Error ? error.stack : undefined;
344
+ logger.error("user_management_roles_update_error", {
345
+ filename: get_filename(),
346
+ line_number: get_line_number(),
347
+ error_message,
348
+ error_stack,
349
+ });
350
+ return NextResponse.json({ error: "Failed to update roles" }, { status: 500 });
351
+ }
352
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ export declare const dynamic = "force-dynamic";
3
+ /**
4
+ * GET - Get roles assigned to a user
5
+ * Query params: user_id (string)
6
+ */
7
+ export declare function GET(request: NextRequest): Promise<NextResponse<{
8
+ error: string;
9
+ }> | NextResponse<{
10
+ success: boolean;
11
+ role_ids: number[];
12
+ }>>;
13
+ /**
14
+ * POST - Assign a role to a user
15
+ * Body: { user_id: string, role_id: number }
16
+ */
17
+ export declare function POST(request: NextRequest): Promise<NextResponse<{
18
+ error: string;
19
+ }> | NextResponse<{
20
+ success: boolean;
21
+ assignment: {
22
+ user_id: string;
23
+ role_id: number;
24
+ };
25
+ }>>;
26
+ /**
27
+ * PUT - Update user roles (bulk assignment/removal)
28
+ * Body: { user_id: string, role_ids: number[] }
29
+ */
30
+ export declare function PUT(request: NextRequest): Promise<NextResponse<{
31
+ error: string;
32
+ }> | NextResponse<{
33
+ success: boolean;
34
+ added: number;
35
+ removed: number;
36
+ }>>;
37
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/app/api/hazo_auth/user_management/users/roles/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AASxD,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAGvC;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;IAoD7C;AAED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;;;;IAiG9C;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;;;;;IA2L7C"}
@@ -0,0 +1,276 @@
1
+ // file_description: API route for managing user roles (assigning roles to users)
2
+ // section: imports
3
+ import { NextResponse } from "next/server";
4
+ import { get_hazo_connect_instance } from "../../../../../../lib/hazo_connect_instance.server";
5
+ import { createCrudService, getSqliteAdminService } from "hazo_connect/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 { get_auth_cache } from "../../../../../../lib/auth/auth_cache";
9
+ import { get_auth_utility_config } from "../../../../../../lib/auth_utility_config.server";
10
+ // section: route_config
11
+ export const dynamic = 'force-dynamic';
12
+ // section: api_handler
13
+ /**
14
+ * GET - Get roles assigned to a user
15
+ * Query params: user_id (string)
16
+ */
17
+ export async function GET(request) {
18
+ const logger = create_app_logger();
19
+ try {
20
+ const { searchParams } = new URL(request.url);
21
+ const user_id = searchParams.get("user_id");
22
+ if (!user_id || typeof user_id !== "string") {
23
+ return NextResponse.json({ error: "user_id is required as a query parameter" }, { status: 400 });
24
+ }
25
+ const hazoConnect = get_hazo_connect_instance();
26
+ const user_roles_service = createCrudService(hazoConnect, "hazo_user_roles");
27
+ // Get all roles assigned to this user
28
+ const user_roles = await user_roles_service.findBy({
29
+ user_id,
30
+ });
31
+ if (!Array.isArray(user_roles)) {
32
+ return NextResponse.json({ error: "Failed to fetch user roles" }, { status: 500 });
33
+ }
34
+ // Extract role IDs
35
+ const role_ids = user_roles.map((ur) => ur.role_id).filter((id) => id !== undefined);
36
+ return NextResponse.json({
37
+ success: true,
38
+ role_ids,
39
+ }, { status: 200 });
40
+ }
41
+ catch (error) {
42
+ const error_message = error instanceof Error ? error.message : "Unknown error";
43
+ logger.error("user_management_user_roles_fetch_failed", {
44
+ filename: get_filename(),
45
+ line_number: get_line_number(),
46
+ error: error_message,
47
+ });
48
+ return NextResponse.json({ error: "Failed to fetch user roles" }, { status: 500 });
49
+ }
50
+ }
51
+ /**
52
+ * POST - Assign a role to a user
53
+ * Body: { user_id: string, role_id: number }
54
+ */
55
+ export async function POST(request) {
56
+ const logger = create_app_logger();
57
+ try {
58
+ const body = await request.json();
59
+ const { user_id, role_id } = body;
60
+ if (!user_id || typeof user_id !== "string") {
61
+ return NextResponse.json({ error: "user_id is required and must be a string" }, { status: 400 });
62
+ }
63
+ if (!role_id || typeof role_id !== "number") {
64
+ return NextResponse.json({ error: "role_id is required and must be a number" }, { status: 400 });
65
+ }
66
+ const hazoConnect = get_hazo_connect_instance();
67
+ const user_roles_service = createCrudService(hazoConnect, "hazo_user_roles");
68
+ // Check if user exists
69
+ const users_service = createCrudService(hazoConnect, "hazo_users");
70
+ const users = await users_service.findBy({ id: user_id });
71
+ if (!Array.isArray(users) || users.length === 0) {
72
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
73
+ }
74
+ // Check if role exists
75
+ const roles_service = createCrudService(hazoConnect, "hazo_roles");
76
+ const roles = await roles_service.findBy({ id: role_id });
77
+ if (!Array.isArray(roles) || roles.length === 0) {
78
+ return NextResponse.json({ error: "Role not found" }, { status: 404 });
79
+ }
80
+ // Check if role is already assigned to user
81
+ const existing_assignments = await user_roles_service.findBy({
82
+ user_id,
83
+ role_id,
84
+ });
85
+ if (Array.isArray(existing_assignments) && existing_assignments.length > 0) {
86
+ return NextResponse.json({ error: "Role is already assigned to this user" }, { status: 409 });
87
+ }
88
+ // Assign role to user
89
+ const now = new Date().toISOString();
90
+ const new_assignment = await user_roles_service.insert({
91
+ user_id,
92
+ role_id,
93
+ created_at: now,
94
+ changed_at: now,
95
+ });
96
+ logger.info("user_management_user_role_assigned", {
97
+ filename: get_filename(),
98
+ line_number: get_line_number(),
99
+ user_id,
100
+ role_id,
101
+ assignment_id: new_assignment.user_id,
102
+ });
103
+ return NextResponse.json({
104
+ success: true,
105
+ assignment: {
106
+ user_id,
107
+ role_id,
108
+ },
109
+ }, { status: 201 });
110
+ }
111
+ catch (error) {
112
+ const error_message = error instanceof Error ? error.message : "Unknown error";
113
+ logger.error("user_management_user_role_assign_failed", {
114
+ filename: get_filename(),
115
+ line_number: get_line_number(),
116
+ error: error_message,
117
+ });
118
+ return NextResponse.json({ error: "Failed to assign role to user" }, { status: 500 });
119
+ }
120
+ }
121
+ /**
122
+ * PUT - Update user roles (bulk assignment/removal)
123
+ * Body: { user_id: string, role_ids: number[] }
124
+ */
125
+ export async function PUT(request) {
126
+ const logger = create_app_logger();
127
+ try {
128
+ const body = await request.json();
129
+ const { user_id, role_ids } = body;
130
+ if (!user_id || typeof user_id !== "string") {
131
+ return NextResponse.json({ error: "user_id is required and must be a string" }, { status: 400 });
132
+ }
133
+ if (!Array.isArray(role_ids)) {
134
+ return NextResponse.json({ error: "role_ids is required and must be an array" }, { status: 400 });
135
+ }
136
+ const hazoConnect = get_hazo_connect_instance();
137
+ const user_roles_service = createCrudService(hazoConnect, "hazo_user_roles");
138
+ // Check if user exists
139
+ const users_service = createCrudService(hazoConnect, "hazo_users");
140
+ const users = await users_service.findBy({ id: user_id });
141
+ if (!Array.isArray(users) || users.length === 0) {
142
+ return NextResponse.json({ error: "User not found" }, { status: 404 });
143
+ }
144
+ // Get current user roles
145
+ const current_user_roles = await user_roles_service.findBy({
146
+ user_id,
147
+ });
148
+ if (!Array.isArray(current_user_roles)) {
149
+ return NextResponse.json({ error: "Failed to fetch current user roles" }, { status: 500 });
150
+ }
151
+ const current_role_ids = current_user_roles.map((ur) => ur.role_id).filter((id) => id !== undefined);
152
+ const target_role_ids = role_ids.filter((id) => typeof id === "number");
153
+ // Find roles to add and remove
154
+ const to_add = target_role_ids.filter((id) => !current_role_ids.includes(id));
155
+ const to_remove = current_role_ids.filter((id) => !target_role_ids.includes(id));
156
+ const now = new Date().toISOString();
157
+ // Add new roles
158
+ for (const role_id of to_add) {
159
+ // Check if role exists
160
+ const roles_service = createCrudService(hazoConnect, "hazo_roles");
161
+ const roles = await roles_service.findBy({ id: role_id });
162
+ if (Array.isArray(roles) && roles.length > 0) {
163
+ await user_roles_service.insert({
164
+ user_id,
165
+ role_id,
166
+ created_at: now,
167
+ changed_at: now,
168
+ });
169
+ }
170
+ }
171
+ // Remove roles
172
+ // Note: hazo_user_roles is a junction table without an id column
173
+ // We need to use SQLite admin service to delete by composite key (user_id, role_id)
174
+ if (to_remove.length > 0) {
175
+ try {
176
+ const admin_service = getSqliteAdminService();
177
+ for (const role_id of to_remove) {
178
+ // Delete using SQLite admin service with criteria (user_id and role_id)
179
+ await admin_service.deleteRows("hazo_user_roles", {
180
+ user_id,
181
+ role_id,
182
+ });
183
+ }
184
+ }
185
+ catch (adminError) {
186
+ // Fallback: try using createCrudService deleteById if rowid exists
187
+ // SQLite tables have a hidden rowid column that can be used
188
+ const error_message = adminError instanceof Error ? adminError.message : "Unknown error";
189
+ logger.warn("user_management_user_role_delete_admin_failed", {
190
+ filename: get_filename(),
191
+ line_number: get_line_number(),
192
+ error: error_message,
193
+ note: "Trying fallback method",
194
+ });
195
+ // Fallback: try to find and delete using rowid if available
196
+ for (const role_id of to_remove) {
197
+ const assignments_to_remove = await user_roles_service.findBy({
198
+ user_id,
199
+ role_id,
200
+ });
201
+ if (Array.isArray(assignments_to_remove) && assignments_to_remove.length > 0) {
202
+ for (const assignment of assignments_to_remove) {
203
+ // Try deleteById with rowid (SQLite has hidden rowid)
204
+ try {
205
+ // Check if assignment has an id field (could be rowid)
206
+ if (assignment.id !== undefined) {
207
+ await user_roles_service.deleteById(assignment.id);
208
+ }
209
+ else if (assignment.rowid !== undefined) {
210
+ await user_roles_service.deleteById(assignment.rowid);
211
+ }
212
+ else {
213
+ // Last resort: log error
214
+ logger.error("user_management_user_role_delete_no_id", {
215
+ filename: get_filename(),
216
+ line_number: get_line_number(),
217
+ user_id,
218
+ role_id,
219
+ assignment,
220
+ });
221
+ }
222
+ }
223
+ catch (deleteError) {
224
+ const delete_error_message = deleteError instanceof Error ? deleteError.message : "Unknown error";
225
+ logger.error("user_management_user_role_delete_failed", {
226
+ filename: get_filename(),
227
+ line_number: get_line_number(),
228
+ user_id,
229
+ role_id,
230
+ error: delete_error_message,
231
+ });
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ // Invalidate user cache after role assignment changes
239
+ try {
240
+ const config = get_auth_utility_config();
241
+ const cache = get_auth_cache(config.cache_max_users, config.cache_ttl_minutes, config.cache_max_age_minutes);
242
+ cache.invalidate_user(user_id);
243
+ }
244
+ catch (cache_error) {
245
+ // Log but don't fail role update if cache invalidation fails
246
+ const cache_error_message = cache_error instanceof Error ? cache_error.message : "Unknown error";
247
+ logger.warn("user_management_user_roles_cache_invalidation_failed", {
248
+ filename: get_filename(),
249
+ line_number: get_line_number(),
250
+ user_id,
251
+ error: cache_error_message,
252
+ });
253
+ }
254
+ logger.info("user_management_user_roles_updated", {
255
+ filename: get_filename(),
256
+ line_number: get_line_number(),
257
+ user_id,
258
+ added: to_add.length,
259
+ removed: to_remove.length,
260
+ });
261
+ return NextResponse.json({
262
+ success: true,
263
+ added: to_add.length,
264
+ removed: to_remove.length,
265
+ }, { status: 200 });
266
+ }
267
+ catch (error) {
268
+ const error_message = error instanceof Error ? error.message : "Unknown error";
269
+ logger.error("user_management_user_roles_update_failed", {
270
+ filename: get_filename(),
271
+ line_number: get_line_number(),
272
+ error: error_message,
273
+ });
274
+ return NextResponse.json({ error: "Failed to update user roles" }, { status: 500 });
275
+ }
276
+ }