Skip to content
Merged
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
"version": "1.6.30",
"version": "1.6.31",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down
72 changes: 72 additions & 0 deletions config/sms.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Default SMS Provider
|--------------------------------------------------------------------------
|
| This option controls the default SMS provider that will be used to send
| messages. You may set this to any of the providers defined below.
|
| Supported: "twilio", "callpro"
|
*/

'default_provider' => env('SMS_DEFAULT_PROVIDER', 'twilio'),

/*
|--------------------------------------------------------------------------
| SMS Provider Routing Rules
|--------------------------------------------------------------------------
|
| Define routing rules based on phone number prefixes. When a phone number
| matches a prefix, it will be routed to the specified provider.
|
| Format: 'prefix' => 'provider'
|
| Example:
| '+976' => 'callpro', // Mongolia numbers route to CallPro
| '+1' => 'twilio', // USA/Canada numbers route to Twilio
|
*/

'routing_rules' => [
'+976' => 'callpro', // Mongolia
],

/*
|--------------------------------------------------------------------------
| Throw On Error
|--------------------------------------------------------------------------
|
| When set to true, SMS sending failures will throw exceptions. When false,
| errors will be logged but the application will continue execution.
|
| This is useful for non-critical SMS notifications where you don't want
| to interrupt the user flow if SMS delivery fails.
|
*/

'throw_on_error' => env('SMS_THROW_ON_ERROR', false),

/*
|--------------------------------------------------------------------------
| Provider Configurations
|--------------------------------------------------------------------------
|
| Here you may configure the SMS providers used by your application.
| Each provider has its own configuration options.
|
*/

'providers' => [
'twilio' => [
'enabled' => env('TWILIO_ENABLED', true),
],

'callpro' => [
'enabled' => env('CALLPRO_ENABLED', true),
],
],
];
20 changes: 15 additions & 5 deletions src/Http/Controllers/Internal/v1/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,11 @@ public function session(Request $request)
}

$sessionData = [
'token' => $request->bearerToken(),
'user' => $user->uuid,
'verified' => $user->isVerified(),
'type' => $user->getType(),
'token' => $request->bearerToken(),
'user' => $user->uuid,
'verified' => $user->isVerified(),
'type' => $user->getType(),
'last_modified' => $user->updated_at,
];

if (session()->has('impersonator')) {
Expand All @@ -132,7 +133,15 @@ public function session(Request $request)
return response()->error('Session has expired.', 401, ['restore' => false]);
}

return response()->json($session)->header('Cache-Control', 'private, max-age=300'); // 5 minutes
// Generate an etag
$etag = sha1(json_encode($session));

return response()
->json($session)
->setEtag($etag)
->setLastModified($session['last_modified'])
->header('Cache-Control', 'private, no-cache, must-revalidate')
->header('X-Cache-Hit', 'false');
}

/**
Expand Down Expand Up @@ -661,6 +670,7 @@ public function getUserOrganizations(Request $request)
*/
$etagPayload = $companies->map(function ($company) {
$ownerTimestamp = $company->owner?->updated_at?->timestamp ?? 0;

return $company->uuid . ':' . $company->updated_at->timestamp . ':' . $ownerTimestamp;
})->join('|');

Expand Down
27 changes: 22 additions & 5 deletions src/Http/Controllers/Internal/v1/FileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,15 +312,32 @@ public function uploadBase64(UploadBase64FileRequest $request)
/**
* Handle file download.
*
* @return \Illuminate\Http\Response
* Supports both:
* - /download/{id}
* - /download?file={id}
*
* @param string|null $id File UUID from route parameter
*
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function download(?string $id, DownloadFileRequest $request)
public function download(DownloadFileRequest $request, ?string $id = null)
{
$disk = $request->input('disk', config('filesystems.default'));
$file = File::where('uuid', $id)->first();
// Resolve file ID from route or query string
$fileId = $id ?? $request->query('file');
if (!$fileId) {
abort(400, 'Missing file identifier.');
}

$disk = $request->input('disk', config('filesystems.default'));
$file = File::where('uuid', $fileId)->firstOrFail();
$filename = $file->original_filename ?: basename($file->path);

/** @var \Illuminate\Filesystem\FilesystemAdapter $filesystem */
$filesystem = Storage::disk($disk);

return $filesystem->download($file->path, $file->original_filename);
return $filesystem->download(
$file->path,
$filename
);
}
}
6 changes: 3 additions & 3 deletions src/Http/Controllers/Internal/v1/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
use Fleetbase\Models\User;
use Fleetbase\Notifications\UserAcceptedCompanyInvite;
use Fleetbase\Notifications\UserInvited;
use Fleetbase\Services\UserCacheService;
use Fleetbase\Support\Auth;
use Fleetbase\Support\NotificationRegistry;
use Fleetbase\Support\TwoFactorAuth;
use Fleetbase\Support\Utils;
use Fleetbase\Services\UserCacheService;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
Expand Down Expand Up @@ -181,7 +181,7 @@ public function current(Request $request)
$etag = UserCacheService::generateETag($user);

// Try to get from server cache
$companyId = session('company');
$companyId = session('company');
$cachedData = UserCacheService::get($user->id, $companyId);

