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/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) { 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')); });