* $servicesStats ServiceStat HasMany relation. */ class Service extends Model { use ActivityLog; use HasFactory; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'brand_id', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'bearer_token_date' => 'datetime', ]; /** * Perform any actions required after the model boots. * * @return void */ protected static function booted(): void { static::creating(function (self $model) { if (config('app.env') !== 'production') { $model->generateBearerToken(); $model->generateBearerPassword(); } }); static::created(function () { self::resetCache(); }); static::updated(function () { self::resetCache(); }); static::deleted(function () { self::resetCache(); }); } // * RELATIONS /** * Brand belongs to relation. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function brand(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(Brand::class); } /** * ServiceStat has many relation. * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function servicesStats(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(ServiceStat::class); } // * METHODS /** * Generates new bearer token. * * @return string */ public function generateBearerToken(): string { $this->bearer_token = hash('ripemd160', Str::uuid()); $this->bearer_token_date = now(); return $this->bearer_token; } /** * Generates new bearer password * It store the crypted password in model * and returns the plain password. * * @return string */ public function generateBearerPassword(): string { $pwd = hash('sha256', Str::uuid()); $this->bearer_pwd = Crypt::encryptString($pwd); return $pwd; } /** * Increment API CRM Stat. * * @return void */ public function incrementStat(): void { SetServiceStats::dispatch($this)->afterResponse() ->onConnection('database')->onQueue('stats'); } /** * Get Service from bearerToken with its brand. * * @param string $bearerToken * @return \App\Models\Service * @throws \Illuminate\Database\Eloquent\ModelNotFoundException If the token matches no known service. */ public static function getFromBearerToken(string $bearerToken): self { $service = static::getServices()->first(function ($service) use ($bearerToken) { if (! $service->bearer_token) { return false; } return !Hash::needsRehash($bearerToken) ? Hash::check($bearerToken, $service->bearer_token) : $bearerToken === $service->bearer_token; }); if (! $service) { throw new ModelNotFoundException('Could not find service from Service Token'); } return $service->load('brand'); } /** * Authenticate the request for this service * * This will check hash-mac and that the request is wellformed * * The request should have the minimum following parameters * - timestamp using format Y-m-d H:i:s, and must be UTC (Greenwich Mean Time) * - bearerToken the public token generated by the crm * - signature the hashmac signature of the request * * ! Signature must be calculated with every parameter urlencode once only * ! All parameters to calculate the signature must be sorted (natural sort asc) * * ! Signature should look like * * httpVerb\n * * uri\n * * queryString * * ! httpVerb as well as uri must be lower case * ! uri should include scheme, host and path and nothing else, if path is empty a trailing slash should be added * ! eg: https://ldc-crm.fr/customer/1 or https://ldc-crm.fr/ * ! The query string shall be separated with '&', eg param1=value1¶m2=value2 * ! Key names and values of the query string must be url encoded using RFC 3986 * ! http://www.faqs.org/rfcs/rfc3986.html * ! Signature must be hmac sha256 using the private key generatedby the crm * * * ! They should then be concat in a string separated by \n * * @param \Illuminate\Http\Request $request * @return boolean * @throws \App\Exceptions\Api\V4\CrmException If an error occurs. */ public function authenticateApi(Request $request): bool { // ! Hack to prevent Laravel to cast empty strings to null or the opposite. // ! If cannot decode json user legacy all() method. $data = \json_decode($request->getContent(), true); $data = (\is_array($data) and \json_last_error() === \JSON_ERROR_NONE) ? array_replace_recursive($request->all(), $data) : $request->all(); $calculatedHash = Helper::signRequest( $this->bearer_pwd, $request->getUri(), $request->getMethod(), $data ); // ! Très important hash_equals https://fr.wikipedia.org/wiki/Attaque_temporelle return \hash_equals($calculatedHash, $request->signature); } /** * Reset all cache elements related to this model. * * @return void */ private static function resetCache(): void { Cache::forget('services'); self::getServices(); } /** * Get all services. * * @return \Illuminate\Database\Eloquent\Collection */ public static function getServices(): \Illuminate\Database\Eloquent\Collection { return Cache::remember('services', 86400, function () { return self::query()->orderBy('name', 'ASC')->get(); }); } }