From 3a7b1b9794e4115cbb4d1944a37a2e686f6e3d2b Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Fri, 2 Jan 2026 14:26:54 +0000 Subject: [PATCH 1/2] fix:websocket errors & polict change --- api/app/Policies/GamePolicy.php | 26 +++++++++++++-------- bruno_collection/Games/Create Game.bru | 15 ++++++++++++ websockets/classes/BiscaGame.js | 32 ++++++++++++++------------ websockets/events/connection.js | 16 ++++++------- websockets/package.json | 1 + websockets/state/game.js | 16 +++++-------- 6 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 bruno_collection/Games/Create Game.bru diff --git a/api/app/Policies/GamePolicy.php b/api/app/Policies/GamePolicy.php index 157f98c..7345bfb 100644 --- a/api/app/Policies/GamePolicy.php +++ b/api/app/Policies/GamePolicy.php @@ -20,7 +20,7 @@ class GamePolicy */ public function view(User $user, Game $game): bool { - if ($user->type === 'A') { + if ($user->type === "A") { return true; } @@ -31,6 +31,10 @@ class GamePolicy return true; } + if ($game->status === "Pending") { + return true; + } + return false; } @@ -39,7 +43,7 @@ class GamePolicy */ public function create(User $user): bool { - if ($user->type === 'A') { + if ($user->type === "A") { return false; } @@ -48,28 +52,32 @@ class GamePolicy public function join(User $user, Game $game): bool { - if ($user->type === 'A') { + if ($user->type === "A") { return false; } if ( - $game->status !== 'waiting' || + $game->status !== "Pending" || $game->player1_user_id === $user->id ) { return false; } + if ($game->player2_user_id !== null && $game->player2_user_id !== 1) { + return false; + } + return true; } public function resign(User $user, Game $game): bool { - if ($user->type === 'A') { + if ($user->type === "A") { return false; } if ( - $game->status !== 'ongoing' || + $game->status !== "Playing" || ($game->player1_user_id !== $user->id && $game->player2_user_id !== $user->id) ) { @@ -84,12 +92,12 @@ class GamePolicy */ public function update(User $user, Game $game): bool { - if ($user->type === 'A') { + if ($user->type === "A") { return false; } if ( - $game->status !== 'ongoing' || + $game->status !== "Playing" || ($game->player1_user_id !== $user->id && $game->player2_user_id !== $user->id) ) { @@ -105,7 +113,7 @@ class GamePolicy public function delete(User $user, Game $game): bool { // Only admins can delete games - return $user->type === 'A'; + return $user->type === "A"; } /** diff --git a/bruno_collection/Games/Create Game.bru b/bruno_collection/Games/Create Game.bru new file mode 100644 index 0000000..d027b66 --- /dev/null +++ b/bruno_collection/Games/Create Game.bru @@ -0,0 +1,15 @@ +meta { + name: Create Game + type: http + seq: 3 +} + +post { + url: {{api_url}}/games + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/websockets/classes/BiscaGame.js b/websockets/classes/BiscaGame.js index 8ee4574..8ee957b 100644 --- a/websockets/classes/BiscaGame.js +++ b/websockets/classes/BiscaGame.js @@ -17,7 +17,7 @@ class BiscaGame { }; this.currentTurn = null; - this.status = "pending"; + this.status = 'pending'; this.startTime = null; this.players = { @@ -50,7 +50,7 @@ class BiscaGame { this.currentTurn = playerIds.find((id) => id != 1) || playerIds[0]; this.startTime = Date.now(); - this.status = "playing"; + this.status = 'playing'; } startTurnTimer(callback) { @@ -74,10 +74,12 @@ class BiscaGame { } handleTimeout() { - console.log(`[Game ${this.id}] Timeout! Auto-playing for ${this.currentTurn}`); - + console.log( + `[Game ${this.id}] Timeout! Auto-playing for ${this.currentTurn}`, + ); + const lowestCard = this.getLowestCard(this.currentTurn); - + if (lowestCard && this.timeoutCallback) { this.timeoutCallback(this.currentTurn, lowestCard.id); } @@ -92,7 +94,7 @@ class BiscaGame { } createDeck() { - const suits = ["c", "e", "o", "p"]; + const suits = ['c', 'e', 'o', 'p']; const ranks = [1, 2, 3, 4, 5, 6, 7, 11, 12, 13]; let deck = []; @@ -128,7 +130,7 @@ class BiscaGame { } playCard(userId, cardId) { - if (this.currentTurn != userId) return { error: "Not your turn!" }; + if (this.currentTurn != userId) return { error: 'Not your turn!' }; const player = this.players[userId]; const cardIndex = player.hand.findIndex((c) => c.id === cardId); @@ -142,7 +144,7 @@ class BiscaGame { const suitToFollow = this.table.playerCard.suit; const hasSuit = player.hand.some((c) => c.suit === suitToFollow); if (hasSuit && card.suit !== suitToFollow) { - return { error: "You must follow the suit!" }; + return { error: 'You must follow the suit!' }; } } @@ -154,7 +156,7 @@ class BiscaGame { this.table.playerCard = card; this.table.firstPlayerId = userId; this.currentTurn = this.getOpponentId(userId); - return { action: "next_turn" }; + return { action: 'next_turn' }; } else { this.table.opponentCard = card; return this.resolveTrick(); @@ -194,7 +196,7 @@ class BiscaGame { this.table = { playerCard: null, opponentCard: null, firstPlayerId: null }; if (this.players[winnerId].hand.length === 0) { - this.status = "finished"; + this.status = 'finished'; this.stopTimer(); const pIds = Object.keys(this.players); @@ -214,11 +216,11 @@ class BiscaGame { const finalLoser = isDraw ? null : gameWinnerId == p1Obj.id - ? p2Obj.id - : p1Obj.id; + ? p2Obj.id + : p1Obj.id; return { - action: "game_ended", + action: 'game_ended', trickResult, winnerId: finalWinner, loserId: finalLoser, @@ -231,7 +233,7 @@ class BiscaGame { }; } - return { action: "trick_resolved", trickResult }; + return { action: 'trick_resolved', trickResult }; } getStateForPlayer(userId) { @@ -277,4 +279,4 @@ class BiscaGame { } } -module.exports = BiscaGame; +export default BiscaGame; diff --git a/websockets/events/connection.js b/websockets/events/connection.js index b4f914b..dcc5f9c 100644 --- a/websockets/events/connection.js +++ b/websockets/events/connection.js @@ -1,4 +1,4 @@ -import { addUser, removeUser } from "../state/connection"; +import { addUser, removeUser } from '../state/connection.js'; const HEARTBEAT_INTERVAL = 30000; // 30 seconds const HEARTBEAT_TIMEOUT = 10000; // 10 seconds @@ -8,36 +8,36 @@ export const handleConnectionEvents = (io, socket) => { let heartbeatTimeout; const startHeartbeat = () => { heartbeatInterval = setInterval(() => { - socket.emit("heartbeat"); + socket.emit('heartbeat'); heartbeatTimeout = setTimeout(() => { console.log( - `[Heartbeat] No response from ${socket.id}, disconnecting...` + `[Heartbeat] No response from ${socket.id}, disconnecting...`, ); socket.disconnect(true); }, HEARTBEAT_TIMEOUT); }, HEARTBEAT_INTERVAL); }; - socket.on("heartbeat_ack", () => { + socket.on('heartbeat_ack', () => { clearTimeout(heartbeatTimeout); console.log(`[Heartbeat] Received pong from ${socket.id}`); }); - socket.on("join", (user) => { + socket.on('join', (user) => { addUser(socket, user); console.log(`[Connection] User ${user.name} has joined the server`); }); - socket.on("leave", () => { + socket.on('leave', () => { const user = removeUser(socket.id); if (user) { console.log(`[Connection] User ${user.name} has left the server`); } }); - socket.on("disconnect", () => { - console.log("Connection Lost:", socket.id); + socket.on('disconnect', () => { + console.log('Connection Lost:', socket.id); removeUser(socket.id); }); }; diff --git a/websockets/package.json b/websockets/package.json index 1dc9741..c5fbd36 100644 --- a/websockets/package.json +++ b/websockets/package.json @@ -2,6 +2,7 @@ "name": "websockets", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { "dev": "nodemon index.js", "start": "bun index.js " diff --git a/websockets/state/game.js b/websockets/state/game.js index 2a3f7d8..79a1d6e 100644 --- a/websockets/state/game.js +++ b/websockets/state/game.js @@ -1,4 +1,4 @@ -const BiscaGame = require("../classes/BiscaGame"); +import BiscaGame from '../classes/BiscaGame.js'; const activeGames = new Map(); @@ -18,16 +18,12 @@ const getGame = (id) => { }; const removeGame = (id) => { -const game = activeGames.get(id); + const game = activeGames.get(id); if (game) { - game.stopTimer(); - activeGames.delete(id); - console.log(`[Game State] Game removed: ${id}`); + game.stopTimer(); + activeGames.delete(id); + console.log(`[Game State] Game removed: ${id}`); } }; -module.exports = { - createGame, - getGame, - removeGame, -}; +export { createGame, getGame, removeGame }; From e89b60c844dc74b9ffa6e046583a49f7898aff69 Mon Sep 17 00:00:00 2001 From: Edd Date: Fri, 2 Jan 2026 16:17:55 +0000 Subject: [PATCH 2/2] Admin platform, with users, transactions, games and admin creation --- api/app/Http/Controllers/GameController.php | 3 - .../Controllers/TransactionController.php | 31 + api/app/Http/Controllers/UserController.php | 6 +- api/app/Policies/CoinTransactionPolicy.php | 18 + api/routes/api.php | 2 + api/sample-requests.http | 23 +- frontend/src/App.vue | 6 +- frontend/src/pages/admin/AdminPage.vue | 752 ++++++++++++++++++ frontend/src/pages/home/HomePage.vue | 32 + frontend/src/pages/user/UserPage.vue | 15 +- frontend/src/router/index.js | 16 +- frontend/src/stores/api.js | 64 +- frontend/src/stores/auth.js | 13 +- frontend/src/stores/user.js | 2 + 14 files changed, 959 insertions(+), 24 deletions(-) create mode 100644 api/app/Http/Controllers/TransactionController.php create mode 100644 api/app/Policies/CoinTransactionPolicy.php create mode 100644 frontend/src/pages/admin/AdminPage.vue diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 5590763..22f068a 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -17,15 +17,12 @@ class GameController extends Controller $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); diff --git a/api/app/Http/Controllers/TransactionController.php b/api/app/Http/Controllers/TransactionController.php new file mode 100644 index 0000000..41b62c8 --- /dev/null +++ b/api/app/Http/Controllers/TransactionController.php @@ -0,0 +1,31 @@ +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); + } +} \ No newline at end of file diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index 2fa423c..98a063d 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -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', diff --git a/api/app/Policies/CoinTransactionPolicy.php b/api/app/Policies/CoinTransactionPolicy.php new file mode 100644 index 0000000..6b3ce71 --- /dev/null +++ b/api/app/Policies/CoinTransactionPolicy.php @@ -0,0 +1,18 @@ +type === 'A' ? true : false; + } +} diff --git a/api/routes/api.php b/api/routes/api.php index 8e60833..8ff3405 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -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 () { @@ -71,6 +72,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']); diff --git a/api/sample-requests.http b/api/sample-requests.http index 1172b90..4311fae 100644 --- a/api/sample-requests.http +++ b/api/sample-requests.http @@ -5,7 +5,7 @@ Content-Type: application/json Accept: application/json { - "email": "pa@mail.pt", + "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}} \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 562dc55..7e26030 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,8 +3,7 @@