feat: api admin users endpoints #74

Merged
FernandoJVideira merged 1 commits from feature/api-admin-user-endpoint into develop 2025-12-24 15:54:11 +00:00
12 changed files with 294 additions and 118 deletions
+5 -2
View File
@@ -2,7 +2,10 @@
namespace App\Http\Controllers; 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;
} }
@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProfileController extends Controller
{
public function show(Request $request)
{
$user = $request->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);
}
}
+53 -16
View File
@@ -2,8 +2,8 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Game; use App\Models\Game;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@@ -17,25 +17,45 @@ class UserController extends Controller
*/ */
public function index(Request $request) public function index(Request $request)
{ {
if ($request->user()->type !== 'A') {
return response()->json(['message' => 'Unauthorized'], 403);
}
$query = User::query(); $query = User::query();
// Filtros úteis para o Backoffice // Filtros úteis para o Backoffice
if ($request->has('type')) { if ($request->has('type')) {
$query->where('type', $request->type); $query->where('type', $request->type);
} }
if ($request->has('search')) { if ($request->has('search')) {
$query->where('name', 'like', '%' . $request->search . '%') $query->where(function ($q) use ($request) {
->orWhere('email', 'like', '%' . $request->search . '%') $q->where('name', 'like', '%'.$request->search.'%')
->orWhere('nickname', 'like', '%' . $request->search . '%'); ->orWhere('email', 'like', '%'.$request->search.'%')
->orWhere('nickname', 'like', '%'.$request->search.'%');
});
} }
return response()->json($query->paginate(15)); 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 * GET /api/users/me
* Retorna o perfil do utilizador autenticado atual. * Retorna o perfil do utilizador autenticado atual.
@@ -59,7 +79,7 @@ class UserController extends Controller
'name' => $user->name, 'name' => $user->name,
'nickname' => $user->nickname, 'nickname' => $user->nickname,
'photo_avatar_filename' => $user->photo_avatar_filename, 'photo_avatar_filename' => $user->photo_avatar_filename,
'type' => $user->type 'type' => $user->type,
]); ]);
} }
@@ -80,14 +100,26 @@ class UserController extends Controller
$validated = $request->validate([ $validated = $request->validate([
'name' => 'sometimes|string|max:255', 'name' => 'sometimes|string|max:255',
'nickname' => ['sometimes', 'string', 'max:20', Rule::unique('users')->ignore($user->id)], 'nickname' => [
'email' => ['sometimes', 'email', Rule::unique('users')->ignore($user->id)], 'sometimes',
'string',
'max:20',
Rule::unique('users')->ignore($user->id),
],
'email' => [
'sometimes',
'email',
Rule::unique('users')->ignore($user->id),
],
'password' => 'sometimes|string|min:3', 'password' => 'sometimes|string|min:3',
'blocked' => 'sometimes|boolean', 'blocked' => 'sometimes|boolean',
]); ]);
if (isset($validated['blocked']) && $currentUser->type !== 'A') { 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) // Lógica de Upload de Foto (Exemplo Básico)
@@ -115,7 +147,10 @@ class UserController extends Controller
if ($currentUser->id === $user->id && $currentUser->type === 'A') { if ($currentUser->id === $user->id && $currentUser->type === 'A') {
if ($user->type === 'A') { if ($user->type === 'A') {
return response()->json(['message' => 'Admins cannot delete their own account'], 403); return response()->json(
['message' => 'Admins cannot delete their own account'],
403,
);
} }
} }
@@ -137,9 +172,11 @@ class UserController extends Controller
} }
$matches = Game::query() $matches = Game::query()
->where(function($q) use ($user) { ->where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id) $q->where('player1_user_id', $user->id)->orWhere(
->orWhere('player2_user_id', $user->id); 'player2_user_id',
$user->id,
);
}) })
->with(['winner', 'player1', 'player2']) ->with(['winner', 'player1', 'player2'])
->orderBy('began_at', 'desc') ->orderBy('began_at', 'desc')
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckUserType
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(
Request $request,
Closure $next,
string $type,
): Response {
$user = $request->user();
if (!$user || $user->type !== $type) {
return response()->json(
["message" => "Unauthorized. User type {$type} required."],
403,
);
}
return $next($request);
}
}
+9 -6
View File
@@ -6,14 +6,17 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__ . "/../routes/web.php",
api: __DIR__.'/../routes/api.php', api: __DIR__ . "/../routes/api.php",
commands: __DIR__.'/../routes/console.php', commands: __DIR__ . "/../routes/console.php",
health: '/up', health: "/up",
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
// $middleware->alias([
"user.type" => App\Http\Middleware\CheckUserType::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //
})->create(); })
->create();
View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+61 -88
View File
@@ -1,109 +1,82 @@
<?php <?php
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\GameController; use App\Http\Controllers\GameController;
use App\Http\Controllers\MatchController; use App\Http\Controllers\MatchController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
// Authentication Routes Route::prefix('v1')->group(function () {
Route::post('/login', [AuthController::class, 'login']); // Public Routes
Route::post('/register', [AuthController::class, 'register']); Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
Route::middleware('auth:sanctum')->group(function () { // Authenticated Routes
Route::post('logout', [AuthController::class, 'logout']); Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::prefix('users')->group(function () { Route::prefix('users/me')->group(function () {
Route::get('/me', function (Request $request) { Route::get('/', [ProfileController::class, 'show']);
return $request->user(); 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) // User Resources
Route::put('/me', function (Request $request) { Route::prefix('/users')->group(function () {
$request->validate([ Route::get('/{user}', [UserController::class, 'show']);
'name' => 'required|string|max:255', Route::get('/{user}/matches', [
'nickname' => 'required|string|max:255|unique:users,nickname,' . $request->user()->id, UserController::class,
]); 'getMatches',
$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'
]); ]);
}); });
// Upload avatar // Game Resources
Route::post('/me/avatar', function (Request $request) {
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
$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);
});
// Delete account
Route::delete('/me', function (Request $request) {
$user = $request->user();
$user->tokens()->delete();
$user->delete();
return response()->json([
'message' => 'Account deleted successfully'
]);
});
Route::get('/{user}/matches', [UserController::class, 'getMatches']);
});
Route::prefix('games')->group(function () { Route::prefix('games')->group(function () {
Route::apiResource('/', GameController::class)->parameters(['' => 'game']); Route::apiResource('/', GameController::class)->parameters([
Route::post('/{game}/resign', [GameController::class, 'resign']); '' => 'game',
]);
Route::post('/{game}/join', [GameController::class, 'join']); Route::post('/{game}/join', [GameController::class, 'join']);
Route::post('/{game}/resign', [GameController::class, 'resign']);
}); });
// Match Resources
Route::prefix('matches')->group(function () { Route::prefix('matches')->group(function () {
Route::apiResource('/', MatchController::class)->parameters(['' => 'match']); Route::apiResource('/', MatchController::class)->parameters([
Route::post('/{match}/join', [MatchController::class, 'join']); '' => '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',
]);
});
});
}); });
}); });
+20
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
meta {
name: Admin
seq: 4
}
auth {
mode: inherit
}
+1 -1
View File
@@ -5,7 +5,7 @@ meta {
} }
post { post {
url: {{api_url}}/auth/login url: {{api_url}}/login
body: json body: json
auth: none auth: none
} }
+1 -1
View File
@@ -5,7 +5,7 @@ meta {
} }
get { get {
url: {{api_url}}/games url: {{api_url}}/games/1
body: none body: none
auth: none auth: none
} }
@@ -1,5 +1,5 @@
vars { vars {
base_url: http://localhost:8085 base_url: http://localhost:8000
api_url: http://localhost:8085/api api_url: http://localhost:8000/api/v1
token: token:
} }