Add room participant moderation

This commit is contained in:
2026-09-03 11:35:05 +02:00
parent e30d2e61aa
commit 2f05e56089
14 changed files with 453 additions and 15 deletions
+1
View File
@@ -6,6 +6,7 @@ enum RoomStateChange: string
{
case PlayerJoined = 'player_joined';
case PlayerUpdated = 'player_updated';
case PlayerKicked = 'player_kicked';
case VoteChanged = 'vote_changed';
case CardsRevealed = 'cards_revealed';
case VotesReset = 'votes_reset';
+9 -2
View File
@@ -21,6 +21,7 @@ class RoomStateChanged implements ShouldBroadcastNow
public readonly int $version,
public readonly RoomStateChange $change,
public readonly ?array $state = null,
public readonly ?int $affectedPlayerId = null,
) {}
public function broadcastOn(): Channel
@@ -34,14 +35,20 @@ class RoomStateChanged implements ShouldBroadcastNow
}
/**
* @return array{version: int, change: string, state: array<string, mixed>|null}
* @return array{version: int, change: string, state: array<string, mixed>|null, affected_player_id?: int}
*/
public function broadcastWith(): array
{
return [
$payload = [
'version' => $this->version,
'change' => $this->change->value,
'state' => $this->state,
];
if ($this->affectedPlayerId !== null) {
$payload['affected_player_id'] = $this->affectedPlayerId;
}
return $payload;
}
}
@@ -0,0 +1,34 @@
<?php
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;
class RoomPlayerKickController extends Controller
{
public function __construct(
private readonly RoomParticipationService $roomParticipation,
) {}
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);
Inertia::flash('toast', [
'type' => 'success',
'message' => "{$player->name} wurde aus dem Raum entfernt.",
]);
return back();
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use App\Models\Player;
use App\Models\Room;
use Illuminate\Foundation\Http\FormRequest;
class KickRoomPlayerRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$room = $this->route('room');
$player = $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;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<int, string>>
*/
public function rules(): array
{
return [];
}
}
@@ -19,6 +19,17 @@ class DashboardRoomResource extends JsonResource
'current_round' => $this->resource->current_round,
'players_count' => $this->resource->players_count,
'active_player_count' => $this->resource->active_player_count,
'players' => $this->resource->relationLoaded('activePlayers')
? $this->resource->activePlayers
->map(fn ($player): array => [
'id' => $player->id,
'name' => $player->name,
'is_moderator' => $player->is_moderator,
'is_owner' => $player->user_id === $this->resource->owner_id,
])
->values()
->all()
: [],
'last_activity_at' => $this->resource->last_activity_at?->toIso8601String(),
'closed_at' => $this->resource->closed_at?->toIso8601String(),
];
@@ -15,6 +15,12 @@ class DashboardRoomService
{
$ownedRooms = $user
->ownedRooms()
->with([
'activePlayers' => fn ($query) => $query
->select(['id', 'room_id', 'user_id', 'name', 'is_moderator', 'created_at'])
->orderByDesc('is_moderator')
->orderBy('created_at'),
])
->withCount('players')
->withCount(['activePlayers as active_player_count'])
->orderByRaw('closed_at IS NOT NULL')
@@ -179,4 +179,47 @@ class RoomParticipationService
return $player;
}
public function kick(Room $room, Player $player, User $user): void
{
$stateVersion = DB::transaction(function () use ($room, $player, $user): ?int {
$lockedRoom = Room::query()
->lockForUpdate()
->findOrFail($room->id);
abort_unless($lockedRoom->owner_id === $user->id, 403);
$lockedPlayer = $lockedRoom->players()
->lockForUpdate()
->findOrFail($player->id);
abort_if($lockedPlayer->user_id === $lockedRoom->owner_id, 403);
if ($lockedPlayer->inactive_at !== null) {
return null;
}
$lockedPlayer->update([
'inactive_at' => now(),
]);
$lockedRoom->update([
'last_activity_at' => now(),
'state_version' => $lockedRoom->state_version + 1,
]);
return $lockedRoom->state_version;
});
if ($stateVersion === null) {
return;
}
RoomStateChanged::dispatch(
$room->id,
$stateVersion,
RoomStateChange::PlayerKicked,
$this->realtimeState->for($room),
$player->id,
);
}
}