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