Sync email team invites, meeting room fixes, Afia, and org settings.
Deploy Ladill Meet / deploy (push) Successful in 47s
Deploy Ladill Meet / deploy (push) Successful in 47s
Publish monorepo meet changes: identity API invites, join/room session fixes, Afia assistant panel, settings nav, and SSO team provisioning. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Member;
|
||||
use App\Models\Recording;
|
||||
use App\Models\Room;
|
||||
use App\Services\Afia\AfiaService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AiController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function chat(Request $request, AfiaService $afia): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'message' => ['required', 'string', 'max:2000'],
|
||||
'history' => ['nullable', 'array', 'max:20'],
|
||||
'history.*.role' => ['nullable', 'string'],
|
||||
'history.*.text' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if (! $afia->enabled()) {
|
||||
return response()->json(['message' => 'Afia is not available right now.'], 503);
|
||||
}
|
||||
|
||||
try {
|
||||
$reply = $afia->chat(trim($validated['message']), $validated['history'] ?? [], $this->context($request));
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json(['message' => 'Afia could not respond right now. Please try again.'], 502);
|
||||
}
|
||||
|
||||
return response()->json(['reply' => $reply]);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function context(Request $request): array
|
||||
{
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return [
|
||||
'signed_in' => 'yes',
|
||||
'organization' => $organization->name,
|
||||
'meetings_total' => Room::owned($owner)->where('organization_id', $organization->id)->count(),
|
||||
'meetings_upcoming' => Room::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('status', 'scheduled')
|
||||
->where('starts_at', '>=', now())
|
||||
->count(),
|
||||
'recordings_total' => Recording::owned($owner)
|
||||
->whereHas('session.room', fn ($q) => $q->where('organization_id', $organization->id))
|
||||
->count(),
|
||||
'team_members' => Member::owned($owner)->where('organization_id', $organization->id)->count(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Services\Meet\SessionService;
|
||||
use App\Services\Meet\WebinarService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class JoinController extends Controller
|
||||
@@ -50,7 +51,9 @@ class JoinController extends Controller
|
||||
{
|
||||
$request->validate(['passcode' => ['required', 'string']]);
|
||||
|
||||
abort_unless($room->passcode === $request->input('passcode'), 422);
|
||||
if ($room->passcode !== $request->input('passcode')) {
|
||||
return back()->withErrors(['passcode' => 'Incorrect passcode.']);
|
||||
}
|
||||
|
||||
$request->session()->put("meet.passcode.{$room->uuid}", true);
|
||||
|
||||
@@ -63,13 +66,24 @@ class JoinController extends Controller
|
||||
return redirect()->route('meet.join', $room);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'display_name' => ['required_without:auth', 'string', 'max:100'],
|
||||
$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'];
|
||||
$displayName = $user?->name ?? ($validated['display_name'] ?? 'Guest');
|
||||
$email = $user?->email ?? ($validated['email'] ?? null);
|
||||
|
||||
[$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local');
|
||||
@@ -86,12 +100,16 @@ class JoinController extends Controller
|
||||
} 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' => 'Meeting has not started yet. Please wait for the host.']);
|
||||
} else {
|
||||
abort_unless($room->activeSession(), 422, 'Meeting has not started yet.');
|
||||
$session = $room->activeSession();
|
||||
}
|
||||
} else {
|
||||
abort_unless($room->activeSession(), 422, 'Meeting has not started yet. Please wait for the host.');
|
||||
if (! $room->activeSession()) {
|
||||
return back()->withErrors(['join' => 'Meeting has not started yet. Please wait for the host.']);
|
||||
}
|
||||
|
||||
$session = $room->activeSession();
|
||||
}
|
||||
|
||||
@@ -116,7 +134,7 @@ class JoinController extends Controller
|
||||
|
||||
app(InvitationService::class)->markAcceptedOnJoin($room, $user, $email);
|
||||
|
||||
$request->session()->put("meet.participant.{$session->uuid}", $participant->uuid);
|
||||
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
||||
|
||||
return redirect()->route('meet.room', $session);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Services\Identity\IdentityTeamClient;
|
||||
use App\Services\Meet\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -51,22 +52,41 @@ class MemberController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
public function store(Request $request, IdentityTeamClient $identity): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'user_ref' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'role' => ['required', 'string', 'in:'.implode(',', array_keys(config('meet.roles')))],
|
||||
'branch_id' => ['nullable', 'integer', 'exists:meet_branches,id'],
|
||||
]);
|
||||
|
||||
$email = strtolower(trim($validated['email']));
|
||||
|
||||
if ($email === strtolower((string) $request->user()->email)) {
|
||||
return back()->withErrors(['email' => 'You cannot invite yourself.']);
|
||||
}
|
||||
|
||||
$identity->inviteAppMember(
|
||||
$owner,
|
||||
$email,
|
||||
['meet'],
|
||||
[
|
||||
'meet' => [
|
||||
'organization_id' => $organization->id,
|
||||
'role' => $validated['role'],
|
||||
'branch_id' => $validated['branch_id'] ?? null,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$member = Member::updateOrCreate(
|
||||
[
|
||||
'organization_id' => $organization->id,
|
||||
'user_ref' => $validated['user_ref'],
|
||||
'user_ref' => $email,
|
||||
],
|
||||
[
|
||||
'owner_ref' => $owner,
|
||||
@@ -75,9 +95,9 @@ class MemberController extends Controller
|
||||
],
|
||||
);
|
||||
|
||||
AuditLogger::record($owner, 'member.created', $organization->id, $owner, Member::class, $member->id);
|
||||
AuditLogger::record($owner, 'member.invited', $organization->id, $owner, Member::class, $member->id);
|
||||
|
||||
return redirect()->route('meet.members.index')->with('success', 'Member saved.');
|
||||
return redirect()->route('meet.members.index')->with('success', 'Invitation sent to '.$email.'.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Member $member): RedirectResponse
|
||||
|
||||
@@ -172,7 +172,11 @@ class RoomController extends Controller
|
||||
{
|
||||
$this->authorizeAbility($request, 'meetings.host');
|
||||
$this->authorizeOwner($request, $room);
|
||||
abort_if($room->status === 'cancelled', 422, 'Meeting was cancelled.');
|
||||
|
||||
if ($room->status === 'cancelled') {
|
||||
return redirect()->route('meet.rooms.show', $room)
|
||||
->withErrors(['start' => 'Meeting was cancelled.']);
|
||||
}
|
||||
|
||||
if ($room->isTownHall() && $room->setting('green_room')) {
|
||||
$session = app(\App\Services\Meet\TownHallService::class)->startGreenRoom($room, $request->user());
|
||||
@@ -180,6 +184,11 @@ class RoomController extends Controller
|
||||
$session = $this->sessions->start($room, $request->user());
|
||||
}
|
||||
|
||||
$participant = $this->sessions->joinedParticipantForUser($session, $request->user());
|
||||
abort_unless($participant, 500, 'Unable to join as host.');
|
||||
|
||||
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
||||
|
||||
return redirect()->route('meet.room', $session);
|
||||
}
|
||||
|
||||
@@ -204,6 +213,11 @@ class RoomController extends Controller
|
||||
|
||||
$session = $this->sessions->start($room, $request->user());
|
||||
|
||||
$participant = $this->sessions->joinedParticipantForUser($session, $request->user());
|
||||
abort_unless($participant, 500, 'Unable to join as host.');
|
||||
|
||||
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
||||
|
||||
return redirect()->route('meet.room', $session);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Meet\AuditLogger;
|
||||
use App\Services\Meet\MeetPermissions;
|
||||
use App\Services\Meet\SecurityPolicyService;
|
||||
use App\Support\OrganizationBranding;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -13,6 +17,71 @@ class SettingsController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.view');
|
||||
$organization = $this->organization($request);
|
||||
$canManage = app(MeetPermissions::class)->can($this->member($request), 'settings.manage');
|
||||
|
||||
$branchCount = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->count();
|
||||
|
||||
return view('meet.settings.edit', [
|
||||
'organization' => $organization,
|
||||
'canManage' => $canManage,
|
||||
'branchCount' => $branchCount,
|
||||
'isAdmin' => app(MeetPermissions::class)->isAdmin($this->member($request)),
|
||||
'orgTypes' => [
|
||||
'business' => 'Business',
|
||||
'healthcare' => 'Healthcare',
|
||||
'education' => 'Education',
|
||||
'nonprofit' => 'Nonprofit',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'org_type' => ['required', 'string', 'in:business,healthcare,education,nonprofit'],
|
||||
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
|
||||
'remove_logo' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
$settings = $organization->settings ?? [];
|
||||
$settings['onboarded'] = true;
|
||||
$settings['org_type'] = $validated['org_type'];
|
||||
|
||||
$logoPath = $organization->logo_path;
|
||||
|
||||
if ($request->boolean('remove_logo')) {
|
||||
OrganizationBranding::deleteStoredLogo($organization);
|
||||
$logoPath = null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
$logoPath = OrganizationBranding::storeLogo($organization, $request->file('logo'));
|
||||
}
|
||||
|
||||
$organization->update([
|
||||
'name' => $validated['name'],
|
||||
'timezone' => $validated['timezone'],
|
||||
'logo_path' => $logoPath,
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, \App\Models\Organization::class, $organization->id);
|
||||
|
||||
return back()->with('success', 'Settings saved.');
|
||||
}
|
||||
|
||||
public function security(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'meetings.manage');
|
||||
|
||||
Reference in New Issue
Block a user