Merge branch 'develop' into feature/home-page

This commit is contained in:
Edd
2025-12-24 23:42:00 +00:00
85 changed files with 4584 additions and 667 deletions
+5 -2
View File
@@ -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;
}
@@ -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);
}
}
+56 -19
View File
@@ -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);
}
}
}
+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);
}
}
+12 -18
View File
@@ -2,7 +2,6 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -11,30 +10,27 @@ use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'nickname',
'photo_avatar_filename',
'type',
'blocked',
'photo_avatar_filename',
'coins_balance',
'custom',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
* @var array<int, string>
*/
protected $hidden = [
'password',
@@ -42,16 +38,14 @@ class User extends Authenticatable
];
/**
* Get the attributes that should be cast.
* The attributes that should be cast.
*
* @return array<string, string>
* @var array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'blocked' => 'boolean',
];
}
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'blocked' => 'boolean',
'deleted_at' => 'datetime',
];
}
+9 -6
View File
@@ -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();
+3
View File
@@ -102,12 +102,15 @@
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+67 -19
View File
@@ -3,32 +3,80 @@
use App\Http\Controllers\AuthController;
use App\Http\Controllers\GameController;
use App\Http\Controllers\MatchController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\UserController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// Authentication Routes
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
Route::prefix('v1')->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']);
});
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']);
});
// User Resources
Route::prefix('/users')->group(function () {
Route::get('/{user}', [UserController::class, 'show']);
Route::get('/{user}/matches', [
UserController::class,
'getMatches',
]);
});
Route::prefix('matches')->group(function () {
Route::apiResource('/', MatchController::class)->parameters(['' => 'match']);
Route::post('/{match}/join', [MatchController::class, 'join']);
// Game Resources
Route::prefix('games')->group(function () {
Route::apiResource('/', GameController::class)->parameters([
'' => 'game',
]);
Route::post('/{game}/join', [GameController::class, 'join']);
Route::post('/{game}/resign', [GameController::class, 'resign']);
});
// Match Resources
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',
]);
});
});
});
});
+13 -3
View File
@@ -1,9 +1,19 @@
### Get All Students
### Get All Students (Login)
# @name login
POST http://localhost:8000/api/login
content-Type: application/json
Content-Type: application/json
Accept: application/json
{
"email": "[email protected]",
"password": "123"
}
}
### Capture the token
@token = {{login.response.body.token}}
### Get All matches
GET http://localhost:8000/api/matches
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}