Files
ladill-meet/app/Http/Controllers/Meet/JoinController.php
T
isaaccladandCursor f5f599d1e3
Deploy Ladill Meet / deploy (push) Successful in 1m30s
Let invited speakers bypass Events registration and join as panelists.
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>
2026-07-03 15:48:35 +00:00

428 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Meet;
use App\Http\Controllers\Controller;
use App\Models\Participant;
use App\Models\Room;
use App\Models\Session;
use App\Services\Integrations\EventsClient;
use App\Services\Meet\ConferenceService;
use App\Services\Meet\InvitationService;
use App\Services\Meet\SecurityPolicyService;
use App\Services\Meet\SessionService;
use App\Services\Meet\SpaceService;
use App\Support\EventsRegistrationSession;
use App\Support\EventsSourceLink;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class JoinController extends Controller
{
public function __construct(
protected SessionService $sessions,
protected SecurityPolicyService $security,
protected SpaceService $spaces,
protected ConferenceService $conferences,
protected EventsClient $events,
) {}
public function show(Request $request, Room $room): View|RedirectResponse
{
$room->loadMissing('organization');
if ($room->status === 'cancelled') {
return view('meet.join.cancelled', [
'room' => $room,
'organization' => $room->organization,
]);
}
if ($room->isWebinar() && $request->query('token')) {
$request->session()->put("meet.webinar.{$room->uuid}", $request->query('token'));
}
[$allowed, $reason] = $this->security->canJoin($room, $request->user(), $request->user()?->email);
if (! $allowed && $reason !== 'passcode_required') {
return view('meet.join.denied', [
'room' => $room,
'organization' => $room->organization,
'reason' => $reason,
]);
}
if ($room->passcode && ! $request->session()->get("meet.passcode.{$room->uuid}")) {
return view('meet.join.passcode', [
'room' => $room,
'organization' => $room->organization,
]);
}
$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 === 'waiting') {
return redirect()->route('meet.join.waiting', $room);
}
if ($participant?->status === 'joined') {
return redirect()->route('meet.room', $session);
}
}
$badge = trim((string) $request->query('badge', ''));
if ($badge !== '' && $this->requiresEventsRegistration($room, $request)) {
if ($this->verifyAndStoreBadge($request, $room, $badge)) {
return redirect()->route('meet.join', $room);
}
return $this->gateView($request, $room)->withErrors([
'badge_code' => 'Badge code not found or registration is not confirmed.',
]);
}
$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);
}
return $this->displayNameView($request, $room);
}
public function verifyBadge(Request $request, Room $room): RedirectResponse
{
if ($room->passcode && ! $request->session()->get("meet.passcode.{$room->uuid}")) {
return redirect()->route('meet.join', $room);
}
$request->validate(['badge_code' => ['required', 'string', 'max:16']]);
if ($this->verifyAndStoreBadge($request, $room, $request->input('badge_code'))) {
return redirect()->route('meet.join', $room);
}
return back()->withErrors([
'badge_code' => 'Badge code not found or registration is not confirmed.',
]);
}
public function verifyPasscode(Request $request, Room $room): RedirectResponse
{
$request->validate(['passcode' => ['required', 'string']]);
if ($room->passcode !== $request->input('passcode')) {
return back()->withErrors(['passcode' => 'Incorrect passcode.']);
}
$request->session()->put("meet.passcode.{$room->uuid}", true);
return redirect()->route('meet.join', $room);
}
public function enter(Request $request, Room $room): RedirectResponse
{
if ($room->passcode && ! $request->session()->get("meet.passcode.{$room->uuid}")) {
return redirect()->route('meet.join', $room);
}
if ($this->requiresEventsRegistration($room, $request)) {
return redirect()->route('meet.join', $room);
}
$rules = [
'email' => ['nullable', 'email'],
];
if ($request->user()) {
$rules['display_name'] = ['nullable', 'string', 'max:100'];
} else {
$rules['display_name'] = ['required', 'string', 'max:100'];
}
try {
$validated = $request->validate($rules);
} catch (ValidationException $e) {
throw $e->redirectTo(route('meet.join', $room));
}
$user = $request->user();
$registration = EventsRegistrationSession::get($request, $room);
$displayName = $user?->name ?? ($validated['display_name'] ?? ($registration['name'] ?? 'Guest'));
$email = $user?->email ?? ($registration['email'] ?? null);
$kind = $room->isConference() ? 'conference' : ($room->isWebinar() ? 'webinar' : 'meeting');
[$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local');
if (! $allowed && $reason !== 'passcode_required') {
return back()->withErrors(['join' => $reason ?? "Unable to join {$kind}."]);
}
try {
if ($room->isLive() && $room->activeSession()) {
$session = $room->activeSession();
} elseif ($room->isTownHall() && $user && $user->ownerRef() === $room->host_user_ref && $room->setting('green_room')) {
$session = app(\App\Services\Meet\TownHallService::class)->startGreenRoom($room, $user);
} elseif ($user && $user->ownerRef() === $room->host_user_ref) {
$session = $this->sessions->start($room, $user);
} elseif ($room->setting('join_before_host') || ($user && $user->ownerRef() === $room->host_user_ref)) {
if (! $room->activeSession() && $user && $user->ownerRef() === $room->host_user_ref) {
$session = $this->sessions->start($room, $user);
} elseif (! $room->activeSession()) {
return back()->withErrors(['join' => ucfirst($kind).' has not started yet. Please wait for the host.']);
} else {
$session = $room->activeSession();
}
} else {
if (! $room->activeSession()) {
return back()->withErrors(['join' => ucfirst($kind).' has not started yet. Please wait for the host.']);
}
$session = $room->activeSession();
}
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
if (in_array($e->getStatusCode(), [402, 503], true)) {
return back()->withErrors(['join' => $e->getMessage() ?: "Unable to start {$kind}."]);
}
throw $e;
}
$role = ($user && $user->ownerRef() === $room->host_user_ref) ? 'host' : 'guest';
if ($room->isSpace()) {
$role = $this->spaces->resolveJoinRole($room, $user, $role);
} elseif ($room->isConference() && $role !== 'host') {
$role = $this->conferences->resolveJoinRole($room, $user, $email, $role);
} elseif ($room->isWebinar() && $role !== 'host') {
if (EventsRegistrationSession::isSpeakerVerified($request, $room)) {
$role = 'panelist';
} elseif ($room->isEventsLinked()) {
$role = 'attendee';
} else {
return back()->withErrors([
'join' => 'This webinar must be linked to a Ladill Events event before attendees can register and join.',
]);
}
}
if ($room->isConference() && ! $this->conferences->canJoinSession($room, $session, $role)) {
return back()->withErrors([
'join' => 'The conference has not gone live yet. Assigned speakers can join from the green room link when the host starts.',
]);
}
$joinStatus = $room->requiresWaitingRoom($user, $role) ? 'waiting' : 'joined';
$participant = $this->sessions->joinParticipant($session, $user, $role, $displayName, $email, $joinStatus);
if ($room->isConference() && $session->session_mode === 'green_room') {
$this->conferences->registerPresenter($session, $user, $role);
}
app(InvitationService::class)->markAcceptedOnJoin($room, $user, $email);
$this->sessions->rememberParticipantSession($request, $participant, $session);
if ($participant->status === 'waiting') {
return redirect()->route('meet.join.waiting', $room);
}
return redirect()->route('meet.room', $session);
}
public function waiting(Request $request, Room $room): View|RedirectResponse
{
$room->loadMissing('organization');
$session = $room->activeSession() ?? $this->participantSessionForRoom($request, $room);
if (! $session || ! $session->isLive()) {
if ($session) {
return redirect()->route('meet.ended', $session);
}
return redirect()->route('meet.join', $room);
}
$participant = $this->participantFromSession($request, $session);
if (! $participant || $participant->status === 'removed') {
return redirect()->route('meet.join', $room);
}
if ($participant->status === 'joined') {
return redirect()->route('meet.room', $session);
}
return view('meet.join.waiting', [
'room' => $room,
'session' => $session,
'organization' => $room->organization,
'participant' => $participant,
]);
}
public function waitingStatus(Request $request, Room $room): JsonResponse
{
$session = $room->activeSession() ?? $this->participantSessionForRoom($request, $room);
if (! $session || ! $session->isLive()) {
if ($session) {
return response()->json([
'ended' => true,
'redirect' => route('meet.ended', $session),
]);
}
abort(404);
}
$participant = $this->participantFromSession($request, $session);
return response()->json([
'status' => $participant?->status ?? 'unknown',
'redirect' => ($participant && $participant->status === 'joined')
? route('meet.room', $session)
: null,
'denied' => $participant?->status === 'removed',
]);
}
protected function participantFromSession(Request $request, Session $session): ?Participant
{
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
if (! $participantUuid) {
return null;
}
return Participant::query()
->where('uuid', $participantUuid)
->where('session_id', $session->id)
->first();
}
protected function participantSessionForRoom(Request $request, Room $room): ?Session
{
foreach ($room->sessions()->orderByDesc('started_at')->get() as $session) {
if ($request->session()->has("meet.participant.{$session->uuid}")) {
return $session;
}
}
return null;
}
protected function isRoomHost(Request $request, Room $room): bool
{
$user = $request->user();
return $user && $user->ownerRef() === $room->host_user_ref;
}
protected function requiresEventsRegistration(Room $room, Request $request): bool
{
if (! $room->isEventsLinked()) {
return false;
}
if ($this->isRoomHost($request, $room)) {
return false;
}
if (! $room->isWebinar() && ! $room->isConference()) {
return false;
}
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
{
$eventId = EventsSourceLink::eventId($room);
if (! $eventId || ! $this->events->isConfigured()) {
return false;
}
$registration = $this->events->verifyRegistration($eventId, $room->owner_ref, $badgeCode);
if (! $registration) {
return false;
}
EventsRegistrationSession::store($request, $room, [
'email' => $registration['attendee_email'] ?? null,
'name' => $registration['attendee_name'] ?? null,
'badge_code' => $registration['badge_code'] ?? $badgeCode,
]);
return EventsRegistrationSession::isVerified($request, $room);
}
protected function gateView(Request $request, Room $room): View
{
$meetReturn = EventsSourceLink::meetReturnUrl($room);
$registrationUrl = EventsSourceLink::eventRegistrationUrl($room, $meetReturn);
$sessionNoun = $room->isConference() ? 'conference' : 'webinar';
return view('meet.join.gate', [
'room' => $room,
'organization' => $room->organization,
'eventTitle' => EventsSourceLink::eventTitle($room),
'registrationUrl' => $registrationUrl,
'sessionNoun' => $sessionNoun,
]);
}
protected function displayNameView(Request $request, Room $room): View
{
$registration = EventsRegistrationSession::get($request, $room);
$sessionNoun = $room->isConference() ? 'conference' : ($room->isWebinar() ? 'webinar' : 'meeting');
return view('meet.join.show', [
'room' => $room,
'organization' => $room->organization,
'user' => $request->user(),
'isWebinar' => $room->isWebinar(),
'sessionNoun' => $sessionNoun,
'defaultDisplayName' => $registration['name'] ?? '',
]);
}
}