Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b485e19c9d | ||
|
|
5270e3c093 | ||
|
|
5473a4cc69 | ||
|
|
cf8f93d984 | ||
|
|
7686aff546 | ||
|
|
af41990807 | ||
|
|
20f53197ec | ||
|
|
7c6d8ed446 | ||
|
|
daa72ce4c6 | ||
|
|
117f007b83 | ||
|
|
69705ce61c
|
||
|
|
8472263f91 | ||
|
|
d7a6862dcb | ||
|
|
a335e713ee | ||
|
|
1e5792567f |
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"postman.settings.dotenv-detection-notification-visibility": false
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ namespace App\Http\Controllers;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Http\Requests\RegisterRequest;
|
||||||
|
|
||||||
class AuthController extends Controller
|
class AuthController extends Controller
|
||||||
{
|
{
|
||||||
@@ -38,4 +40,30 @@ class AuthController extends Controller
|
|||||||
'message' => 'Logged out successfully',
|
'message' => 'Logged out successfully',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function register(RegisterRequest $request)
|
||||||
|
{
|
||||||
|
if (User::where('email', $request->email)->exists()) {
|
||||||
|
return response()->json(['message' => 'Email already registered'], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'email' => $request->email,
|
||||||
|
'nickname' => $request->nickname,
|
||||||
|
'name' => $request->name,
|
||||||
|
'password' => bcrypt($request->password),
|
||||||
|
'photo_avatar_filename' => $request->photo,
|
||||||
|
'type' => 'P',
|
||||||
|
'blocked' => false,
|
||||||
|
'coins_balance' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $user->createToken('auth-token')->plainTextToken;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'User registered successfully',
|
||||||
|
'user' => $user,
|
||||||
|
'token' => $token,
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Models\Game;
|
use App\Models\Game;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Requests\StoreGameRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use App\Models\MatchGame;
|
||||||
|
|
||||||
class GameController extends Controller
|
class GameController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = Game::query()->with(['winner']);
|
$query = Game::query()->with(['winner', 'player1', 'player2']);
|
||||||
|
|
||||||
if ($request->has('type') && in_array($request->type, ['3', '9'])) {
|
if ($request->has('type') && in_array($request->type, ['3', '9'])) {
|
||||||
$query->where('type', $request->type);
|
$query->where('type', $request->type);
|
||||||
@@ -22,66 +22,117 @@ class GameController extends Controller
|
|||||||
$query->where('status', $request->status);
|
$query->where('status', $request->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
$query->orderBy('began_at', 'desc');
|
||||||
$sortField = $request->input('sort_by', 'began_at');
|
|
||||||
$sortDirection = $request->input('sort_direction', 'desc');
|
|
||||||
|
|
||||||
$allowedSortFields = [
|
return response()->json($query->paginate(15));
|
||||||
'began_at',
|
|
||||||
'ended_at',
|
|
||||||
'total_time',
|
|
||||||
'type',
|
|
||||||
'status'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (in_array($sortField, $allowedSortFields)) {
|
|
||||||
$query->orderBy($sortField, $sortDirection === 'asc' ? 'asc' : 'desc');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination
|
|
||||||
$perPage = $request->input('per_page', 15);
|
|
||||||
$games = $query->paginate($perPage);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $games->items(),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $games->currentPage(),
|
|
||||||
'last_page' => $games->lastPage(),
|
|
||||||
'per_page' => $games->perPage(),
|
|
||||||
'total' => $games->total()
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
//
|
$validated = $request->validate([
|
||||||
|
'type' => 'required|in:3,9',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$activeMatch = MatchGame::where(function ($q) use ($user) {
|
||||||
|
$q->where('player1_user_id', $user->id)
|
||||||
|
->orWhere('player2_user_id', $user->id);
|
||||||
|
})->whereIn('status', ['Pending', 'Playing'])->exists();
|
||||||
|
|
||||||
|
$activeGame = Game::where(function ($q) use ($user) {
|
||||||
|
$q->where('player1_user_id', $user->id)
|
||||||
|
->orWhere('player2_user_id', $user->id);
|
||||||
|
})->whereIn('status', ['Pending', 'Playing'])->exists();
|
||||||
|
|
||||||
|
if ($activeMatch || $activeGame) {
|
||||||
|
return response()->json(['message' => 'You already have an active game or match.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$game = Game::create([
|
||||||
|
'type' => $validated['type'],
|
||||||
|
'status' => 'Pending',
|
||||||
|
'player1_user_id' => $user->id,
|
||||||
|
'player2_user_id' => $user->id,
|
||||||
|
'winner_user_id' => $user->id,
|
||||||
|
'loser_user_id' => $user->id,
|
||||||
|
'began_at' => now(),
|
||||||
|
'player1_points' => 0,
|
||||||
|
'player2_points' => 0,
|
||||||
|
'custom' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Multiplayer game room created',
|
||||||
|
'game' => $game
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function join(Game $game)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($game->player1_user_id == $user->id) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Waiting for opponent...',
|
||||||
|
'game' => $game
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($game->status !== 'Pending') {
|
||||||
|
return response()->json(['message' => 'Game is full or already started'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeGame = Game::where(function ($query) use ($user) {
|
||||||
|
$query->where('player1_user_id', $user->id)
|
||||||
|
->orWhere('player2_user_id', $user->id);
|
||||||
|
})
|
||||||
|
->whereIn('status', ['Pending', 'Playing'])
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if ($activeGame) {
|
||||||
|
return response()->json(['message' => 'You are already in another active game.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$game->update([
|
||||||
|
'status' => 'Playing',
|
||||||
|
'player2_user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Joined successfully! Game starts now.',
|
||||||
|
'game' => $game
|
||||||
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(Game $game)
|
public function show(Game $game)
|
||||||
{
|
{
|
||||||
//
|
$game->load(['player1', 'player2', 'winner']);
|
||||||
|
return response()->json($game);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, Game $game)
|
public function update(Request $request, Game $game)
|
||||||
{
|
{
|
||||||
//
|
if ($game->status == 'Ended') {
|
||||||
|
return response()->json(['message' => 'Game already ended'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
$data = $request->validate([
|
||||||
* Remove the specified resource from storage.
|
'status' => 'in:Playing,Ended,Interrupted',
|
||||||
*/
|
'winner_user_id' => 'exists:users,id',
|
||||||
public function destroy(Game $game)
|
'loser_user_id' => 'exists:users,id',
|
||||||
{
|
'player1_points' => 'integer',
|
||||||
//
|
'player2_points' => 'integer',
|
||||||
|
'is_draw' => 'boolean',
|
||||||
|
'total_time' => 'numeric'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isset($data['status']) && $data['status'] === 'Ended') {
|
||||||
|
$data['ended_at'] = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$game->update($data);
|
||||||
|
|
||||||
|
return response()->json($game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\MatchGame;
|
||||||
|
use App\Models\Game;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Requests\StoreMatchRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class MatchController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = MatchGame::query()->with(['winner', 'player1', 'player2']);
|
||||||
|
|
||||||
|
if ($request->has('type')) {
|
||||||
|
$query->where('type', $request->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('status')) {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->orderBy('began_at', 'desc');
|
||||||
|
|
||||||
|
return response()->json($query->paginate(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreMatchRequest $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$activeMatch = MatchGame::where(function ($q) use ($user) {
|
||||||
|
$q->where('player1_user_id', $user->id)
|
||||||
|
->orWhere('player2_user_id', $user->id);
|
||||||
|
})->whereIn('status', ['Pending', 'Playing'])->exists();
|
||||||
|
|
||||||
|
$activeGame = Game::where(function ($q) use ($user) {
|
||||||
|
$q->where('player1_user_id', $user->id)
|
||||||
|
->orWhere('player2_user_id', $user->id);
|
||||||
|
})->whereIn('status', ['Pending', 'Playing'])->exists();
|
||||||
|
|
||||||
|
if ($activeMatch || $activeGame) {
|
||||||
|
return response()->json(['message' => 'You already have an active game or match.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$match = MatchGame::create([
|
||||||
|
'type' => $validated['type'],
|
||||||
|
'status' => 'Pending',
|
||||||
|
'player1_user_id' => $user->id,
|
||||||
|
'player2_user_id' => $user->id,
|
||||||
|
'winner_user_id' => $user->id,
|
||||||
|
'loser_user_id' => $user->id,
|
||||||
|
'stake' => $validated['stake'],
|
||||||
|
'began_at' => now(),
|
||||||
|
'player1_marks' => 0,
|
||||||
|
'player2_marks' => 0,
|
||||||
|
'player1_points' => 0,
|
||||||
|
'player2_points' => 0,
|
||||||
|
'custom' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Match created successfully',
|
||||||
|
'match' => $match
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(MatchGame $match)
|
||||||
|
{
|
||||||
|
$match->load(['player1', 'player2', 'winner']);
|
||||||
|
return response()->json($match);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function join(MatchGame $match)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($match->player1_user_id == $user->id) {
|
||||||
|
return response()->json(['message' => 'Waiting for opponent...', 'match' => $match]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($match->status !== 'Pending') {
|
||||||
|
return response()->json(['message' => 'Match is full or started'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeMatch = MatchGame::where(function ($q) use ($user) {
|
||||||
|
$q->where('player1_user_id', $user->id)
|
||||||
|
->orWhere('player2_user_id', $user->id);
|
||||||
|
})->whereIn('status', ['Pending', 'Playing'])->exists();
|
||||||
|
|
||||||
|
if ($activeMatch) {
|
||||||
|
return response()->json(['message' => 'You are already in another active match.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$match->update([
|
||||||
|
'status' => 'Playing',
|
||||||
|
'player2_user_id' => $user->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Joined match successfully!',
|
||||||
|
'match' => $match
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, MatchGame $match)
|
||||||
|
{
|
||||||
|
if ($match->status == 'Ended') {
|
||||||
|
return response()->json(['message' => 'Match already ended'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'status' => 'in:Playing,Ended,Interrupted',
|
||||||
|
'winner_user_id' => 'exists:users,id',
|
||||||
|
'loser_user_id' => 'exists:users,id',
|
||||||
|
'player1_marks' => 'integer',
|
||||||
|
'player2_marks' => 'integer',
|
||||||
|
'player1_points' => 'integer',
|
||||||
|
'player2_points' => 'integer',
|
||||||
|
'total_time' => 'numeric'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isset($data['status']) && $data['status'] === 'Ended') {
|
||||||
|
$data['ended_at'] = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$match->update($data);
|
||||||
|
|
||||||
|
return response()->json($match);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Game;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* GET /api/users
|
||||||
|
* Lista todos os utilizadores (Apenas para Admins)
|
||||||
|
*/
|
||||||
|
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 . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($query->paginate(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users/me
|
||||||
|
* Retorna o perfil do utilizador autenticado atual.
|
||||||
|
* Usado pelo VueJS para saber quem está logado.
|
||||||
|
*/
|
||||||
|
public function me(Request $request)
|
||||||
|
{
|
||||||
|
return response()->json($request->user());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users/{user}
|
||||||
|
* Mostra um perfil específico.
|
||||||
|
*/
|
||||||
|
public function show(User $user)
|
||||||
|
{
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
if ($currentUser->id !== $user->id && $currentUser->type !== 'A') {
|
||||||
|
return response()->json([
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->name,
|
||||||
|
'nickname' => $user->nickname,
|
||||||
|
'photo_avatar_filename' => $user->photo_avatar_filename,
|
||||||
|
'type' => $user->type
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT/PATCH /api/users/{user}
|
||||||
|
* Atualizar perfil (Nome, Nickname, Password, Foto)
|
||||||
|
*/
|
||||||
|
public function update(Request $request, User $user)
|
||||||
|
{
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
|
||||||
|
if ($currentUser->id !== $user->id && $currentUser->type !== 'A') {
|
||||||
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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)],
|
||||||
|
'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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lógica de Upload de Foto (Exemplo Básico)
|
||||||
|
// if ($request->hasFile('photo_avatar')) {
|
||||||
|
// $path = $request->file('photo_avatar')->store('avatars', 'public');
|
||||||
|
// $validated['photo_avatar_filename'] = basename($path);
|
||||||
|
// }
|
||||||
|
|
||||||
|
$user->update($validated);
|
||||||
|
|
||||||
|
return response()->json($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/users/{user}
|
||||||
|
* Apagar conta (Soft Delete)
|
||||||
|
*/
|
||||||
|
public function destroy(User $user)
|
||||||
|
{
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
|
||||||
|
if ($currentUser->id !== $user->id && $currentUser->type !== 'A') {
|
||||||
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentUser->id === $user->id && $currentUser->type === 'A') {
|
||||||
|
if ($user->type === 'A') {
|
||||||
|
return response()->json(['message' => 'Admins cannot delete their own account'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
return response()->json(['message' => 'User deleted successfully']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/users/{user}/matches
|
||||||
|
* Histórico de Jogos Multiplayer do Utilizador
|
||||||
|
*/
|
||||||
|
public function getMatches(Request $request, User $user)
|
||||||
|
{
|
||||||
|
$currentUser = Auth::user();
|
||||||
|
|
||||||
|
if ($currentUser->id !== $user->id && $currentUser->type !== 'A') {
|
||||||
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = Game::query()
|
||||||
|
->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')
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return response()->json($matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class RegisterRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => 'required|email|unique:users,email',
|
||||||
|
'nickname' => 'required|string|max:255',
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'password' => 'required|string|min:3',
|
||||||
|
'photo' => 'nullable|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class StoreGameRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
// Só utilizadores autenticados podem criar jogos
|
||||||
|
return Auth::check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Agora validamos diretamente '3' ou '9' para bater certo com a BD
|
||||||
|
'type' => 'required|in:3,9',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mensagens personalizadas (Opcional)
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type.required' => 'You must choose a game type (Bisca de 3 or 9).',
|
||||||
|
'type.in' => 'Invalid game type. Choose 3 or 9.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class StoreMatchRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return Auth::check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'required|in:3,9',
|
||||||
|
'stake' => 'required|integer|min:1',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,45 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Http\Requests\StoreGameRequest;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
|
||||||
class Game extends Model
|
class Game extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'player1_user_id',
|
||||||
|
'player2_user_id',
|
||||||
|
'winner_user_id',
|
||||||
|
'loser_user_id',
|
||||||
|
'began_at',
|
||||||
|
'ended_at',
|
||||||
|
'total_time',
|
||||||
|
'player1_points',
|
||||||
|
'player2_points',
|
||||||
|
'custom',
|
||||||
|
'is_draw',
|
||||||
|
'match_id'
|
||||||
|
];
|
||||||
|
|
||||||
public function winner()
|
public function winner()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, "winner_user_id");
|
return $this->belongsTo(User::class, "winner_user_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function player1()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'player1_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function player2()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'player2_user_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class MatchGame extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'matches';
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'player1_user_id',
|
||||||
|
'player2_user_id',
|
||||||
|
'winner_user_id',
|
||||||
|
'loser_user_id',
|
||||||
|
'began_at',
|
||||||
|
'ended_at',
|
||||||
|
'total_time',
|
||||||
|
'player1_marks',
|
||||||
|
'player2_marks',
|
||||||
|
'player1_points',
|
||||||
|
'player2_points',
|
||||||
|
'stake',
|
||||||
|
'custom'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'began_at' => 'datetime',
|
||||||
|
'ended_at' => 'datetime',
|
||||||
|
'player1_marks' => 'integer',
|
||||||
|
'player2_marks' => 'integer',
|
||||||
|
'player1_points' => 'integer',
|
||||||
|
'player2_points' => 'integer',
|
||||||
|
'stake' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function player1()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'player1_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function player2()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'player2_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function winner()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'winner_user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
@@ -11,7 +12,7 @@ use Laravel\Sanctum\HasApiTokens;
|
|||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable, HasApiTokens;
|
use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -22,6 +23,12 @@ class User extends Authenticatable
|
|||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
|
'nickname',
|
||||||
|
'photo_avatar_filename',
|
||||||
|
'type',
|
||||||
|
'blocked',
|
||||||
|
'coins_balance',
|
||||||
|
'custom',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +51,7 @@ class User extends Authenticatable
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'blocked' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/pint": "^1.26",
|
||||||
"laravel/sail": "^1.48",
|
"laravel/sail": "^1.48",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
|
|||||||
Generated
+15
-14
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6ad8f0aa2267878e2374cd4ba4042946",
|
"content-hash": "353b4e6bf34bd12906db0059bb3a9207",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -6575,16 +6575,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/pint",
|
"name": "laravel/pint",
|
||||||
"version": "v1.25.1",
|
"version": "v1.26.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/pint.git",
|
"url": "https://github.com/laravel/pint.git",
|
||||||
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
|
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||||
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6595,13 +6595,13 @@
|
|||||||
"php": "^8.2.0"
|
"php": "^8.2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"friendsofphp/php-cs-fixer": "^3.87.2",
|
"friendsofphp/php-cs-fixer": "^3.90.0",
|
||||||
"illuminate/view": "^11.46.0",
|
"illuminate/view": "^12.40.1",
|
||||||
"larastan/larastan": "^3.7.1",
|
"larastan/larastan": "^3.8.0",
|
||||||
"laravel-zero/framework": "^11.45.0",
|
"laravel-zero/framework": "^12.0.4",
|
||||||
"mockery/mockery": "^1.6.12",
|
"mockery/mockery": "^1.6.12",
|
||||||
"nunomaduro/termwind": "^2.3.1",
|
"nunomaduro/termwind": "^2.3.3",
|
||||||
"pestphp/pest": "^2.36.0"
|
"pestphp/pest": "^3.8.4"
|
||||||
},
|
},
|
||||||
"bin": [
|
"bin": [
|
||||||
"builds/pint"
|
"builds/pint"
|
||||||
@@ -6627,6 +6627,7 @@
|
|||||||
"description": "An opinionated code formatter for PHP.",
|
"description": "An opinionated code formatter for PHP.",
|
||||||
"homepage": "https://laravel.com",
|
"homepage": "https://laravel.com",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"dev",
|
||||||
"format",
|
"format",
|
||||||
"formatter",
|
"formatter",
|
||||||
"lint",
|
"lint",
|
||||||
@@ -6637,7 +6638,7 @@
|
|||||||
"issues": "https://github.com/laravel/pint/issues",
|
"issues": "https://github.com/laravel/pint/issues",
|
||||||
"source": "https://github.com/laravel/pint"
|
"source": "https://github.com/laravel/pint"
|
||||||
},
|
},
|
||||||
"time": "2025-09-19T02:57:12+00:00"
|
"time": "2025-11-25T21:15:52+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/sail",
|
"name": "laravel/sail",
|
||||||
@@ -9380,12 +9381,12 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": {},
|
"stability-flags": [],
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.13.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.13.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|||||||
+21
-2
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\GameController;
|
use App\Http\Controllers\GameController;
|
||||||
|
use App\Http\Controllers\UserController;
|
||||||
|
use App\Http\Controllers\MatchController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
|
||||||
|
# Authentication Routes
|
||||||
Route::post('/login', [AuthController::class, 'login']);
|
Route::post('/login', [AuthController::class, 'login']);
|
||||||
|
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
@@ -16,4 +18,21 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::post('logout', [AuthController::class, 'logout']);
|
Route::post('logout', [AuthController::class, 'logout']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::apiResource('games', GameController::class);
|
Route::post('/register', [AuthController::class, 'register']);
|
||||||
|
|
||||||
|
# Game Routes - Protected
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::apiResource('games', GameController::class);
|
||||||
|
Route::post('games', [GameController::class, 'store']);
|
||||||
|
Route::post('/games/{game}/resign', [GameController::class, 'resign']);
|
||||||
|
Route::get('/users/{user}/matches', [UserController::class, 'getMatches']);
|
||||||
|
Route::post('games/{game}/join', [GameController::class, 'join']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::get('matches', [MatchController::class, 'index']);
|
||||||
|
Route::post('matches', [MatchController::class, 'store']);
|
||||||
|
Route::get('matches/{match}', [MatchController::class, 'show']);
|
||||||
|
Route::post('matches/{match}/join', [MatchController::class, 'join']);
|
||||||
|
Route::put('matches/{match}', [MatchController::class, 'update']);
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { useApiStore } from './api'
|
||||||
|
|
||||||
|
export const useGameStore = defineStore('game', () => {
|
||||||
|
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PORT=
|
||||||
@@ -1,5 +1,46 @@
|
|||||||
|
import { addUser, removeUser } from "../state/connection";
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
||||||
|
const HEARTBEAT_TIMEOUT = 10000; // 10 seconds
|
||||||
|
|
||||||
export const handleConnectionEvents = (io, socket) => {
|
export const handleConnectionEvents = (io, socket) => {
|
||||||
socket.on("echo", (msg) => {
|
let heartbeatInterval;
|
||||||
socket.emit("echo", msg);
|
let heartbeatTimeout;
|
||||||
|
const startHeartbeat = () => {
|
||||||
|
heartbeatInterval = setInterval(() => {
|
||||||
|
socket.emit("heartbeat");
|
||||||
|
|
||||||
|
heartbeatTimeout = setTimeout(() => {
|
||||||
|
console.log(
|
||||||
|
`[Heartbeat] No response from ${socket.id}, disconnecting...`
|
||||||
|
);
|
||||||
|
socket.disconnect(true);
|
||||||
|
}, HEARTBEAT_TIMEOUT);
|
||||||
|
}, HEARTBEAT_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("heartbeat_ack", () => {
|
||||||
|
clearTimeout(heartbeatTimeout);
|
||||||
|
console.log(`[Heartbeat] Received pong from ${socket.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("join", (user) => {
|
||||||
|
addUser(socket, user);
|
||||||
|
console.log(`[Connection] User ${user.name} has joined the server`);
|
||||||
|
console.log(`[Connection] ${getUserCount()} users online`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("leave", () => {
|
||||||
|
const user = removeUser(socket.id);
|
||||||
|
if (user) {
|
||||||
|
console.log(`[Connection] User ${user.name} has left the server`);
|
||||||
|
console.log(`[Connection] ${getUserCount()} users online`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("Connection Lost:", socket.id);
|
||||||
|
removeUser(socket.id);
|
||||||
|
console.log(`[Connection] ${getUserCount()} users online`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const users = new Map();
|
||||||
|
|
||||||
|
export const addUser = (socket, user) => {
|
||||||
|
users.set(socket.id, user);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeUser = (socketID) => {
|
||||||
|
const userToDelete = { ...users.get(socketID) };
|
||||||
|
users.delete(socketID);
|
||||||
|
return userToDelete;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUser = (socketID) => {
|
||||||
|
return users.get(socketID);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserCount = () => {
|
||||||
|
return users.size;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user