diff --git a/app/Http/Controllers/RoomPlayerKickController.php b/app/Http/Controllers/RoomPlayerKickController.php index ea790a7..db5ddcd 100644 --- a/app/Http/Controllers/RoomPlayerKickController.php +++ b/app/Http/Controllers/RoomPlayerKickController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers; use App\Http\Requests\KickRoomPlayerRequest; use App\Models\Player; use App\Models\Room; -use App\Models\User; use App\Services\Rooms\RoomParticipationService; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; @@ -18,11 +17,12 @@ class RoomPlayerKickController extends Controller public function __invoke(KickRoomPlayerRequest $request, Room $room, Player $player): RedirectResponse { - $user = $request->user(); - - abort_unless($user instanceof User, 403); - - $this->roomParticipation->kick($room, $player, $user); + $this->roomParticipation->kick( + $room, + $player, + $request->user(), + $request->currentPlayer($room), + ); Inertia::flash('toast', [ 'type' => 'success', diff --git a/app/Http/Requests/KickRoomPlayerRequest.php b/app/Http/Requests/KickRoomPlayerRequest.php index 3c68847..38b7193 100644 --- a/app/Http/Requests/KickRoomPlayerRequest.php +++ b/app/Http/Requests/KickRoomPlayerRequest.php @@ -14,13 +14,41 @@ class KickRoomPlayerRequest extends FormRequest public function authorize(): bool { $room = $this->route('room'); - $player = $this->route('player'); + $targetPlayer = $this->route('player'); - return $room instanceof Room - && $player instanceof Player - && $this->user()?->id === $room->owner_id - && $player->room_id === $room->id - && $player->user_id !== $room->owner_id; + if ( + ! $room instanceof Room + || ! $targetPlayer instanceof Player + || $targetPlayer->room_id !== $room->id + || $targetPlayer->is_moderator + || ($room->owner_id !== null && $targetPlayer->user_id === $room->owner_id) + ) { + return false; + } + + $user = $this->user(); + + if ($user !== null && $user->id === $room->owner_id) { + return true; + } + + $currentPlayer = $this->currentPlayer($room); + + return ! $room->isClosed() + && $currentPlayer !== null + && $currentPlayer->id !== $targetPlayer->id + && ($currentPlayer->is_moderator || $room->allow_participants_to_manage_rounds); + } + + public function currentPlayer(Room $room): ?Player + { + $playerId = $this->session()->get("room_players.{$room->id}"); + + if (! is_numeric($playerId)) { + return null; + } + + return $room->activePlayers()->find((int) $playerId); } /** diff --git a/app/Services/Rooms/RoomParticipationService.php b/app/Services/Rooms/RoomParticipationService.php index 91a4e4a..e2189e7 100644 --- a/app/Services/Rooms/RoomParticipationService.php +++ b/app/Services/Rooms/RoomParticipationService.php @@ -180,20 +180,41 @@ class RoomParticipationService return $player; } - public function kick(Room $room, Player $player, User $user): void - { - $stateVersion = DB::transaction(function () use ($room, $player, $user): ?int { + public function kick( + Room $room, + Player $player, + ?User $user, + ?Player $currentPlayer, + ): void { + $stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): ?int { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); - abort_unless($lockedRoom->owner_id === $user->id, 403); + $lockedCurrentPlayer = $currentPlayer === null + ? null + : $lockedRoom->activePlayers() + ->lockForUpdate() + ->find($currentPlayer->id); + $isOwner = $user !== null && $lockedRoom->owner_id === $user->id; + $participantCanManage = ! $lockedRoom->isClosed() + && $lockedCurrentPlayer !== null + && ($lockedCurrentPlayer->is_moderator + || $lockedRoom->allow_participants_to_manage_rounds); + + abort_unless($isOwner || $participantCanManage, 403); $lockedPlayer = $lockedRoom->players() ->lockForUpdate() ->findOrFail($player->id); - abort_if($lockedPlayer->user_id === $lockedRoom->owner_id, 403); + abort_if( + $lockedPlayer->is_moderator + || ($lockedRoom->owner_id !== null + && $lockedPlayer->user_id === $lockedRoom->owner_id) + || $lockedCurrentPlayer?->id === $lockedPlayer->id, + 403, + ); if ($lockedPlayer->inactive_at !== null) { return null; diff --git a/resources/js/components/rooms/RoomExperience.vue b/resources/js/components/rooms/RoomExperience.vue index 0376512..803d0dd 100644 --- a/resources/js/components/rooms/RoomExperience.vue +++ b/resources/js/components/rooms/RoomExperience.vue @@ -67,7 +67,13 @@ const { :title="realtimeRoom.title" :join_code="realtimeRoom.join_code" :is_favorite="realtimeRoom.is_favorite" + :players="realtimeRoom.players" :can-favorite="canFavorite" + :can-manage-participants=" + realtimeCurrentPlayer.is_moderator || + realtimeRoom.allow_participants_to_manage_rounds + " + :current-player-id="realtimeCurrentPlayer.id" :embedded="embedded" :player-name="realtimeCurrentPlayer.name" v-model:presenter-mode="presenterMode" diff --git a/resources/js/components/rooms/RoomHeader.vue b/resources/js/components/rooms/RoomHeader.vue index dc87aa3..1d8cba5 100644 --- a/resources/js/components/rooms/RoomHeader.vue +++ b/resources/js/components/rooms/RoomHeader.vue @@ -1,17 +1,65 @@ diff --git a/resources/js/components/ui/dialog/DialogContent.vue b/resources/js/components/ui/dialog/DialogContent.vue index 05172f0..3e89b0c 100644 --- a/resources/js/components/ui/dialog/DialogContent.vue +++ b/resources/js/components/ui/dialog/DialogContent.vue @@ -16,19 +16,26 @@ defineOptions({ inheritAttrs: false, }) -const props = withDefaults(defineProps(), { - showCloseButton: true, -}) +const props = withDefaults( + defineProps(), + { + showCloseButton: true, + }, +) const emits = defineEmits() -const delegatedProps = reactiveOmit(props, "class") +const delegatedProps = reactiveOmit(props, "class", "overlayClass", "showCloseButton") const forwarded = useForwardPropsEmits(delegatedProps, emits)