Deploy Ladill Meet / deploy (push) Successful in 42s
Enforce invite-only room access, validate invite emails on create, assign attendee role for listener invitations, and add invite UI on the room show page. Redesign meetings, rooms, conferences, and webinars indexes to match the Events hero, stats cards, and icon list layout. Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
3.7 KiB
PHP
118 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Meet;
|
|
|
|
use App\Models\Member;
|
|
use App\Models\Organization;
|
|
use App\Models\Room;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Str;
|
|
|
|
class SecurityPolicyService
|
|
{
|
|
public function canJoin(Room $room, ?User $user, ?string $email, ?string $ip = null): array
|
|
{
|
|
$org = $room->organization;
|
|
$policy = $this->mergedPolicy($org, $room);
|
|
$kind = $room->isConference() ? 'conference' : ($room->isSpace() ? 'room' : 'meeting');
|
|
|
|
if ($policy['org_only'] && ! $user) {
|
|
return [false, "This {$kind} requires a Ladill account."];
|
|
}
|
|
|
|
if ($policy['authenticated_only'] && ! $user) {
|
|
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 {$kind}."];
|
|
}
|
|
}
|
|
|
|
if ($room->isSpace() && $room->setting('invite_only', false) && ! $this->canJoinInviteOnlyRoom($room, $user, $email)) {
|
|
return [false, 'This room is private. Ask the host for an invitation.'];
|
|
}
|
|
|
|
if ($room->passcode && ! session()->get("meet.passcode.{$room->uuid}")) {
|
|
return [false, 'passcode_required'];
|
|
}
|
|
|
|
if ($room->activeSession()?->is_locked && ! $this->isHost($room, $user)) {
|
|
return [false, "This {$kind} is locked."];
|
|
}
|
|
|
|
if ($policy['recording_host_only'] && ! $this->isHost($room, $user)) {
|
|
// enforced at recording start
|
|
}
|
|
|
|
return [true, null];
|
|
}
|
|
|
|
public function canJoinInviteOnlyRoom(Room $room, ?User $user, ?string $email): bool
|
|
{
|
|
if ($user && $this->isHost($room, $user)) {
|
|
return true;
|
|
}
|
|
|
|
if ($user && $room->organization_id) {
|
|
$isMember = Member::query()
|
|
->where('organization_id', $room->organization_id)
|
|
->where('user_ref', $user->ownerRef())
|
|
->exists();
|
|
|
|
if ($isMember) {
|
|
return true;
|
|
}
|
|
|
|
$speakerRefs = (array) $room->setting('speaker_refs', []);
|
|
if (in_array($user->ownerRef(), $speakerRefs, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$email = strtolower(trim((string) ($email ?? $user?->email ?? '')));
|
|
if ($email === '') {
|
|
return false;
|
|
}
|
|
|
|
return $room->invitations()
|
|
->whereRaw('LOWER(email) = ?', [$email])
|
|
->whereIn('status', ['pending', 'accepted'])
|
|
->exists();
|
|
}
|
|
|
|
public function isHost(Room $room, ?User $user): bool
|
|
{
|
|
return $user && $user->ownerRef() === $room->host_user_ref;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $policy
|
|
*/
|
|
public function updateOrganizationPolicy(Organization $org, array $policy): Organization
|
|
{
|
|
$org->update([
|
|
'security_policy' => array_merge($org->security_policy ?? [], $policy),
|
|
]);
|
|
|
|
AuditLogger::record($org->owner_ref, 'organization.security_updated', $org->id, auth()->user()?->ownerRef());
|
|
|
|
return $org->fresh();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function mergedPolicy(?Organization $org, Room $room): array
|
|
{
|
|
return array_merge(
|
|
config('meet.security_defaults'),
|
|
$org?->security_policy ?? [],
|
|
$room->settings['security'] ?? [],
|
|
);
|
|
}
|
|
}
|