From 1bc9d8b7cc974abd019fe84b43fe6b1c87711d45 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 6 Nov 2024 11:29:55 +0800 Subject: [PATCH 1/3] fix route caching by generating and providing unique name via Fleetbase `RESTRegistrar` --- composer.json | 2 +- src/Expansions/Route.php | 19 ++++------- src/Routing/RESTRegistrar.php | 62 +++++++++++++++++++++++------------ 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/composer.json b/composer.json index 67af499..339984a 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/core-api", - "version": "1.5.18", + "version": "1.5.19", "description": "Core Framework and Resources for Fleetbase API", "keywords": [ "fleetbase", diff --git a/src/Expansions/Route.php b/src/Expansions/Route.php index 9e9cca7..ac785f7 100644 --- a/src/Expansions/Route.php +++ b/src/Expansions/Route.php @@ -37,6 +37,7 @@ public function fleetbaseRestRoutes() * @return PendingResourceRegistration */ return function (string $name, $controller = null, $options = [], ?\Closure $callback = null) { + /** @var \Illuminate\Routing\Router $this */ if (is_callable($controller) && $callback === null) { $callback = $controller; $controller = null; @@ -51,9 +52,6 @@ public function fleetbaseRestRoutes() $controller = Str::studly(Str::singular($name)) . 'Controller'; } - /** - * @var \Illuminate\Routing\Router $this - */ if ($this->container && $this->container->bound(RESTRegistrar::class)) { $registrar = $this->container->make(RESTRegistrar::class); } else { @@ -67,6 +65,7 @@ public function fleetbaseRestRoutes() public function fleetbaseRoutes() { return function (string $name, callable|array|null $registerFn = null, $options = [], $controller = null) { + /** @var \Illuminate\Routing\Router $this */ if (is_array($registerFn) && !empty($registerFn) && empty($options)) { $options = $registerFn; } @@ -89,14 +88,13 @@ public function fleetbaseRoutes() $options['controller'] = $controller; } - // if (!isset($options['prefix'])) { - // $options['prefix'] = $name; - // } - $make = function (string $routeName) use ($controller) { return $controller . '@' . $routeName; }; + // Add groupstack to options + $options['groupStack'] = $this->getGroupStack(); + $register = function ($router) use ($name, $registerFn, $make, $controller, $options) { if (is_callable($registerFn)) { $router->group( @@ -110,9 +108,6 @@ function ($router) use ($registerFn, $make, $controller) { $router->fleetbaseRestRoutes($name, $controller, $options); }; - /* - * @var \Illuminate\Routing\Router $this - */ return $this->group($options, $register); }; } @@ -120,9 +115,7 @@ function ($router) use ($registerFn, $make, $controller) { public function fleetbaseAuthRoutes() { return function (?callable $registerFn = null, ?callable $registerProtectedFn = null) { - /* - * @var \Illuminate\Routing\Router $this - */ + /** @var \Illuminate\Routing\Router $this */ return $this->group( ['prefix' => 'auth'], function ($router) use ($registerFn, $registerProtectedFn) { diff --git a/src/Routing/RESTRegistrar.php b/src/Routing/RESTRegistrar.php index 75c2cd7..30cd87a 100644 --- a/src/Routing/RESTRegistrar.php +++ b/src/Routing/RESTRegistrar.php @@ -3,6 +3,8 @@ namespace Fleetbase\Routing; use Illuminate\Routing\ResourceRegistrar; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; class RESTRegistrar extends ResourceRegistrar { @@ -11,7 +13,7 @@ class RESTRegistrar extends ResourceRegistrar * * @var string[] */ - protected $resourceDefaults = ['query', 'find', 'create', 'update', 'delete', 'options']; + protected $resourceDefaults = ['query', 'find', 'create', 'update', 'delete']; /** * Build a set of prefixed resource routes. @@ -47,11 +49,15 @@ protected function prefixedResource($name, $controller = null, array $options) */ protected function addResourceQuery($name, $id, $controller, $options) { + $name = $this->getShallowName($name, $options); + $uri = $this->getResourceUri($name); $action = $this->getResourceAction($name, $controller, 'queryRecord', $options); - return $this->router->get($uri, $action); + $uniqueName = $this->getUniqueRouteName(['query', 'get'], $name, $options); + + return $this->router->get($uri, $action)->name($uniqueName); } /** @@ -74,7 +80,9 @@ protected function addResourceFind($name, $id, $controller, $options) $action = $this->getResourceAction($name, $controller, 'findRecord', $options); - return $this->router->get($uri, $action); + $uniqueName = $this->getUniqueRouteName(['find', 'get'], $name, $options); + + return $this->router->get($uri, $action)->name($uniqueName); } /** @@ -91,11 +99,15 @@ protected function addResourceFind($name, $id, $controller, $options) */ protected function addResourceCreate($name, $id, $controller, $options) { + $name = $this->getShallowName($name, $options); + $uri = $this->getResourceUri($name); $action = $this->getResourceAction($name, $controller, 'createRecord', $options); - return $this->router->post($uri, $action); + $uniqueName = $this->getUniqueRouteName(['create', 'post'], $name, $options); + + return $this->router->post($uri, $action)->name($uniqueName); } /** @@ -118,7 +130,9 @@ protected function addResourceUpdate($name, $id, $controller, $options) $action = $this->getResourceAction($name, $controller, 'updateRecord', $options); - return $this->router->match(['PUT', 'PATCH'], $uri, $action); + $uniqueName = $this->getUniqueRouteName(['update', 'put.patch'], $name, $options); + + return $this->router->match(['PUT', 'PATCH'], $uri, $action)->name($uniqueName); } /** @@ -141,30 +155,36 @@ protected function addResourceDelete($name, $id, $controller, $options) $action = $this->getResourceAction($name, $controller, 'deleteRecord', $options); - return $this->router->delete($uri, $action); + $uniqueName = $this->getUniqueRouteName(['delete', 'delete'], $name, $options); + + return $this->router->delete($uri, $action)->name($uniqueName); } /** - * Add the query method for a resourceful route. + * Generate a unique route name based on the group namespace, base name, and additional name segments. * - * OPTIONS /resource + * This function constructs a unique route name by combining the namespace from the last group stack + * (if available) with the specified base route name and any additional segments provided. + * It formats the namespace as a lowercase, hyphen-separated string, which is prepended to the route name. * - * @param string $name - * @param string $id - * @param string $controller - * @param array $options + * This approach ensures route name uniqueness within different route groups and allows for + * safe route caching when identical route names are used across multiple namespaces or prefixes. * - * @return \Illuminate\Routing\Route + * @param array $append Additional segments to append to the route name, often representing + * specific actions (e.g., ['query', 'find']). + * @param string $name the base name of the route, typically representing the resource name + * @param array $options optional settings that may contain 'groupStack', from which the + * namespace of the last group is extracted if available + * + * @return string a dot-separated string representing the unique route name */ - protected function addResourceOptions($name, $id, $controller, $options) + protected function getUniqueRouteName(array $append, string $name, array $options = []): string { - $uri = $this->getResourceUri($name); - $resourceUri = $this->getResourceUri($name) . '/{' . $id . '}'; - - $action = $this->getResourceAction($name, $controller, 'options', $options); - - $this->router->options($resourceUri, $action); + $lastGroupStack = is_array($options) && isset($options['groupStack']) ? Arr::last($options['groupStack']) : null; + $lastGroupStackNamespace = empty($lastGroupStack) ? null : $lastGroupStack['namespace']; + $groupPrefix = $lastGroupStackNamespace ? strtolower(Str::replace('\\', '-', $lastGroupStackNamespace)) : null; + $nameStack = array_filter([$groupPrefix, $name, ...$append], fn ($segment) => !empty($segment)); - return $this->router->options($uri, $action); + return implode('.', $nameStack); } } From 1058dfdf37f4ddb8b0b3cf77a52182e0686e1bde Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 7 Nov 2024 17:34:08 +0800 Subject: [PATCH 2/3] implemented impersonation feature for system admin, fixed mail settings for smtp and added support for mailgun, sendgrid, postmark and resend, removed model cache, updated test mail message --- composer.json | 7 -- config/laravel-model-caching.php | 11 --- .../Internal/v1/AuthController.php | 73 +++++++++++++++++- .../v1/CustomFieldValueController.php | 15 ++++ .../Internal/v1/SettingController.php | 74 ++++++++++++++++--- .../Requests/Internal/BulkActionRequest.php | 43 +++++++++++ src/Mail/TestMail.php | 11 ++- src/Models/Model.php | 5 +- src/Providers/CoreServiceProvider.php | 1 - src/Support/EnvironmentMapper.php | 10 +++ src/Support/Http.php | 2 +- src/routes.php | 2 + views/mail/test.blade.php | 4 +- 13 files changed, 218 insertions(+), 40 deletions(-) delete mode 100644 config/laravel-model-caching.php create mode 100644 src/Http/Controllers/Internal/v1/CustomFieldValueController.php create mode 100644 src/Http/Requests/Internal/BulkActionRequest.php diff --git a/composer.json b/composer.json index 339984a..d4fb4b1 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,6 @@ "fleetbase/twilio": "^5.0.1", "aws/aws-sdk-php-laravel": "^3.7", "fleetbase/laravel-mysql-spatial": "^1.0.2", - "genealabs/laravel-model-caching": "dev-fix/handle-datetime-values", "giggsey/libphonenumber-for-php": "^8.13", "guzzlehttp/guzzle": "^7.4", "hammerstone/fast-paginate": "^1.0", @@ -60,12 +59,6 @@ "phpstan/phpstan": "^1.10.38", "symfony/var-dumper": "^5.4.29" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/fleetbase/laravel-model-caching" - } - ], "autoload": { "psr-4": { "Fleetbase\\": "src/", diff --git a/config/laravel-model-caching.php b/config/laravel-model-caching.php deleted file mode 100644 index 3856f16..0000000 --- a/config/laravel-model-caching.php +++ /dev/null @@ -1,11 +0,0 @@ - 'fleetbase-model-cache', - - 'enabled' => env('MODEL_CACHE_ENABLED', true), - - 'use-database-keying' => env('MODEL_CACHE_USE_DATABASE_KEYING', true), - - 'store' => env('MODEL_CACHE_STORE'), -]; \ No newline at end of file diff --git a/src/Http/Controllers/Internal/v1/AuthController.php b/src/Http/Controllers/Internal/v1/AuthController.php index 9d4b460..e2402c5 100644 --- a/src/Http/Controllers/Internal/v1/AuthController.php +++ b/src/Http/Controllers/Internal/v1/AuthController.php @@ -4,6 +4,7 @@ use Fleetbase\Exceptions\InvalidVerificationCodeException; use Fleetbase\Http\Controllers\Controller; +use Fleetbase\Http\Requests\AdminRequest; use Fleetbase\Http\Requests\Internal\ResetPasswordRequest; use Fleetbase\Http\Requests\Internal\UserForgotPasswordRequest; use Fleetbase\Http\Requests\JoinOrganizationRequest; @@ -104,7 +105,12 @@ public function session(Request $request) return response()->error('Session has expired.', 401, ['restore' => false]); } - return response()->json(['token' => $request->bearerToken(), 'user' => $user->uuid, 'verified' => $user->isVerified(), 'type' => $user->getType()]); + $session = ['token' => $request->bearerToken(), 'user' => $user->uuid, 'verified' => $user->isVerified(), 'type' => $user->getType()]; + if (session()->has('impersonator')) { + $session['impersonator'] = session()->get('impersonator'); + } + + return response()->json($session); } /** @@ -676,4 +682,69 @@ public function changeUserPassword(Request $request) return response()->json(['status' => 'ok']); } + + /** + * Allows system admin to impersonate a user. + * + * @return \Illuminate\Http\Response + */ + public function impersonate(AdminRequest $request) + { + $currentUser = Auth::getUserFromSession($request); + if ($currentUser->isNotAdmin()) { + return response()->error('Not authorized to impersonate users.'); + } + + $targetUserId = $request->input('user'); + if (!$targetUserId) { + return response()->error('Not target user selected to impersonate.'); + } + + $targetUser = User::where('uuid', $targetUserId)->first(); + if (!$targetUser) { + return response()->error('The selected user to impersonate was not found.'); + } + + try { + Auth::setSession($targetUser); + session()->put('impersonator', $currentUser->uuid); + $token = $targetUser->createToken($targetUser->uuid); + } catch (\Exception $e) { + return response()->error($e->getMessage()); + } + + return response()->json(['status' => 'ok', 'token' => $token->plainTextToken]); + } + + /** + * Ends the impersonation session. + * + * @return \Illuminate\Http\Response + */ + public function endImpersonation() + { + $impersonatorId = session()->get('impersonator'); + if (!$impersonatorId) { + return response()->error('Not impersonator session found.'); + } + + $impersonator = User::where('uuid', $impersonatorId)->first(); + if (!$impersonator) { + return response()->error('The impersonator user was not found.'); + } + + if ($impersonator->isNotAdmin()) { + return response()->error('The impersonator does not have permissions. Logout.'); + } + + try { + Auth::setSession($impersonator); + session()->remove('impersonator'); + $token = $impersonator->createToken($impersonator->uuid); + } catch (\Exception $e) { + return response()->error($e->getMessage()); + } + + return response()->json(['status' => 'ok', 'token' => $token->plainTextToken]); + } } diff --git a/src/Http/Controllers/Internal/v1/CustomFieldValueController.php b/src/Http/Controllers/Internal/v1/CustomFieldValueController.php new file mode 100644 index 0000000..d12690d --- /dev/null +++ b/src/Http/Controllers/Internal/v1/CustomFieldValueController.php @@ -0,0 +1,15 @@ + $mailer, @@ -226,6 +231,22 @@ public function getMailConfig(AdminRequest $request) $config['smtp' . ucfirst($key)] = $value; } + foreach ($mailgunConfig as $key => $value) { + $config['mailgun' . ucfirst($key)] = $value; + } + + foreach ($postmarkConfig as $key => $value) { + $config['postmark' . ucfirst($key)] = $value; + } + + foreach ($sendgridConfig as $key => $value) { + $config['sendgrid' . ucfirst($key)] = $value; + } + + foreach ($resendConfig as $key => $value) { + $config['resend' . ucfirst($key)] = $value; + } + return response()->json($config); } @@ -236,13 +257,21 @@ public function getMailConfig(AdminRequest $request) */ public function saveMailConfig(AdminRequest $request) { - $mailer = $request->input('mailer', 'smtp'); - $from = $request->input('from', []); - $smtp = $request->input('smtp', []); + $mailer = $request->input('mailer', 'smtp'); + $from = $request->array('from', []); + $smtp = $request->array('smtp'); + $mailgun = $request->array('mailgun'); + $postmark = $request->array('postmark'); + $sendgrid = $request->array('sendgrid'); + $resend = $request->array('resend'); Setting::configureSystem('mail.mailer', $mailer); Setting::configureSystem('mail.from', $from); Setting::configureSystem('mail.smtp', array_merge(['transport' => 'smtp'], $smtp)); + Setting::configureSystem('services.mailgun', $mailgun); + Setting::configureSystem('services.postmark', $postmark); + Setting::configureSystem('services.sendgrid', $sendgrid); + Setting::configureSystem('services.resend', $resend); return response()->json(['status' => 'OK']); } @@ -268,16 +297,37 @@ public function testMailConfig(AdminRequest $request) 'name' => 'Fleetbase', ] ); - $smtp = $request->input('smtp', []); - $user = $request->user(); - $message = 'Mail configuration is successful, check your inbox for the test email to confirm.'; - $status = 'success'; + $smtp = $request->input('smtp', []); + $mailgun = $request->array('mailgun'); + $postmark = $request->array('postmark'); + $sendgrid = $request->array('sendgrid'); + $resend = $request->array('resend'); + $user = $request->user(); + $message = 'Mail configuration is successful, check your inbox for the test email to confirm.'; + $status = 'success'; // set config values from input config(['mail.default' => $mailer, 'mail.from' => $from, 'mail.mailers.smtp' => array_merge(['transport' => 'smtp'], $smtp)]); + // set mailer configs + if ($mailer === 'mailgun') { + config(['services.mailgun' => $mailgun]); + } + + if ($mailer === 'postmark') { + config(['services.postmark' => $postmark]); + } + + if ($mailer === 'sendgrid') { + config(['services.sendgrid' => $sendgrid]); + } + + if ($mailer === 'resend') { + config(['services.resend' => $resend]); + } + try { - Mail::send(new \Fleetbase\Mail\TestMail($user)); + Mail::send(new \Fleetbase\Mail\TestMail($user, $mailer)); } catch (\Aws\Ses\Exception\SesException|\Exception $e) { $message = $e->getMessage(); $status = 'error'; diff --git a/src/Http/Requests/Internal/BulkActionRequest.php b/src/Http/Requests/Internal/BulkActionRequest.php new file mode 100644 index 0000000..d92e0ce --- /dev/null +++ b/src/Http/Requests/Internal/BulkActionRequest.php @@ -0,0 +1,43 @@ +session()->has('user'); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'ids' => ['required', 'array'], + ]; + } + + /** + * Get the validation rules error messages. + * + * @return array + */ + public function messages() + { + return [ + 'ids.required' => 'Please provide a resource ID.', + 'ids.array' => 'Please provide multiple resource ID\'s.', + ]; + } +} diff --git a/src/Mail/TestMail.php b/src/Mail/TestMail.php index 3cd0d6e..d33730f 100644 --- a/src/Mail/TestMail.php +++ b/src/Mail/TestMail.php @@ -26,12 +26,18 @@ class TestMail extends Mailable implements ShouldQueue */ public User $user; + /** + * The mailer used to send the email. + */ + public string $sendingMailer; + /** * Creates an instance of the TestMail. */ - public function __construct(User $user) + public function __construct(User $user, string $sendingMailer = 'smtp') { - $this->user = $user; + $this->user = $user; + $this->sendingMailer = $sendingMailer; } /** @@ -54,6 +60,7 @@ public function content(): Content markdown: 'fleetbase::mail.test', with: [ 'user' => $this->user, + 'mailer' => $this->sendingMailer, 'currentHour' => now()->hour, ] ); diff --git a/src/Models/Model.php b/src/Models/Model.php index 1ad6cf5..e0560ee 100644 --- a/src/Models/Model.php +++ b/src/Models/Model.php @@ -8,7 +8,6 @@ use Fleetbase\Traits\HasCacheableAttributes; use Fleetbase\Traits\Insertable; use Fleetbase\Traits\Searchable; -use GeneaLabs\LaravelModelCaching\Traits\Cachable; use Illuminate\Database\Eloquent\Model as EloquentModel; use Illuminate\Database\Eloquent\SoftDeletes; @@ -19,9 +18,7 @@ class Model extends EloquentModel use ClearsHttpCache; use Insertable; use Filterable; - use Expandable, Cachable { - Expandable::__call insteadof Cachable; - } + use Expandable; /** * Create a new instance of the model. diff --git a/src/Providers/CoreServiceProvider.php b/src/Providers/CoreServiceProvider.php index 64ee102..c1fef01 100644 --- a/src/Providers/CoreServiceProvider.php +++ b/src/Providers/CoreServiceProvider.php @@ -106,7 +106,6 @@ public function register() $this->mergeConfigFrom(__DIR__ . '/../../config/excel.php', 'excel'); $this->mergeConfigFrom(__DIR__ . '/../../config/sentry.php', 'sentry'); $this->mergeConfigFrom(__DIR__ . '/../../config/laravel-mysql-s3-backup.php', 'laravel-mysql-s3-backup'); - $this->mergeConfigFrom(__DIR__ . '/../../config/laravel-model-caching.php', 'laravel-model-caching'); $this->mergeConfigFrom(__DIR__ . '/../../config/responsecache.php', 'responsecache'); } diff --git a/src/Support/EnvironmentMapper.php b/src/Support/EnvironmentMapper.php index af4b664..0615840 100644 --- a/src/Support/EnvironmentMapper.php +++ b/src/Support/EnvironmentMapper.php @@ -46,6 +46,12 @@ class EnvironmentMapper 'GOOGLE_CLOUD_STORAGE_API_URI' => 'filesystem.gcs.storage_api_uri', 'SENTRY_DSN' => 'services.sentry.dsn', 'IPINFO_API_KEY' => 'services.ipinfo.api_key', + 'MAILGUN_DOMAIN' => 'services.mailgun.domain', + 'MAILGUN_SECRET' => 'services.mailgun.secret', + 'MAILGUN_ENDPOINT' => 'services.mailgun.endpoint', + 'POSTMARK_TOKEN' => 'services.postmark.token', + 'SENDGRID_API_KEY' => 'services.sendgrid.api_key', + 'RESEND_KEY' => 'services.resend.key', ]; /** @@ -86,6 +92,10 @@ class EnvironmentMapper ['settingsKey' => 'services.twilio', 'configKey' => 'twilio.twilio.connections.twilio'], ['settingsKey' => 'services.ipinfo', 'configKey' => 'services.ipinfo'], ['settingsKey' => 'services.ipinfo', 'configKey' => 'fleetbase.services.ipinfo'], + ['settingsKey' => 'services.mailgun', 'configKey' => 'services.mailgun'], + ['settingsKey' => 'services.postmark', 'configKey' => 'services.postmark'], + ['settingsKey' => 'services.sendgrid', 'configKey' => 'services.sendgrid'], + ['settingsKey' => 'services.resend', 'configKey' => 'services.resend'], ['settingsKey' => 'services.sentry.dsn', 'configKey' => 'sentry.dsn'], ['settingsKey' => 'broadcasting.apn', 'configKey' => 'broadcasting.connections.apn'], ['settingsKey' => 'firebase.app', 'configKey' => 'firebase.projects.app'], diff --git a/src/Support/Http.php b/src/Support/Http.php index 8e3e7c5..d9b5d00 100644 --- a/src/Support/Http.php +++ b/src/Support/Http.php @@ -137,7 +137,7 @@ public static function lookupIp($ip = null) public static function action(?string $verb = null) { - $verb = $verb ?? $_SERVER['REQUEST_METHOD']; + $verb = $verb ? $verb : (isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null); $action = Str::lower($verb); switch ($verb) { diff --git a/src/routes.php b/src/routes.php index 4115471..53e8f4e 100644 --- a/src/routes.php +++ b/src/routes.php @@ -144,6 +144,8 @@ function ($router) { ['prefix' => 'auth'], function ($router) { $router->post('change-user-password', 'AuthController@changeUserPassword'); + $router->post('impersonate', 'AuthController@impersonate'); + $router->delete('impersonate', 'AuthController@endImpersonation'); } ); $router->fleetbaseRoutes( diff --git a/views/mail/test.blade.php b/views/mail/test.blade.php index 089c2eb..53b3d72 100644 --- a/views/mail/test.blade.php +++ b/views/mail/test.blade.php @@ -9,5 +9,7 @@ @endif -🎉 This is a test email from Fleetbase to confirm that your mail configuration works. +

🎉 This is a test email from Fleetbase to confirm that your mail configuration works.

+

Test Email sent using Mailer: {{ Str::title($mailer) }}

+

Environment: {{ app()->environment() }}

\ No newline at end of file From 3b3597d5c1991f59d1bc7dad9f74fea95810998a Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 7 Nov 2024 17:38:35 +0800 Subject: [PATCH 3/3] improve the test mail html a bit --- views/mail/test.blade.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/views/mail/test.blade.php b/views/mail/test.blade.php index 53b3d72..0692326 100644 --- a/views/mail/test.blade.php +++ b/views/mail/test.blade.php @@ -10,6 +10,16 @@

🎉 This is a test email from Fleetbase to confirm that your mail configuration works.

-

Test Email sent using Mailer: {{ Str::title($mailer) }}

-

Environment: {{ app()->environment() }}

+ + + + + + + + + + + +
MAILER:{{ strtoupper($mailer) }}
ENVIRONMENT:{{ strtoupper(app()->environment()) }}
\ No newline at end of file