Let invited speakers bypass Events registration and join as panelists.
Deploy Ladill Meet / deploy (push) Successful in 1m30s
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:
@@ -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)) {
|
if ($this->requiresEventsRegistration($room, $request)) {
|
||||||
return $this->gateView($request, $room);
|
return $this->gateView($request, $room);
|
||||||
}
|
}
|
||||||
@@ -200,7 +211,9 @@ class JoinController extends Controller
|
|||||||
} elseif ($room->isConference() && $role !== 'host') {
|
} elseif ($room->isConference() && $role !== 'host') {
|
||||||
$role = $this->conferences->resolveJoinRole($room, $user, $email, $role);
|
$role = $this->conferences->resolveJoinRole($room, $user, $email, $role);
|
||||||
} elseif ($room->isWebinar() && $role !== 'host') {
|
} elseif ($room->isWebinar() && $role !== 'host') {
|
||||||
if ($room->isEventsLinked()) {
|
if (EventsRegistrationSession::isSpeakerVerified($request, $room)) {
|
||||||
|
$role = 'panelist';
|
||||||
|
} elseif ($room->isEventsLinked()) {
|
||||||
$role = 'attendee';
|
$role = 'attendee';
|
||||||
} else {
|
} else {
|
||||||
return back()->withErrors([
|
return back()->withErrors([
|
||||||
@@ -337,7 +350,28 @@ class JoinController extends Controller
|
|||||||
return false;
|
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
|
protected function verifyAndStoreBadge(Request $request, Room $room, string $badgeCode): bool
|
||||||
|
|||||||
@@ -43,6 +43,31 @@ class EventsClient
|
|||||||
return (array) ($response['event'] ?? []);
|
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 */
|
/** @return array<string, mixed>|null */
|
||||||
public function verifyRegistration(int $eventId, string $ownerRef, string $badgeCode): ?array
|
public function verifyRegistration(int $eventId, string $ownerRef, string $badgeCode): ?array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class EventsRegistrationSession
|
|||||||
return "meet.events.registration.{$room->uuid}";
|
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
|
public static function get(Request $request, Room $room): ?array
|
||||||
{
|
{
|
||||||
$data = $request->session()->get(self::key($room));
|
$data = $request->session()->get(self::key($room));
|
||||||
@@ -27,13 +27,26 @@ class EventsRegistrationSession
|
|||||||
return filled($data['badge_code'] ?? null) && filled($data['email'] ?? null);
|
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
|
public static function store(Request $request, Room $room, array $data): void
|
||||||
{
|
{
|
||||||
$request->session()->put(self::key($room), [
|
$request->session()->put(self::key($room), [
|
||||||
'email' => $data['email'] ?? null,
|
'email' => $data['email'] ?? null,
|
||||||
'name' => $data['name'] ?? null,
|
'name' => $data['name'] ?? null,
|
||||||
'badge_code' => $data['badge_code'] ?? null,
|
'badge_code' => $data['badge_code'] ?? null,
|
||||||
|
'speaker_token' => $data['speaker_token'] ?? null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class EventsSourceLink
|
|||||||
{
|
{
|
||||||
$eventId = self::eventId($room);
|
$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
|
public static function eventBadgesUrl(Room $room): ?string
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ class EventsJoinGateTest extends TestCase
|
|||||||
'badge_code' => 'BADGE123',
|
'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);
|
->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 */
|
/** @param array<string, mixed> $overrides */
|
||||||
protected function createEventsLinkedRoom(string $type, array $overrides = []): Room
|
protected function createEventsLinkedRoom(string $type, array $overrides = []): Room
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user