1414use Fleetbase \Http \Requests \Internal \ResendUserInvite ;
1515use Fleetbase \Http \Requests \Internal \UpdatePasswordRequest ;
1616use Fleetbase \Http \Requests \Internal \ValidatePasswordRequest ;
17+ use Fleetbase \Http \Requests \UpdateUserRequest ;
1718use Fleetbase \Models \Company ;
1819use Fleetbase \Models \CompanyUser ;
1920use Fleetbase \Models \Invite ;
@@ -57,6 +58,16 @@ class UserController extends FleetbaseController
5758 */
5859 public $ createRequest = CreateUserRequest::class;
5960
61+ /**
62+ * Update user request.
63+ *
64+ * Enforces that email and phone cannot be set to an empty string
65+ * and that uniqueness constraints are respected on update.
66+ *
67+ * @var UpdateUserRequest
68+ */
69+ public $ updateRequest = UpdateUserRequest::class;
70+
6071 /**
6172 * Creates a record with request payload.
6273 *
@@ -126,6 +137,11 @@ public function createRecord(Request $request)
126137 */
127138 public function updateRecord (Request $ request , string $ id )
128139 {
140+ // Run the UpdateUserRequest validation rules before delegating to the
141+ // model trait. This prevents email/phone being set to an empty string
142+ // and enforces uniqueness constraints on partial (PATCH) updates.
143+ $ this ->validateRequest ($ request );
144+
129145 try {
130146 $ record = $ this ->model ->updateRecordFromRequest ($ request , $ id , function (&$ request , &$ user ) {
131147 // Assign role if set
@@ -182,7 +198,7 @@ public function current(Request $request)
182198
183199 // Try to get from server cache
184200 $ companyId = session ('company ' );
185- $ cachedData = UserCacheService::get ($ user-> id , $ companyId );
201+ $ cachedData = UserCacheService::get ($ user , $ companyId );
186202
187203 if ($ cachedData ) {
188204 // Return cached data with cache headers
@@ -202,7 +218,7 @@ public function current(Request $request)
202218 $ userArray = $ userData ->toArray ($ request );
203219
204220 // Store in cache
205- UserCacheService::put ($ user-> id , $ companyId , $ userArray );
221+ UserCacheService::put ($ user , $ companyId , $ userArray );
206222
207223 // Return with cache headers
208224 return response ()->json (['user ' => $ userArray ])
@@ -404,18 +420,56 @@ public function deactivate($id)
404420 return response ()->error ('No user to deactivate ' , 401 );
405421 }
406422
407- $ user = User::where ('uuid ' , $ id )->first ();
423+ $ currentUser = request ()->user ();
424+
425+ // Scope the lookup to the current company to prevent cross-organization IDOR.
426+ $ user = User::where ('uuid ' , $ id )
427+ ->whereHas ('companyUsers ' , function ($ query ) {
428+ $ query ->where ('company_uuid ' , session ('company ' ));
429+ })
430+ ->first ();
408431
409432 if (!$ user ) {
410- return response ()->error ('No user found ' , 401 );
433+ return response ()->error ('No user found ' , 404 );
411434 }
412435
413- $ user ->deactivate ();
414- $ user = $ user ->refresh ();
436+ // Prevent a user from deactivating their own account via this endpoint.
437+ if ($ currentUser && $ currentUser ->uuid === $ user ->uuid ) {
438+ return response ()->error ('You cannot deactivate your own account. ' , 403 );
439+ }
440+
441+ // Layered privilege check:
442+ //
443+ // Tier 1 — System admins (isAdmin()) can deactivate anyone except other
444+ // system admins. This is the highest privilege tier.
445+ if ($ user ->isAdmin ()) {
446+ return response ()->error ('Insufficient permissions to deactivate this user. ' , 403 );
447+ }
448+
449+ // Tier 2 — Users holding the 'Administrator' role can only be deactivated
450+ // by a system admin (handled above). A regular user or another
451+ // role-based Administrator cannot deactivate them.
452+ if ($ user ->hasRole ('Administrator ' ) && $ currentUser && !$ currentUser ->isAdmin ()) {
453+ return response ()->error ('Insufficient permissions to deactivate this user. ' , 403 );
454+ }
455+
456+ // Only deactivate the CompanyUser record for the current organisation.
457+ // Calling User::deactivate() would set the user's global status to
458+ // 'inactive', locking them out of every organisation they belong to.
459+ // Instead we update only the pivot record so the user remains active
460+ // in any other organisations they are a member of.
461+ $ companyUser = $ user ->companyUsers ()->where ('company_uuid ' , session ('company ' ))->first ();
462+
463+ if (!$ companyUser ) {
464+ return response ()->error ('User is not a member of this organisation. ' , 404 );
465+ }
466+
467+ $ companyUser ->status = 'inactive ' ;
468+ $ companyUser ->save ();
415469
416470 return response ()->json ([
417471 'message ' => 'User deactivated ' ,
418- 'status ' => $ user -> session_status ,
472+ 'status ' => $ companyUser -> status ,
419473 ]);
420474 }
421475
@@ -430,12 +484,24 @@ public function activate($id)
430484 return response ()->error ('No user to activate ' , 401 );
431485 }
432486
433- $ user = User::where ('uuid ' , $ id )->first ();
487+ $ currentUser = request ()->user ();
488+
489+ // Scope the lookup to the current company to prevent cross-organisation IDOR.
490+ $ user = User::where ('uuid ' , $ id )
491+ ->whereHas ('companyUsers ' , function ($ query ) {
492+ $ query ->where ('company_uuid ' , session ('company ' ));
493+ })
494+ ->first ();
434495
435496 if (!$ user ) {
436- return response ()->error ('No user found ' , 401 );
497+ return response ()->error ('No user found ' , 404 );
437498 }
438499
500+ // Activate both the User record and the CompanyUser record.
501+ // Unlike deactivation (which is scoped to the current organisation only),
502+ // activation must also update users.status because a newly created user
503+ // starts with a global status of 'inactive' and needs to be unblocked
504+ // at the user level before they can access any organisation.
439505 $ user ->activate ();
440506 $ user = $ user ->refresh ();
441507
0 commit comments