diff --git a/app/Http/Controllers/Meet/JoinController.php b/app/Http/Controllers/Meet/JoinController.php index 8c1841f..3711d88 100644 --- a/app/Http/Controllers/Meet/JoinController.php +++ b/app/Http/Controllers/Meet/JoinController.php @@ -61,6 +61,31 @@ class JoinController extends Controller ]); } + $speakerToken = trim((string) $request->query('speaker', '')); + if ($speakerToken !== '' && $this->acceptsEventsSpeakerToken($room, $request)) { + if ($this->verifyAndStoreSpeaker($request, $room, $speakerToken)) { + $session = $room->activeSession(); + if ($session) { + $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); + $participant = $participantUuid + ? Participant::where('uuid', $participantUuid)->where('session_id', $session->id)->first() + : null; + + if ($participant?->status === 'joined') { + $this->sessions->syncEventsSpeakerRole($request, $participant); + + return redirect()->route('meet.room', $session); + } + } + + return redirect()->route('meet.join', $room); + } + + return $this->gateView($request, $room)->withErrors([ + 'speaker' => 'Speaker invitation link is invalid or expired.', + ]); + } + $session = $room->activeSession(); if ($session) { $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); @@ -88,17 +113,6 @@ class JoinController extends Controller ]); } - $speakerToken = trim((string) $request->query('speaker', '')); - if ($speakerToken !== '' && $this->requiresEventsRegistration($room, $request)) { - if ($this->verifyAndStoreSpeaker($request, $room, $speakerToken)) { - return redirect()->route('meet.join', $room); - } - - return $this->gateView($request, $room)->withErrors([ - 'speaker' => 'Speaker invitation link is invalid or expired.', - ]); - } - if ($this->requiresEventsRegistration($room, $request)) { return $this->gateView($request, $room); } @@ -142,6 +156,11 @@ class JoinController extends Controller return redirect()->route('meet.join', $room); } + $speakerToken = trim((string) $request->query('speaker', '')); + if ($speakerToken !== '' && $this->acceptsEventsSpeakerToken($room, $request)) { + $this->verifyAndStoreSpeaker($request, $room, $speakerToken); + } + if ($this->requiresEventsRegistration($room, $request)) { return redirect()->route('meet.join', $room); } @@ -164,9 +183,15 @@ class JoinController extends Controller $user = $request->user(); $registration = EventsRegistrationSession::get($request, $room); + $speakerVerified = EventsRegistrationSession::isSpeakerVerified($request, $room); $displayName = $user?->name ?? ($validated['display_name'] ?? ($registration['name'] ?? 'Guest')); $email = $user?->email ?? ($registration['email'] ?? null); + if ($speakerVerified && filled($registration['email'] ?? null)) { + $email = (string) $registration['email']; + $displayName = (string) ($registration['name'] ?? $displayName); + } + $kind = $room->isConference() ? 'conference' : ($room->isWebinar() ? 'webinar' : 'meeting'); [$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local'); @@ -353,6 +378,15 @@ class JoinController extends Controller return ! EventsRegistrationSession::canJoinEventsLinked($request, $room); } + protected function acceptsEventsSpeakerToken(Room $room, Request $request): bool + { + if (! $room->isEventsLinked() || $this->isRoomHost($request, $room)) { + return false; + } + + return $room->isWebinar() || $room->isConference(); + } + protected function verifyAndStoreSpeaker(Request $request, Room $room, string $speakerToken): bool { $eventId = EventsSourceLink::eventId($room); diff --git a/app/Support/EventsRegistrationSession.php b/app/Support/EventsRegistrationSession.php index 46e2f54..64555af 100644 --- a/app/Support/EventsRegistrationSession.php +++ b/app/Support/EventsRegistrationSession.php @@ -42,11 +42,13 @@ class EventsRegistrationSession /** @param array{email?: ?string, name?: ?string, badge_code?: ?string, speaker_token?: ?string} $data */ public static function store(Request $request, Room $room, array $data): void { + $existing = self::get($request, $room) ?? []; + $request->session()->put(self::key($room), [ - 'email' => $data['email'] ?? null, - 'name' => $data['name'] ?? null, - 'badge_code' => $data['badge_code'] ?? null, - 'speaker_token' => $data['speaker_token'] ?? null, + 'email' => array_key_exists('email', $data) ? $data['email'] : ($existing['email'] ?? null), + 'name' => array_key_exists('name', $data) ? $data['name'] : ($existing['name'] ?? null), + 'badge_code' => array_key_exists('badge_code', $data) ? $data['badge_code'] : ($existing['badge_code'] ?? null), + 'speaker_token' => array_key_exists('speaker_token', $data) ? $data['speaker_token'] : ($existing['speaker_token'] ?? null), ]); } diff --git a/tests/Feature/EventsJoinGateTest.php b/tests/Feature/EventsJoinGateTest.php index e40da65..8f6a10a 100644 --- a/tests/Feature/EventsJoinGateTest.php +++ b/tests/Feature/EventsJoinGateTest.php @@ -267,6 +267,81 @@ class EventsJoinGateTest extends TestCase ]); } + public function test_speaker_token_applies_after_attendee_badge_verification(): void + { + $room = $this->createEventsLinkedRoom('webinar', [ + 'settings' => array_merge(config('meet.default_settings'), [ + 'join_before_host' => true, + 'waiting_room' => false, + ]), + ]); + + $session = Session::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'media_room_name' => 'room-'.$room->uuid, + 'status' => 'live', + 'started_at' => now(), + ]); + $room->update(['status' => 'live']); + + $this->post('/r/'.$room->uuid.'/verify-badge', ['badge_code' => 'BADGE123']) + ->assertRedirect(route('meet.join', $room)); + + $this->get('/r/'.$room->uuid.'?speaker=speaker-token-123') + ->assertRedirect(route('meet.join', $room)); + + $this->post('/r/'.$room->uuid.'/enter', [ + 'display_name' => 'Keynote Speaker', + ])->assertRedirect(route('meet.room', $session)); + + $this->assertDatabaseHas('meet_participants', [ + 'session_id' => $session->id, + 'display_name' => 'Keynote Speaker', + 'email' => 'speaker@example.com', + 'role' => 'panelist', + ]); + } + + public function test_speaker_link_promotes_already_joined_attendee(): void + { + $room = $this->createEventsLinkedRoom('webinar', [ + 'settings' => array_merge(config('meet.default_settings'), [ + 'join_before_host' => true, + 'waiting_room' => false, + ]), + ]); + + $session = Session::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'media_room_name' => 'room-'.$room->uuid, + 'status' => 'live', + 'started_at' => now(), + ]); + $room->update(['status' => 'live']); + + $participant = \App\Models\Participant::create([ + 'owner_ref' => $this->user->public_id, + 'session_id' => $session->id, + 'display_name' => 'Keynote Speaker', + 'email' => 'speaker@example.com', + 'role' => 'attendee', + 'status' => 'joined', + 'joined_at' => now(), + ]); + + $this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid]) + ->get('/r/'.$room->uuid.'?speaker=speaker-token-123') + ->assertRedirect(route('meet.room', $session)); + + $this->assertDatabaseHas('meet_participants', [ + 'session_id' => $session->id, + 'email' => 'speaker@example.com', + 'role' => 'panelist', + ]); + } + /** @param array $overrides */ protected function createEventsLinkedRoom(string $type, array $overrides = []): Room {