diff --git a/api/app/Http/Controllers/AuthController.php b/api/app/Http/Controllers/AuthController.php index f60cabd..afd66e9 100644 --- a/api/app/Http/Controllers/AuthController.php +++ b/api/app/Http/Controllers/AuthController.php @@ -2,11 +2,11 @@ namespace App\Http\Controllers; +use App\Http\Requests\RegisterRequest; +use App\Models\User; 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 { @@ -24,6 +24,13 @@ class AuthController extends Controller } $user = Auth::user(); + + if ($user->blocked) { + Auth::logout(); + + return response()->json(['message' => 'Your account is blocked. Please contact support.'], 403); + } + $token = $user->createToken('auth-token')->plainTextToken; return response()->json([ @@ -53,8 +60,8 @@ class AuthController extends Controller 'name' => $request->name, 'password' => bcrypt($request->password), 'photo_avatar_filename' => $request->photo, - 'type' => 'P', - 'blocked' => false, + 'type' => 'P', + 'blocked' => false, 'coins_balance' => 10, ]); diff --git a/api/app/Http/Controllers/GameController.php b/api/app/Http/Controllers/GameController.php index 1a245fc..886243e 100644 --- a/api/app/Http/Controllers/GameController.php +++ b/api/app/Http/Controllers/GameController.php @@ -14,7 +14,20 @@ 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); @@ -43,6 +56,8 @@ class GameController extends Controller public function store(Request $request) { + $this->authorize('create', Game::class); + $validated = $request->validate([ "type" => "required|in:3,9", ]); @@ -81,6 +96,8 @@ class GameController extends Controller public function join(Game $game) { + $this->authorize('join', $game); + $user = Auth::user(); if ($game->player1_user_id == $user->id) { @@ -107,12 +124,16 @@ class GameController extends Controller public function show(Game $game) { + $this->authorize('view', $game); + $game->load(["player1", "player2", "winner"]); return response()->json($game); } public function update(Request $request, Game $game) { + $this->authorize('update', $game); + if ($game->status == "Ended") { return response()->json(["message" => "Game already ended"], 400); } @@ -135,4 +156,25 @@ class GameController extends Controller return response()->json($game); } + + 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 + : $game->player1_user_id, + 'loser_user_id' => $user->id, + 'ended_at' => now(), + ]); + + return response()->json([ + 'message' => 'You have resigned from the game', + 'game' => $game->fresh(), + ]); + } } diff --git a/api/app/Http/Controllers/UserController.php b/api/app/Http/Controllers/UserController.php index 32bc653..7825179 100644 --- a/api/app/Http/Controllers/UserController.php +++ b/api/app/Http/Controllers/UserController.php @@ -17,6 +17,8 @@ class UserController extends Controller */ public function index(Request $request) { + $this->authorize('viewAny', User::class); + $query = User::query(); // Filtros úteis para o Backoffice @@ -41,6 +43,8 @@ class UserController extends Controller */ public function store(Request $request) { + $this->authorize('create', User::class); + $validated = $request->validate([ 'name' => 'required|string|max:255', 'nickname' => 'required|string|max:20|unique:users,nickname', @@ -73,7 +77,11 @@ class UserController extends Controller public function show(User $user) { $currentUser = Auth::user(); - if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { + + // Admins and owners get full profile, others get limited view + if ($currentUser->type === 'A' || $currentUser->id === $user->id) { + return response()->json($user); + } else { return response()->json([ 'id' => $user->id, 'name' => $user->name, @@ -82,8 +90,6 @@ class UserController extends Controller 'type' => $user->type, ]); } - - return response()->json($user); } /** @@ -92,12 +98,8 @@ class UserController extends Controller */ public function update(Request $request, User $user) { - $currentUser = Auth::user(); - - if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { - return response()->json(['message' => 'Unauthorized'], 403); - } - + $this->authorize('update', $user); + $validated = $request->validate([ 'name' => 'sometimes|string|max:255', 'nickname' => [ @@ -112,16 +114,8 @@ class UserController extends Controller 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'); @@ -139,24 +133,24 @@ class UserController extends Controller */ public function destroy(User $user) { - $currentUser = Auth::user(); + $this->authorize('delete', $user); - if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { - return response()->json(['message' => 'Unauthorized'], 403); + // Check if user has transactions or games - if so, soft delete + $hasTransactions = $user->transactions()->exists(); + $hasGames = Game::where(function ($q) use ($user) { + $q->where('player1_user_id', $user->id) + ->orWhere('player2_user_id', $user->id); + })->exists(); + + if ($hasTransactions || $hasGames) { + $user->delete(); // Soft delete + $message = 'User account deactivated (soft-deleted due to transaction/game history)'; + } else { + $user->forceDelete(); // Hard delete + $message = 'User permanently deleted'; } - 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']); + return response()->json(['message' => $message]); } /** @@ -165,11 +159,7 @@ class UserController extends Controller */ public function getMatches(Request $request, User $user) { - $currentUser = Auth::user(); - - if ($currentUser->id !== $user->id && $currentUser->type !== 'A') { - return response()->json(['message' => 'Unauthorized'], 403); - } + $this->authorize('view', $user); $matches = Game::query() ->where(function ($q) use ($user) { @@ -184,4 +174,27 @@ class UserController extends Controller return response()->json($matches); } + + public function block(User $user) + { + $this->authorize('block', $user); + + $user->blocked = true; + $user->save(); + + // Revoke all active tokens for immediate effect + $user->tokens()->delete(); + + return response()->json(['message' => 'User blocked successfully']); + } + + public function unblock(User $user) + { + $this->authorize('unblock', $user); + + $user->blocked = false; + $user->save(); + + return response()->json(['message' => 'User unblocked successfully']); + } } diff --git a/api/app/Policies/GamePolicy.php b/api/app/Policies/GamePolicy.php new file mode 100644 index 0000000..157f98c --- /dev/null +++ b/api/app/Policies/GamePolicy.php @@ -0,0 +1,126 @@ +type === 'A') { + return true; + } + + if ( + $game->player1_user_id === $user->id || + $game->player2_user_id === $user->id + ) { + return true; + } + + return false; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + if ($user->type === 'A') { + return false; + } + + return true; + } + + public function join(User $user, Game $game): bool + { + if ($user->type === 'A') { + return false; + } + + if ( + $game->status !== 'waiting' || + $game->player1_user_id === $user->id + ) { + return false; + } + + return true; + } + + public function resign(User $user, Game $game): bool + { + if ($user->type === 'A') { + return false; + } + + if ( + $game->status !== 'ongoing' || + ($game->player1_user_id !== $user->id && + $game->player2_user_id !== $user->id) + ) { + return false; + } + + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Game $game): bool + { + if ($user->type === 'A') { + return false; + } + + if ( + $game->status !== 'ongoing' || + ($game->player1_user_id !== $user->id && + $game->player2_user_id !== $user->id) + ) { + return false; + } + + return true; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Game $game): bool + { + // Only admins can delete games + return $user->type === 'A'; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Game $game): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Game $game): bool + { + return false; + } +} diff --git a/api/app/Policies/UserPolicy.php b/api/app/Policies/UserPolicy.php new file mode 100644 index 0000000..548f1c7 --- /dev/null +++ b/api/app/Policies/UserPolicy.php @@ -0,0 +1,74 @@ +type === 'A'; + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, User $model): bool + { + return $user->id === $model->id || $user->type === 'A'; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->type === 'A'; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, User $model): bool + { + return $user->id === $model->id; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, User $model): bool + { + return ($user->type === 'A' && $user->id !== $model->id) || ($user->type === 'P' && $user->id === $model->id); + } + + public function block(User $user, User $model): bool + { + return $user->type === 'A' && $user->id !== $model->id && $model->type === 'P'; + } + + public function unblock(User $user, User $model): bool + { + return $user->type === 'A' && $user->id !== $model->id && $model->type === 'P'; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, User $model): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, User $model): bool + { + return false; + } +} diff --git a/frontend/.env.example b/frontend/.env.example index 49c40b8..94a7886 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,2 +1,2 @@ VITE_API_DOMAIN=localhost:8000 -VITE_WS_CONNECTION=ws://localhost:3000 \ No newline at end of file +VITE_WS_CONNECTION=ws://localhost:3001 diff --git a/frontend/src/components/layout/NavBar.vue b/frontend/src/components/layout/NavBar.vue index f967e44..015f987 100644 --- a/frontend/src/components/layout/NavBar.vue +++ b/frontend/src/components/layout/NavBar.vue @@ -47,7 +47,6 @@ import { } from '@/components/ui/navigation-menu' import router from '@/router'; - const emits = defineEmits(['logout']) const { userLoggedIn } = defineProps(['userLoggedIn']) @@ -68,6 +67,7 @@ const logoutClickHandler = () => { } emits('logout') - router.push('/login') + + router.push({ name: 'home' }) } \ No newline at end of file diff --git a/frontend/src/pages/home/HomePage.vue b/frontend/src/pages/home/HomePage.vue index 290f804..468ea96 100644 --- a/frontend/src/pages/home/HomePage.vue +++ b/frontend/src/pages/home/HomePage.vue @@ -239,4 +239,4 @@ onMounted(async () => { - \ No newline at end of file + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index ad9abcc..4d9485b 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,14 +2,14 @@ import HomePage from '@/pages/home/HomePage.vue' import LoginPage from '@/pages/login/LoginPage.vue' import LaravelPage from '@/pages/testing/LaravelPage.vue' import WebsocketsPage from '@/pages/testing/WebsocketsPage.vue' -import UserPage from '@/pages/user/UserPage.vue' -import { createRouter, createWebHistory } from 'vue-router' -import { toast } from 'vue-sonner' import TestAnimations from '@/pages/TestAnimations.vue' import TestDealing from '@/pages/TestDealing.vue' import TestAllAnimations from '@/pages/TestAllAnimations.vue' import TestGameBoard from '@/pages/TestGameBoard.vue' +import UserPage from '@/pages/user/UserPage.vue' import SinglePlayerGamePage from '@/pages/game/SinglePlayerGamePage.vue' +import { createRouter, createWebHistory } from 'vue-router' +import { toast } from 'vue-sonner' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -24,7 +24,21 @@ const router = createRouter({ name: 'login', component: LoginPage, }, - { + { + path: '/game/3', + name: 'bisca3', + component: SinglePlayerGamePage, + props: { gameType: 3 }, + meta: { requiresAuth: true }, + }, + { + path: '/game/9', + name: 'bisca9', + component: SinglePlayerGamePage, + props: { gameType: 9 }, + meta: { requiresAuth: true }, + }, + { path: '/game/3', name: 'bisca3', component: SinglePlayerGamePage,