if ($cachedData) {
Expand All @@ -198,7 +198,7 @@ public function current(Request $request)
$user->loadCompanyUser();

// Transform to resource
$userData = new $this->resource($user);
$userData = new $this->resource($user);
$userArray = $userData->toArray($request);

// Store in cache
Expand Down
8 changes: 1 addition & 7 deletions src/Http/Middleware/ValidateETag.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Fleetbase\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ValidateETag
Expand All @@ -13,13 +12,8 @@ class ValidateETag
* This middleware checks if the client sent an If-None-Match header with an ETag.
* If the ETag matches the response ETag, it returns a 304 Not Modified response,
* allowing the browser to use its cached version.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
public function handle(Request $request, \Closure $next)
{
// Handle the request and get response
$response = $next($request);
Expand Down
32 changes: 30 additions & 2 deletions src/Http/Requests/Internal/DownloadFileRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ public function authorize()
return $this->session()->has('user');
}

/**
* Prepare the data for validation.
*
* Ensures route parameters are available for validation rules.
*/
protected function prepareForValidation(): void
{
if ($this->route('id')) {
$this->merge([
'id' => $this->route('id'),
]);
}
}

/**
* Get the validation rules that apply to the request.
*
Expand All @@ -24,7 +38,9 @@ public function authorize()
public function rules()
{
return [
'id' => ['required', 'string', 'exists:files,uuid'],
'file' => ['required_without:id', 'uuid', 'exists:files,uuid'],
'id' => ['required_without:file', 'uuid', 'exists:files,uuid'],
'disk' => ['sometimes', 'string'],
];
}

Expand All @@ -36,8 +52,20 @@ public function rules()
public function messages()
{
return [
'id.required' => 'Please provide a file ID.',
// Missing identifier
'id.required_without' => 'Please provide a file identifier.',
'file.required_without' => 'Please provide a file identifier.',

// Invalid format
'id.uuid' => 'The file identifier must be a valid UUID.',
'file.uuid' => 'The file identifier must be a valid UUID.',

// File not found
'id.exists' => 'The requested file does not exist.',
'file.exists' => 'The requested file does not exist.',

// Disk override
'disk.string' => 'The storage disk must be a valid string.',
];
}
}
30 changes: 26 additions & 4 deletions src/Models/VerificationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Fleetbase\Casts\Json;
use Fleetbase\Mail\VerificationMail;
use Fleetbase\Services\SmsService;
use Fleetbase\Support\Utils;
use Fleetbase\Traits\Expirable;
use Fleetbase\Traits\HasMetaAttributes;
Expand Down Expand Up @@ -180,9 +181,10 @@ public static function generateSmsVerificationFor($subject, $for = 'phone_verifi
$message = $messageCallback($verificationCode);
}

// Twilio params
$twilioParams = [];
// SMS service options
$smsOptions = [];

// Check for company-specific sender ID
$companyUuid = data_get($options, 'company_uuid') ?? session('company') ?? data_get($subject, 'company_uuid');
if ($companyUuid) {
$company = Company::select(['uuid', 'options'])->find($companyUuid);
Expand All @@ -192,13 +194,33 @@ public static function generateSmsVerificationFor($subject, $for = 'phone_verifi
$senderId = $company->getOption('alpha_numeric_sender_id');

if ($enabled && !empty($senderId)) {
$twilioParams['from'] = $senderId;
// Alphanumeric sender IDs are Twilio-specific
// Do NOT set in $smsOptions['from'] as it would be passed to all providers
$smsOptions['twilioParams']['from'] = $senderId;
}
}
}

// Allow explicit provider selection
$provider = data_get($options, 'provider');

// Send SMS using SmsService with automatic provider routing
if ($subject->phone) {
Twilio::message($subject->phone, $message, [], $twilioParams);
try {
$smsService = new SmsService();
$smsService->send($subject->phone, $message, $smsOptions, $provider);
} catch (\Throwable $e) {
// Log error but don't fail the verification code generation
\Illuminate\Support\Facades\Log::error('Failed to send SMS verification', [
'phone' => $subject->phone,
'error' => $e->getMessage(),
]);

// Optionally rethrow based on configuration
if (config('sms.throw_on_error', false)) {
throw $e;
}
}
}

return $verificationCode;
Expand Down
12 changes: 0 additions & 12 deletions src/Observers/UserObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ class UserObserver
{
/**
* Handle the User "updated" event.
*
* @param \Fleetbase\Models\User $user
*
* @return void
*/
public function updated(User $user): void
{
Expand Down Expand Up @@ -46,10 +42,6 @@ public function deleted(User $user)

/**
* Handle the User "restored" event.
*
* @param \Fleetbase\Models\User $user
*
* @return void
*/
public function restored(User $user): void
{
Expand All @@ -66,10 +58,6 @@ public function restored(User $user): void
* This clears the cached organizations list which includes owner relationships.
* When a user updates their profile and they are an owner of organizations,
* the cached organization data needs to be refreshed to reflect the updated owner info.
*
* @param \Fleetbase\Models\User $user
*
* @return void
*/
private function invalidateOrganizationsCache(User $user): void
{
Expand Down
1 change: 1 addition & 0 deletions src/Providers/CoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public function register()
$this->mergeConfigFrom(__DIR__ . '/../../config/laravel-mysql-s3-backup.php', 'laravel-mysql-s3-backup');
$this->mergeConfigFrom(__DIR__ . '/../../config/responsecache.php', 'responsecache');
$this->mergeConfigFrom(__DIR__ . '/../../config/image.php', 'image');
$this->mergeConfigFrom(__DIR__ . '/../../config/sms.php', 'sms');

// setup report schema registry
$this->app->singleton(ReportSchemaRegistry::class, function () {
Expand Down
Loading