From 026e7af6a5ef0876f1144c1ac352bd6126736ba5 Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Wed, 24 Dec 2025 15:03:37 +0000 Subject: [PATCH] feat: api admin users endpoints --- api/app/Http/Controllers/Controller.php | 7 +- .../Http/Controllers/ProfileController.php | 100 ++++++++++++ api/app/Http/Controllers/UserController.php | 75 ++++++--- api/app/Http/Middleware/CheckUserType.php | 32 ++++ api/bootstrap/app.php | 15 +- api/database/seeders/photos/anonymous.png | Bin api/routes/api.php | 147 +++++++----------- bruno_collection/Admin/Get Users.bru | 20 +++ bruno_collection/Admin/folder.bru | 8 + bruno_collection/Auth/Login.bru | 2 +- bruno_collection/Games/Get Games.bru | 2 +- .../environments/Local Development.bru | 4 +- 12 files changed, 294 insertions(+), 118 deletions(-) create mode 100644 api/app/Http/Controllers/ProfileController.php create mode 100644 api/app/Http/Middleware/CheckUserType.php mode change 100644 => 100755 api/database/seeders/photos/anonymous.png create mode 100644 bruno_collection/Admin/Get Users.bru create mode 100644 bruno_collection/Admin/folder.bru diff --git a/api/app/Http/Controllers/Controller.php b/api/app/Http/Controllers/Controller.php index 8677cd5..62f1dd9 100644 --- a/api/app/Http/Controllers/Controller.php +++ b/api/app/Http/Controllers/Controller.php @@ -2,7 +2,10 @@ namespace App\Http\Controllers; -abstract class Controller +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Routing\Controller as BaseController; + +abstract class Controller extends BaseController { - // + use AuthorizesRequests; } diff --git a/api/app/Http/Controllers/ProfileController.php b/api/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..eaf8b4c --- /dev/null +++ b/api/app/Http/Controllers/ProfileController.php @@ -0,0 +1,100 @@ +user(); + + return response()->json($user); + } + + public function update(Request $request) + { + $request->validate([ + "name" => "required|string|max:255", + "nickname" => + "required|string|max:255|unique:users,nickname," . + $request->user()->id, + ]); + + $user = $request->user(); + $user->name = $request->name; + $user->nickname = $request->nickname; + $user->save(); + + return response()->json($user); + } + + public function destroy(User $user) + { + $currentUser = Auth::user(); + + if ($currentUser->id !== $user->id && $currentUser->type !== "A") { + return response()->json(["message" => "Unauthorized"], 403); + } + + $user->delete(); + + return response()->json(["message" => "User deleted successfully"]); + } + + public function updatePassword(Request $request) + { + $request->validate([ + "current_password" => "required", + "new_password" => "required|min:8|confirmed", + ]); + + $user = $request->user(); + + if (!\Hash::check($request->current_password, $user->password)) { + return response()->json( + [ + "message" => "Current password is incorrect", + ], + 422, + ); + } + + $user->password = \Hash::make($request->new_password); + $user->save(); + + return response()->json([ + "message" => "Password changed successfully", + ]); + } + + public function updateAvatar(Request $request) + { + $request->validate([ + "avatar" => "required|image|mimes:jpeg,png,jpg,gif|max:2048", + ]); + + $user = $request->user(); + + if ($user->photo_avatar_filename) { + \Storage::disk("public")->delete( + "photos_avatars/" . $user->photo_avatar_filename, + ); + } + + $file = $request->file("avatar"); + $filename = + str_pad($user->id, 5, "0", STR_PAD_LEFT) . + "_" . + \Str::random(10) . + "." . + $file->extension(); + $file->storeAs("photos_avatars", $filename, "public"); + + $user->photo_avatar_filename = $filename; + $user->save(); + + return response()->json($user); + } +} diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index d1ee12e..32bc653 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; -use App\Models\User; use App\Models\Game; +use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -17,25 +17,45 @@ class UserController extends Controller */ public function index(Request $request) { - if ($request->user()->type !== 'A') { - return response()->json(['message' => 'Unauthorized'], 403); - } - $query = User::query(); // Filtros úteis para o Backoffice if ($request->has('type')) { $query->where('type', $request->type); } + if ($request->has('search')) { - $query->where('name', 'like', '%' . $request->search . '%') - ->orWhere('email', 'like', '%' . $request->search . '%') - ->orWhere('nickname', 'like', '%' . $request->search . '%'); + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%'.$request->search.'%') + ->orWhere('email', 'like', '%'.$request->search.'%') + ->orWhere('nickname', 'like', '%'.$request->search.'%'); + }); } return response()->json($query->paginate(15)); } + /** + * POST /api/users + * Registar um novo utilizador (Admin only) + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'nickname' => 'required|string|max:20|unique:users,nickname', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|string|min:3', + 'type' => 'required|in:A,U', + ]); + + $validated['password'] = Hash::make($validated['password']); + + $user = User::create($validated); + + return response()->json($user, 201); + } + /** * GET /api/users/me * Retorna o perfil do utilizador autenticado atual. @@ -59,7 +79,7 @@ class UserController extends Controller 'name' => $user->name, 'nickname' => $user->nickname, 'photo_avatar_filename' => $user->photo_avatar_filename, - 'type' => $user->type + 'type' => $user->type, ]); } @@ -80,14 +100,26 @@ class UserController extends Controller $validated = $request->validate([ 'name' => 'sometimes|string|max:255', - 'nickname' => ['sometimes', 'string', 'max:20', Rule::unique('users')->ignore($user->id)], - 'email' => ['sometimes', 'email', Rule::unique('users')->ignore($user->id)], + 'nickname' => [ + 'sometimes', + 'string', + 'max:20', + Rule::unique('users')->ignore($user->id), + ], + 'email' => [ + 'sometimes', + 'email', + Rule::unique('users')->ignore($user->id), + ], 'password' => 'sometimes|string|min:3', 'blocked' => 'sometimes|boolean', ]); if (isset($validated['blocked']) && $currentUser->type !== 'A') { - return response()->json(['message' => 'Only admins can block users'], 403); + return response()->json( + ['message' => 'Only admins can block users'], + 403, + ); } // Lógica de Upload de Foto (Exemplo Básico) @@ -114,9 +146,12 @@ class UserController extends Controller } if ($currentUser->id === $user->id && $currentUser->type === 'A') { - if ($user->type === 'A') { - return response()->json(['message' => 'Admins cannot delete their own account'], 403); - } + if ($user->type === 'A') { + return response()->json( + ['message' => 'Admins cannot delete their own account'], + 403, + ); + } } $user->delete(); @@ -137,9 +172,11 @@ class UserController extends Controller } $matches = Game::query() - ->where(function($q) use ($user) { - $q->where('player1_user_id', $user->id) - ->orWhere('player2_user_id', $user->id); + ->where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id)->orWhere( + 'player2_user_id', + $user->id, + ); }) ->with(['winner', 'player1', 'player2']) ->orderBy('began_at', 'desc') @@ -147,4 +184,4 @@ class UserController extends Controller return response()->json($matches); } -} \ No newline at end of file +} diff --git a/api/app/Http/Middleware/CheckUserType.php b/api/app/Http/Middleware/CheckUserType.php new file mode 100644 index 0000000..1761648 --- /dev/null +++ b/api/app/Http/Middleware/CheckUserType.php @@ -0,0 +1,32 @@ +user(); + + if (!$user || $user->type !== $type) { + return response()->json( + ["message" => "Unauthorized. User type {$type} required."], + 403, + ); + } + + return $next($request); + } +} diff --git a/api/bootstrap/app.php b/api/bootstrap/app.php index c3928c5..94db66d 100644 --- a/api/bootstrap/app.php +++ b/api/bootstrap/app.php @@ -6,14 +6,17 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__.'/../routes/web.php', - api: __DIR__.'/../routes/api.php', - commands: __DIR__.'/../routes/console.php', - health: '/up', + web: __DIR__ . "/../routes/web.php", + api: __DIR__ . "/../routes/api.php", + commands: __DIR__ . "/../routes/console.php", + health: "/up", ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->alias([ + "user.type" => App\Http\Middleware\CheckUserType::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // - })->create(); + }) + ->create(); diff --git a/api/database/seeders/photos/anonymous.png b/api/database/seeders/photos/anonymous.png old mode 100644 new mode 100755 diff --git a/api/routes/api.php b/api/routes/api.php index e14d021..0601edb 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -1,109 +1,82 @@ group(function () { + // Public Routes + Route::post('/login', [AuthController::class, 'login']); + Route::post('/register', [AuthController::class, 'register']); -Route::middleware('auth:sanctum')->group(function () { - Route::post('logout', [AuthController::class, 'logout']); + // Authenticated Routes + Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () { + Route::post('/logout', [AuthController::class, 'logout']); - Route::prefix('users')->group(function () { - Route::get('/me', function (Request $request) { - return $request->user(); + Route::prefix('users/me')->group(function () { + Route::get('/', [ProfileController::class, 'show']); + Route::put('/', [ProfileController::class, 'update']); + Route::delete('/', [ProfileController::class, 'destroy']); + Route::put('/password', [ + ProfileController::class, + 'updatePassword', + ]); + Route::post('/avatar', [ProfileController::class, 'uploadAvatar']); }); - // Update user profile (name and nickname) - Route::put('/me', function (Request $request) { - $request->validate([ - 'name' => 'required|string|max:255', - 'nickname' => 'required|string|max:255|unique:users,nickname,' . $request->user()->id, - ]); - - $user = $request->user(); - $user->name = $request->name; - $user->nickname = $request->nickname; - $user->save(); - - return response()->json($user); - }); - - // Change password - Route::put('/me/password', function (Request $request) { - $request->validate([ - 'current_password' => 'required', - 'new_password' => 'required|min:8|confirmed', - ]); - - $user = $request->user(); - - if (!\Hash::check($request->current_password, $user->password)) { - return response()->json([ - 'message' => 'Current password is incorrect' - ], 422); - } - - $user->password = \Hash::make($request->new_password); - $user->save(); - - return response()->json([ - 'message' => 'Password changed successfully' + // User Resources + Route::prefix('/users')->group(function () { + Route::get('/{user}', [UserController::class, 'show']); + Route::get('/{user}/matches', [ + UserController::class, + 'getMatches', ]); }); - // Upload avatar - Route::post('/me/avatar', function (Request $request) { - $request->validate([ - 'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', + // Game Resources + Route::prefix('games')->group(function () { + Route::apiResource('/', GameController::class)->parameters([ + '' => 'game', ]); - - $user = $request->user(); - - // Delete old avatar if exists - if ($user->photo_avatar_filename) { - \Storage::disk('public')->delete('photos_avatars/' . $user->photo_avatar_filename); - } - - // Store new avatar - $file = $request->file('avatar'); - $filename = str_pad($user->id, 5, '0', STR_PAD_LEFT) . '_' . \Str::random(10) . '.' . $file->extension(); - $file->storeAs('photos_avatars', $filename, 'public'); - - $user->photo_avatar_filename = $filename; - $user->save(); - - return response()->json($user); + Route::post('/{game}/join', [GameController::class, 'join']); + Route::post('/{game}/resign', [GameController::class, 'resign']); }); - // Delete account - Route::delete('/me', function (Request $request) { - $user = $request->user(); - - $user->tokens()->delete(); - - $user->delete(); - - return response()->json([ - 'message' => 'Account deleted successfully' + // Match Resources + Route::prefix('matches')->group(function () { + Route::apiResource('/', MatchController::class)->parameters([ + '' => 'match', + ]); + Route::post('/{match}/join', [ + MatchController::class, + 'join', ]); }); - Route::get('/{user}/matches', [UserController::class, 'getMatches']); - }); - - Route::prefix('games')->group(function () { - Route::apiResource('/', GameController::class)->parameters(['' => 'game']); - Route::post('/{game}/resign', [GameController::class, 'resign']); - Route::post('/{game}/join', [GameController::class, 'join']); - }); - - Route::prefix('matches')->group(function () { - Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); - Route::post('/{match}/join', [MatchController::class, 'join']); + // Admin Routes + Route::middleware('user.type:A') + ->prefix('admin') + ->group(function () { + Route::prefix('users')->group(function () { + Route::get('/', [UserController::class, 'index']); + Route::post('/', [UserController::class, 'store']); + Route::put('/{user}', [UserController::class, 'update']); + Route::delete('/{user}', [ + UserController::class, + 'destroy', + ]); + Route::post('/{user}/block', [ + UserController::class, + 'block', + ]); + Route::post('/{user}/unblock', [ + UserController::class, + 'unblock', + ]); + }); + }); }); }); diff --git a/bruno_collection/Admin/Get Users.bru b/bruno_collection/Admin/Get Users.bru new file mode 100644 index 0000000..da9b579 --- /dev/null +++ b/bruno_collection/Admin/Get Users.bru @@ -0,0 +1,20 @@ +meta { + name: Get Users + type: http + seq: 1 +} + +get { + url: {{api_url}}/admin/users/ + body: none + auth: inherit +} + +headers { + Authorization: Bearer {{token}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_collection/Admin/folder.bru b/bruno_collection/Admin/folder.bru new file mode 100644 index 0000000..00a536e --- /dev/null +++ b/bruno_collection/Admin/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Admin + seq: 4 +} + +auth { + mode: inherit +} diff --git a/bruno_collection/Auth/Login.bru b/bruno_collection/Auth/Login.bru index 1ee59da..3f1d35a 100644 --- a/bruno_collection/Auth/Login.bru +++ b/bruno_collection/Auth/Login.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{api_url}}/auth/login + url: {{api_url}}/login body: json auth: none } diff --git a/bruno_collection/Games/Get Games.bru b/bruno_collection/Games/Get Games.bru index 7bb9043..8dadb34 100644 --- a/bruno_collection/Games/Get Games.bru +++ b/bruno_collection/Games/Get Games.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{api_url}}/games + url: {{api_url}}/games/1 body: none auth: none } diff --git a/bruno_collection/environments/Local Development.bru b/bruno_collection/environments/Local Development.bru index b193304..6bc5376 100644 --- a/bruno_collection/environments/Local Development.bru +++ b/bruno_collection/environments/Local Development.bru @@ -1,5 +1,5 @@ vars { - base_url: http://localhost:8085 - api_url: http://localhost:8085/api + base_url: http://localhost:8000 + api_url: http://localhost:8000/api/v1 token: } -- 2.54.0