Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.51",
"version": "1.6.52",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down
55 changes: 54 additions & 1 deletion config/sms.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
| 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"
| Supported: "twilio", "callpro", "vonage", "messagebird", "aws_sns",
| "smpp", "custom_http"
|
*/

Expand Down Expand Up @@ -68,5 +69,57 @@
'callpro' => [
'enabled' => env('CALLPRO_ENABLED', true),
],

'vonage' => [
'enabled' => env('VONAGE_SMS_ENABLED', false),
'api_key' => env('VONAGE_API_KEY', ''),
'api_secret' => env('VONAGE_API_SECRET', ''),
'from' => env('VONAGE_SMS_FROM', ''),
'base_url' => env('VONAGE_SMS_BASE_URL', 'https://rest.nexmo.com/sms/json'),
],

'messagebird' => [
'enabled' => env('MESSAGEBIRD_SMS_ENABLED', false),
'access_key' => env('MESSAGEBIRD_ACCESS_KEY', ''),
'originator' => env('MESSAGEBIRD_ORIGINATOR', ''),
'base_url' => env('MESSAGEBIRD_SMS_BASE_URL', 'https://rest.messagebird.com/messages'),
],

'aws_sns' => [
'enabled' => env('AWS_SNS_SMS_ENABLED', false),
'key' => env('AWS_SNS_ACCESS_KEY_ID', env('AWS_ACCESS_KEY_ID')),
'secret' => env('AWS_SNS_SECRET_ACCESS_KEY', env('AWS_SECRET_ACCESS_KEY')),
'region' => env('AWS_SNS_REGION', env('AWS_DEFAULT_REGION', 'us-east-1')),
'sender_id' => env('AWS_SNS_SMS_SENDER_ID', ''),
'sms_type' => env('AWS_SNS_SMS_TYPE', 'Transactional'),
],

'smpp' => [
'enabled' => env('SMPP_SMS_ENABLED', false),
'host' => env('SMPP_HOST', ''),
'port' => env('SMPP_PORT', 2775),
'tls' => env('SMPP_TLS', false),
'system_id' => env('SMPP_SYSTEM_ID', ''),
'password' => env('SMPP_PASSWORD', ''),
'system_type' => env('SMPP_SYSTEM_TYPE', ''),
'source_addr' => env('SMPP_SOURCE_ADDR', ''),
'bind_type' => env('SMPP_BIND_TYPE', 'transceiver'),
'interface_version' => env('SMPP_INTERFACE_VERSION', 0x34),
'timeout' => env('SMPP_TIMEOUT', 10),
],

'custom_http' => [
'enabled' => env('CUSTOM_HTTP_SMS_ENABLED', false),
'url' => env('CUSTOM_HTTP_SMS_URL', ''),
'from' => env('CUSTOM_HTTP_SMS_FROM', ''),
'auth_header' => env('CUSTOM_HTTP_SMS_AUTH_HEADER', ''),
'auth_token' => env('CUSTOM_HTTP_SMS_AUTH_TOKEN', ''),
'headers' => [],
'body' => [
'to' => '{{to}}',
'text' => '{{text}}',
'from' => '{{from}}',
],
],
],
];
16 changes: 15 additions & 1 deletion src/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Fleetbase\Support\Utils;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Str;

class Handler extends ExceptionHandler
{
Expand Down Expand Up @@ -136,7 +138,7 @@ public function getCloudwatchLoggableException(\Throwable $exception)
private function shouldManuallyHandleException(\Throwable $exception): bool
{
$type = Utils::classBasename($exception);
$exceptions = ['TokenMismatchException', 'ThrottleRequestsException', 'AuthenticationException', 'NotFoundHttpException', 'FleetbaseRequestValidationException'];
$exceptions = ['TokenMismatchException', 'ThrottleRequestsException', 'AuthenticationException', 'NotFoundHttpException', 'FleetbaseRequestValidationException', 'ModelNotFoundException'];

return in_array($type, $exceptions);
}
Expand Down Expand Up @@ -165,6 +167,9 @@ private function manuallyHandleException(\Throwable $exception): ?\Illuminate\Ht
case 'NotFoundHttpException':
return response()->error('There is nothing to see here.');

case 'ModelNotFoundException':
return response()->error($this->modelNotFoundMessage($exception), 404);

case 'FleetbaseRequestValidationException':
/** @var FleetbaseRequestValidationException $exception */
return response()->error($exception->getErrors());
Expand All @@ -173,4 +178,13 @@ private function manuallyHandleException(\Throwable $exception): ?\Illuminate\Ht
return response()->error($exception->getMessage());
}
}

