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