Fix guest join flow redirecting to Care marketing page.
Deploy Ladill Meet / deploy (push) Successful in 52s

Guests were sent through silent SSO after entering the meeting room because
/room routes required auth; failed SSO fell back to leftover Care config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-01 12:05:41 +00:00
co-authored by Cursor
parent 08c4974a31
commit ba77acdb9d
7 changed files with 95 additions and 46 deletions
@@ -88,7 +88,11 @@ class SsoLoginController extends Controller
if ($request->filled('error')) {
if (in_array($request->query('error'), ['login_required', 'interaction_required', 'consent_required'], true)
&& ! $request->boolean('interactive')) {
return redirect()->away((string) config('ladill.marketing_url'));
return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
'fallback' => 1,
]);
}
return $this->finishCallback($request, $intended, (string) $request->query('error_description', $request->query('error')));
@@ -285,6 +289,10 @@ class SsoLoginController extends Controller
private function resolveLanding(IdentityTeamClient $identity, User $user, string $intended): string
{
if ($this->isMeetJoinUrl($intended)) {
return $intended;
}
try {
return $identity->postAuthRedirect($user->ownerRef(), $intended);
} catch (\Throwable) {
@@ -292,6 +300,14 @@ class SsoLoginController extends Controller
}
}
private function isMeetJoinUrl(string $url): bool
{
$meetHost = config('app.meet_domain', 'meet.ladill.com');
return str_starts_with($url, 'https://'.$meetHost.'/r/')
|| str_starts_with($url, 'http://'.$meetHost.'/r/');
}
private function safeReturnUrl(string $url): string
{
$host = parse_url($url, PHP_URL_HOST);
@@ -68,6 +68,7 @@ class EnsurePlatformSession
return redirect()->route('sso.connect', [
'redirect' => $request->fullUrl(),
'interactive' => 1,
]);
}
}
+9 -3
View File
@@ -54,9 +54,15 @@ return Application::configure(basePath: dirname(__DIR__))
},
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [
'redirect' => $request->fullUrl(),
]));
$middleware->redirectGuestsTo(function (Request $request) {
$params = ['redirect' => $request->fullUrl()];
if ($request->is('r/*', 'room/*')) {
$params['interactive'] = 1;
}
return route('sso.connect', $params);
});
$middleware->web(append: [
SetActingAccount::class,
]);
+2 -2
View File
@@ -1,6 +1,6 @@
<?php
return [
'product_slug' => 'care',
'marketing_url' => env('LADILL_MARKETING_URL', 'https://ladill.com/products/care'),
'product_slug' => 'meet',
'marketing_url' => env('LADILL_MARKETING_URL', 'https://ladill.com'),
];
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="max-w-md rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<h1 class="text-xl font-semibold text-slate-900">Unable to join</h1>
<p class="mt-2 text-sm text-slate-600">{{ $reason ?? 'You do not have access to this meeting.' }}</p>
<a href="{{ route('sso.connect', ['redirect' => route('meet.join', $room)]) }}" class="btn-primary mt-6 inline-block">Sign in</a>
<a href="{{ route('sso.connect', ['redirect' => route('meet.join', $room), 'interactive' => 1]) }}" class="btn-primary mt-6 inline-block">Sign in</a>
</div>
</body>
</html>
+39 -39
View File
@@ -27,7 +27,7 @@ use Illuminate\Support\Facades\Route;
Route::get('/', fn () => auth()->check()
? redirect()->route('meet.dashboard')
: redirect()->route('sso.connect'))->name('meet.root');
: redirect()->route('sso.connect', ['interactive' => 1]))->name('meet.root');
Route::get('/login', [SsoLoginController::class, 'connect'])->name('login');
Route::get('/sso/connect', [SsoLoginController::class, 'connect'])->name('sso.connect');
@@ -48,6 +48,44 @@ Route::match(['get', 'post'], '/invite/{invitation}/rsvp', [InvitationController
Route::get('/w/{room}/register', [WebinarRegistrationController::class, 'show'])->name('meet.webinar.register');
Route::post('/w/{room}/register', [WebinarRegistrationController::class, 'store'])->name('meet.webinar.register.store');
Route::get('/room/{session}', [MeetingRoomController::class, 'show'])->name('meet.room');
Route::post('/room/{session}/end', [MeetingRoomController::class, 'end'])->name('meet.room.end');
Route::post('/room/{session}/leave', [MeetingRoomController::class, 'leave'])->name('meet.room.leave');
Route::post('/room/{session}/raise-hand', [MeetingRoomController::class, 'raiseHand'])->name('meet.room.raise-hand');
Route::post('/room/{session}/chat', [MeetingRoomController::class, 'sendMessage'])->name('meet.room.chat');
Route::post('/room/{session}/reaction', [MeetingRoomController::class, 'sendReaction'])->name('meet.room.reaction');
Route::get('/room/{session}/poll', [MeetingRoomController::class, 'poll'])->name('meet.room.poll');
Route::post('/room/{session}/recording/start', [MeetingRoomController::class, 'startRecording'])->name('meet.room.recording.start');
Route::post('/room/{session}/recording/stop', [MeetingRoomController::class, 'stopRecording'])->name('meet.room.recording.stop');
Route::post('/room/{session}/lock', [MeetingRoomController::class, 'lock'])->name('meet.room.lock');
Route::post('/room/{session}/unlock', [MeetingRoomController::class, 'unlock'])->name('meet.room.unlock');
Route::post('/room/{session}/caption', [MeetingRoomController::class, 'caption'])->name('meet.room.caption');
Route::post('/room/{session}/qa', [MeetingFeaturesController::class, 'submitQuestion'])->name('meet.room.qa.submit');
Route::post('/room/{session}/qa/{question}/approve', [MeetingFeaturesController::class, 'approveQuestion'])->name('meet.room.qa.approve');
Route::post('/room/{session}/qa/{question}/answer', [MeetingFeaturesController::class, 'answerQuestion'])->name('meet.room.qa.answer');
Route::post('/room/{session}/qa/{question}/upvote', [MeetingFeaturesController::class, 'upvoteQuestion'])->name('meet.room.qa.upvote');
Route::post('/room/{session}/polls', [MeetingFeaturesController::class, 'createPoll'])->name('meet.room.polls.create');
Route::post('/room/{session}/polls/{poll}/vote', [MeetingFeaturesController::class, 'votePoll'])->name('meet.room.polls.vote');
Route::post('/room/{session}/polls/{poll}/close', [MeetingFeaturesController::class, 'closePoll'])->name('meet.room.polls.close');
Route::post('/room/{session}/breakouts', [MeetingFeaturesController::class, 'createBreakouts'])->name('meet.room.breakouts.create');
Route::post('/room/{session}/breakouts/{breakout}/join', [MeetingFeaturesController::class, 'moveToBreakout'])->name('meet.room.breakouts.join');
Route::post('/room/{session}/breakouts/return', [MeetingFeaturesController::class, 'returnFromBreakout'])->name('meet.room.breakouts.return');
Route::post('/room/{session}/breakouts/broadcast', [MeetingFeaturesController::class, 'broadcastBreakout'])->name('meet.room.breakouts.broadcast');
Route::post('/room/{session}/breakouts/close', [MeetingFeaturesController::class, 'closeBreakouts'])->name('meet.room.breakouts.close');
Route::post('/room/{session}/files', [MeetingFeaturesController::class, 'uploadFile'])->name('meet.room.files.upload');
Route::get('/room/{session}/files/{file}', [MeetingFeaturesController::class, 'downloadFile'])->name('meet.room.files.download');
Route::get('/room/{session}/whiteboard', [MeetingFeaturesController::class, 'showWhiteboard'])->name('meet.room.whiteboard.show');
Route::post('/room/{session}/whiteboard', [MeetingFeaturesController::class, 'saveWhiteboard'])->name('meet.room.whiteboard.save');
Route::post('/room/{session}/whiteboard/export', [MeetingFeaturesController::class, 'exportWhiteboard'])->name('meet.room.whiteboard.export');
Route::post('/room/{session}/live-stream', [MeetingFeaturesController::class, 'configureLiveStream'])->name('meet.room.livestream.configure');
Route::post('/room/{session}/live-stream/start', [MeetingFeaturesController::class, 'startLiveStream'])->name('meet.room.livestream.start');
Route::post('/room/{session}/live-stream/stop', [MeetingFeaturesController::class, 'stopLiveStream'])->name('meet.room.livestream.stop');
Route::post('/room/{session}/town-hall/green-room', [MeetingFeaturesController::class, 'startGreenRoom'])->name('meet.room.townhall.green');
Route::post('/room/{session}/town-hall/go-live', [MeetingFeaturesController::class, 'goLiveTownHall'])->name('meet.room.townhall.live');
Route::post('/room/{session}/town-hall/handoff', [MeetingFeaturesController::class, 'handoffCoHost'])->name('meet.room.townhall.handoff');
Route::middleware(['auth', 'platform.session'])->group(function () {
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
Route::get('/notifications/unread', [NotificationController::class, 'unread'])->name('notifications.unread');
@@ -131,44 +169,6 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
Route::get('/settings', [SettingsController::class, 'edit'])->name('meet.settings');
Route::put('/settings', [SettingsController::class, 'update'])->name('meet.settings.update');
Route::get('/room/{session}', [MeetingRoomController::class, 'show'])->name('meet.room');
Route::post('/room/{session}/end', [MeetingRoomController::class, 'end'])->name('meet.room.end');
Route::post('/room/{session}/leave', [MeetingRoomController::class, 'leave'])->name('meet.room.leave');
Route::post('/room/{session}/raise-hand', [MeetingRoomController::class, 'raiseHand'])->name('meet.room.raise-hand');
Route::post('/room/{session}/chat', [MeetingRoomController::class, 'sendMessage'])->name('meet.room.chat');
Route::post('/room/{session}/reaction', [MeetingRoomController::class, 'sendReaction'])->name('meet.room.reaction');
Route::get('/room/{session}/poll', [MeetingRoomController::class, 'poll'])->name('meet.room.poll');
Route::post('/room/{session}/recording/start', [MeetingRoomController::class, 'startRecording'])->name('meet.room.recording.start');
Route::post('/room/{session}/recording/stop', [MeetingRoomController::class, 'stopRecording'])->name('meet.room.recording.stop');
Route::post('/room/{session}/lock', [MeetingRoomController::class, 'lock'])->name('meet.room.lock');
Route::post('/room/{session}/unlock', [MeetingRoomController::class, 'unlock'])->name('meet.room.unlock');
Route::post('/room/{session}/caption', [MeetingRoomController::class, 'caption'])->name('meet.room.caption');
Route::post('/room/{session}/qa', [MeetingFeaturesController::class, 'submitQuestion'])->name('meet.room.qa.submit');
Route::post('/room/{session}/qa/{question}/approve', [MeetingFeaturesController::class, 'approveQuestion'])->name('meet.room.qa.approve');
Route::post('/room/{session}/qa/{question}/answer', [MeetingFeaturesController::class, 'answerQuestion'])->name('meet.room.qa.answer');
Route::post('/room/{session}/qa/{question}/upvote', [MeetingFeaturesController::class, 'upvoteQuestion'])->name('meet.room.qa.upvote');
Route::post('/room/{session}/polls', [MeetingFeaturesController::class, 'createPoll'])->name('meet.room.polls.create');
Route::post('/room/{session}/polls/{poll}/vote', [MeetingFeaturesController::class, 'votePoll'])->name('meet.room.polls.vote');
Route::post('/room/{session}/polls/{poll}/close', [MeetingFeaturesController::class, 'closePoll'])->name('meet.room.polls.close');
Route::post('/room/{session}/breakouts', [MeetingFeaturesController::class, 'createBreakouts'])->name('meet.room.breakouts.create');
Route::post('/room/{session}/breakouts/{breakout}/join', [MeetingFeaturesController::class, 'moveToBreakout'])->name('meet.room.breakouts.join');
Route::post('/room/{session}/breakouts/return', [MeetingFeaturesController::class, 'returnFromBreakout'])->name('meet.room.breakouts.return');
Route::post('/room/{session}/breakouts/broadcast', [MeetingFeaturesController::class, 'broadcastBreakout'])->name('meet.room.breakouts.broadcast');
Route::post('/room/{session}/breakouts/close', [MeetingFeaturesController::class, 'closeBreakouts'])->name('meet.room.breakouts.close');
Route::post('/room/{session}/files', [MeetingFeaturesController::class, 'uploadFile'])->name('meet.room.files.upload');
Route::get('/room/{session}/files/{file}', [MeetingFeaturesController::class, 'downloadFile'])->name('meet.room.files.download');
Route::get('/room/{session}/whiteboard', [MeetingFeaturesController::class, 'showWhiteboard'])->name('meet.room.whiteboard.show');
Route::post('/room/{session}/whiteboard', [MeetingFeaturesController::class, 'saveWhiteboard'])->name('meet.room.whiteboard.save');
Route::post('/room/{session}/whiteboard/export', [MeetingFeaturesController::class, 'exportWhiteboard'])->name('meet.room.whiteboard.export');
Route::post('/room/{session}/live-stream', [MeetingFeaturesController::class, 'configureLiveStream'])->name('meet.room.livestream.configure');
Route::post('/room/{session}/live-stream/start', [MeetingFeaturesController::class, 'startLiveStream'])->name('meet.room.livestream.start');
Route::post('/room/{session}/live-stream/stop', [MeetingFeaturesController::class, 'stopLiveStream'])->name('meet.room.livestream.stop');
Route::post('/room/{session}/town-hall/green-room', [MeetingFeaturesController::class, 'startGreenRoom'])->name('meet.room.townhall.green');
Route::post('/room/{session}/town-hall/go-live', [MeetingFeaturesController::class, 'goLiveTownHall'])->name('meet.room.townhall.live');
Route::post('/room/{session}/town-hall/handoff', [MeetingFeaturesController::class, 'handoffCoHost'])->name('meet.room.townhall.handoff');
Route::get('/members', [MemberController::class, 'index'])->name('meet.members.index');
Route::get('/members/create', [MemberController::class, 'create'])->name('meet.members.create');
Route::post('/members', [MemberController::class, 'store'])->name('meet.members.store');
+26
View File
@@ -126,6 +126,32 @@ class MeetWebTest extends TestCase
->assertSessionHasErrors('join');
}
public function test_guest_can_join_live_meeting_and_enter_room(): void
{
$room = $this->createRoom([
'settings' => array_merge(config('meet.default_settings'), [
'join_before_host' => true,
]),
]);
$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->post('/r/'.$room->uuid.'/enter', [
'display_name' => 'Guest User',
])
->assertRedirect(route('meet.room', $session));
$this->get('/room/'.$session->uuid)
->assertOk()
->assertSee('data-display-name="Guest User"', false);
}
public function test_authenticated_user_can_join_live_meeting_without_display_name(): void
{
$room = $this->createRoom();