Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.51% covered (warning)
74.51%
38 / 51
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Service
74.51% covered (warning)
74.51%
38 / 51
50.00% covered (danger)
50.00%
5 / 10
20.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 booted
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
1.06
 brand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 servicesStats
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheBaseTags
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 generateBearerToken
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 generateBearerPassword
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 incrementStat
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFromBearerToken
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 authenticateApi
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace App\Models;
4
5use App\Jobs\SetServiceStats;
6use App\Lib\Helper;
7use App\Traits\Models\ActivityLog;
8use Illuminate\Database\Eloquent\Factories\HasFactory;
9use Illuminate\Database\Eloquent\Model;
10use Illuminate\Database\Eloquent\ModelNotFoundException;
11use Illuminate\Http\Request;
12use Illuminate\Support\Facades\Crypt;
13use Illuminate\Support\Facades\Hash;
14use Illuminate\Support\Str;
15use Rennokki\QueryCache\Traits\QueryCacheable;
16
17/**
18 * @property integer                         $id                Id.
19 * @property integer                         $brand_id          Foreign key Brand.
20 * @property string                          $name              Nom.
21 * @property string                          $bearer_token
22 * @property string                          $bearer_pwd
23 * @property \Carbon\Carbon                  $bearer_token_date
24 * @property-read \Illuminate\Support\Carbon $created_at        Created date.
25 * @property-read \Illuminate\Support\Carbon $updated_at        Updated date.
26 *
27 * @property-read \App\Models\Brand    $brand Brand BelongsTo relation.
28 * @property-read \Illuminate\Database\Eloquent\Collection<App\Models\ServiceStat>
29 * $servicesStats ServiceStat HasMany relation.
30 */
31class Service extends Model
32{
33    use ActivityLog;
34    use HasFactory;
35    use QueryCacheable;
36
37    /**
38     * The timeout cache for this model
39     *
40     * @var integer
41     */
42    protected $cacheFor = 0;
43
44    /**
45     * The attributes that are mass assignable.
46     *
47     * @var string[]
48     */
49    protected $fillable = [
50        'name',
51        'brand_id',
52    ];
53
54    /**
55     * The attributes that should be cast to native types.
56     *
57     * @var array
58     */
59    protected $casts = [
60        'bearer_token_date' => 'datetime',
61    ];
62
63    /**
64     * Service.
65     *
66     * @param array $attributes
67     */
68    public function __construct(array $attributes = [])
69    {
70        parent::__construct($attributes);
71        $this->cacheFor = config('api.servicesDBCache') * 60;
72    }
73
74
75    /**
76     * Perform any actions required after the model boots.
77     *
78     * @return void
79     */
80    protected static function booted(): void
81    {
82        static::creating(function (self $model) {
83            $model->generateBearerToken();
84            $model->generateBearerPassword();
85        });
86        static::created(function () {
87            // * Flush Eloquent cache.
88            self::flushQueryCache();
89        });
90        static::updated(function () {
91            // * Flush Eloquent cache.
92            self::flushQueryCache();
93        });
94        static::deleted(function () {
95            // * Flush Eloquent cache.
96            self::flushQueryCache();
97        });
98    }
99
100    // * RELATIONS
101
102    /**
103     * Brand BelongsTo relation.
104     *
105     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
106     */
107    public function brand(): \Illuminate\Database\Eloquent\Relations\BelongsTo
108    {
109        return $this->belongsTo(Brand::class);
110    }
111
112    /**
113     * ServiceStat hasMany relation.
114     *
115     * @return \Illuminate\Database\Eloquent\Relations\HasMany
116     */
117    public function servicesStats(): \Illuminate\Database\Eloquent\Relations\HasMany
118    {
119        return $this->hasMany(ServiceStat::class);
120    }
121
122    // * METHODS
123
124    /**
125     * Set the base cache tags that will be present
126     * on all queries.
127     *
128     * ! This is used to flush caching
129     *
130     * @return array
131     */
132    protected function getCacheBaseTags(): array
133    {
134        return [
135            'crm_services',
136        ];
137    }
138
139    /**
140     * Generates new bearer token.
141     *
142     * @return string
143     */
144    public function generateBearerToken(): string
145    {
146        $this->bearer_token      = hash('ripemd160', Str::uuid());
147        $this->bearer_token_date = now();
148        return $this->bearer_token;
149    }
150
151    /**
152     * Generates new bearer password
153     * It store the crypted password in model
154     * and returns the plain password.
155     *
156     * @return string
157     */
158    public function generateBearerPassword(): string
159    {
160        $pwd              = hash('sha256', Str::uuid());
161        $this->bearer_pwd = Crypt::encryptString($pwd);
162        return $pwd;
163    }
164
165    /**
166     * Increment API CRM Stat.
167     *
168     * @return void
169     */
170    public function incrementStat(): void
171    {
172        SetServiceStats::dispatch($this)->afterResponse()
173            ->onConnection('database')->onQueue('stats');
174    }
175
176    /**
177     * Get Service from bearerToken with its brand.
178     *
179     * @param string $bearerToken
180     * @return \App\Models\Service
181     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException If the token matches no known service.
182     */
183    public static function getFromBearerToken(string $bearerToken): self
184    {
185        $services = static::all();
186        /** @var \App\Models\Service $service */
187        foreach ($services as $service) {
188            $tokenEquals = false;
189            // * Keep compatibility with V1.
190            if (!Hash::needsRehash($bearerToken)) {
191                // TODO: Remove this method when no more service will be in V1 (hashed instead of encrypted).
192                $tokenEquals = Hash::check($bearerToken, $service->bearer_token);
193            } else {
194                $tokenEquals = $bearerToken === $service->bearer_token;
195            }
196            if ($service->bearer_token and $tokenEquals) {
197                return $service->load([
198                    'brand' => function ($query) use ($service) {
199                        return $query->cacheTags(["service:{$service->id}:brand"]);
200                    },
201                ]);
202            }
203        }
204        throw new ModelNotFoundException('Could not find service from Service Token');
205    }
206
207    /**
208     * Authenticate the request for this service
209     *
210     * This will check hash-mac and that the request is wellformed
211     *
212     * The request should have the minimum following parameters
213     *  - timestamp using format Y-m-d H:i:s, and must be UTC (Greenwich Mean Time)
214     *  - bearerToken the public token generated by the crm
215     *  - signature the hashmac signature of the request
216     *
217     * ! Signature must be calculated with every parameter urlencode once only
218     * ! All parameters to calculate the signature must be sorted (natural sort asc)
219     *
220     * ! Signature should look like
221     * * httpVerb\n
222     * * uri\n
223     * * queryString
224     *
225     * ! httpVerb as well as uri must be lower case
226     * ! uri should include scheme, host and path and nothing else, if path is empty a trailing slash should be added
227     * ! eg: https://ldc-crm.fr/customer/1  or  https://ldc-crm.fr/
228     * ! The query string shall be separated with '&', eg param1=value1&param2=value2
229     * ! Key names and values of the query string must be url encoded using RFC 3986
230     * ! http://www.faqs.org/rfcs/rfc3986.html
231     * ! Signature must be hmac sha256 using the private key generatedby the crm
232     *
233     *     * ! They should then be concat in a string separated by \n
234     *
235     * @param \Illuminate\Http\Request $request
236     * @return boolean
237     * @throws \App\Exceptions\Api\V4\CrmException If an error occurs.
238     */
239    public function authenticateApi(Request $request): bool
240    {
241        // ! Hack to prevent Laravel to cast empty strings to null or the opposite.
242        // ! If cannot decode json user legacy all() method.
243        $data = \json_decode($request->getContent(), true);
244        $data = (\is_array($data) and \json_last_error() === \JSON_ERROR_NONE) ?
245            array_replace_recursive($request->all(), $data) : $request->all();
246
247        $calculatedHash = Helper::signRequest(
248            $this->bearer_pwd,
249            $request->getUri(),
250            $request->getMethod(),
251            $data
252        );
253
254        // ! Très important hash_equals https://fr.wikipedia.org/wiki/Attaque_temporelle
255        return \hash_equals($calculatedHash, $request->signature);
256    }
257}