ldc-crm/app/Models/Service.php
2025-08-05 14:12:05 +02:00

239 lines
7.2 KiB
PHP

<?php
namespace App\Models;
use App\Jobs\SetServiceStats;
use App\Lib\Helper;
use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @property integer $id Id.
* @property integer $brand_id Foreign key Brand.
* @property string $name Nom.
* @property string $bearer_token
* @property string $bearer_pwd
* @property \Carbon\Carbon $bearer_token_date
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
*
* @property-read \App\Models\Brand $brand Brand BelongsTo relation.
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\ServiceStat>
* $servicesStats ServiceStat HasMany relation.
*/
class Service extends Model
{
use ActivityLog;
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'brand_id',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, mixed>
*/
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&param2=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();
});
}
}