From 2ab95cebdf8605033afc112d112fbe002c72b897 Mon Sep 17 00:00:00 2001 From: Twyco Date: Thu, 3 Sep 2026 15:30:43 +0200 Subject: [PATCH] do not need login --- .../Controllers/EmbeddedLogoutController.php | 17 +++ .../Controllers/EmbeddedRoomController.php | 5 +- app/Http/Requests/ShowEmbeddedRoomRequest.php | 23 +++- app/Providers/FortifyServiceProvider.php | 1 - .../Rooms/RoomParticipationService.php | 72 +++++++++-- bootstrap/app.php | 7 -- .../js/components/rooms/RoomExperience.vue | 3 +- resources/js/components/rooms/RoomHeader.vue | 2 +- resources/js/pages/auth/Login.vue | 2 - resources/js/types/room.ts | 2 +- routes/room.php | 6 +- tests/Feature/EmbeddedRoomTest.php | 115 +++++++++++++----- 12 files changed, 184 insertions(+), 71 deletions(-) diff --git a/app/Http/Controllers/EmbeddedLogoutController.php b/app/Http/Controllers/EmbeddedLogoutController.php index f920573..6a297aa 100644 --- a/app/Http/Controllers/EmbeddedLogoutController.php +++ b/app/Http/Controllers/EmbeddedLogoutController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Models\Room; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -15,11 +16,27 @@ class EmbeddedLogoutController extends Controller { $joinCode = $request->string('join_code')->trim()->upper()->toString(); $playerName = $request->string('player_name')->trim()->toString(); + $room = Room::query()->where('join_code', $joinCode)->first(); + $playerId = null; + + if ($room !== null) { + $sessionPlayerId = $request->session()->get("room_players.{$room->id}"); + + if (is_numeric($sessionPlayerId)) { + $playerId = $room->players() + ->whereKey((int) $sessionPlayerId) + ->value('id'); + } + } Auth::guard('web')->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); + if ($room !== null && is_numeric($playerId)) { + $request->session()->put("room_players.{$room->id}", (int) $playerId); + } + return to_route('embedded.room', [ 'join_code' => $joinCode, 'player_name' => $playerName, diff --git a/app/Http/Controllers/EmbeddedRoomController.php b/app/Http/Controllers/EmbeddedRoomController.php index 0c363f6..e6d33cf 100644 --- a/app/Http/Controllers/EmbeddedRoomController.php +++ b/app/Http/Controllers/EmbeddedRoomController.php @@ -6,7 +6,6 @@ use App\Http\Requests\ShowEmbeddedRoomRequest; use App\Http\Resources\CurrentPlayerResource; use App\Http\Resources\RoomResource; use App\Models\Room; -use App\Models\User; use App\Services\Rooms\RoomPageService; use App\Services\Rooms\RoomParticipationService; use Inertia\Inertia; @@ -25,8 +24,6 @@ class EmbeddedRoomController extends Controller $validated = $request->validated(); $user = $request->user(); - abort_unless($user instanceof User, 403); - $room = Room::query() ->where('join_code', $validated['join_code']) ->firstOrFail(); @@ -34,10 +31,10 @@ class EmbeddedRoomController extends Controller $room, $user, $validated['player_name'], + $request->session()->get("room_players.{$room->id}"), ); $request->session()->put("room_players.{$room->id}", $player->id); - $request->session()->forget('embedded_login'); $pageData = $this->roomPage->prepare($room, $player, $user); diff --git a/app/Http/Requests/ShowEmbeddedRoomRequest.php b/app/Http/Requests/ShowEmbeddedRoomRequest.php index fe463b8..2e14d7a 100644 --- a/app/Http/Requests/ShowEmbeddedRoomRequest.php +++ b/app/Http/Requests/ShowEmbeddedRoomRequest.php @@ -18,7 +18,7 @@ class ShowEmbeddedRoomRequest extends FormRequest */ public function authorize(): bool { - return $this->user() !== null; + return true; } protected function prepareForValidation(): void @@ -67,17 +67,28 @@ class ShowEmbeddedRoomRequest extends FormRequest return; } - if ($room === null || $this->user() === null) { + if ($room === null) { return; } + $sessionPlayerId = $this->session()->get("room_players.{$room->id}"); + $existingPlayer = is_numeric($sessionPlayerId) + ? $room->players()->find((int) $sessionPlayerId) + : null; + + if ($existingPlayer === null && $this->user() !== null) { + $existingPlayer = $room->players() + ->where('user_id', $this->user()->id) + ->first(); + } + $nameIsTaken = Player::query() ->where('room_id', $room->id) ->where('name', $this->input('player_name')) - ->where(function ($query): void { - $query->whereNull('user_id') - ->orWhere('user_id', '!=', $this->user()->id); - }) + ->when( + $existingPlayer !== null, + fn ($query) => $query->whereKeyNot($existingPlayer->id), + ) ->exists(); if ($nameIsTaken) { diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index c87641d..24cf595 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -51,7 +51,6 @@ class FortifyServiceProvider extends ServiceProvider Fortify::loginView(fn (Request $request) => Inertia::render('auth/Login', [ 'canResetPassword' => Features::enabled(Features::resetPasswords()), 'status' => $request->session()->get('status'), - 'rememberByDefault' => $request->session()->get('embedded_login', false), ])); Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/ResetPassword', [ diff --git a/app/Services/Rooms/RoomParticipationService.php b/app/Services/Rooms/RoomParticipationService.php index e2189e7..40b6724 100644 --- a/app/Services/Rooms/RoomParticipationService.php +++ b/app/Services/Rooms/RoomParticipationService.php @@ -102,9 +102,13 @@ class RoomParticipationService return [$room, $player]; } - public function joinEmbedded(Room $room, User $user, string $playerName): Player - { - [$player, $change, $stateVersion] = DB::transaction(function () use ($room, $user, $playerName): array { + public function joinEmbedded( + Room $room, + ?User $user, + string $playerName, + mixed $sessionPlayerId, + ): Player { + [$player, $change, $stateVersion] = DB::transaction(function () use ($room, $user, $playerName, $sessionPlayerId): array { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); @@ -115,12 +119,18 @@ class RoomParticipationService ]); } + $player = $this->findExistingEmbeddedPlayer( + $lockedRoom, + $user, + $sessionPlayerId, + ); + $nameIsTaken = $lockedRoom->players() ->where('name', $playerName) - ->where(function ($query) use ($user): void { - $query->whereNull('user_id') - ->orWhere('user_id', '!=', $user->id); - }) + ->when( + $player !== null, + fn ($query) => $query->whereKeyNot($player->id), + ) ->exists(); if ($nameIsTaken) { @@ -129,14 +139,11 @@ class RoomParticipationService ]); } - $player = $lockedRoom->players() - ->where('user_id', $user->id) - ->first(); $change = null; if ($player === null) { $player = $lockedRoom->players()->create([ - 'user_id' => $user->id, + 'user_id' => $user?->id, 'name' => $playerName, 'is_moderator' => false, 'last_seen_at' => now(), @@ -149,11 +156,24 @@ class RoomParticipationService $change = RoomStateChange::PlayerUpdated; } - $player->update([ + $updates = [ 'name' => $playerName, 'last_seen_at' => now(), 'inactive_at' => null, - ]); + ]; + + if ( + $user !== null + && $player->user_id === null + && ! $lockedRoom->players() + ->where('user_id', $user->id) + ->whereKeyNot($player->id) + ->exists() + ) { + $updates['user_id'] = $user->id; + } + + $player->update($updates); } if ($change === null) { @@ -180,6 +200,32 @@ class RoomParticipationService return $player; } + private function findExistingEmbeddedPlayer( + Room $room, + ?User $user, + mixed $sessionPlayerId, + ): ?Player { + if (is_numeric($sessionPlayerId)) { + $sessionPlayer = $room->players() + ->whereKey((int) $sessionPlayerId) + ->lockForUpdate() + ->first(); + + if ($sessionPlayer !== null) { + return $sessionPlayer; + } + } + + if ($user === null) { + return null; + } + + return $room->players() + ->where('user_id', $user->id) + ->lockForUpdate() + ->first(); + } + public function kick( Room $room, Player $player, diff --git a/bootstrap/app.php b/bootstrap/app.php index e6cbc5e..409954c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -18,13 +18,6 @@ return Application::configure(basePath: dirname(__DIR__)) ) ->withMiddleware(function (Middleware $middleware): void { $middleware->trustProxies(at: '*'); - $middleware->redirectGuestsTo(function (Request $request): string { - if ($request->isMethod('GET') && $request->is('embedded/scrum-poker')) { - $request->session()->put('embedded_login', true); - } - - return route('login'); - }); $middleware->encryptCookies(except: ['appearance', 'sidebar_state']); $middleware->alias([ 'room.player' => EnsureRoomParticipant::class, diff --git a/resources/js/components/rooms/RoomExperience.vue b/resources/js/components/rooms/RoomExperience.vue index 803d0dd..a9bb125 100644 --- a/resources/js/components/rooms/RoomExperience.vue +++ b/resources/js/components/rooms/RoomExperience.vue @@ -16,6 +16,7 @@ const props = withDefaults(defineProps(), { const page = usePage(); const presenterMode = ref(false); const roomNotice = computed(() => page.props.roomNotice); +const isAuthenticated = computed(() => page.props.auth.user !== null); const embeddedBackgroundColor = computed(() => { if (!props.embedded) { return undefined; @@ -74,7 +75,7 @@ const { realtimeRoom.allow_participants_to_manage_rounds " :current-player-id="realtimeCurrentPlayer.id" - :embedded="embedded" + :can-logout="embedded && isAuthenticated" :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 1d8cba5..fcfba18 100644 --- a/resources/js/components/rooms/RoomHeader.vue +++ b/resources/js/components/rooms/RoomHeader.vue @@ -99,7 +99,7 @@ async function copyInviteLink(): Promise {
Remember me diff --git a/resources/js/types/room.ts b/resources/js/types/room.ts index f65e4e5..952e86d 100644 --- a/resources/js/types/room.ts +++ b/resources/js/types/room.ts @@ -116,7 +116,7 @@ export type RoomHeaderProps = Pick< canManageParticipants: boolean; currentPlayerId: number; presenterMode: boolean; - embedded: boolean; + canLogout: boolean; playerName: string; }; diff --git a/routes/room.php b/routes/room.php index fd00292..5b1a68d 100644 --- a/routes/room.php +++ b/routes/room.php @@ -25,10 +25,8 @@ Route::get('/room/join', RoomJoinController::class)->name('room.join'); Route::post('room/join', [RoomPlayerController::class, 'join'])->name('room.code'); Route::post('/room', [RoomController::class, 'store'])->name('room.store'); -Route::middleware(['auth', 'verified'])->group(function () { - Route::get('/embedded/scrum-poker', EmbeddedRoomController::class) - ->name('embedded.room'); -}); +Route::get('/embedded/scrum-poker', EmbeddedRoomController::class) + ->name('embedded.room'); Route::post('/embedded/scrum-poker/logout', EmbeddedLogoutController::class) ->middleware('auth') diff --git a/tests/Feature/EmbeddedRoomTest.php b/tests/Feature/EmbeddedRoomTest.php index 8db7c62..812fecb 100644 --- a/tests/Feature/EmbeddedRoomTest.php +++ b/tests/Feature/EmbeddedRoomTest.php @@ -19,22 +19,43 @@ function createEmbeddedRoom(array $attributes = []): Room ]); } -test('embedded room access sends guests through login and remembers the intended room', function () { +test('a guest automatically joins the embedded room', function () { $room = createEmbeddedRoom(); - $url = route('embedded.room', [ - 'join_code' => $room->join_code, - 'player_name' => 'Nico', + $response = $this->get(route('embedded.room', [ + 'join_code' => strtolower($room->join_code), + 'player_name' => ' Nico ', + ])) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('embedded/Room') + ->where('room.id', $room->id) + ->where('currentPlayer.name', 'Nico')); + + $player = $room->players()->sole(); + + expect($player->user_id)->toBeNull() + ->and($player->is_moderator)->toBeFalse(); + + $response->assertSessionHas("room_players.{$room->id}", $player->id); +}); + +test('embedded guest access reuses the session player and applies a changed display name', function () { + $room = createEmbeddedRoom(); + $player = $room->players()->create([ + 'name' => 'Old name', + 'inactive_at' => now(), ]); - $this->get($url) - ->assertRedirect(route('login')) - ->assertSessionHas('embedded_login', true) - ->assertSessionHas('url.intended', $url); + $this->withSession(["room_players.{$room->id}" => $player->id]) + ->get(route('embedded.room', [ + 'join_code' => $room->join_code, + 'player_name' => 'New name', + ])) + ->assertOk(); - $this->get(route('login')) - ->assertInertia(fn (Assert $page) => $page - ->component('auth/Login') - ->where('rememberByDefault', true)); + expect($room->players()->count())->toBe(1) + ->and($player->fresh()->name)->toBe('New name') + ->and($player->fresh()->inactive_at)->toBeNull(); }); test('an authenticated user automatically joins the embedded room', function () { @@ -83,16 +104,39 @@ test('embedded access reuses the user player and applies a changed display name' ->and($player->fresh()->inactive_at)->toBeNull(); }); -test('embedded access shows a dedicated error when the display name is taken', function () { - $user = User::factory()->create(); +test('an embedded guest keeps the same player after logging in separately', function () { + $user = User::factory()->create([ + 'email' => 'nico@example.com', + ]); + $room = createEmbeddedRoom(); + $url = route('embedded.room', [ + 'join_code' => $room->join_code, + 'player_name' => 'Nico', + ]); + + $this->get($url)->assertOk(); + + $player = $room->players()->sole(); + + $this->post(route('login.store'), [ + 'email' => 'nico@example.com', + 'password' => 'password', + ])->assertRedirect(); + + $this->get($url)->assertOk(); + + expect($room->players()->count())->toBe(1) + ->and($player->fresh()->user_id)->toBe($user->id); +}); + +test('embedded guest access shows a dedicated error when the display name is taken', function () { $room = createEmbeddedRoom(); $room->players()->create(['name' => 'Nico']); - $this->actingAs($user) - ->get(route('embedded.room', [ - 'join_code' => $room->join_code, - 'player_name' => 'Nico', - ])) + $this->get(route('embedded.room', [ + 'join_code' => $room->join_code, + 'player_name' => 'Nico', + ])) ->assertUnprocessable() ->assertInertia(fn (Assert $page) => $page ->component('embedded/Error') @@ -100,30 +144,39 @@ test('embedded access shows a dedicated error when the display name is taken', f }); test('embedded access shows a dedicated error for an unknown room code', function () { - $this->actingAs(User::factory()->create()) - ->get(route('embedded.room', [ - 'join_code' => 'ABC234', - 'player_name' => 'Nico', - ])) + $this->get(route('embedded.room', [ + 'join_code' => 'ABC234', + 'player_name' => 'Nico', + ])) ->assertUnprocessable() ->assertInertia(fn (Assert $page) => $page ->component('embedded/Error') ->where('message', 'Dieser Raumcode existiert nicht.')); }); -test('embedded logout returns to the same configured room and signs the user out', function () { +test('embedded logout returns to the same room as a guest without duplicating the player', function () { $user = User::factory()->create(); $room = createEmbeddedRoom(); + $url = route('embedded.room', [ + 'join_code' => $room->join_code, + 'player_name' => 'Nico', + ]); - $this->actingAs($user) + $this->actingAs($user)->get($url)->assertOk(); + + $player = $room->players()->sole(); + + $this + ->followingRedirects() ->post(route('embedded.logout', [ 'join_code' => $room->join_code, 'player_name' => 'Nico', ])) - ->assertRedirect(route('embedded.room', [ - 'join_code' => $room->join_code, - 'player_name' => 'Nico', - ])); + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('embedded/Room') + ->where('currentPlayer.id', $player->id)); - $this->assertGuest(); + $this->assertGuest() + ->assertDatabaseCount('players', 1); });