Files
ladill-meet/app/Http/Controllers/Meet/JoinController.php
T
isaaccladandCursor 799c302e2a
Deploy Ladill Meet / deploy (push) Successful in 50s
Improve conferences, guest entry, Afia, and cross-app scheduling.
Route guests through silent SSO to the Meet product page, fix Afia context
query, add conference green-room UX and copy, and extend service API for
Care/CRM/Invoice calendar integration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 20:12:57 +00:00

290 lines
11 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\Meet\ConferenceService;
use App\Services\Meet\InvitationService;
use App\Services\Meet\SecurityPolicyService;
use App\Services\Meet\SessionService;
use App\Services\Meet\SpaceService;
use App\Services\Meet\WebinarService;
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 WebinarService $webinars,
protected SpaceService $spaces,
protected ConferenceService $conferences,
) {}
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);
}
}
return view('meet.join.show', [
'room' => $room,
'organization' => $room->organization,
'user' => $request->user(),
'isWebinar' => $room->isWebinar(),
]);
}
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);
}
$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();
$displayName = $user?->name ?? ($validated['display_name'] ?? 'Guest');
$email = $user?->email ?? ($validated['email'] ?? null);
$kind = $room->isConference() ? 'conference' : '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') {
$token = $request->session()->get("meet.webinar.{$room->uuid}");
$reg = $token
? $this->webinars->findByAccessToken($token)
: ($email ? $this->webinars->findApprovedRegistration($room, $email) : null);
if (! $reg) {
return redirect()->route('meet.webinar.register', $room)
->withErrors(['register' => 'Approved registration is required for this webinar.']);
}
$role = 'attendee';
$displayName = $reg->display_name;
}
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;
}
}