Merge branch 'develop' into feature/websockets-matches

This commit is contained in:
2026-01-02 21:47:30 +00:00
23 changed files with 1786 additions and 317 deletions
+28 -39
View File
@@ -13,19 +13,16 @@ class GameController extends Controller
public function index(Request $request)
{
$this->authorize('viewAny', Game::class);
$user = Auth::user();
$query = Game::query()->with(["winner", "player1", "player2"]);
// Filter games based on user type
if ($user->type !== 'A') {
// Players can only see games they participate in
$query->where(function ($q) use ($user) {
$q->where('player1_user_id', $user->id)
->orWhere('player2_user_id', $user->id);
});
}
// Admins see all games (no filter needed)
if ($request->has("type") && in_array($request->type, ["3", "9"])) {
$query->where("type", $request->type);
@@ -55,7 +52,7 @@ class GameController extends Controller
public function store(Request $request)
{
$this->authorize('create', Game::class);
$validated = $request->validate([
"type" => "required|in:3,9",
]);
@@ -77,7 +74,7 @@ class GameController extends Controller
"type" => $validated["type"],
"status" => "Pending",
"player1_user_id" => $user->id,
"player2_user_id" => self::GHOST_ID,
"player2_user_id" => self::GHOST_ID,
"winner_user_id" => self::GHOST_ID,
"loser_user_id" => self::GHOST_ID,
"began_at" => now(),
@@ -92,38 +89,30 @@ class GameController extends Controller
], 201);
}
public function join(Game $game)
{
$this->authorize('join', $game);
$user = Auth::user();
if ($game->player1_user_id == $user->id) {
return response()->json([
"message" => "You are the host. Waiting for opponent...",
"game" => $game
], 200);
}
if ($game->player2_user_id !== self::GHOST_ID) {
return response()->json(["message" => "Game is full"], 400);
}
$game->update([
"status" => "Playing",
"player2_user_id" => $user->id,
]);
return response()->json([
"message" => "Joined successfully!",
"game" => $game,
], 200);
public function join(Request $request, Game $game)
{
// Prevent joining if already full or started
if ($game->status !== 'Pending' || $game->player2_user_id !== 1) {
return response()->json(['message' => 'Game is not available'], 400);
}
// Prevent joining your own game
if ($game->player1_user_id === $request->user()->id) {
return response()->json(['message' => 'Cannot join your own game'], 400);
}
$game->update([
'player2_user_id' => $request->user()->id,
'status' => 'Playing',
]);
return response()->json($game);
}
public function show(Game $game)
{
$this->authorize('view', $game);
$game->load(["player1", "player2", "winner"]);
return response()->json($game);
}
@@ -131,7 +120,7 @@ class GameController extends Controller
public function update(Request $request, Game $game)
{
$this->authorize('update', $game);
if ($game->status == "Ended") {
return response()->json(["message" => "Game already ended"], 400);
}
@@ -158,13 +147,13 @@ class GameController extends Controller
public function resign(Game $game)
{
$this->authorize('resign', $game);
$user = Auth::user();
$game->update([
'status' => 'Ended',
'winner_user_id' => $game->player1_user_id === $user->id
? $game->player2_user_id
'winner_user_id' => $game->player1_user_id === $user->id
? $game->player2_user_id
: $game->player1_user_id,
'loser_user_id' => $user->id,
'ended_at' => now(),
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers;
use App\Models\CoinTransaction;
use Illuminate\Http\Request;
class TransactionController extends Controller
{
public function index(Request $request)
{
$this->authorize('viewAny', CoinTransaction::class);
$query = CoinTransaction::with('user:id,nickname,email', 'type:id,name');
if ($request->has('sort_coins')) {
$direction = $request->get('sort_coins') === 'asc' ? 'asc' : 'desc';
$query->orderBy('coins', $direction);
}
$dateDirection = $request->get('sort_datetime') === 'asc' ? 'asc' : 'desc';
$query->orderBy('transaction_datetime', $dateDirection);
$transactions = $query->paginate(15);
return response()->json($transactions);
}
}
+4 -2
View File
@@ -21,11 +21,14 @@ class UserController extends Controller
$query = User::query();
// Filtros úteis para o Backoffice
if ($request->has('type')) {
$query->where('type', $request->type);
}
if ($request->has('blocked')) {
$query->where('blocked', $request->type);
}
if ($request->has('search')) {
$query->where(function ($q) use ($request) {
$q->where('name', 'like', '%'.$request->search.'%')
@@ -47,7 +50,6 @@ class UserController extends Controller
$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',
+11
View File
@@ -29,11 +29,22 @@ class Game extends Model
'match_id'
];
protected $casts = [
'began_at' => 'datetime',
'ended_at' => 'datetime',
'is_draw' => 'boolean',
];
public function winner()
{
return $this->belongsTo(User::class, "winner_user_id");
}
public function loser()
{
return $this->belongsTo(User::class, "loser_user_id");
}
public function player1()
{
return $this->belongsTo(User::class, 'player1_user_id');
@@ -0,0 +1,18 @@
<?php
namespace App\Policies;
use App\Models\CoinTransaction;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class CoinTransactionPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->type === 'A' ? true : false;
}
}
+41 -13
View File
@@ -7,6 +7,7 @@ use App\Http\Controllers\ProfileController;
use App\Http\Controllers\StatisticsController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\CoinPurchaseController;
use App\Http\Controllers\TransactionController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function () {
@@ -46,24 +47,50 @@ Route::prefix('v1')->group(function () {
Route::get('/{user}/transactions', [UserController::class, 'getTransactions']);
});
// Game Resources
Route::prefix('games')->group(function () {
Route::apiResource('/', GameController::class)->parameters([
'' => 'game',
]);
Route::post('/{game}/join', [GameController::class, 'join']);
//Host a new multiplayer game - MUST come before resource routes
Route::post('host', function (\Illuminate\Http\Request $request) {
$request->validate([
'type' => 'required|in:3,9'
]);
$game = \App\Models\Game::create([
'type' => $request->type,
'status' => 'Pending',
'player1_user_id' => $request->user()->id,
'player2_user_id' => 1, // Ghost player
'began_at' => now(),
]);
return response()->json($game);
});
// List open games (available to join)
Route::get('open', function (\Illuminate\Http\Request $request) {
$type = $request->query('type', '9');
$games = \App\Models\Game::where('status', 'Pending')
->where('player2_user_id', 1) // Only games waiting for a second player
->where('type', $type)
->with('player1') // Load player1 relationship
->orderBy('began_at', 'asc')
->paginate(10);
return response()->json($games);
});
Route::get('/', [GameController::class, 'index']);
Route::get('/{game}', [GameController::class, 'show']);
Route::post('/', [GameController::class, 'store']);
Route::put('/{game}', [GameController::class, 'update']);
Route::delete('/{game}', [GameController::class, 'destroy']);
Route::post('/{game}/resign', [GameController::class, 'resign']);
Route::post('/{game}/join', [GameController::class, 'join']);
});
// Match Resources
Route::prefix('matches')->group(function () {
Route::apiResource('/', MatchController::class)->parameters([
'' => 'match',
]);
Route::post('/{match}/join', [
MatchController::class,
'join',
]);
Route::apiResource('/', MatchController::class)->parameters(['' => 'match']);
Route::post('/{match}/join', [MatchController::class, 'join']);
});
// Admin Routes
@@ -71,6 +98,7 @@ Route::prefix('v1')->group(function () {
->prefix('admin')
->group(function () {
Route::get('/statistics', [StatisticsController::class, 'getAdminStats']);
Route::get('/transactions', [TransactionController::class, 'index']);
Route::prefix('users')->group(function () {
Route::get('/', [UserController::class, 'index']);
Route::post('/', [UserController::class, 'store']);
+20 -3
View File
@@ -5,7 +5,7 @@ Content-Type: application/json
Accept: application/json
{
"email": "p[email protected]",
"email": "a1@mail.pt",
"password": "123"
}
@@ -14,7 +14,7 @@ Accept: application/json
@id = {{login.response.body.user.id}}
### Get All matches
GET http://localhost:8000/api/v1/matches
GET http://localhost:8000/api/v1/games
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}
@@ -55,7 +55,7 @@ Authorization: Bearer {{token}}
"payment_reference": "912345678"
}
### Get public stats
### Get leaderboard
GET http://localhost:8000/api/v1/leaderboard
Content-Type: application/json
Accept: application/json
@@ -64,4 +64,21 @@ Accept: application/json
GET http://localhost:8000/api/v1/statistics/me
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}
### Get public stats
GET http://localhost:8000/api/v1/statistics/public
Content-Type: application/json
Accept: application/json
### Get admin stats
GET http://localhost:8000/api/v1/admin/statistics
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}
### Get admin stats
GET http://localhost:8000/api/v1/admin/users
Content-Type: application/json
Accept: application/json
Authorization: Bearer {{token}}