Compare commits

..
Author SHA1 Message Date
KZix b485e19c9d Added routes, endpoints to get all matches, get a specific match, create a match, join a match, and update the match 2025-12-18 20:29:10 +00:00
Edd 5270e3c093 Merge pull request 'feature/game-endpoints' (#62) from feature/game-endpoints into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/62
Reviewed-by: FernandoJVideira <[email protected]>
Reviewed-by: Edd <[email protected]>
2025-12-14 00:56:05 +00:00
KZix 5473a4cc69 removed crap i did, sorry frontends 2025-12-14 00:28:58 +00:00
KZix cf8f93d984 Added endpoits to create, join and update the game state, playability should work now 2025-12-14 00:26:41 +00:00
KZix 7686aff546 Added initial game logic 2025-12-12 00:05:37 +00:00
KZix af41990807 Merge pull request 'Added the register endpoint' (#61) from feature/register-api-endpoint into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/61
Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]>
Reviewed-by: FernandoJVideira <[email protected]>
2025-12-11 21:18:05 +00:00
KZix 20f53197ec Merge branch 'develop' into feature/register-api-endpoint 2025-12-11 21:17:58 +00:00
KZix 7c6d8ed446 Merge pull request 'feat: websocket init' (#60) from feature/websocket-init into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/60
Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]>
Reviewed-by: KZix <[email protected]>
2025-12-11 21:17:23 +00:00
KZix daa72ce4c6 Added the register endpoint 2025-12-11 21:13:56 +00:00
AfonsoCMSousa 117f007b83 Merge branch 'develop' into feature/websocket-init 2025-12-11 21:05:10 +00:00
FernandoJVideira 69705ce61c feat: websocket init 2025-12-11 21:02:02 +00:00
FernandoJVideira 8472263f91 Merge pull request 'feature/database-and-auth-api' (#59) from feature/database-and-auth-api into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/59
Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]>
Reviewed-by: FernandoJVideira <[email protected]>
2025-12-11 20:47:09 +00:00
KZix d7a6862dcb Added necessary stuff for authentication 2025-12-11 20:42:30 +00:00
FernandoJVideira a335e713ee Merge pull request 'Rebase Develop' (#58) from master into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/58
Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]>
Reviewed-by: KZix <[email protected]>
Reviewed-by: Edd <[email protected]>
2025-12-04 21:01:39 +00:00
FernandoJVideira 1e5792567f Merge pull request 'Added base project' (#57) from na/change-project-base into master
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/57
Reviewed-by: FernandoJVideira <[email protected]>
Reviewed-by: Edd <[email protected]>
Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]>
2025-12-04 20:56:11 +00:00
20 changed files with 725 additions and 74 deletions
+3
View File
@@ -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\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use App\Models\User;
use App\Http\Requests\RegisterRequest;
class AuthController extends Controller
{
@@ -38,4 +40,30 @@ class AuthController extends Controller
'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);
}
}
+103 -52
View File
@@ -4,15 +4,15 @@ namespace App\Http\Controllers;
use App\Models\Game;
use Illuminate\Http\Request;
use App\Http\Requests\StoreGameRequest;
use Illuminate\Support\Facades\Auth;
use App\Models\MatchGame;
class GameController extends Controller
{
/**
* Display a listing of the resource.
*/
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'])) {
$query->where('type', $request->type);
@@ -22,66 +22,117 @@ class GameController extends Controller
$query->where('status', $request->status);
}
// Sorting
$sortField = $request->input('sort_by', 'began_at');
$sortDirection = $request->input('sort_direction', 'desc');
$query->orderBy('began_at', 'desc');
$allowedSortFields = [
'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()
]
]);
return response()->json($query->paginate(15));
}
/**
* Store a newly created resource in storage.
*/
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)
{
//
$game->load(['player1', 'player2', 'winner']);
return response()->json($game);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Game $game)
{
//
}
if ($game->status == 'Ended') {
return response()->json(['message' => 'Game already ended'], 400);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Game $game)
{
//
$data = $request->validate([
'status' => 'in:Playing,Ended,Interrupted',
'winner_user_id' => 'exists:users,id',
'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);
}
}
+150
View File
@@ -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);
}
}
+32
View File
@@ -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',
];
}
}
+34
View File
@@ -3,11 +3,45 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Http\Requests\StoreGameRequest;
use Illuminate\Database\Eloquent\Factories\HasFactory;
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()
{
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');
}
}
+58
View File
@@ -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');
}
}
+9 -1
View File
@@ -4,6 +4,7 @@ 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;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
@@ -11,7 +12,7 @@ use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasApiTokens;
use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
/**
* The attributes that are mass assignable.
@@ -22,6 +23,12 @@ class User extends Authenticatable
'name',
'email',
'password',
'nickname',
'photo_avatar_filename',
'type',
'blocked',
'coins_balance',
'custom',
];
/**
@@ -44,6 +51,7 @@ class User extends Authenticatable
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'blocked' => 'boolean',
];
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/pint": "^1.26",
"laravel/sail": "^1.48",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
+15 -14
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6ad8f0aa2267878e2374cd4ba4042946",
"content-hash": "353b4e6bf34bd12906db0059bb3a9207",
"packages": [
{
"name": "brick/math",
@@ -6575,16 +6575,16 @@
},
{
"name": "laravel/pint",
"version": "v1.25.1",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
"shasum": ""
},
"require": {
@@ -6595,13 +6595,13 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.87.2",
"illuminate/view": "^11.46.0",
"larastan/larastan": "^3.7.1",
"laravel-zero/framework": "^11.45.0",
"friendsofphp/php-cs-fixer": "^3.90.0",
"illuminate/view": "^12.40.1",
"larastan/larastan": "^3.8.0",
"laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
"nunomaduro/termwind": "^2.3.3",
"pestphp/pest": "^3.8.4"
},
"bin": [
"builds/pint"
@@ -6627,6 +6627,7 @@
"description": "An opinionated code formatter for PHP.",
"homepage": "https://laravel.com",
"keywords": [
"dev",
"format",
"formatter",
"lint",
@@ -6637,7 +6638,7 @@
"issues": "https://github.com/laravel/pint/issues",
"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",
@@ -9380,12 +9381,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": {},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}
+1 -1
View File
@@ -6,7 +6,7 @@
"": {
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"axios": "^1.11.0",
"axios": "^1.13.2",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0",
+1 -1
View File
@@ -8,7 +8,7 @@
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"axios": "^1.11.0",
"axios": "^1.13.2",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"tailwindcss": "^4.0.0",
+21 -2
View File
@@ -2,11 +2,13 @@
use App\Http\Controllers\AuthController;
use App\Http\Controllers\GameController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\MatchController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
# Authentication Routes
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
@@ -16,4 +18,21 @@ Route::middleware('auth:sanctum')->group(function () {
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']);
});
+8
View File
@@ -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', () => {
})
+1
View File
@@ -0,0 +1 @@
PORT=
+43 -2
View File
@@ -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) => {
socket.on("echo", (msg) => {
socket.emit("echo", msg);
let heartbeatInterval;
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`);
});
};
+19
View File
@@ -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;
};