diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index a59a12d..b117b66 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -10,6 +10,7 @@ $rules = [ '@Symfony' => true, 'phpdoc_no_empty_return' => false, + 'phpdoc_to_comment' => false, 'array_syntax' => ['syntax' => 'short'], 'yoda_style' => false, 'binary_operator_spaces' => [ diff --git a/composer.json b/composer.json index 834c334..950c5d8 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/core-api", - "version": "1.4.24", + "version": "1.4.25", "description": "Core Framework and Resources for Fleetbase API", "keywords": [ "fleetbase", diff --git a/config/laravel-model-caching.php b/config/laravel-model-caching.php index 62f2658..3856f16 100644 --- a/config/laravel-model-caching.php +++ b/config/laravel-model-caching.php @@ -1,7 +1,7 @@ '', + 'cache-prefix' => 'fleetbase-model-cache', 'enabled' => env('MODEL_CACHE_ENABLED', true), diff --git a/config/responsecache.php b/config/responsecache.php index 1771101..f46be37 100644 --- a/config/responsecache.php +++ b/config/responsecache.php @@ -79,7 +79,7 @@ * * You may use a string or an array here. */ - 'cache_tag' => '', + 'cache_tag' => 'fleetbase-responsecache', /* * This class is responsible for generating a hash for a request. This hash diff --git a/src/Expansions/Request.php b/src/Expansions/Request.php index c6044db..ded0166 100644 --- a/src/Expansions/Request.php +++ b/src/Expansions/Request.php @@ -4,17 +4,20 @@ use Fleetbase\Build\Expansion; use Fleetbase\Models\Company; +use Fleetbase\Models\File; use Illuminate\Support\Str; /** + * Expands the Illuminate\Http\Request class with additional helper methods. + * * @mixin \Illuminate\Support\Facades\Request */ class Request implements Expansion { /** - * Get the target class to expand. + * Specifies the class this expansion targets. * - * @return string|Class + * @return string the name of the class to expand */ public static function target() { @@ -22,9 +25,9 @@ public static function target() } /** - * Extends Request to find the current organization/company. + * Retrieves the current company based on the session data. * - * @return Company|null + * @return \Closure returns a closure that resolves to a Company instance or null */ public function company() { @@ -39,21 +42,15 @@ public function company() } /** - * Iterates request params until a param is found. + * Attempts to retrieve the first available parameter from a specified set. * - * @return Closure + * @return \Closure returns a closure that checks for the presence of parameters + * in a specific order and returns the value of the first parameter found */ public function or() { - /* - * Iterates request params until a param is found. - * - * @param array $params - * @param mixed $default - * @return mixed - */ return function (array $params = [], $default = null) { - /* @var \Illuminate\Http\Request $this */ + /** @var \Illuminate\Http\Request $this */ foreach ($params as $param) { if ($this->has($param)) { return $this->input($param); @@ -65,18 +62,13 @@ public function or() } /** - * Retrieve input from the request as a array. + * Converts a specified request parameter into an array by splitting it by commas. * - * @return Closure + * @return \Closure returns a closure that splits a string parameter into an array, + * or directly returns the array parameter */ public function array() { - /* - * Retrieve input from the request as a array. - * - * @param string $param - * @return array - */ return function (string $param) { /** @var \Illuminate\Http\Request $this */ if (is_string($this->input($param)) && Str::contains($this->input($param), ',')) { @@ -88,52 +80,40 @@ public function array() } /** - * Check if param is string value. + * Checks if a specified parameter is a string. * - * @return Closure + * @return \Closure returns a closure that determines if a parameter is a string */ public function isString() { return function ($param) { - /* - * Context. - * - * @var \Illuminate\Support\Facades\Request $this - */ + /** @var \Illuminate\Http\Request $this */ return $this->has($param) && is_string($this->input($param)); }; } /** - * Check if param is array value. + * Checks if a specified parameter is an array. * - * @return Closure + * @return \Closure returns a closure that determines if a parameter is an array */ public function isArray() { return function ($param) { - /* - * Context. - * - * @var \Illuminate\Support\Facades\Request $this - */ + /** @var \Illuminate\Http\Request $this */ return $this->has($param) && is_array($this->input($param)); }; } /** - * Check value exists in request array param. + * Checks if a specific value exists within an array parameter. * - * @return Closure + * @return \Closure returns a closure that checks if a value is present in an array parameter */ public function inArray() { return function ($param, $needle) { - /** - * Context. - * - * @var \Illuminate\Support\Facades\Request $this - */ + /** @var \Illuminate\Http\Request $this */ $haystack = (array) $this->input($param, []); if (is_array($haystack)) { @@ -145,55 +125,39 @@ public function inArray() } /** - * Retrieve input from the request as a integer. + * Retrieves an integer value from a specified request parameter. * - * @return Closure + * @return \Closure returns a closure that fetches an integer from the request */ public function integer() { - /* - * Retrieve input from the request as a integer. - * - * @param string $key - * @return array - */ return function (string $key, $default = 0) { - /* @var \Illuminate\Http\Request $this */ + /** @var \Illuminate\Http\Request $this */ return intval($this->input($key, $default)); }; } /** - * Removes a param from the request. + * Removes a specified parameter from the request. * - * @return Closure + * @return \Closure returns a closure that removes a parameter from the request */ public function removeParam() { - /* - * Retrieve input from the request as a integer. - * - * @param string $key - * @return array - */ return function (string $key) { - /* @var \Illuminate\Http\Request $this */ + /** @var \Illuminate\Http\Request $this */ return $this->request->remove($key); }; } /** - * Retrieves the search query parameter. + * Retrieves the search query from the request, with prioritization over multiple possible keys. * - * @return Closure + * @return \Closure returns a closure that fetches a search query parameter, prioritizing + * specific keys and handling potential casing and encoding issues */ public function searchQuery() { - /* - * Retrieve the search query parameter. - * - * @return string - */ return function () { /** @var \Illuminate\Http\Request $this */ $searchQueryParam = $this->or(['query', 'searchQuery', 'nestedQuery']); @@ -207,9 +171,22 @@ public function searchQuery() } /** - * Returns all Fleetbase global filters. + * Fetches File models based on UUIDs provided in a specified request parameter. * - * @return Closure + * @return \Closure returns a closure that retrieves a collection of File models from UUIDs specified in the request + */ + public function resolveFilesFromIds() + { + return function (string $param = 'files') { + /** @var \Illuminate\Http\Request $this */ + return File::fromRequest($this, $param); + }; + } + + /** + * Retrieves all request parameters except for those related to Fleetbase's global filters. + * + * @return \Closure returns a closure that filters out global parameters and retrieves the rest */ public function getFilters() { @@ -244,7 +221,7 @@ public function getFilters() ]; $filters = is_array($additionalFilters) ? array_merge($defaultFilters, $additionalFilters) : $defaultFilters; - /* @var \Illuminate\Http\Request $this */ + /** @var \Illuminate\Http\Request $this */ return $this->except($filters); }; } diff --git a/src/Http/Middleware/ClearCacheAfterDelete.php b/src/Http/Middleware/ClearCacheAfterDelete.php new file mode 100644 index 0000000..df022f7 --- /dev/null +++ b/src/Http/Middleware/ClearCacheAfterDelete.php @@ -0,0 +1,23 @@ +isMethod('delete')) { + // Clear the cache after the response has been sent + ResponseCache::clear(); + } + + return $response; + } +} diff --git a/src/Http/Requests/ImportRequest.php b/src/Http/Requests/ImportRequest.php index cea5116..a1cc5bc 100644 --- a/src/Http/Requests/ImportRequest.php +++ b/src/Http/Requests/ImportRequest.php @@ -2,6 +2,8 @@ namespace Fleetbase\Http\Requests; +use Fleetbase\Models\File; + class ImportRequest extends FleetbaseRequest { /** @@ -22,7 +24,21 @@ public function authorize() public function rules() { return [ - 'files' => ['required', 'array'] + 'files' => ['required', 'array', 'exists:files,uuid', + function ($attribute, $value, $fail) { + foreach ($value as $uuid) { + $file = File::where('uuid', $uuid)->first(); + if (!$file) { + return $fail('One of the files sent for import is invalid.'); + } + + $validExtensions = ['csv', 'tsv', 'xls', 'xlsx']; + $extension = pathinfo($file->path, PATHINFO_EXTENSION); + if (!in_array($extension, $validExtensions)) { + return $fail('The file (' . $file->original_filename . ') format with the extension ' . $extension . ' is not valid for import.'); + } + } + }], ]; } } diff --git a/src/Http/Requests/OnboardRequest.php b/src/Http/Requests/OnboardRequest.php index ad68fc5..2897f46 100644 --- a/src/Http/Requests/OnboardRequest.php +++ b/src/Http/Requests/OnboardRequest.php @@ -14,7 +14,7 @@ class OnboardRequest extends FleetbaseRequest * * @return array */ - protected $excludedWords = ['test', 'testing', 'example', 'trial', 'trialing', 'asdf', '1234', 'asdas', 'dsdsds']; + protected $excludedWords = ['test', 'test123', 'abctest', 'testing', 'example', 'trial', 'trialing', 'asdf', '1234', 'asdas', 'dsdsds', 'dummy', 'xxxx', 'aaa', 'demo', 'zzz', 'zzzz', 'none']; /** * Determine if the user is authorized to make this request. diff --git a/src/Jobs/LogApiRequest.php b/src/Jobs/LogApiRequest.php index e6d85d2..eef590d 100644 --- a/src/Jobs/LogApiRequest.php +++ b/src/Jobs/LogApiRequest.php @@ -14,6 +14,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Spatie\ResponseCache\Facades\ResponseCache; class LogApiRequest implements ShouldQueue { @@ -58,6 +59,8 @@ public function handle() { // Log::info('Logging API Request ' . print_r($this->payload, true)); ApiRequestLog::on($this->dbConnection)->create($this->payload); + // Clear response cache + ResponseCache::clear(); } /** diff --git a/src/Mail/VerificationMail.php b/src/Mail/VerificationMail.php index b687af7..ab470c4 100644 --- a/src/Mail/VerificationMail.php +++ b/src/Mail/VerificationMail.php @@ -58,6 +58,7 @@ public function content(): Content 'currentHour' => now()->hour, 'user' => $this->verificationCode->subject, 'code' => $this->verificationCode->code, + 'type' => $this->verificationCode->for, 'content' => $this->content, ] ); diff --git a/src/Models/ApiCredential.php b/src/Models/ApiCredential.php index 7ec81b3..e717f04 100644 --- a/src/Models/ApiCredential.php +++ b/src/Models/ApiCredential.php @@ -172,4 +172,14 @@ public static function generateKeys($encode, $testKey = false) 'secret' => $hash, ]; } + + /** + * Update the datetime of the last usage. + * + * @return bool + */ + public function trackLastUsed() + { + return $this->update(['last_used_at' => now()]); + } } diff --git a/src/Models/ApiEvent.php b/src/Models/ApiEvent.php index 73e87ab..98c736c 100644 --- a/src/Models/ApiEvent.php +++ b/src/Models/ApiEvent.php @@ -5,12 +5,14 @@ use Fleetbase\Casts\Json; use Fleetbase\Traits\Filterable; use Fleetbase\Traits\HasApiModelBehavior; +use Fleetbase\Traits\HasPublicId; use Fleetbase\Traits\HasUuid; use Fleetbase\Traits\Searchable; class ApiEvent extends Model { use HasUuid; + use HasPublicId; use HasApiModelBehavior; use Searchable; use Filterable; diff --git a/src/Models/ApiRequestLog.php b/src/Models/ApiRequestLog.php index bbd9c83..84b7719 100644 --- a/src/Models/ApiRequestLog.php +++ b/src/Models/ApiRequestLog.php @@ -5,12 +5,14 @@ use Fleetbase\Casts\Json; use Fleetbase\Traits\Filterable; use Fleetbase\Traits\HasApiModelBehavior; +use Fleetbase\Traits\HasPublicId; use Fleetbase\Traits\HasUuid; use Fleetbase\Traits\Searchable; class ApiRequestLog extends Model { use HasUuid; + use HasPublicId; use HasApiModelBehavior; use Searchable; use Filterable; diff --git a/src/Models/File.php b/src/Models/File.php index 0956e6d..2589602 100644 --- a/src/Models/File.php +++ b/src/Models/File.php @@ -11,6 +11,7 @@ use Fleetbase\Traits\SendsWebhooks; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Mimey\MimeTypes; @@ -417,4 +418,25 @@ public static function randomFileNameFromRequest($request, ?string $extension = return static::randomFileName($extension); } + + /** + * Retrieves a collection of files based on UUIDs provided via a request. + * + * This method extracts an array of UUIDs from the request and retrieves + * corresponding file models. It is static, allowing it to be called on the class itself + * without needing an instance of the class. + * + * @param Request $request the HTTP request containing the 'files' array with UUIDs + * @param string $param the Param which should hold the array of files + * + * @return Collection a collection of File models that match the provided UUIDs + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException if no model is found + */ + public static function fromRequest(Request $request, string $param = 'files'): Collection + { + $ids = $request->array($param); + + return static::whereIn('uuid', $ids)->get(); + } } diff --git a/src/Models/User.php b/src/Models/User.php index 76d7acb..d5fb6a2 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -775,6 +775,25 @@ public static function applyUserInfoFromRequest($request, array $attributes = [] return $attributes; } + /** + * Create a new User instance with enriched attributes from the request. + * + * This static method constructs a new User object using information obtained from + * a request object. It enhances the initial user attributes with additional details + * such as country, timezone, and IP-related metadata by leveraging the + * applyUserInfoFromRequest() method. This method is ideal for initializing a user with + * comprehensive details at the point of creation, particularly during registration processes. + * + * @param \Illuminate\Http\Request $request the request object containing user's IP address and possibly other details + * @param array $attributes an optional array of initial attributes that may be provided for the user + * + * @return User returns the newly created User instance with enriched attributes + */ + public static function newUserWithRequestInfo($request, $attributes = []): User + { + return new User(static::applyUserInfoFromRequest($request, $attributes)); + } + /** * Sets user information from the request on the current User model instance. * @@ -804,11 +823,29 @@ public function setUserInfoFromRequest($request, bool $save = false): User return $this; } + /** + * Retrieve the last seen timestamp of the user. + * + * This method acts as an accessor for the 'lastSeenAt' attribute of the User model. + * It returns the datetime when the user was last active in the system. This can be + * used to display the last seen status or to calculate if the user is offline. + * + * @return \Carbon\Carbon|null returns the Carbon instance for the last seen timestamp or null if not set + */ public function getLastSeenAtAttribute() { return $this->lastSeenAt(); } + /** + * Check if the user is currently online. + * + * This accessor method for the 'isOnline' attribute determines if the user is considered + * online based on certain criteria like their last activity timestamp. It leverages the + * isOnline() method, which should contain the logic to ascertain the user's online status. + * + * @return bool returns true if the user is online, otherwise false + */ public function getIsOnlineAttribute() { return $this->isOnline(); diff --git a/src/Providers/CoreServiceProvider.php b/src/Providers/CoreServiceProvider.php index cd0f734..df19ffc 100644 --- a/src/Providers/CoreServiceProvider.php +++ b/src/Providers/CoreServiceProvider.php @@ -45,6 +45,7 @@ class CoreServiceProvider extends ServiceProvider \Fleetbase\Http\Middleware\SetupFleetbaseSession::class, \Fleetbase\Http\Middleware\TrackPresence::class, \Spatie\ResponseCache\Middlewares\CacheResponse::class, + \Fleetbase\Http\Middleware\ClearCacheAfterDelete::class, ], 'fleetbase.api' => [ 'throttle:60,1', @@ -53,6 +54,7 @@ class CoreServiceProvider extends ServiceProvider \Illuminate\Routing\Middleware\SubstituteBindings::class, \Fleetbase\Http\Middleware\LogApiRequests::class, \Spatie\ResponseCache\Middlewares\CacheResponse::class, + \Fleetbase\Http\Middleware\ClearCacheAfterDelete::class, ], ]; diff --git a/src/Support/Auth.php b/src/Support/Auth.php index a84183d..d6eec69 100644 --- a/src/Support/Auth.php +++ b/src/Support/Auth.php @@ -65,6 +65,9 @@ public static function setSession($user = null, $login = false): bool session(['is_admin' => $user->isAdmin()]); } + // track last usage of api credential + $apiCredential->trackLastUsed(); + return true; } diff --git a/src/Support/TwoFactorAuth.php b/src/Support/TwoFactorAuth.php index 4982698..f3e937e 100644 --- a/src/Support/TwoFactorAuth.php +++ b/src/Support/TwoFactorAuth.php @@ -354,6 +354,9 @@ public static function isEnabled(User $user): bool return $twoFaSettings->getBoolean('enabled'); } + /** + * True if 2FA should be enforced for a user. + */ public static function shouldEnforce(User $user): bool { $systemEnforced = static::isSystemEnforced(); @@ -379,6 +382,9 @@ public static function isCompanyEnforced(Company $company): bool return false; } + /** + * True if 2FA is enforced system wide. + */ public static function isSystemEnforced(): bool { $twoFaSettings = static::getTwoFaConfiguration(); @@ -404,7 +410,7 @@ public static function start(string $identity, int $tokenLength = 40): ?string if ($user) { $token = Str::random($tokenLength); - $twoFaSessionKey = static::createTwoFaSessionKey($user, $token, true); + $twoFaSessionKey = static::createTwoFaSessionKey($user, $token); return static::encryptSessionKey($twoFaSessionKey, $user->uuid); } @@ -579,13 +585,12 @@ public static function forgetTwoFaSession(string $token, string $identity): bool * * @return string the Two-Factor Authentication session key */ - private static function createTwoFaSessionKey(User $user, string $token, bool $storeInCache = false, int $expiresAfter = 600): string + private static function createTwoFaSessionKey(User $user, string $token, bool $storeInCache = true, int $expiresAfter = 600): string { $twoFaSessionKey = 'two_fa_session:' . $user->uuid . ':' . $token; if ($storeInCache) { - $expirationTime = Carbon::now()->addSeconds($expiresAfter)->timestamp; - Redis::set($twoFaSessionKey, $user->uuid, 'EX', $expirationTime); + Redis::set($twoFaSessionKey, $user->uuid, 'EX', now()->addSeconds($expiresAfter)->timestamp); } return $twoFaSessionKey; diff --git a/src/Traits/Insertable.php b/src/Traits/Insertable.php index 5d5f0c0..ea36de4 100644 --- a/src/Traits/Insertable.php +++ b/src/Traits/Insertable.php @@ -43,7 +43,12 @@ public static function bulkInsert(array $rows = []): bool } $result = static::insert($rows); - $model->flushCache(); + + // flush cache + if (method_exists($model, 'flushCache')) { + $model->flushCache(); + } + return $result; } } diff --git a/src/Types/Country.php b/src/Types/Country.php index 736f1c8..db36297 100644 --- a/src/Types/Country.php +++ b/src/Types/Country.php @@ -126,7 +126,18 @@ public function only($keys = []): array $key = Arr::first(array_keys($key)); } - $result[$as] = strpos($key, '.') > 0 ? Utils::get($this, $key) : $this->{$key}; + if (!is_string($key)) { + continue; + } + + if (strpos($key, '.') > 0) { + $result[$as] = Utils::get($this, $key); + continue; + } + + if (isset($this->{$key})) { + $result[$as] = $this->{$key}; + } } return $result; @@ -232,7 +243,7 @@ function ($country) use ($query) { strtolower($country->getCode()) === $query, strtolower($country->getCca2()) === $query, Str::contains(strtolower($country->getAbbrev()), $query), - Str::contains(strtolower($country->getName()), $query), + // Str::contains(strtolower($country->getName()), $query), ]; return count(array_filter($matches)); diff --git a/src/routes.php b/src/routes.php index 5fc95c9..2ece2bb 100644 --- a/src/routes.php +++ b/src/routes.php @@ -190,7 +190,7 @@ function ($router, $controller) { $router->fleetbaseRoutes( 'two-fa', function ($router, $controller) { - $router->post('config', $controller('saveSystemConfig')); + $router->post('config', $controller('saveSystemConfig'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]); $router->get('config', $controller('getSystemConfig'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]); $router->get('enforce', $controller('shouldEnforce'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]); } diff --git a/views/mail/verification.blade.php b/views/mail/verification.blade.php index 7ff0178..0b6df5f 100644 --- a/views/mail/verification.blade.php +++ b/views/mail/verification.blade.php @@ -18,8 +18,10 @@
@endif -@component('mail::button', ['url' => \Fleetbase\Support\Utils::consoleUrl('onboard/verify-email', ['hello' => base64_encode($user->uuid), 'code' => $code ])]) - Verify Email -@endcomponent +@if($type === 'email_verification') + @component('mail::button', ['url' => \Fleetbase\Support\Utils::consoleUrl('onboard/verify-email', ['hello' => base64_encode($user->uuid), 'code' => $code ])]) + Verify Email + @endcomponent +@endif