Deploy Ladill Meet / deploy (push) Successful in 43s
Attendees register or sign in with a badge before the display-name screen; email is no longer collected on the Meet join form. Co-authored-by: Cursor <cursoragent@cursor.com>
394 lines
14 KiB
PHP
394 lines
14 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.',
|
|
]);
|
|
}
|
|
|
|
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 ($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::isVerified($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'] ?? '',
|
|
]);
|
|
}
|
|
}
|