Improve conferences, guest entry, Afia, and cross-app scheduling.
Deploy Ladill Meet / deploy (push) Successful in 50s

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>
This commit is contained in:
isaacclad
2026-07-01 20:12:57 +00:00
co-authored by Cursor
parent be8f76cd47
commit 799c302e2a
32 changed files with 601 additions and 89 deletions
@@ -17,6 +17,11 @@ class MailCalendarProvider
return null;
}
return $this->createEventForMailbox($mailbox, $room);
}
public function createEventForMailbox(string $mailbox, Room $room): ?string
{
$response = Http::withToken((string) config('meet.calendar.mail.api_key'))
->acceptJson()
->timeout(10)
@@ -32,13 +37,18 @@ class MailCalendarProvider
}
public function updateEvent(CalendarConnection $connection, Room $room, User $host): bool
{
$mailbox = $connection->calendar_id ?? $host->email;
return $mailbox ? $this->updateEventForMailbox($mailbox, $room) : false;
}
public function updateEventForMailbox(string $mailbox, Room $room): bool
{
if (! $room->calendar_event_id) {
return false;
}
$mailbox = $connection->calendar_id ?? $host->email;
return Http::withToken((string) config('meet.calendar.mail.api_key'))
->acceptJson()
->timeout(10)
@@ -47,6 +57,11 @@ class MailCalendarProvider
}
public function deleteEvent(CalendarConnection $connection, Room $room): bool
{
return $this->deleteEventForMailbox($connection->calendar_id, $room);
}
public function deleteEventForMailbox(?string $mailbox, Room $room): bool
{
if (! $room->calendar_event_id) {
return false;
@@ -55,10 +70,10 @@ class MailCalendarProvider
return Http::withToken((string) config('meet.calendar.mail.api_key'))
->acceptJson()
->timeout(10)
->delete($this->baseUrl().'/calendar/events/'.$room->calendar_event_id, [
'mailbox_email' => $connection->calendar_id,
->delete($this->baseUrl().'/calendar/events/'.$room->calendar_event_id, array_filter([
'mailbox_email' => $mailbox ? strtolower(trim($mailbox)) : null,
'external_ref' => $room->uuid,
])
]))
->successful();
}
+30 -11
View File
@@ -20,35 +20,54 @@ class CalendarService
public function syncRoomCreated(Room $room, User $host): void
{
$connection = $this->connectionFor($host);
if (! $connection) {
if (! $room->scheduled_at) {
return;
}
$eventId = $this->provider($connection)->createEvent($connection, $room, $host);
if ($eventId) {
$room->update(['calendar_event_id' => $eventId]);
if ($host->email) {
$eventId = $this->mail->createEventForMailbox($host->email, $room);
if ($eventId) {
$room->update(['calendar_event_id' => $eventId]);
}
}
$connection = $this->connectionFor($host);
if ($connection && in_array($connection->provider, ['google', 'microsoft'], true)) {
$this->provider($connection)->createEvent($connection, $room, $host);
}
}
public function syncRoomUpdated(Room $room, User $host): void
{
$connection = $this->connectionFor($host);
if (! $connection || ! $room->calendar_event_id) {
if (! $room->scheduled_at) {
return;
}
$this->provider($connection)->updateEvent($connection, $room, $host);
if ($host->email) {
if ($room->calendar_event_id) {
$this->mail->updateEventForMailbox($host->email, $room);
} else {
$this->syncRoomCreated($room, $host);
}
}
$connection = $this->connectionFor($host);
if ($connection && $room->calendar_event_id && in_array($connection->provider, ['google', 'microsoft'], true)) {
$this->provider($connection)->updateEvent($connection, $room, $host);
}
}
public function syncRoomCancelled(Room $room, User $host): void
{
if ($host->email && $room->calendar_event_id) {
$this->mail->deleteEventForMailbox($host->email, $room);
}
$connection = $this->connectionFor($host);
if (! $connection || ! $room->calendar_event_id) {
return;
if ($connection && $room->calendar_event_id && in_array($connection->provider, ['google', 'microsoft'], true)) {
$this->provider($connection)->deleteEvent($connection, $room);
}
$this->provider($connection)->deleteEvent($connection, $room);
$room->update(['calendar_event_id' => null]);
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace App\Services\Meet;
use App\Models\Invitation;
use App\Models\Participant;
use App\Models\Room;
use App\Models\Session;
use App\Models\User;
class ConferenceService
{
/**
* @return list<string>
*/
public function presenterRefs(Room $room): array
{
return array_values(array_unique(array_filter(
(array) $room->setting('presenter_refs', []),
)));
}
public function resolveJoinRole(Room $room, ?User $user, ?string $email, string $fallbackRole = 'guest'): string
{
if ($user && $user->ownerRef() === $room->host_user_ref) {
return 'host';
}
$presenterRefs = $this->presenterRefs($room);
if ($user && in_array($user->ownerRef(), $presenterRefs, true)) {
return 'panelist';
}
if ($email && $this->invitationRole($room, $email) === 'panelist') {
return 'panelist';
}
if ($room->isTownHall()) {
return 'attendee';
}
return $fallbackRole;
}
public function canJoinSession(Room $room, Session $session, string $role): bool
{
if ($session->session_mode !== 'green_room') {
return true;
}
return in_array($role, ['host', 'co_host', 'panelist'], true);
}
public function isPresenter(Session $session, Participant $participant): bool
{
if (in_array($participant->role, ['host', 'co_host', 'panelist'], true)) {
return true;
}
$refs = (array) $session->presenter_refs;
return $participant->user_ref && in_array($participant->user_ref, $refs, true);
}
public function canPublish(Room $room, Participant $participant): bool
{
return in_array($participant->role, ['host', 'co_host', 'panelist'], true);
}
public function registerPresenter(Session $session, ?User $user, string $role): void
{
if (! $user || ! in_array($role, ['host', 'co_host', 'panelist'], true)) {
return;
}
app(TownHallService::class)->addPresenter($session, $user->ownerRef());
}
protected function invitationRole(Room $room, string $email): ?string
{
return Invitation::query()
->where('room_id', $room->id)
->where('email', strtolower(trim($email)))
->value('role');
}
}
@@ -36,6 +36,7 @@ class ParticipantPresenter
{
return [
'uuid' => $participant->uuid,
'user_ref' => $participant->user_ref,
'display_name' => $participant->display_name,
'role' => $participant->role,
'status' => $participant->status,
+5 -4
View File
@@ -13,20 +13,21 @@ class SecurityPolicyService
{
$org = $room->organization;
$policy = $this->mergedPolicy($org, $room);
$kind = $room->isConference() ? 'conference' : 'meeting';
if ($policy['org_only'] && ! $user) {
return [false, 'This meeting requires a Ladill account.'];
return [false, "This {$kind} requires a Ladill account."];
}
if ($policy['authenticated_only'] && ! $user) {
return [false, 'Please sign in to join this meeting.'];
return [false, "Please sign in to join this {$kind}."];
}
if (! empty($policy['allowed_domains']) && $email) {
$domain = Str($email)->after('@')->lower()->toString();
$allowed = collect($policy['allowed_domains'])->map(fn ($d) => strtolower(trim($d)))->filter();
if ($allowed->isNotEmpty() && ! $allowed->contains($domain)) {
return [false, 'Your email domain is not allowed to join this meeting.'];
return [false, "Your email domain is not allowed to join this {$kind}."];
}
}
@@ -35,7 +36,7 @@ class SecurityPolicyService
}
if ($room->activeSession()?->is_locked && ! $this->isHost($room, $user)) {
return [false, 'This meeting is locked.'];
return [false, "This {$kind} is locked."];
}
if ($policy['recording_host_only'] && ! $this->isHost($room, $user)) {