From 5aaede9f693d90c89962b56e8ba00075d85ae1d3 Mon Sep 17 00:00:00 2001 From: Prem Kumar Sharma Date: Tue, 2 Jun 2026 16:50:53 +0530 Subject: [PATCH 1/2] fix: reuse lifecycle event payload for webhooks Compute the resource lifecycle payload once in SendResourceLifecycleWebhook and use the same value for both the persisted ApiEvent data and the outbound webhook request body. This prevents webhook deliveries from diverging from the api_events.data record when the event snapshot and live model payload differ. --- src/Listeners/SendResourceLifecycleWebhook.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Listeners/SendResourceLifecycleWebhook.php b/src/Listeners/SendResourceLifecycleWebhook.php index f125ce1e..cfd6c2f2 100644 --- a/src/Listeners/SendResourceLifecycleWebhook.php +++ b/src/Listeners/SendResourceLifecycleWebhook.php @@ -37,12 +37,18 @@ public function handle($event) $apiEnvironment = session()->get('api_environment', $event->apiEnvironment ?? 'live'); $isSandbox = session()->get('is_sandbox', $event->isSandbox); + // Compute the event payload exactly once so the persisted ApiEvent record and the + // outbound webhook body are guaranteed to be identical. $event->getEventData() resolves + // the model live at handle time, whereas $event->data is a snapshot frozen at dispatch + // time; using each in a different place lets the DB record and the webhook diverge. + $payload = $event->getEventData(); + // Prepare event $eventData = [ 'company_uuid' => $companyId, 'event' => $event->broadcastAs(), 'source' => $apiCredentialId ? 'api' : 'console', - 'data' => $event->getEventData(), + 'data' => $payload, 'method' => $event->requestMethod, 'description' => $this->getHumanReadableEventDescription($event), ]; @@ -100,7 +106,7 @@ public function handle($event) 'sent_at' => Carbon::now(), ]) ->url($webhook->url) - ->payload($event->data) + ->payload($payload) ->useSecret($apiSecret) ->dispatch(); } catch (\Exception|\Aws\Sqs\Exception\SqsException $exception) { From 1948c2939ee39d879ce6e18a4b01771f669daae9 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 4 Jun 2026 21:17:03 +0800 Subject: [PATCH 2/2] v1.6.50 --- composer.json | 2 +- .../Controllers/Api/v1/CommentController.php | 32 ++++++--- .../Internal/v1/AuthController.php | 8 +++ .../Internal/v1/ChatChannelController.php | 72 +++++++++++++++++++ .../Internal/v1/LookupController.php | 2 +- src/Http/Filter/CommentFilter.php | 7 ++ src/Http/Requests/CreateCommentRequest.php | 14 ++-- src/Http/Resources/ChatChannel.php | 2 +- src/routes.php | 1 + 9 files changed, 125 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index d8b26ee6..f6216637 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/core-api", - "version": "1.6.49", + "version": "1.6.50", "description": "Core Framework and Resources for Fleetbase API", "keywords": [ "fleetbase", diff --git a/src/Http/Controllers/Api/v1/CommentController.php b/src/Http/Controllers/Api/v1/CommentController.php index 8c648140..24fd93ce 100644 --- a/src/Http/Controllers/Api/v1/CommentController.php +++ b/src/Http/Controllers/Api/v1/CommentController.php @@ -23,10 +23,10 @@ public function create(CreateCommentRequest $request) { $content = $request->input('content'); $subject = $request->input('subject', [ - 'id' => $request->input('subject_id'), + 'id' => $request->input('subject_id') ?: $request->input('subject_uuid'), 'type' => $request->input('subject_type'), ]); - $parent = $request->input('parent'); + $parent = $request->input('parent') ?: $request->input('parent_comment_uuid'); // Prepare comment creation data $data = [ @@ -38,7 +38,14 @@ public function create(CreateCommentRequest $request) // Resolve the parent $parentComment = null; if ($parent) { - $parentComment = Comment::where(['public_id' => $parent, 'company_uuid' => session('company')])->first(); + $parentComment = Comment::where('company_uuid', session('company')) + ->where(function ($query) use ($parent) { + $query->where('uuid', $parent) + ->orWhere('public_id', $parent) + ->orWhere('id', $parent); + }) + ->first(); + if ($parentComment) { $data['parent_comment_uuid'] = $parentComment->uuid; $data['subject_uuid'] = $parentComment->subject_uuid; @@ -47,14 +54,19 @@ public function create(CreateCommentRequest $request) } // Resolve the subject - if ($subject && !$parentComment) { + if ($subject && data_get($subject, 'id') && data_get($subject, 'type') && !$parentComment) { $subjectClass = Utils::getMutationType(data_get($subject, 'type')); $subjectUuid = null; if ($subjectClass) { - $subjectUuid = Utils::getUuid(app($subjectClass)->getTable(), [ - 'public_id' => data_get($subject, 'id'), - 'company_uuid' => session('company'), - ]); + $subjectId = data_get($subject, 'id'); + $subjectUuid = app($subjectClass) + ->newQuery() + ->where('company_uuid', session('company')) + ->where(function ($query) use ($subjectId) { + $query->where('uuid', $subjectId) + ->orWhere('public_id', $subjectId); + }) + ->value('uuid'); } // If on subject found @@ -66,6 +78,10 @@ public function create(CreateCommentRequest $request) $data['subject_type'] = $subjectClass; } + if (empty($data['subject_uuid']) || empty($data['subject_type'])) { + return response()->apiError('Invalid subject provided for comment.'); + } + // create the comment try { $comment = Comment::publish($data); diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index e35b9730..03a0cb4e 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -61,6 +61,10 @@ public function login(LoginRequest $request) $tokenOwner instanceof User && ($tokenOwner->email === $identity || $tokenOwner->phone === $identity) ) { + if ($tokenOwner->type === 'customer') { + return response()->error('Customer accounts must sign in through the customer portal.', 403, ['code' => 'customer_login_not_allowed']); + } + return response()->json([ 'token' => $authToken, 'type' => $tokenOwner->getType(), @@ -78,6 +82,10 @@ public function login(LoginRequest $request) $query->where('email', $identity)->orWhere('phone', $identity); })->first(); + if ($user && $user->type === 'customer') { + return response()->error('Customer accounts must sign in through the customer portal.', 403, ['code' => 'customer_login_not_allowed']); + } + // If the user exists but has no password set (e.g. SSO-invited or provisioned // accounts), silently fall through to the generic credentials error below. // This guard MUST come before isInvalidPassword() which has a strict string diff --git a/src/Http/Controllers/Internal/v1/ChatChannelController.php b/src/Http/Controllers/Internal/v1/ChatChannelController.php index 3f571123..a342c43c 100644 --- a/src/Http/Controllers/Internal/v1/ChatChannelController.php +++ b/src/Http/Controllers/Internal/v1/ChatChannelController.php @@ -3,7 +3,10 @@ namespace Fleetbase\Http\Controllers\Internal\v1; use Fleetbase\Http\Controllers\FleetbaseController; +use Fleetbase\Http\Resources\User as UserResource; use Fleetbase\Models\ChatChannel; +use Fleetbase\Models\ChatParticipant; +use Fleetbase\Models\User; use Illuminate\Http\Request; class ChatChannelController extends FleetbaseController @@ -15,6 +18,75 @@ class ChatChannelController extends FleetbaseController */ public $resource = 'chat_channel'; + /** + * Creates a chat channel and optional initial participants. + * + * @return \Fleetbase\Http\Resources\ChatChannel|\Illuminate\Http\Response + */ + public function createRecord(Request $request) + { + try { + $name = $request->input('chatChannel.name'); + $meta = $request->input('chatChannel.meta', []); + $participants = $request->array('chatChannel.participants'); + + $chatChannel = ChatChannel::create([ + 'company_uuid' => session('company'), + 'created_by_uuid' => session('user'), + 'name' => $name, + 'meta' => $meta, + ]); + + foreach ($participants as $userId) { + $user = User::where('uuid', $userId)->orWhere('public_id', $userId)->first(); + + if (!$user || $user->uuid === session('user')) { + continue; + } + + ChatParticipant::firstOrCreate([ + 'company_uuid' => session('company'), + 'user_uuid' => $user->uuid, + 'chat_channel_uuid' => $chatChannel->uuid, + ]); + } + + $chatChannel->load(['participants.user', 'lastMessage']); + $this->resource::wrap($this->resourceSingularlName); + + return new $this->resource($chatChannel); + } catch (\Exception $e) { + return response()->error(app()->hasDebugModeEnabled() ? $e->getMessage() : 'Unable to create chat channel.'); + } + } + + /** + * Query users available for a new or existing chat channel. + * + * @return \Fleetbase\Http\Resources\UserCollection + */ + public function getAvailableParticipants(Request $request) + { + $query = $request->input('query'); + $chatChannelId = $request->input('channel'); + $chatChannel = $chatChannelId ? ChatChannel::where('uuid', $chatChannelId)->orWhere('public_id', $chatChannelId)->first() : null; + + $users = User::whereHas('companyUsers', function ($query) { + $query->where('company_uuid', session('company')); + }) + ->where('uuid', '!=', session('user')) + ->when($query, function ($builder) use ($query) { + $builder->search($query); + }); + + if ($chatChannel) { + $participantUserUuids = $chatChannel->participants()->pluck('user_uuid'); + $users->whereNotIn('uuid', $participantUserUuids); + } + + return UserResource::collection($users->limit(25)->get()); + } + /** * Retrieves the unread message count for a specific chat channel. * diff --git a/src/Http/Controllers/Internal/v1/LookupController.php b/src/Http/Controllers/Internal/v1/LookupController.php index 464231b8..7ea60540 100644 --- a/src/Http/Controllers/Internal/v1/LookupController.php +++ b/src/Http/Controllers/Internal/v1/LookupController.php @@ -189,7 +189,7 @@ public function fleetbaseBlog(Request $request) { $limit = max(1, min($request->integer('limit', 6), 20)); $cacheKey = $this->getFleetbaseBlogCacheKey($limit); - $cacheTTL = now()->addDays(4); // 4 days as requested + $cacheTTL = now()->addDays(1); // Try to get from cache $posts = Cache::remember($cacheKey, $cacheTTL, function () use ($limit) { diff --git a/src/Http/Filter/CommentFilter.php b/src/Http/Filter/CommentFilter.php index ae42a204..da327424 100644 --- a/src/Http/Filter/CommentFilter.php +++ b/src/Http/Filter/CommentFilter.php @@ -30,6 +30,13 @@ public function subjectUuid(string $id) $this->builder->where('subject_uuid', $id); } + public function subjectType(string $type) + { + $resolved = Utils::getMutationType($type); + + $this->builder->where('subject_type', $resolved ?: $type); + } + public function parent(string $id) { if (Str::isUuid($id)) { diff --git a/src/Http/Requests/CreateCommentRequest.php b/src/Http/Requests/CreateCommentRequest.php index 2de63141..6d16696e 100644 --- a/src/Http/Requests/CreateCommentRequest.php +++ b/src/Http/Requests/CreateCommentRequest.php @@ -25,16 +25,22 @@ public function rules() { return [ 'subject' => [Rule::requiredIf(function () { - return !$this->filled('subject_id') && !$this->filled('subject_type') && !$this->filled('parent') && $this->isMethod('POST'); + return !$this->filled('subject_id') && !$this->filled('subject_uuid') && !$this->filled('subject_type') && !$this->filled('parent') && !$this->filled('parent_comment_uuid') && $this->isMethod('POST'); })], 'subject_id' => [Rule::requiredIf(function () { - return !$this->filled('parent') && !$this->filled('subject') && $this->isMethod('POST'); + return !$this->filled('subject_uuid') && !$this->filled('parent') && !$this->filled('parent_comment_uuid') && !$this->filled('subject') && $this->isMethod('POST'); + })], + 'subject_uuid' => [Rule::requiredIf(function () { + return !$this->filled('subject_id') && !$this->filled('parent') && !$this->filled('parent_comment_uuid') && !$this->filled('subject') && $this->isMethod('POST'); })], 'subject_type' => [Rule::requiredIf(function () { - return !$this->filled('parent') && !$this->filled('subject') && $this->isMethod('POST'); + return !$this->filled('parent') && !$this->filled('parent_comment_uuid') && !$this->filled('subject') && $this->isMethod('POST'); })], 'parent' => [Rule::requiredIf(function () { - return !$this->filled('subject') && !$this->filled('subject_type') && !$this->filled('subject_id') && $this->isMethod('POST'); + return !$this->filled('parent_comment_uuid') && !$this->filled('subject') && !$this->filled('subject_type') && !$this->filled('subject_id') && !$this->filled('subject_uuid') && $this->isMethod('POST'); + })], + 'parent_comment_uuid' => [Rule::requiredIf(function () { + return !$this->filled('parent') && !$this->filled('subject') && !$this->filled('subject_type') && !$this->filled('subject_id') && !$this->filled('subject_uuid') && $this->isMethod('POST'); })], 'content' => ['required'], ]; diff --git a/src/Http/Resources/ChatChannel.php b/src/Http/Resources/ChatChannel.php index b64aefa0..055ddf5d 100644 --- a/src/Http/Resources/ChatChannel.php +++ b/src/Http/Resources/ChatChannel.php @@ -28,7 +28,7 @@ public function toArray($request) 'created_by' => $this->when(Http::isPublicRequest(), fn () => $this->createdBy ? $this->createdBy->public_id : null), 'name' => $this->name, 'title' => $this->title, - 'last_message' => new ChatMessage($this->last_message), + 'last_message' => $this->last_message ? new ChatMessage($this->last_message) : null, 'unread_count' => $this->when($user, fn () => $this->getUnreadMessageCountForUser($user)), 'slug' => $this->slug, 'feed' => $this->resource_feed, diff --git a/src/routes.php b/src/routes.php index 8e8b50d2..233ad35a 100644 --- a/src/routes.php +++ b/src/routes.php @@ -257,6 +257,7 @@ function ($router, $controller) { $router->fleetbaseRoutes('custom-fields'); $router->fleetbaseRoutes('custom-field-values'); $router->fleetbaseRoutes('chat-channels', function ($router, $controller) { + $router->get('available-participants', $controller('getAvailableParticipants')); $router->get('unread-count/{channelId}', $controller('getUnreadCountForChannel')); $router->get('unread-count', $controller('getUnreadCount')); });