private function modelNotFoundMessage(\Throwable $exception): string
{
if ($exception instanceof ModelNotFoundException && $exception->getModel()) {
return Str::headline(class_basename($exception->getModel())) . ' not found.';
}

return 'Requested resource not found.';
}
}
104 changes: 99 additions & 5 deletions src/Http/Controllers/Internal/v1/SettingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Fleetbase\Models\File;
use Fleetbase\Models\Setting;
use Fleetbase\Notifications\TestPushNotification;
use Fleetbase\Services\SmsService;
use Fleetbase\Support\Utils;
use Illuminate\Http\Request;
use Illuminate\Notifications\AnonymousNotifiable;
Expand Down Expand Up @@ -461,6 +462,7 @@ public function getServicesConfig(AdminRequest $request)
$twilioSid = config('services.twilio.sid', env('TWILIO_SID'));
$twilioToken = config('services.twilio.token', env('TWILIO_TOKEN'));
$twilioFrom = config('services.twilio.from', env('TWILIO_FROM'));
$sms = $this->getSmsServicesConfig();

/** sentry service */
$sentryDsn = config('sentry.dsn', env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')));
Expand All @@ -475,6 +477,7 @@ public function getServicesConfig(AdminRequest $request)
'twilioSid' => $twilioSid,
'twilioToken' => $twilioToken,
'twilioFrom' => $twilioFrom,
'sms' => $sms,
'sentryDsn' => $sentryDsn,
]);
}
Expand All @@ -486,16 +489,24 @@ public function getServicesConfig(AdminRequest $request)
*/
public function saveServicesConfig(AdminRequest $request)
{
$aws = $request->input('aws', config('services.aws'));
$ipinfo = $request->input('ipinfo', config('services.ipinfo'));
$googleMaps = $request->input('googleMaps', config('services.google_maps'));
$twilio = $request->input('twilio', config('services.twilio'));
$sentry = $request->input('sentry', config('sentry.dsn'));
$aws = $request->input('aws', config('services.aws'));
$ipinfo = $request->input('ipinfo', config('services.ipinfo'));
$googleMaps = $request->input('googleMaps', config('services.google_maps'));
$twilio = $request->input('twilio', config('services.twilio'));
$sentry = $request->input('sentry', config('sentry.dsn'));
$smsProviders = $request->input('sms.providers', config('services.sms.providers', config('sms.providers', [])));
$smsDefaultProvider = $request->input('sms.defaultProvider', config('sms.default_provider'));
$smsRoutingRules = $request->input('sms.routingRules', config('sms.routing_rules', []));

Setting::configureSystem('services.aws', array_merge(config('services.aws', []), $aws));
Setting::configureSystem('services.ipinfo', array_merge(config('services.ipinfo', []), $ipinfo));
Setting::configureSystem('services.google_maps', array_merge(config('services.google_maps', []), $googleMaps));
Setting::configureSystem('services.twilio', array_merge(config('services.twilio', []), $twilio));
Setting::configureSystem('services.sms', array_merge(config('services.sms', []), [
'providers' => $smsProviders,
]));
Setting::configureSystem('sms.default_provider', $smsDefaultProvider);
Setting::configureSystem('sms.routing_rules', $smsRoutingRules);
Setting::configureSystem('services.sentry', array_merge(config('sentry', []), $sentry));

// Refresh config
Expand All @@ -504,6 +515,47 @@ public function saveServicesConfig(AdminRequest $request)
return response()->json(['status' => 'OK']);
}

/**
* Sends a test SMS message using any configured SMS provider.
*
* @return \Illuminate\Http\JsonResponse
*/
public function testSmsProviderConfig(AdminRequest $request)
{
$provider = $request->input('provider', config('sms.default_provider', SmsService::PROVIDER_TWILIO));
$phone = $request->input('phone');
$message = $request->input('message', 'This is a Fleetbase SMS test.');
$config = $request->input('config', []);

if (!$phone) {
return response()->json(['status' => 'error', 'message' => 'No test phone number provided!']);
}

$this->setTemporarySmsProviderConfig($provider, is_array($config) ? $config : []);

$status = 'success';
$responseMessage = 'SMS configuration is successful, SMS sent to ' . $phone . '.';
$result = null;

try {
$result = (new SmsService())->send($phone, $message, [], $provider);
if (is_array($result) && data_get($result, 'success') === false) {
$status = 'error';
$responseMessage = data_get($result, 'error', 'SMS provider returned an error.');
}
} catch (\Throwable $e) {
$responseMessage = $e->getMessage();
$status = 'error';
}

return response()->json([
'status' => $status,
'message' => $responseMessage,
'provider' => $provider,
'result' => $result,
]);
}

