* $servicesStats ServiceStat HasMany relation. */ class Service extends Model { use ActivityLog; use HasFactory; use QueryCacheable; /** * The timeout cache for this model * * @var integer */ protected $cacheFor = 0; /** * 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', ]; /** * Service. * * @param array $attributes */ public function __construct(array $attributes = []) { parent::__construct($attributes); $this->cacheFor = config('api.servicesDBCache') * 60; } /** * 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 () { // * Flush Eloquent cache. self::flushQueryCache(); }); static::updated(function () { // * Flush Eloquent cache. self::flushQueryCache(); }); static::deleted(function () { // * Flush Eloquent cache. self::flushQueryCache(); }); } // * 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 /** * Set the base cache tags that will be present * on all queries. * * ! This is used to flush caching * * @return array */ protected function getCacheBaseTags(): array { return [ 'crm_services', ]; } /** * 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 { $services = static::all(); /** @var \App\Models\Service $service */ foreach ($services as $service) { $tokenEquals = false; // * Keep compatibility with V2. if (!Hash::needsRehash($bearerToken)) { $tokenEquals = Hash::check($bearerToken, $service->bearer_token); } else { $tokenEquals = $bearerToken === $service->bearer_token; } if ($service->bearer_token and $tokenEquals) { return $service->load([ 'brand' => function ($query) use ($service) { return $query->cacheTags(["service:{$service->id}:brand"]); }, ]); } } throw new ModelNotFoundException('Could not find service from Service Token'); } /** * 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); } }