Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserController
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 14
2352
0.00% covered (danger)
0.00%
0 / 1
 profile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 updateProfile
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 updatePassword
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 deleteProfile
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 index
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 store
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 show
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 update
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 destroy
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 assignRoles
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 changeStatus
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 canUpdateUser
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 canDeleteUser
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 canCreateUserWithRoles
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace App\Http\Controllers\Api\v1;
4
5use App\Http\Controllers\Controller;
6use App\Http\Requests\StoreUserRequest;
7use App\Http\Requests\UpdateUserRequest;
8use App\Http\Requests\UpdatePasswordRequest;
9use App\Models\User;
10use App\Responses\ApiResponse;
11use Illuminate\Http\Request;
12use Illuminate\Http\JsonResponse;
13use Illuminate\Support\Facades\Hash;
14use Illuminate\Support\Facades\Gate;
15use Spatie\Permission\Models\Role;
16
17/**
18 * UserController
19 * 
20 * Handles all user-related API operations including profile management,
21 * user administration, and role management. Implements role-based permissions
22 * where users can manage their own profiles, while staff+ can manage all users.
23 * 
24 * @package App\Http\Controllers\Api\v1
25 * @author Ting Liu
26 * @version 1.0.0
27 * @since 2025-10-04
28 * 
29 * @method JsonResponse profile() Get current user profile
30 * @method JsonResponse updateProfile(UpdateUserRequest $request) Update current user profile
31 * @method JsonResponse updatePassword(UpdatePasswordRequest $request) Update current user password
32 * @method JsonResponse deleteProfile() Delete current user profile
33 * @method JsonResponse index(Request $request) List all users (admin)
34 * @method JsonResponse store(StoreUserRequest $request) Create new user (admin)
35 * @method JsonResponse show(string $id) Get specific user (admin)
36 * @method JsonResponse update(UpdateUserRequest $request, string $id) Update specific user (admin)
37 * @method JsonResponse destroy(string $id) Delete specific user (admin)
38 * @method JsonResponse assignRoles(Request $request, string $id) Assign roles to user (admin)
39 * @method JsonResponse changeStatus(Request $request, string $id) Change user status (admin)
40 * 
41 */
42class UserController extends Controller
43{
44    /**
45     * Get current user profile.
46     * 
47     * Retrieves the authenticated user's profile information including
48     * personal details, roles, and permissions. This endpoint is accessible
49     * to all authenticated users for their own profile.
50     * 
51     * @return JsonResponse Standardized JSON response containing:
52     *                     - success: boolean indicating operation success
53     *                     - message: Human-readable status message
54     *                     - data: Object containing user profile with roles and permissions
55     * 
56     * @api GET /api/v1/me
57     * @permission None (authenticated users only)
58     */
59    public function profile(): JsonResponse
60    {
61        $user = request()->user();
62        $user->load(['roles', 'permissions']);
63
64        return ApiResponse::success(['user' => $user], 'Profile retrieved');
65    }
66
67    /**
68     * Update current user profile.
69     * 
70     * Updates the authenticated user's profile information including
71     * personal details like name, email, and other profile fields.
72     * Users can only update their own profile information.
73     * 
74     * @param UpdateUserRequest $request Validated request containing:
75     *                                  - name (optional): User's display name
76     *                                  - given_name (optional): User's given name
77     *                                  - family_name (optional): User's family name
78     *                                  - email (optional): User's email address
79     * 
80     * @return JsonResponse Standardized JSON response containing:
81     *                     - success: boolean indicating operation success
82     *                     - message: Human-readable status message
83     *                     - data: Object containing the updated user profile
84     * 
85     * @throws \Illuminate\Validation\ValidationException When validation fails
86     * 
87     * @api PUT /api/v1/me
88     * @permission None (authenticated users only, own profile)
89     */
90    public function updateProfile(UpdateUserRequest $request): JsonResponse
91    {
92        $user = request()->user();
93        $user->update($request->validated());
94        $user->load(['roles', 'permissions']);
95
96        return ApiResponse::success(['user' => $user], 'Profile updated');
97    }
98
99    /**
100     * Update current user password.
101     * 
102     * Updates the authenticated user's password with proper validation
103     * and security measures. Requires current password confirmation
104     * and enforces strong password requirements.
105     * 
106     * @param UpdatePasswordRequest $request Validated request containing:
107     *                                      - current_password (required): Current password for verification
108     *                                      - password (required): New password (min 8 chars, confirmed)
109     * 
110     * @return JsonResponse Standardized JSON response containing:
111     *                     - success: boolean indicating operation success
112     *                     - message: Human-readable status message
113     *                     - data: null (password updated)
114     * 
115     * @throws \Illuminate\Validation\ValidationException When validation fails
116     * 
117     * @api PUT /api/v1/me/password
118     * @permission None (authenticated users only, own profile)
119     */
120    public function updatePassword(UpdatePasswordRequest $request): JsonResponse
121    {
122        $user = request()->user();
123
124        // Verify current password
125        if (!Hash::check($request->current_password, $user->password)) {
126            return ApiResponse::error(['errors' => ['current_password' => ['The current password is incorrect.']]], 'Current password is incorrect', 422);
127        }
128
129        // Update password
130        $user->update([
131            'password' => Hash::make($request->password),
132        ]);
133
134        // Revoke all existing tokens for security
135        $user->tokens()->delete();
136
137        return ApiResponse::success(null, 'Password updated successfully. Please log in again.');
138    }
139
140    /**
141     * Delete current user profile.
142     * 
143     * Soft deletes the authenticated user's profile, making it unavailable
144     * but preserving the data for potential restoration. This action
145     * revokes all user tokens and logs them out immediately.
146     * 
147     * @return JsonResponse Standardized JSON response containing:
148     *                     - success: boolean indicating operation success
149     *                     - message: Human-readable status message
150     *                     - data: null (user is soft deleted)
151     * 
152     * @api DELETE /api/v1/me
153     * @permission None (authenticated users only, own profile)
154     */
155    public function deleteProfile(): JsonResponse
156    {
157        $user = request()->user();
158
159        // Revoke all tokens
160        $user->tokens()->delete();
161
162        // Soft delete the user
163        $user->delete();
164
165        return ApiResponse::success(null, 'Profile deleted successfully');
166    }
167
168    /**
169     * Display a listing of users (admin only).
170     * 
171     * Retrieves a paginated list of all users in the system with optional
172     * search and filtering capabilities. This endpoint is restricted to
173     * staff+ level users for administrative purposes.
174     * 
175     * @param Request $request The HTTP request containing optional query parameters:
176     *                        - q (string): Search term for name or email
177     *                        - status (string): Filter by user status
178     *                        - role (string): Filter by user role
179     * 
180     * @return JsonResponse Standardized JSON response containing:
181     *                     - success: boolean indicating operation success
182     *                     - message: Human-readable status message
183     *                     - data: Object containing paginated users with roles
184     * 
185     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user lacks admin permissions
186     * 
187     * @api GET /api/v1/users
188     * @permission users.browse (Staff level 500+)
189     */
190    public function index(Request $request): JsonResponse
191    {
192        // Check if user can browse users (Staff level 500+)
193        if (!request()->user()->hasPermissionTo('users.browse')) {
194            return ApiResponse::error(null, 'Unauthorized to browse users', 403);
195        }
196
197        $query = User::with(['roles']);
198
199        // Apply search filter
200        if ($search = $request->string('q')->toString()) {
201            $query->where(function ($q) use ($search) {
202                $q->where('name', 'like', "%{$search}%")
203                  ->orWhere('email', 'like', "%{$search}%")
204                  ->orWhere('given_name', 'like', "%{$search}%")
205                  ->orWhere('family_name', 'like', "%{$search}%");
206            });
207        }
208
209        // Apply status filter
210        if ($status = $request->string('status')->toString()) {
211            $query->where('status', $status);
212        }
213
214        // Apply role filter
215        if ($role = $request->string('role')->toString()) {
216            $query->role($role);
217        }
218
219        $users = $query->orderBy('created_at', 'desc')->paginate(15);
220
221        return ApiResponse::success(['users' => $users], 'Users retrieved');
222    }
223
224    /**
225     * Store a newly created user (staff+ only).
226     * 
227     * Creates a new user account with the provided information and assigns
228     * specified roles. Implements detailed permission matrix:
229     * - Staff: Can create clients and applicants only
230     * - Admin: Can create all user types
231     * - Superuser: Can create all user types
232     * 
233     * @param StoreUserRequest $request Validated request containing:
234     *                                 - name (required): User's display name
235     *                                 - given_name (optional): User's given name
236     *                                 - family_name (optional): User's family name
237     *                                 - email (required): User's email address
238     *                                 - password (required): User's password
239     *                                 - status (optional): User's status
240     *                                 - roles (optional): Array of role names to assign
241     * 
242     * @return JsonResponse Standardized JSON response containing:
243     *                     - success: boolean indicating operation success
244     *                     - message: Human-readable status message
245     *                     - data: Object containing the created user with roles
246     * 
247     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user lacks create permissions
248     * @throws \Illuminate\Validation\ValidationException When validation fails
249     * 
250     * @api POST /api/v1/users
251     * @permission users.create (Staff level 500+)
252     */
253    public function store(StoreUserRequest $request): JsonResponse
254    {
255        $currentUser = request()->user();
256
257        // Check if user can create users (Staff level 500+)
258        if (!$currentUser->hasPermissionTo('users.create')) {
259            return ApiResponse::error(null, 'Unauthorized to create users', 403);
260        }
261
262        // Check if user can create the specified roles
263        if ($request->has('roles') && !$this->canCreateUserWithRoles($currentUser, $request->roles)) {
264            return ApiResponse::error(null, 'Unauthorized to create users with these roles', 403);
265        }
266
267        $data = $request->validated();
268        $data['password'] = Hash::make($data['password']);
269
270        $user = User::create($data);
271
272        // Assign roles if provided
273        if ($request->has('roles')) {
274            $user->assignRole($request->roles);
275        }
276
277        $user->load(['roles']);
278
279        return ApiResponse::success(['user' => $user], 'User created', 201);
280    }
281
282    /**
283     * Display the specified user (admin only).
284     * 
285     * Retrieves a specific user's information including roles and permissions.
286     * This endpoint is restricted to staff+ level users for administrative
287     * purposes and user management.
288     * 
289     * @param string $id The user ID to retrieve
290     * 
291     * @return JsonResponse Standardized JSON response containing:
292     *                     - success: boolean indicating operation success
293     *                     - message: Human-readable status message
294     *                     - data: Object containing the user with roles and permissions
295     * 
296     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user not found or lacks read permissions
297     * 
298     * @api GET /api/v1/users/{id}
299     * @permission users.read (Staff level 500+)
300     */
301    public function show(string $id): JsonResponse
302    {
303        // Check if user can read users (Staff level 500+)
304        if (!request()->user()->hasPermissionTo('users.read')) {
305            return ApiResponse::error(null, 'Unauthorized to read users', 403);
306        }
307
308        $user = User::with(['roles', 'permissions'])->find($id);
309
310        if (!$user) {
311            return ApiResponse::error(null, "User not found", 404);
312        }
313
314        return ApiResponse::success(['user' => $user], 'User retrieved');
315    }
316
317    /**
318     * Update the specified user (admin only).
319     * 
320     * Updates an existing user's information including personal details
321     * and status. Implements detailed permission matrix:
322     * - Staff: Can edit own profile, clients, and applicants only
323     * - Admin: Can edit all users
324     * - Superuser: Can edit all users
325     * 
326     * @param UpdateUserRequest $request Validated request containing:
327     *                                  - name (optional): User's display name
328     *                                  - given_name (optional): User's given name
329     *                                  - family_name (optional): User's family name
330     *                                  - email (optional): User's email address
331     *                                  - status (optional): User's status
332     * @param string $id The user ID to update
333     * 
334     * @return JsonResponse Standardized JSON response containing:
335     *                     - success: boolean indicating operation success
336     *                     - message: Human-readable status message
337     *                     - data: Object containing the updated user with roles
338     * 
339     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user not found or lacks update permissions
340     * @throws \Illuminate\Validation\ValidationException When validation fails
341     * 
342     * @api PUT /api/v1/users/{id}
343     * @permission users.update (Staff level 500+)
344     */
345    public function update(UpdateUserRequest $request, string $id): JsonResponse
346    {
347        $currentUser = request()->user();
348        $targetUser = User::find($id);
349
350        if (!$targetUser) {
351            return ApiResponse::error(null, "User not found", 404);
352        }
353
354        // Check permission based on detailed matrix
355        if (!$this->canUpdateUser($currentUser, $targetUser)) {
356            return ApiResponse::error(null, 'Unauthorized to update this user', 403);
357        }
358
359        $targetUser->update($request->validated());
360        $targetUser->load(['roles']);
361
362        return ApiResponse::success(['user' => $targetUser], 'User updated');
363    }
364
365    /**
366     * Remove the specified user (admin only).
367     * 
368     * Soft deletes a user account, making it unavailable but preserving
369     * the data for potential restoration. Implements detailed permission matrix:
370     * - Staff: Can delete clients and applicants only
371     * - Admin: Can delete clients, applicants, and staff only
372     * - Superuser: Can delete any user except themselves
373     * - No user can delete themselves
374     * 
375     * @param string $id The user ID to delete
376     * 
377     * @return JsonResponse Standardized JSON response containing:
378     *                     - success: boolean indicating operation success
379     *                     - message: Human-readable status message
380     *                     - data: null (user is soft deleted)
381     * 
382     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user not found or lacks delete permissions
383     * 
384     * @api DELETE /api/v1/users/{id}
385     * @permission users.delete (Staff level 500+)
386     */
387    public function destroy(string $id): JsonResponse
388    {
389        $currentUser = request()->user();
390        $targetUser = User::find($id);
391
392        if (!$targetUser) {
393            return ApiResponse::error(null, "User not found", 404);
394        }
395
396        // Check permission based on detailed matrix
397        if (!$this->canDeleteUser($currentUser, $targetUser)) {
398            return ApiResponse::error(null, 'Unauthorized to delete this user', 403);
399        }
400
401        // Revoke all user tokens
402        $targetUser->tokens()->delete();
403
404        // Soft delete the user
405        $targetUser->delete();
406
407        return ApiResponse::success(null, 'User deleted');
408    }
409
410    /**
411     * Assign roles to a user (admin only).
412     * 
413     * Assigns or updates roles for a specific user. This endpoint is
414     * restricted to admin+ level users for role management purposes.
415     * 
416     * @param Request $request The HTTP request containing:
417     *                        - roles (required): Array of role names to assign
418     * @param string $id The user ID to assign roles to
419     * 
420     * @return JsonResponse Standardized JSON response containing:
421     *                     - success: boolean indicating operation success
422     *                     - message: Human-readable status message
423     *                     - data: Object containing the user with updated roles
424     * 
425     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user not found or lacks role assignment permissions
426     * 
427     * @api PUT /api/v1/users/{id}/roles
428     * @permission users.assign-roles (Admin level 750+)
429     */
430    public function assignRoles(Request $request, string $id): JsonResponse
431    {
432        // Check if user can assign roles (Admin level 750+)
433        if (!request()->user()->hasPermissionTo('users.assign-roles')) {
434            return ApiResponse::error(null, 'Unauthorized to assign roles', 403);
435        }
436
437        $user = User::find($id);
438
439        if (!$user) {
440            return ApiResponse::error(null, "User not found", 404);
441        }
442
443        $request->validate([
444            'roles' => 'required|array',
445            'roles.*' => 'string|exists:roles,name',
446        ]);
447
448        // Sync roles (replace existing roles)
449        $user->syncRoles($request->roles);
450        $user->load(['roles']);
451
452        return ApiResponse::success(['user' => $user], 'Roles assigned successfully');
453    }
454
455    /**
456     * Change user status (admin only).
457     * 
458     * Updates a user's status (active, suspended, banned). This endpoint
459     * is restricted to staff+ level users for user management purposes.
460     * 
461     * @param Request $request The HTTP request containing:
462     *                        - status (required): New status (active, suspended, banned)
463     * @param string $id The user ID to change status for
464     * 
465     * @return JsonResponse Standardized JSON response containing:
466     *                     - success: boolean indicating operation success
467     *                     - message: Human-readable status message
468     *                     - data: Object containing the user with updated status
469     * 
470     * @throws \Illuminate\Http\Exceptions\HttpResponseException When user not found or lacks status change permissions
471     * 
472     * @api PUT /api/v1/users/{id}/status
473     * @permission users.change-status (Staff level 500+)
474     */
475    public function changeStatus(Request $request, string $id): JsonResponse
476    {
477        // Check if user can change status (Staff level 500+)
478        if (!request()->user()->hasPermissionTo('users.change-status')) {
479            return ApiResponse::error(null, 'Unauthorized to change user status', 403);
480        }
481
482        $user = User::find($id);
483
484        if (!$user) {
485            return ApiResponse::error(null, "User not found", 404);
486        }
487
488        $request->validate([
489            'status' => 'required|string|in:active,suspended,banned',
490        ]);
491
492        $user->update(['status' => $request->status]);
493
494        // If user is suspended or banned, revoke all tokens
495        if (in_array($request->status, ['suspended', 'banned'])) {
496            $user->tokens()->delete();
497        }
498
499        $user->load(['roles']);
500
501        return ApiResponse::success(['user' => $user], 'User status updated successfully');
502    }
503
504    /**
505     * Check if the current user can update the target user based on permission matrix.
506     * 
507     * @param User $currentUser The user making the request
508     * @param User $targetUser The user being updated
509     * @return bool True if update is allowed, false otherwise
510     */
511    private function canUpdateUser(User $currentUser, User $targetUser): bool
512    {
513        // Superuser can edit all users
514        if ($currentUser->hasRole('superuser')) {
515            return true;
516        }
517
518        // Admin can edit all users
519        if ($currentUser->hasRole('admin')) {
520            return true;
521        }
522
523        // Staff can edit own profile, clients, and applicants (users with 'user' role)
524        if ($currentUser->hasRole('staff')) {
525            // Can edit own profile
526            if ($currentUser->id === $targetUser->id) {
527                return true;
528            }
529            
530            // Can edit clients and applicants (users with 'user' role)
531            return $targetUser->hasRole(['client', 'user']);
532        }
533
534        // Client can only edit own profile
535        if ($currentUser->hasRole('client')) {
536            return $currentUser->id === $targetUser->id;
537        }
538
539        return false;
540    }
541
542    /**
543     * Check if the current user can delete the target user based on permission matrix.
544     * 
545     * @param User $currentUser The user making the request
546     * @param User $targetUser The user being deleted
547     * @return bool True if deletion is allowed, false otherwise
548     */
549    private function canDeleteUser(User $currentUser, User $targetUser): bool
550    {
551        // No user can delete themselves
552        if ($currentUser->id === $targetUser->id) {
553            return false;
554        }
555
556        // Superuser can delete any user except themselves
557        if ($currentUser->hasRole('superuser')) {
558            return true;
559        }
560
561        // Admin can delete clients, applicants, and staff (but not other admins or superusers)
562        if ($currentUser->hasRole('admin')) {
563            return $targetUser->hasRole(['client', 'user', 'staff']);
564        }
565
566        // Staff can delete clients and applicants only
567        if ($currentUser->hasRole('staff')) {
568            return $targetUser->hasRole(['client', 'user']);
569        }
570
571        // Client can only delete their own profile (handled by /me endpoint)
572        return false;
573    }
574
575    /**
576     * Check if the current user can create users with the specified roles.
577     * 
578     * @param User $currentUser The user making the request
579     * @param array $roles The roles to be assigned to the new user
580     * @return bool True if role creation is allowed, false otherwise
581     */
582    private function canCreateUserWithRoles(User $currentUser, array $roles): bool
583    {
584        // Superuser can create users with any roles
585        if ($currentUser->hasRole('superuser')) {
586            return true;
587        }
588
589        // Admin can create users with any roles
590        if ($currentUser->hasRole('admin')) {
591            return true;
592        }
593
594        // Staff can only create clients and applicants (users with 'user' role)
595        if ($currentUser->hasRole('staff')) {
596            $allowedRoles = ['client', 'user'];
597            foreach ($roles as $role) {
598                if (!in_array($role, $allowedRoles)) {
599                    return false;
600                }
601            }
602            return true;
603        }
604
605        return false;
606    }
607}