Let invited speakers bypass Events registration and join as panelists.
Deploy Ladill Meet / deploy (push) Successful in 1m30s

Verify speaker tokens from Events on join, grant panelist role for linked webinars, and point bulk comms links to the attendees page instead of the JSON preview API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-03 15:48:35 +00:00
co-authored by Cursor
parent 08a1437b44
commit f5f599d1e3
5 changed files with 122 additions and 5 deletions
+36 -2
View File
@@ -88,6 +88,17 @@ 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);
}
@@ -200,7 +211,9 @@ class JoinController extends Controller
} elseif ($room->isConference() && $role !== 'host') {
$role = $this->conferences->resolveJoinRole($room, $user, $email, $role);
} elseif ($room->isWebinar() && $role !== 'host') {
if ($room->isEventsLinked()) {
if (EventsRegistrationSession::isSpeakerVerified($request, $room)) {
$role = 'panelist';
} elseif ($room->isEventsLinked()) {
$role = 'attendee';
} else {
return back()->withErrors([
@@ -337,7 +350,28 @@ class JoinController extends Controller
return false;
}
return ! EventsRegistrationSession::isVerified($request, $room);
return ! EventsRegistrationSession::canJoinEventsLinked($request, $room);
}
protected function verifyAndStoreSpeaker(Request $request, Room $room, string $speakerToken): bool
{
$eventId = EventsSourceLink::eventId($room);
if (! $eventId || ! $this->events->isConfigured()) {
return false;
}
$speaker = $this->events->verifySpeakerInvite($eventId, $room->owner_ref, $speakerToken);
if (! $speaker) {
return false;
}
EventsRegistrationSession::store($request, $room, [
'email' => $speaker['email'] ?? null,
'name' => $speaker['name'] ?? null,
'speaker_token' => $speakerToken,
]);
return EventsRegistrationSession::isSpeakerVerified($request, $room);
}
protected function verifyAndStoreBadge(Request $request, Room $room, string $badgeCode): bool
@@ -43,6 +43,31 @@ class EventsClient
return (array) ($response['event'] ?? []);
}
/** @return array<string, mixed>|null */
public function verifySpeakerInvite(int $eventId, string $ownerRef, string $speakerToken): ?array
{
try {
$response = $this->client()->post("events/{$eventId}/verify-speaker", [
'owner_ref' => $ownerRef,
'speaker_token' => $speakerToken,
]);
} catch (ConnectionException) {
throw new \RuntimeException('Could not reach Ladill Events.');
}
if ($response->status() === 404) {
return null;
}
if ($response->failed()) {
throw new \RuntimeException('Events service error: '.($response->json('message') ?? $response->body()));
}
$speaker = $response->json('speaker');
return is_array($speaker) ? $speaker : null;
}
/** @return array<string, mixed>|null */
public function verifyRegistration(int $eventId, string $ownerRef, string $badgeCode): ?array
{
+15 -2
View File
@@ -12,7 +12,7 @@ class EventsRegistrationSession
return "meet.events.registration.{$room->uuid}";
}
/** @return array{email: ?string, name: ?string, badge_code: ?string}|null */
/** @return array{email: ?string, name: ?string, badge_code: ?string, speaker_token: ?string}|null */
public static function get(Request $request, Room $room): ?array
{
$data = $request->session()->get(self::key($room));
@@ -27,13 +27,26 @@ class EventsRegistrationSession
return filled($data['badge_code'] ?? null) && filled($data['email'] ?? null);
}
/** @param array{email?: ?string, name?: ?string, badge_code?: ?string} $data */
public static function isSpeakerVerified(Request $request, Room $room): bool
{
$data = self::get($request, $room);
return filled($data['speaker_token'] ?? null) && filled($data['email'] ?? null);
}
public static function canJoinEventsLinked(Request $request, Room $room): bool
{
return self::isVerified($request, $room) || self::isSpeakerVerified($request, $room);
}
/** @param array{email?: ?string, name?: ?string, badge_code?: ?string, speaker_token?: ?string} $data */
public static function store(Request $request, Room $room, array $data): void
{
$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,
]);
}
+1 -1
View File
@@ -90,7 +90,7 @@ class EventsSourceLink
{
$eventId = self::eventId($room);
return $eventId ? self::baseUrl().'/events/'.$eventId.'/attendees/comms-preview' : null;
return $eventId ? self::baseUrl().'/events/'.$eventId.'/attendees#comms' : null;
}
public static function eventBadgesUrl(Room $room): ?string
+45
View File
@@ -63,6 +63,13 @@ class EventsJoinGateTest extends TestCase
'badge_code' => 'BADGE123',
],
]),
'https://events.test/api/service/v1/events/*/verify-speaker' => Http::response([
'speaker' => [
'name' => 'Keynote Speaker',
'email' => 'speaker@example.com',
'role' => 'Keynote',
],
]),
]);
}
@@ -147,6 +154,44 @@ class EventsJoinGateTest extends TestCase
->assertDontSee('Register for event', false);
}
public function test_speaker_token_skips_registration_gate_and_grants_panelist_role(): 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->get('/r/'.$room->uuid.'?speaker=speaker-token-123')
->assertRedirect(route('meet.join', $room));
$this->get('/r/'.$room->uuid)
->assertOk()
->assertSee('How should we show your name?', false)
->assertDontSee('Register for event', false);
$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',
]);
}
/** @param array<string, mixed> $overrides */
protected function createEventsLinkedRoom(string $type, array $overrides = []): Room
{