EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
customerUsers.js
Go to the documentation of this file.
1/**
2 * @file customerUsers.js
3 * @description Customer Portal User Management Routes
4 *
5 * Provides admin interface for managing customer portal users. Allows MSP/tenant admins
6 * to create, view, update, and delete customer portal login accounts.
7 *
8 * **Permission Model:**
9 * - Admin only: All operations require admin role
10 * - Tenant isolation: Admins can only manage users in their tenant
11 * - Root MSP: Can manage users across all tenants
12 *
13 * **Features:**
14 * - Create customer portal users with bcrypt password hashing
15 * - Reset passwords (admin-initiated)
16 * - Toggle user active status (soft delete)
17 * - List users by customer
18 * - Update user details
19 *
20 * **Security:**
21 * - Passwords hashed with bcrypt (10 rounds)
22 * - Email uniqueness enforced globally
23 * - Audit logging for all actions
24 * - Admin-only access with tenant filtering
25 *
26 * @module routes/customerUsers
27 * @requires express
28 * @requires bcrypt
29 * @requires services/db
30 * @requires middleware/auth
31 * @requires middleware/tenant
32 * @author Independent Business Group
33 * @since 1.0.0
34 * @see module:routes/portalAuth
35 * @see module:routes/portalCustomer
36 */
37
38const express = require('express');
39const router = express.Router();
40const bcrypt = require('bcrypt');
41const pool = require('../services/db');
42const authenticateToken = require('../middleware/auth');
43const { setTenantContext } = require('../middleware/tenant');
44
45/**
46 * @api {get} /customer-users List Customer Portal Users
47 * @apiName ListCustomerUsers
48 * @apiGroup CustomerPortalUsers
49 * @apiVersion 1.0.0
50 *
51 * @apiDescription
52 * Returns all portal users for specified customer. Admin only.
53 * Filtered by tenant for non-root users.
54 *
55 * @apiPermission admin
56 *
57 * @apiHeader {String} Authorization Bearer JWT token (admin required)
58 *
59 * @apiParam {Number} customer_id Customer ID (query parameter, required)
60 *
61 * @apiSuccess {Object[]} users Array of customer portal users
62 * @apiSuccess {Number} users.customer_user_id User ID
63 * @apiSuccess {Number} users.customer_id Customer ID
64 * @apiSuccess {String} users.email Email address (login username)
65 * @apiSuccess {String} users.first_name First name
66 * @apiSuccess {String} users.last_name Last name
67 * @apiSuccess {String} users.phone Phone number
68 * @apiSuccess {Boolean} users.is_primary Primary contact flag
69 * @apiSuccess {Boolean} users.is_active Account active status
70 * @apiSuccess {Date} users.last_login Last login timestamp
71 * @apiSuccess {Date} users.created_at Account creation timestamp
72 * @apiSuccess {String} users.customer_name Customer name
73 * @apiSuccess {String} users.tenant_name Tenant name
74 *
75 * @apiError (400) {String} error customer_id is required
76 * @apiError (403) {String} error Admin access required
77 * @apiError (404) {String} error Customer not found or access denied
78 * @apiError (500) {String} error Failed to fetch customer users
79 *
80 * @apiExample {curl} Example Request:
81 * curl -X GET \
82 * -H "Authorization: Bearer eyJhbGc..." \
83 * "https://api.everydaytech.au/customer-users?customer_id=123"
84 *
85 * @apiSuccessExample {json} Success Response:
86 * HTTP/1.1 200 OK
87 * {
88 * "users": [
89 * {
90 * "customer_user_id": 45,
91 * "customer_id": 123,
92 * "email": "user@example.com",
93 * "first_name": "John",
94 * "last_name": "Doe",
95 * "phone": "1234567890",
96 * "is_primary": true,
97 * "is_active": true,
98 * "last_login": "2026-03-23T02:30:00Z",
99 * "created_at": "2026-03-20T10:00:00Z",
100 * "customer_name": "Acme Corp",
101 * "tenant_name": "IBG MSP"
102 * }
103 * ]
104 * }
105 */
106router.get('/', authenticateToken, setTenantContext, async (req, res) => {
107 try {
108 // Admin only
109 if (!req.user || req.user.role !== 'admin') {
110 return res.status(403).json({ error: 'Admin access required' });
111 }
112
113 const { customer_id } = req.query;
114
115 if (!customer_id) {
116 return res.status(400).json({ error: 'customer_id is required' });
117 }
118
119 // Verify customer exists and belongs to tenant
120 let customerCheck;
121 if (req.tenant?.isMsp) {
122 customerCheck = await pool.query(
123 'SELECT customer_id FROM customers WHERE customer_id = $1',
124 [customer_id]
125 );
126 } else {
127 customerCheck = await pool.query(
128 'SELECT customer_id FROM customers WHERE customer_id = $1 AND tenant_id = $2',
129 [customer_id, req.tenant?.id || req.user?.tenantId]
130 );
131 }
132
133 if (customerCheck.rows.length === 0) {
134 return res.status(404).json({ error: 'Customer not found or access denied' });
135 }
136
137 // Get customer portal users
138 const result = await pool.query(
139 `SELECT
140 cu.customer_user_id,
141 cu.customer_id,
142 cu.email,
143 cu.first_name,
144 cu.last_name,
145 cu.phone,
146 cu.is_primary,
147 cu.is_active,
148 cu.last_login,
149 cu.created_at,
150 c.name as customer_name,
151 t.name as tenant_name
152 FROM customer_users cu
153 JOIN customers c ON cu.customer_id = c.customer_id
154 JOIN tenants t ON cu.tenant_id = t.tenant_id
155 WHERE cu.customer_id = $1
156 ORDER BY cu.is_primary DESC, cu.created_at ASC`,
157 [customer_id]
158 );
159
160 res.json({ users: result.rows });
161 } catch (err) {
162 console.error('Error fetching customer users:', err);
163 res.status(500).json({ error: 'Failed to fetch customer users' });
164 }
165});
166
167/**
168 * @api {post} /customer-users Create Customer Portal User
169 * @apiName CreateCustomerUser
170 * @apiGroup CustomerPortalUsers
171 * @apiVersion 1.0.0
172 *
173 * @apiDescription
174 * Creates portal login for customer. Admin only.
175 * Password is bcrypt-hashed. Sets tenant_id from customer record.
176 * Logs creation to customer_audit_log.
177 *
178 * @apiPermission admin
179 *
180 * @apiHeader {String} Authorization Bearer JWT token (admin required)
181 * @apiHeader {String} Content-Type application/json
182 *
183 * @apiParam {Number} customer_id Customer ID (required)
184 * @apiParam {String} email Email address (required, unique globally)
185 * @apiParam {String} password Plain text password (required, will be hashed)
186 * @apiParam {String} [first_name] First name (optional)
187 * @apiParam {String} [last_name] Last name (optional)
188 * @apiParam {String} [phone] Phone number (optional)
189 * @apiParam {Boolean} [is_primary=false] Set as primary contact
190 *
191 * @apiSuccess (201) {Number} customer_user_id Generated user ID
192 * @apiSuccess (201) {String} tenant_id Tenant UUID
193 * @apiSuccess (201) {Number} customer_id Customer ID
194 * @apiSuccess (201) {String} email Email address
195 * @apiSuccess (201) {String} first_name First name
196 * @apiSuccess (201) {String} last_name Last name
197 * @apiSuccess (201) {String} phone Phone number
198 * @apiSuccess (201) {Boolean} is_primary Primary flag
199 * @apiSuccess (201) {Boolean} is_active Active status (always true)
200 * @apiSuccess (201) {Date} created_at Creation timestamp
201 *
202 * @apiError (400) {String} error customer_id, email, and password are required
203 * @apiError (403) {String} error Admin access required
204 * @apiError (404) {String} error Customer not found or access denied
205 * @apiError (409) {String} error Email already exists
206 * @apiError (500) {String} error Failed to create customer user
207 *
208 * @apiExample {curl} Example Request:
209 * curl -X POST \
210 * -H "Authorization: Bearer eyJhbGc..." \
211 * -H "Content-Type: application/json" \
212 * -d '{
213 * "customer_id": 123,
214 * "email": "user@example.com",
215 * "password": "SecurePass123",
216 * "first_name": "John",
217 * "last_name": "Doe"
218 * }' \
219 * "https://api.everydaytech.au/customer-users"
220 *
221 * @apiSuccessExample {json} Success Response:
222 * HTTP/1.1 201 Created
223 * {
224 * "customer_user_id": 45,
225 * "tenant_id": "...",
226 * "customer_id": 123,
227 * "email": "user@example.com",
228 * "first_name": "John",
229 * "last_name": "Doe",
230 * "phone": null,
231 * "is_primary": false,
232 * "is_active": true,
233 * "created_at": "2026-03-23T02:30:00Z"
234 * }
235 */
236router.post('/', authenticateToken, setTenantContext, async (req, res) => {
237 try {
238 // Admin only
239 if (!req.user || req.user.role !== 'admin') {
240 return res.status(403).json({ error: 'Admin access required' });
241 }
242
243 const {
244 customer_id,
245 email,
246 password,
247 first_name,
248 last_name,
249 phone,
250 is_primary
251 } = req.body;
252
253 // Validation
254 if (!customer_id || !email || !password) {
255 return res.status(400).json({
256 error: 'customer_id, email, and password are required'
257 });
258 }
259
260 // Verify customer exists and belongs to tenant
261 let customerCheck;
262 if (req.tenant?.isMsp) {
263 customerCheck = await pool.query(
264 'SELECT customer_id, tenant_id FROM customers WHERE customer_id = $1',
265 [customer_id]
266 );
267 } else {
268 customerCheck = await pool.query(
269 'SELECT customer_id, tenant_id FROM customers WHERE customer_id = $1 AND tenant_id = $2',
270 [customer_id, req.tenant?.id || req.user?.tenantId]
271 );
272 }
273
274 if (customerCheck.rows.length === 0) {
275 return res.status(404).json({ error: 'Customer not found or access denied' });
276 }
277
278 const tenantId = customerCheck.rows[0].tenant_id;
279
280 // Check if email already exists
281 const emailCheck = await pool.query(
282 'SELECT customer_user_id FROM customer_users WHERE email = $1',
283 [email.toLowerCase()]
284 );
285
286 if (emailCheck.rows.length > 0) {
287 return res.status(409).json({ error: 'Email already exists' });
288 }
289
290 // Hash password
291 const passwordHash = await bcrypt.hash(password, 10);
292
293 // Create customer user
294 const result = await pool.query(
295 `INSERT INTO customer_users
296 (tenant_id, customer_id, email, password_hash, first_name, last_name, phone, is_primary, is_active)
297 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
298 RETURNING
299 customer_user_id,
300 tenant_id,
301 customer_id,
302 email,
303 first_name,
304 last_name,
305 phone,
306 is_primary,
307 is_active,
308 created_at`,
309 [
310 tenantId,
311 customer_id,
312 email.toLowerCase(),
313 passwordHash,
314 first_name || null,
315 last_name || null,
316 phone || null,
317 is_primary || false
318 ]
319 );
320
321 // Log action to customer audit log
322 await pool.query(
323 `INSERT INTO customer_audit_log
324 (customer_user_id, tenant_id, action, resource_type, resource_id, details)
325 VALUES ($1, $2, 'create', 'customer_user', $3, $4)`,
326 [
327 req.user.userId,
328 tenantId,
329 result.rows[0].customer_user_id,
330 JSON.stringify({
331 email: email.toLowerCase(),
332 customer_id,
333 created_by_admin: req.user.email
334 })
335 ]
336 );
337
338 res.status(201).json(result.rows[0]);
339 } catch (err) {
340 console.error('Error creating customer user:', err);
341
342 if (err.code === '23505') { // Unique violation
343 return res.status(409).json({ error: 'Email already exists' });
344 }
345
346 res.status(500).json({ error: 'Failed to create customer user' });
347 }
348});
349
350/**
351 * @api {put} /customer-users/:id Update Customer Portal User
352 * @apiName UpdateCustomerUser
353 * @apiGroup CustomerPortalUsers
354 * @apiVersion 1.0.0
355 *
356 * @apiDescription
357 * Updates user details. Admin only. Cannot update password via this endpoint
358 * (use /reset-password instead). Logs changes to customer_audit_log.
359 *
360 * @apiPermission admin
361 *
362 * @apiHeader {String} Authorization Bearer JWT token (admin required)
363 * @apiHeader {String} Content-Type application/json
364 *
365 * @apiParam {Number} id User ID (URL parameter)
366 * @apiParam {String} [first_name] First name
367 * @apiParam {String} [last_name] Last name
368 * @apiParam {String} [phone] Phone number
369 * @apiParam {String} [email] Email address (must be unique)
370 *
371 * @apiSuccess {Number} customer_user_id User ID
372 * @apiSuccess {String} tenant_id Tenant UUID
373 * @apiSuccess {Number} customer_id Customer ID
374 * @apiSuccess {String} email Email address
375 * @apiSuccess {String} first_name First name
376 * @apiSuccess {String} last_name Last name
377 * @apiSuccess {String} phone Phone number
378 * @apiSuccess {Boolean} is_primary Primary flag
379 * @apiSuccess {Boolean} is_active Active status
380 * @apiSuccess {Date} updated_at Update timestamp
381 *
382 * @apiError (400) {String} error No fields to update
383 * @apiError (403) {String} error Admin access required
384 * @apiError (404) {String} error User not found or access denied
385 * @apiError (409) {String} error Email already exists
386 * @apiError (500) {String} error Failed to update customer user
387 */
388router.put('/:id', authenticateToken, setTenantContext, async (req, res) => {
389 try {
390 // Admin only
391 if (!req.user || req.user.role !== 'admin') {
392 return res.status(403).json({ error: 'Admin access required' });
393 }
394
395 const { id } = req.params;
396 const { first_name, last_name, phone, email } = req.body;
397
398 // Verify user exists and belongs to tenant
399 let userCheck;
400 if (req.tenant?.isMsp) {
401 userCheck = await pool.query(
402 'SELECT customer_user_id, tenant_id, customer_id FROM customer_users WHERE customer_user_id = $1',
403 [id]
404 );
405 } else {
406 userCheck = await pool.query(
407 'SELECT customer_user_id, tenant_id, customer_id FROM customer_users WHERE customer_user_id = $1 AND tenant_id = $2',
408 [id, req.tenant?.id || req.user?.tenantId]
409 );
410 }
411
412 if (userCheck.rows.length === 0) {
413 return res.status(404).json({ error: 'User not found or access denied' });
414 }
415
416 // If email is being changed, verify it's unique
417 if (email && email !== userCheck.rows[0].email) {
418 const emailCheck = await pool.query(
419 'SELECT customer_user_id FROM customer_users WHERE email = $1 AND customer_user_id != $2',
420 [email.toLowerCase(), id]
421 );
422
423 if (emailCheck.rows.length > 0) {
424 return res.status(409).json({ error: 'Email already exists' });
425 }
426 }
427
428 // Build update query dynamically
429 const updates = [];
430 const values = [];
431 let paramCount = 1;
432
433 if (first_name !== undefined) {
434 updates.push(`first_name = $${paramCount++}`);
435 values.push(first_name);
436 }
437 if (last_name !== undefined) {
438 updates.push(`last_name = $${paramCount++}`);
439 values.push(last_name);
440 }
441 if (phone !== undefined) {
442 updates.push(`phone = $${paramCount++}`);
443 values.push(phone);
444 }
445 if (email !== undefined) {
446 updates.push(`email = $${paramCount++}`);
447 values.push(email.toLowerCase());
448 }
449
450 if (updates.length === 0) {
451 return res.status(400).json({ error: 'No fields to update' });
452 }
453
454 updates.push(`updated_at = NOW()`);
455 values.push(id);
456
457 const result = await pool.query(
458 `UPDATE customer_users
459 SET ${updates.join(', ')}
460 WHERE customer_user_id = $${paramCount}
461 RETURNING
462 customer_user_id,
463 tenant_id,
464 customer_id,
465 email,
466 first_name,
467 last_name,
468 phone,
469 is_primary,
470 is_active,
471 updated_at`,
472 values
473 );
474
475 // Log action
476 await pool.query(
477 `INSERT INTO customer_audit_log
478 (customer_user_id, tenant_id, action, resource_type, resource_id, details)
479 VALUES ($1, $2, 'update', 'customer_user', $3, $4)`,
480 [
481 req.user.userId,
482 userCheck.rows[0].tenant_id,
483 id,
484 JSON.stringify({
485 updated_by_admin: req.user.email,
486 changes: { first_name, last_name, phone, email }
487 })
488 ]
489 );
490
491 res.json(result.rows[0]);
492 } catch (err) {
493 console.error('Error updating customer user:', err);
494
495 if (err.code === '23505') {
496 return res.status(409).json({ error: 'Email already exists' });
497 }
498
499 res.status(500).json({ error: 'Failed to update customer user' });
500 }
501});
502
503/**
504 * @api {post} /customer-users/:id/reset-password Reset Customer Password
505 * @apiName ResetCustomerUserPassword
506 * @apiGroup CustomerPortalUsers
507 * @apiVersion 1.0.0
508 *
509 * @apiDescription
510 * Admin-initiated password reset. Allows admin to reset customer user password
511 * without knowing current password. Password is bcrypt-hashed. Logs action to
512 * customer_audit_log.
513 *
514 * @apiPermission admin
515 *
516 * @apiHeader {String} Authorization Bearer JWT token (admin required)
517 * @apiHeader {String} Content-Type application/json
518 *
519 * @apiParam {Number} id User ID (URL parameter)
520 * @apiParam {String} new_password New plain text password (required, will be hashed)
521 *
522 * @apiSuccess {Boolean} success Operation status (true)
523 * @apiSuccess {String} message Success message
524 *
525 * @apiError (400) {String} error new_password is required
526 * @apiError (403) {String} error Admin access required
527 * @apiError (404) {String} error User not found or access denied
528 * @apiError (500) {String} error Failed to reset password
529 *
530 * @apiExample {curl} Example Request:
531 * curl -X POST \
532 * -H "Authorization: Bearer eyJhbGc..." \
533 * -H "Content-Type: application/json" \
534 * -d '{"new_password": "NewSecure456"}' \
535 * "https://api.everydaytech.au/customer-users/45/reset-password"
536 */
537router.post('/:id/reset-password', authenticateToken, setTenantContext, async (req, res) => {
538 try {
539 // Admin only
540 if (!req.user || req.user.role !== 'admin') {
541 return res.status(403).json({ error: 'Admin access required' });
542 }
543
544 const { id } = req.params;
545 const { new_password } = req.body;
546
547 if (!new_password) {
548 return res.status(400).json({ error: 'new_password is required' });
549 }
550
551 // Verify user exists and belongs to tenant
552 let userCheck;
553 if (req.tenant?.isMsp) {
554 userCheck = await pool.query(
555 'SELECT customer_user_id, tenant_id, customer_id, email FROM customer_users WHERE customer_user_id = $1',
556 [id]
557 );
558 } else {
559 userCheck = await pool.query(
560 'SELECT customer_user_id, tenant_id, customer_id, email FROM customer_users WHERE customer_user_id = $1 AND tenant_id = $2',
561 [id, req.tenant?.id || req.user?.tenantId]
562 );
563 }
564
565 if (userCheck.rows.length === 0) {
566 return res.status(404).json({ error: 'User not found or access denied' });
567 }
568
569 // Hash new password
570 const passwordHash = await bcrypt.hash(new_password, 10);
571
572 // Update password
573 await pool.query(
574 'UPDATE customer_users SET password_hash = $1, updated_at = NOW() WHERE customer_user_id = $2',
575 [passwordHash, id]
576 );
577
578 // Log action
579 await pool.query(
580 `INSERT INTO customer_audit_log
581 (customer_user_id, tenant_id, action, resource_type, resource_id, details)
582 VALUES ($1, $2, 'password_reset', 'customer_user', $3, $4)`,
583 [
584 req.user.userId,
585 userCheck.rows[0].tenant_id,
586 id,
587 JSON.stringify({
588 reset_by_admin: req.user.email,
589 user_email: userCheck.rows[0].email,
590 timestamp: new Date().toISOString()
591 })
592 ]
593 );
594
595 res.json({
596 success: true,
597 message: 'Password reset successfully'
598 });
599 } catch (err) {
600 console.error('Error resetting password:', err);
601 res.status(500).json({ error: 'Failed to reset password' });
602 }
603});
604
605/**
606 * @api {put} /customer-users/:id/status Toggle User Active Status
607 * @apiName ToggleCustomerUserStatus
608 * @apiGroup CustomerPortalUsers
609 * @apiVersion 1.0.0
610 *
611 * @apiDescription
612 * Toggle user active status. Admin only. Soft delete - disables login without
613 * deleting user data. Can be reversed. Logs action to customer_audit_log.
614 *
615 * @apiPermission admin
616 *
617 * @apiHeader {String} Authorization Bearer JWT token (admin required)
618 * @apiHeader {String} Content-Type application/json
619 *
620 * @apiParam {Number} id User ID (URL parameter)
621 * @apiParam {Boolean} is_active Active status (true=enable, false=disable)
622 *
623 * @apiSuccess {Number} customer_user_id User ID
624 * @apiSuccess {String} email Email address
625 * @apiSuccess {Boolean} is_active New active status
626 *
627 * @apiError (400) {String} error is_active is required
628 * @apiError (403) {String} error Admin access required
629 * @apiError (404) {String} error User not found or access denied
630 * @apiError (500) {String} error Failed to update user status
631 */
632router.put('/:id/status', authenticateToken, setTenantContext, async (req, res) => {
633 try {
634 // Admin only
635 if (!req.user || req.user.role !== 'admin') {
636 return res.status(403).json({ error: 'Admin access required' });
637 }
638
639 const { id } = req.params;
640 const { is_active } = req.body;
641
642 if (is_active === undefined) {
643 return res.status(400).json({ error: 'is_active is required' });
644 }
645
646 // Verify user exists and belongs to tenant
647 let userCheck;
648 if (req.tenant?.isMsp) {
649 userCheck = await pool.query(
650 'SELECT customer_user_id, tenant_id, email FROM customer_users WHERE customer_user_id = $1',
651 [id]
652 );
653 } else {
654 userCheck = await pool.query(
655 'SELECT customer_user_id, tenant_id, email FROM customer_users WHERE customer_user_id = $1 AND tenant_id = $2',
656 [id, req.tenant?.id || req.user?.tenantId]
657 );
658 }
659
660 if (userCheck.rows.length === 0) {
661 return res.status(404).json({ error: 'User not found or access denied' });
662 }
663
664 // Update status
665 const result = await pool.query(
666 `UPDATE customer_users
667 SET is_active = $1, updated_at = NOW()
668 WHERE customer_user_id = $2
669 RETURNING
670 customer_user_id,
671 email,
672 is_active`,
673 [is_active, id]
674 );
675
676 // Log action
677 await pool.query(
678 `INSERT INTO customer_audit_log
679 (customer_user_id, tenant_id, action, resource_type, resource_id, details)
680 VALUES ($1, $2, $3, 'customer_user', $4, $5)`,
681 [
682 req.user.userId,
683 userCheck.rows[0].tenant_id,
684 is_active ? 'activate' : 'deactivate',
685 id,
686 JSON.stringify({
687 changed_by_admin: req.user.email,
688 user_email: userCheck.rows[0].email,
689 new_status: is_active
690 })
691 ]
692 );
693
694 res.json(result.rows[0]);
695 } catch (err) {
696 console.error('Error updating user status:', err);
697 res.status(500).json({ error: 'Failed to update user status' });
698 }
699});
700
701/**
702 * @api {delete} /customer-users/:id Delete Customer Portal User
703 * @apiName DeleteCustomerUser
704 * @apiGroup CustomerPortalUsers
705 * @apiVersion 1.0.0
706 *
707 * @apiDescription
708 * Permanently delete customer portal user. Admin only. Hard delete - removes
709 * user and all associated sessions. **Use with caution** - prefer deactivation
710 * (is_active = false) instead. Logs deletion to customer_audit_log before removal.
711 *
712 * @apiPermission admin
713 *
714 * @apiHeader {String} Authorization Bearer JWT token (admin required)
715 *
716 * @apiParam {Number} id User ID (URL parameter)
717 *
718 * @apiSuccess {Boolean} success Operation status (true)
719 * @apiSuccess {String} message Success message
720 *
721 * @apiError (403) {String} error Admin access required
722 * @apiError (404) {String} error User not found or access denied
723 * @apiError (500) {String} error Failed to delete customer user
724 *
725 * @apiExample {curl} Example Request:
726 * curl -X DELETE \
727 * -H "Authorization: Bearer eyJhbGc..." \
728 * "https://api.everydaytech.au/customer-users/45"
729 */
730router.delete('/:id', authenticateToken, setTenantContext, async (req, res) => {
731 try {
732 // Admin only
733 if (!req.user || req.user.role !== 'admin') {
734 return res.status(403).json({ error: 'Admin access required' });
735 }
736
737 const { id } = req.params;
738
739 // Verify user exists and belongs to tenant
740 let userCheck;
741 if (req.tenant?.isMsp) {
742 userCheck = await pool.query(
743 'SELECT customer_user_id, tenant_id, email, customer_id FROM customer_users WHERE customer_user_id = $1',
744 [id]
745 );
746 } else {
747 userCheck = await pool.query(
748 'SELECT customer_user_id, tenant_id, email, customer_id FROM customer_users WHERE customer_user_id = $1 AND tenant_id = $2',
749 [id, req.tenant?.id || req.user?.tenantId]
750 );
751 }
752
753 if (userCheck.rows.length === 0) {
754 return res.status(404).json({ error: 'User not found or access denied' });
755 }
756
757 const user = userCheck.rows[0];
758
759 // Log action before deletion
760 await pool.query(
761 `INSERT INTO customer_audit_log
762 (customer_user_id, tenant_id, action, resource_type, resource_id, details)
763 VALUES ($1, $2, 'delete', 'customer_user', $3, $4)`,
764 [
765 req.user.userId,
766 user.tenant_id,
767 id,
768 JSON.stringify({
769 deleted_by_admin: req.user.email,
770 user_email: user.email,
771 customer_id: user.customer_id
772 })
773 ]
774 );
775
776 // Delete user (cascade will delete sessions)
777 await pool.query(
778 'DELETE FROM customer_users WHERE customer_user_id = $1',
779 [id]
780 );
781
782 res.json({
783 success: true,
784 message: 'Customer user deleted successfully'
785 });
786 } catch (err) {
787 console.error('Error deleting customer user:', err);
788 res.status(500).json({ error: 'Failed to delete customer user' });
789 }
790});
791
792module.exports = router;