mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-13 19:12:10 +00:00
Handle authenticated room instance takeover
This commit is contained in:
@@ -7,6 +7,7 @@ enum RoomStateChange: string
|
||||
case PlayerJoined = 'player_joined';
|
||||
case PlayerUpdated = 'player_updated';
|
||||
case PlayerKicked = 'player_kicked';
|
||||
case PlayerInstanceReplaced = 'player_instance_replaced';
|
||||
case VoteChanged = 'vote_changed';
|
||||
case CardsRevealed = 'cards_revealed';
|
||||
case VotesReset = 'votes_reset';
|
||||
|
||||
@@ -22,6 +22,7 @@ class RoomStateChanged implements ShouldBroadcastNow
|
||||
public readonly RoomStateChange $change,
|
||||
public readonly ?array $state = null,
|
||||
public readonly ?int $affectedPlayerId = null,
|
||||
public readonly ?string $affectedInstanceId = null,
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): Channel
|
||||
@@ -35,7 +36,7 @@ class RoomStateChanged implements ShouldBroadcastNow
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{version: int, change: string, state: array<string, mixed>|null, affected_player_id?: int}
|
||||
* @return array{version: int, change: string, state: array<string, mixed>|null, affected_player_id?: int, affected_instance_id?: string}
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
@@ -49,6 +50,10 @@ class RoomStateChanged implements ShouldBroadcastNow
|
||||
$payload['affected_player_id'] = $this->affectedPlayerId;
|
||||
}
|
||||
|
||||
if ($this->affectedInstanceId !== null) {
|
||||
$payload['affected_instance_id'] = $this->affectedInstanceId;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Http\Resources\RoomResource;
|
||||
use App\Models\Room;
|
||||
use App\Services\Rooms\RoomPageService;
|
||||
use App\Services\Rooms\RoomParticipationService;
|
||||
use App\Services\Rooms\RoomPlayerInstanceService;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -16,6 +17,7 @@ class EmbeddedRoomController extends Controller
|
||||
public function __construct(
|
||||
private readonly RoomParticipationService $roomParticipation,
|
||||
private readonly RoomPageService $roomPage,
|
||||
private readonly RoomPlayerInstanceService $playerInstances,
|
||||
) {}
|
||||
|
||||
public function __invoke(ShowEmbeddedRoomRequest $request): Response
|
||||
@@ -33,6 +35,11 @@ class EmbeddedRoomController extends Controller
|
||||
$validated['player_name'],
|
||||
$request->session()->get("room_players.{$room->id}"),
|
||||
);
|
||||
$player = $this->roomParticipation->activateInstance(
|
||||
$room,
|
||||
$player,
|
||||
$this->playerInstances->for($request),
|
||||
);
|
||||
|
||||
$request->session()->put("room_players.{$room->id}", $player->id);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Player;
|
||||
use App\Models\Room;
|
||||
use App\Services\Rooms\RoomCreationService;
|
||||
use App\Services\Rooms\RoomPageService;
|
||||
use App\Services\Rooms\RoomPlayerInstanceService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -20,6 +21,7 @@ class RoomController extends Controller
|
||||
public function __construct(
|
||||
private readonly RoomCreationService $roomCreation,
|
||||
private readonly RoomPageService $roomPage,
|
||||
private readonly RoomPlayerInstanceService $playerInstances,
|
||||
) {}
|
||||
|
||||
public function create(Request $request): Response
|
||||
@@ -40,6 +42,9 @@ class RoomController extends Controller
|
||||
$validated,
|
||||
$request->user(),
|
||||
);
|
||||
$player->update([
|
||||
'active_instance_id' => $this->playerInstances->for($request),
|
||||
]);
|
||||
|
||||
$request->session()->put(
|
||||
"room_players.{$room->id}",
|
||||
|
||||
@@ -4,12 +4,14 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\JoinRoomRequest;
|
||||
use App\Services\Rooms\RoomParticipationService;
|
||||
use App\Services\Rooms\RoomPlayerInstanceService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class RoomPlayerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomParticipationService $roomParticipation,
|
||||
private readonly RoomPlayerInstanceService $playerInstances,
|
||||
) {}
|
||||
|
||||
public function join(JoinRoomRequest $request): RedirectResponse
|
||||
@@ -23,6 +25,11 @@ class RoomPlayerController extends Controller
|
||||
$request->user(),
|
||||
is_array($sessionPlayerIds) ? $sessionPlayerIds : [],
|
||||
);
|
||||
$player = $this->roomParticipation->activateInstance(
|
||||
$room,
|
||||
$player,
|
||||
$this->playerInstances->for($request),
|
||||
);
|
||||
|
||||
$request->session()->put(
|
||||
"room_players.{$room->id}",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Room;
|
||||
use App\Services\Rooms\RoomParticipationService;
|
||||
use App\Services\Rooms\RoomPlayerInstanceService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -10,6 +12,11 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureRoomParticipant
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoomParticipationService $roomParticipation,
|
||||
private readonly RoomPlayerInstanceService $playerInstances,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
@@ -44,13 +51,43 @@ class EnsureRoomParticipant
|
||||
}
|
||||
|
||||
$playerId = $request->session()->get($sessionKey);
|
||||
$hasSessionPlayer = is_numeric($playerId);
|
||||
$instanceId = $this->playerInstances->for($request);
|
||||
|
||||
$player = is_numeric($playerId)
|
||||
$player = $hasSessionPlayer
|
||||
? $room->players()->find((int) $playerId)
|
||||
: null;
|
||||
|
||||
if ($player === null && $playerId) {
|
||||
if ($player === null && $hasSessionPlayer) {
|
||||
$request->session()->forget($sessionKey);
|
||||
|
||||
return to_route('room.join', [
|
||||
'join_code' => $room->join_code,
|
||||
])->withErrors([
|
||||
'join_code' => 'Diese Spieler-Instanz ist nicht mehr aktiv.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
$player !== null
|
||||
&& $player->active_instance_id !== null
|
||||
&& $player->active_instance_id !== $instanceId
|
||||
) {
|
||||
$request->session()->forget($sessionKey);
|
||||
|
||||
return to_route('room.join', [
|
||||
'join_code' => $room->join_code,
|
||||
])->withErrors([
|
||||
'join_code' => 'Dein Account ist diesem Raum in einer anderen Instanz beigetreten.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($player !== null) {
|
||||
$player = $this->roomParticipation->activateInstance(
|
||||
$room,
|
||||
$player,
|
||||
$instanceId,
|
||||
);
|
||||
}
|
||||
|
||||
if ($player === null && $request->user() !== null) {
|
||||
@@ -59,6 +96,11 @@ class EnsureRoomParticipant
|
||||
->first();
|
||||
|
||||
if ($player !== null) {
|
||||
$player = $this->roomParticipation->activateInstance(
|
||||
$room,
|
||||
$player,
|
||||
$instanceId,
|
||||
);
|
||||
$request->session()->put($sessionKey, $player->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ use Inertia\Inertia;
|
||||
|
||||
class ShowEmbeddedRoomRequest extends FormRequest
|
||||
{
|
||||
private const string PLAYER_NAME_TAKEN_MESSAGE = 'Dieser Anzeigename ist in dem Raum bereits vorhanden.';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
@@ -72,16 +74,18 @@ class ShowEmbeddedRoomRequest extends FormRequest
|
||||
}
|
||||
|
||||
$sessionPlayerId = $this->session()->get("room_players.{$room->id}");
|
||||
$existingPlayer = is_numeric($sessionPlayerId)
|
||||
? $room->players()->find((int) $sessionPlayerId)
|
||||
: null;
|
||||
$existingPlayer = null;
|
||||
|
||||
if ($existingPlayer === null && $this->user() !== null) {
|
||||
if ($this->user() !== null) {
|
||||
$existingPlayer = $room->players()
|
||||
->where('user_id', $this->user()->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($existingPlayer === null && is_numeric($sessionPlayerId)) {
|
||||
$existingPlayer = $room->players()->find((int) $sessionPlayerId);
|
||||
}
|
||||
|
||||
$nameIsTaken = Player::query()
|
||||
->where('room_id', $room->id)
|
||||
->where('name', $this->input('player_name'))
|
||||
@@ -94,7 +98,7 @@ class ShowEmbeddedRoomRequest extends FormRequest
|
||||
if ($nameIsTaken) {
|
||||
$validator->errors()->add(
|
||||
'player_name',
|
||||
'Dieser Anzeigename ist in dem Raum bereits vorhanden.',
|
||||
self::PLAYER_NAME_TAKEN_MESSAGE,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -112,9 +116,18 @@ class ShowEmbeddedRoomRequest extends FormRequest
|
||||
|
||||
protected function failedValidation(Validator $validator): never
|
||||
{
|
||||
$message = $validator->errors()->first();
|
||||
$showLoginAction = $this->user() === null
|
||||
&& $message === self::PLAYER_NAME_TAKEN_MESSAGE;
|
||||
|
||||
if ($showLoginAction) {
|
||||
$this->session()->put('url.intended', $this->fullUrl());
|
||||
}
|
||||
|
||||
throw new HttpResponseException(
|
||||
Inertia::render('embedded/Error', [
|
||||
'message' => $validator->errors()->first(),
|
||||
'message' => $message,
|
||||
'showLoginAction' => $showLoginAction,
|
||||
])->toResponse($this)->setStatusCode(422),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class CurrentPlayerResource extends JsonResource
|
||||
|
||||
return [
|
||||
'id' => $player->id,
|
||||
'instance_id' => $player->active_instance_id,
|
||||
'name' => $player->name,
|
||||
'is_moderator' => $player->is_moderator,
|
||||
'current_vote' => $pageData->currentVote,
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Support\Carbon;
|
||||
* @property int $id
|
||||
* @property string $room_id
|
||||
* @property int|null $user_id
|
||||
* @property string|null $active_instance_id
|
||||
* @property string $name
|
||||
* @property bool $is_moderator
|
||||
* @property Carbon|null $last_seen_at
|
||||
@@ -25,6 +26,7 @@ class Player extends Model
|
||||
protected $fillable = [
|
||||
'room_id',
|
||||
'user_id',
|
||||
'active_instance_id',
|
||||
'name',
|
||||
'is_moderator',
|
||||
'last_seen_at',
|
||||
|
||||
@@ -205,6 +205,17 @@ class RoomParticipationService
|
||||
?User $user,
|
||||
mixed $sessionPlayerId,
|
||||
): ?Player {
|
||||
if ($user !== null) {
|
||||
$userPlayer = $room->players()
|
||||
->where('user_id', $user->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($userPlayer !== null) {
|
||||
return $userPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_numeric($sessionPlayerId)) {
|
||||
$sessionPlayer = $room->players()
|
||||
->whereKey((int) $sessionPlayerId)
|
||||
@@ -216,14 +227,53 @@ class RoomParticipationService
|
||||
}
|
||||
}
|
||||
|
||||
if ($user === null) {
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function activateInstance(Room $room, Player $player, string $instanceId): Player
|
||||
{
|
||||
[$activePlayer, $replacedInstanceId, $stateVersion] = DB::transaction(function () use ($room, $player, $instanceId): array {
|
||||
$lockedRoom = Room::query()
|
||||
->lockForUpdate()
|
||||
->findOrFail($room->id);
|
||||
$lockedPlayer = $lockedRoom->players()
|
||||
->lockForUpdate()
|
||||
->findOrFail($player->id);
|
||||
|
||||
if ($lockedPlayer->active_instance_id === $instanceId) {
|
||||
return [$lockedPlayer, null, null];
|
||||
}
|
||||
|
||||
$replacedInstanceId = $lockedPlayer->active_instance_id;
|
||||
$lockedPlayer->update([
|
||||
'active_instance_id' => $instanceId,
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
|
||||
if ($replacedInstanceId === null) {
|
||||
return [$lockedPlayer, null, null];
|
||||
}
|
||||
|
||||
$lockedRoom->update([
|
||||
'last_activity_at' => now(),
|
||||
'state_version' => $lockedRoom->state_version + 1,
|
||||
]);
|
||||
|
||||
return [$lockedPlayer, $replacedInstanceId, $lockedRoom->state_version];
|
||||
});
|
||||
|
||||
if (is_string($replacedInstanceId) && is_int($stateVersion)) {
|
||||
RoomStateChanged::dispatch(
|
||||
$room->id,
|
||||
$stateVersion,
|
||||
RoomStateChange::PlayerInstanceReplaced,
|
||||
$this->realtimeState->for($room),
|
||||
$activePlayer->id,
|
||||
$replacedInstanceId,
|
||||
);
|
||||
}
|
||||
|
||||
return $room->players()
|
||||
->where('user_id', $user->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
return $activePlayer;
|
||||
}
|
||||
|
||||
public function kick(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Rooms;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RoomPlayerInstanceService
|
||||
{
|
||||
private const string SESSION_KEY = 'room_player_instance_id';
|
||||
|
||||
public function for(Request $request): string
|
||||
{
|
||||
$instanceId = $request->session()->get(self::SESSION_KEY);
|
||||
|
||||
if (is_string($instanceId) && Str::isUuid($instanceId)) {
|
||||
return $instanceId;
|
||||
}
|
||||
|
||||
$instanceId = (string) Str::uuid();
|
||||
$request->session()->put(self::SESSION_KEY, $instanceId);
|
||||
|
||||
return $instanceId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user