/**
* Loads and sends the notification channel configurations.
*
Expand Down Expand Up @@ -766,6 +818,48 @@ public function testTwilioConfig(AdminRequest $request)
return response()->json(['status' => $status, 'message' => $message]);
}

protected function getSmsServicesConfig(): array
{
$providers = array_replace_recursive(config('sms.providers', []), config('services.sms.providers', []));

if (!isset($providers[SmsService::PROVIDER_TWILIO])) {
$providers[SmsService::PROVIDER_TWILIO] = [];
}

$providers[SmsService::PROVIDER_TWILIO] = array_replace_recursive($providers[SmsService::PROVIDER_TWILIO], config('services.twilio', []));

return [
'defaultProvider' => config('sms.default_provider', SmsService::PROVIDER_TWILIO),
'routingRules' => config('sms.routing_rules', []),
'providers' => $providers,
'available' => (new SmsService())->getAvailableProviders(),
];
}

protected function setTemporarySmsProviderConfig(string $provider, array $providerConfig): void
{
$providers = array_replace_recursive(config('sms.providers', []), config('services.sms.providers', []));
$providers[$provider] = array_replace_recursive($providers[$provider] ?? [], $providerConfig);

config([
'services.sms.providers' => $providers,
'sms.providers' => array_replace_recursive(config('sms.providers', []), $providers),
]);

if ($provider === SmsService::PROVIDER_TWILIO) {
config([
'services.twilio' => array_replace_recursive(config('services.twilio', []), $providerConfig),
'twilio.twilio.connections.twilio' => array_replace_recursive(config('twilio.twilio.connections.twilio', []), $providerConfig),
]);
}

if ($provider === SmsService::PROVIDER_CALLPRO) {
config([
'services.callpromn' => array_replace_recursive(config('services.callpromn', []), $providerConfig),
]);
}
}

/**
* Sends a test exception to Sentry.
*
Expand Down
97 changes: 97 additions & 0 deletions src/Services/AwsSnsSmsService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace Fleetbase\Services;

use Aws\Sns\SnsClient;
use Illuminate\Support\Facades\Log;

class AwsSnsSmsService
{
protected array $config;

protected ?SnsClient $client;

public function __construct(?array $config = null, ?SnsClient $client = null)
{
$awsConfig = config('services.aws', []);
$smsConfig = $config ?? config('services.sms.providers.aws_sns', config('sms.providers.aws_sns', []));
$this->config = array_merge($awsConfig, $smsConfig);
$this->client = $client;
}

public function send(string $to, string $text, ?string $from = null, array $options = []): array
{
$this->validateParameters($to, $text);

$params = [
'PhoneNumber' => $to,
'Message' => $text,
];

$senderId = $from ?: data_get($this->config, 'sender_id');
$smsType = data_get($options, 'sms_type', data_get($this->config, 'sms_type', 'Transactional'));

$attributes = array_filter([
'AWS.SNS.SMS.SenderID' => $senderId ? [
'DataType' => 'String',
'StringValue' => $senderId,
] : null,
'AWS.SNS.SMS.SMSType' => $smsType ? [
'DataType' => 'String',
'StringValue' => $smsType,
] : null,
]);

if (!empty($attributes)) {
$params['MessageAttributes'] = $attributes;
}

Log::info('Sending SMS via AWS SNS', ['to' => $to]);

$result = $this->client()->publish($params);

return [
'success' => true,
'message_id' => $result->get('MessageId'),
'result' => 'SUCCESS',
'status' => 'sent',
'response' => $result->toArray(),
];
}

public function isConfigured(): bool
{
return !empty(data_get($this->config, 'key')) && !empty(data_get($this->config, 'secret')) && !empty(data_get($this->config, 'region'));
}

protected function client(): SnsClient
{
if ($this->client) {
return $this->client;
}

return $this->client = new SnsClient([
'version' => 'latest',
'region' => data_get($this->config, 'region', env('AWS_DEFAULT_REGION', 'us-east-1')),
'credentials' => [
'key' => data_get($this->config, 'key', env('AWS_ACCESS_KEY_ID')),
'secret' => data_get($this->config, 'secret', env('AWS_SECRET_ACCESS_KEY')),
],
]);
}

protected function validateParameters(string $to, string $text): void
{
if (!$this->isConfigured()) {
throw new \InvalidArgumentException('AWS SNS SMS provider is not configured');
}

if (empty($to)) {
throw new \InvalidArgumentException('Recipient phone number (to) is required');
}

if (empty($text)) {
throw new \InvalidArgumentException('Message text cannot be empty');
}
}
}
8 changes: 5 additions & 3 deletions src/Services/CallProSmsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ class CallProSmsService
*/
public function __construct()
{
$this->apiKey = config('services.callpromn.api_key', '');
$this->from = config('services.callpromn.from', '');
$this->baseUrl = config('services.callpromn.base_url', 'https://api-text.callpro.mn/v1/sms');
$config = array_replace_recursive(config('services.callpromn', []), config('services.sms.providers.callpro', []));

$this->apiKey = data_get($config, 'api_key', '');
$this->from = data_get($config, 'from', '');
$this->baseUrl = data_get($config, 'base_url', 'https://api-text.callpro.mn/v1/sms');

Log::info('CallProSmsService initialized', [
'base_url' => $this->baseUrl,
Expand Down
Loading
Loading