Fix speaker token ignored after badge verification or active join.
Deploy Ladill Meet / deploy (push) Successful in 1m0s

Process ?speaker= before registration gate and already-joined redirects so Events speakers get panelist role even when they previously registered as attendees.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-03 18:35:03 +00:00
co-authored by Cursor
parent 3d68f7a83b
commit ee3f7410f4
3 changed files with 126 additions and 15 deletions
+45 -11
View File
@@ -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(); $session = $room->activeSession();
if ($session) { if ($session) {
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); $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)) { if ($this->requiresEventsRegistration($room, $request)) {
return $this->gateView($request, $room); return $this->gateView($request, $room);
} }
@@ -142,6 +156,11 @@ class JoinController extends Controller
return redirect()->route('meet.join', $room); 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)) { if ($this->requiresEventsRegistration($room, $request)) {
return redirect()->route('meet.join', $room); return redirect()->route('meet.join', $room);
} }
@@ -164,9 +183,15 @@ class JoinController extends Controller
$user = $request->user(); $user = $request->user();
$registration = EventsRegistrationSession::get($request, $room); $registration = EventsRegistrationSession::get($request, $room);
$speakerVerified = EventsRegistrationSession::isSpeakerVerified($request, $room);
$displayName = $user?->name ?? ($validated['display_name'] ?? ($registration['name'] ?? 'Guest')); $displayName = $user?->name ?? ($validated['display_name'] ?? ($registration['name'] ?? 'Guest'));
$email = $user?->email ?? ($registration['email'] ?? null); $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'); $kind = $room->isConference() ? 'conference' : ($room->isWebinar() ? 'webinar' : 'meeting');
[$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local'); [$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local');
@@ -353,6 +378,15 @@ class JoinController extends Controller
return ! EventsRegistrationSession::canJoinEventsLinked($request, $room); 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 protected function verifyAndStoreSpeaker(Request $request, Room $room, string $speakerToken): bool
{ {
$eventId = EventsSourceLink::eventId($room); $eventId = EventsSourceLink::eventId($room);
+6 -4
View File
@@ -42,11 +42,13 @@ class EventsRegistrationSession
/** @param array{email?: ?string, name?: ?string, badge_code?: ?string, speaker_token?: ?string} $data */ /** @param array{email?: ?string, name?: ?string, badge_code?: ?string, speaker_token?: ?string} $data */
public static function store(Request $request, Room $room, array $data): void public static function store(Request $request, Room $room, array $data): void
{ {
$existing = self::get($request, $room) ?? [];
$request->session()->put(self::key($room), [ $request->session()->put(self::key($room), [
'email' => $data['email'] ?? null, 'email' => array_key_exists('email', $data) ? $data['email'] : ($existing['email'] ?? null),
'name' => $data['name'] ?? null, 'name' => array_key_exists('name', $data) ? $data['name'] : ($existing['name'] ?? null),
'badge_code' => $data['badge_code'] ?? null, 'badge_code' => array_key_exists('badge_code', $data) ? $data['badge_code'] : ($existing['badge_code'] ?? null),
'speaker_token' => $data['speaker_token'] ?? null, 'speaker_token' => array_key_exists('speaker_token', $data) ? $data['speaker_token'] : ($existing['speaker_token'] ?? null),
]); ]);
} }
+75
View File
@@ -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<string, mixed> $overrides */ /** @param array<string, mixed> $overrides */
protected function createEventsLinkedRoom(string $type, array $overrides = []): Room protected function createEventsLinkedRoom(string $type, array $overrides = []): Room
{ {