Passed
Push — main ( 5bc95d...2b43e2 )
by ikechukwu
02:49
created

PinService   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 453
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 40
eloc 138
c 1
b 0
f 0
dl 0
loc 453
rs 9.2

22 Methods

Rating   Name   Duplication   Size   Complexity  
A handlePinRequired() 0 16 2
A arrestRequest() 0 23 1
A pinRequestTerminated() 0 4 1
A checkMaxTrial() 0 11 2
A isArrestedRequestValid() 0 17 2
A pinUrlHasValidSignature() 0 8 2
A handlePinChange() 0 12 2
A __construct() 0 5 1
A updateCurrentRequest() 0 12 2
A pinRequestAttempts() 0 3 1
A pinValidationURL() 0 7 1
A requirePinValidationForRequest() 0 16 2
A pinValidation() 0 12 2
A saveOldPin() 0 5 1
A transferSessionsToNewRequest() 0 13 4
A dispatchArrestedRequest() 0 10 2
A sendPinChangeNotification() 0 5 3
A pinUrlHasValidUUID() 0 8 2
A getRequirePin() 0 7 1
A cancelAllOpenArrestedRequests() 0 6 1
A saveNewPin() 0 11 2
A errorResponseForPinRequired() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like PinService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PinService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Ikechukwukalu\Requirepin\Services;
4
5
use App\Models\User;
0 ignored issues
show
Bug introduced by
The type App\Models\User was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Ikechukwukalu\Requirepin\Models\TestUser;
7
use Ikechukwukalu\Requirepin\Models\OldPin;
8
use Ikechukwukalu\Requirepin\Models\RequirePin;
9
use Ikechukwukalu\Requirepin\Notifications\PinChange;
10
use Ikechukwukalu\Requirepin\Requests\ChangePinRequest;
11
use Ikechukwukalu\Requirepin\Rules\CurrentPin;
12
use Ikechukwukalu\Requirepin\Services\ThrottleRequestsService;
13
use Ikechukwukalu\Requirepin\Traits\Helpers;
14
use Illuminate\Http\JsonResponse;
15
use Illuminate\Http\RedirectResponse;
16
use Illuminate\Http\Response;
17
use Illuminate\Http\Request;
18
use Illuminate\Support\Str;
19
use Illuminate\Support\Facades\Auth;
20
use Illuminate\Support\Facades\Crypt;
21
use Illuminate\Support\Facades\Hash;
22
use Illuminate\Support\Facades\Response as ResponseFacade;
23
use Illuminate\Support\Facades\Route;
24
use Illuminate\Support\Facades\Validator;
25
use Illuminate\Support\Facades\URL;
26
27
class PinService {
28
29
    use Helpers;
0 ignored issues
show
Bug introduced by
The trait Ikechukwukalu\Requirepin\Traits\Helpers requires the property $ip which is not provided by Ikechukwukalu\Requirepin\Services\PinService.
Loading history...
30
31
    private Request $arrestedRequest;
32
    private array $payload;
33
34
    public function __construct()
35
    {
36
        $this->throttleRequestsService = new ThrottleRequestsService(
37
            config('requirepin.max_attempts', 3),
38
            config('requirepin.delay_minutes', 1)
39
        );
40
    }
41
42
    /**
43
     * Handle Pin Change.
44
     *
45
     * @param \Ikechukwukalu\Requirepin\Requests\ChangePinRequest $request
46
     *
47
     * @return null
48
     * @return array
49
     */
50
    public function handlePinChange(ChangePinRequest $request) : ?array
51
    {
52
        $validated = $request->validated();
53
54
        if ($user = $this->saveNewPin($validated)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $user is correct as $this->saveNewPin($validated) targeting Ikechukwukalu\Requirepin...inService::saveNewPin() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
55
            $this->saveOldPin($user, $validated);
0 ignored issues
show
Bug introduced by
$user of type void is incompatible with the type App\Models\User|Ikechukw...uirepin\Models\TestUser expected by parameter $user of Ikechukwukalu\Requirepin...inService::saveOldPin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

55
            $this->saveOldPin(/** @scrutinizer ignore-type */ $user, $validated);
Loading history...
56
            $this->sendPinChangeNotification($user);
0 ignored issues
show
Bug introduced by
$user of type void is incompatible with the type App\Models\User|Ikechukw...uirepin\Models\TestUser expected by parameter $user of Ikechukwukalu\Requirepin...PinChangeNotification(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

56
            $this->sendPinChangeNotification(/** @scrutinizer ignore-type */ $user);
Loading history...
57
58
            return ['message' => trans('requirepin::pin.changed')];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('message' =...uirepin::pin.changed')) returns the type array<string,string> which is incompatible with the documented return type null.
Loading history...
59
        }
60
61
        return null;
62
    }
63
64
    /**
65
     * Handle Pin Authentication.
66
     *
67
     * @param \Illuminate\Http\Request $request
68
     * @param string $uuid
69
     *
70
     * @return \Illuminate\Http\JsonResponse
71
     * @return \Illuminate\Http\RedirectResponse
72
     * @return \Illuminate\Http\Response
73
     */
74
    public function handlePinRequired(Request $request, string $uuid): JsonResponse|Response|RedirectResponse
75
    {
76
        $requirePin = $this->getRequirePin($uuid);
77
        $this->throttleRequestsService->clearAttempts($request);
78
79
        $this->updateCurrentRequest($request, $requirePin);
80
        $response = $this->dispatchArrestedRequest($request,
81
                    $requirePin, $uuid);
82
        $this->transferSessionsToNewRequest($request);
83
84
        if (session('return_payload')) {
85
            return redirect($requirePin->redirect_to)->with('return_payload',
0 ignored issues
show
Bug Best Practice introduced by
The expression return redirect($require...sion('return_payload')) returns the type Illuminate\Http\RedirectResponse which is incompatible with the documented return type Illuminate\Http\JsonResponse.
Loading history...
86
                session('return_payload'));
87
        }
88
89
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response also could return the type Illuminate\Http\Redirect...lluminate\Http\Response which is incompatible with the documented return type Illuminate\Http\JsonResponse.
Loading history...
90
    }
91
92
    /**
93
     * Pin Request Attempts.
94
     *
95
     * @param \Illuminate\Http\Request $request
96
     *
97
     * @return null
98
     * @return array
99
     */
100
    public function pinRequestAttempts(Request $request): ?array
101
    {
102
        return $this->requestAttempts($request, 'requirepin::pin.throttle');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestAtt...uirepin::pin.throttle') also could return the type array<string,string> which is incompatible with the documented return type null.
Loading history...
103
    }
104
105
    /**
106
     * Valid Pin URL.
107
     *
108
     * @param \Illuminate\Http\Request $request
109
     *
110
     * @return null
111
     * @return array
112
     */
113
    public function pinUrlHasValidSignature(Request $request): ?array
114
    {
115
        if (!$request->hasValidSignature()) {
116
            return ['message' =>
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('message' =...pin::pin.expired_url')) returns the type array<string,string> which is incompatible with the documented return type null.
Loading history...
117
                trans('requirepin::pin.expired_url')];
118
        }
119
120
        return null;
121
    }
122
123
    /**
124
     * Valid UUID For Pin URL.
125
     *
126
     * @param string $uuid
127
     *
128
     * @return null
129
     * @return array
130
     */
131
    public function pinUrlHasValidUUID(string $uuid): ?array
132
    {
133
        if(!$this->getRequirePin($uuid)) {
134
            return ['message' =>
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('message' =...pin::pin.invalid_url')) returns the type array<string,string> which is incompatible with the documented return type null.
Loading history...
135
                trans('requirepin::pin.invalid_url')];
136
        }
137
138
        return null;
139
    }
140
141
    /**
142
     * Pin Validation.
143
     *
144
     * @param \Illuminate\Http\Request $request
145
     *
146
     * @return null
147
     * @return array
148
     */
149
    public function pinValidation(Request $request): ?array
150
    {
151
        $validator = Validator::make($request->all(), [
152
            config('requirepin.input', '_pin') => ['required', 'string',
153
            new CurrentPin(config('requirepin.allow_default_pin', false))]
154
        ]);
155
156
        if ($validator->fails()) {
157
            return ['message' => $validator->errors()->first()];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('message' =...tor->errors()->first()) returns the type array<string,string> which is incompatible with the documented return type null.
Loading history...
158
        }
159
160
        return null;
161
    }
162
163
    /**
164
     * Get RequirePin Model.
165
     *
166
     * @param string $uuid
167
     *
168
     * @return null
169
     * @return \Ikechukwukalu\Requirepin\Models\RequirePin
170
     */
171
    public function getRequirePin(string $uuid): ?RequirePin
172
    {
173
        return RequirePin::where('user_id', Auth::user()->id)
0 ignored issues
show
Bug introduced by
Accessing id on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug Best Practice introduced by
The expression return Ikechukwukalu\Req...cancelled_at')->first() could return the type Illuminate\Database\Eloq...Relations\HasOneThrough which is incompatible with the type-hinted return Ikechukwukalu\Requirepin\Models\RequirePin|null. Consider adding an additional type-check to rule them out.
Loading history...
174
                        ->where('uuid', $uuid)
175
                        ->whereNull('approved_at')
176
                        ->whereNull('cancelled_at')
177
                        ->first();
178
    }
179
180
    /**
181
     * Error Response For Pin Authentication.
182
     *
183
     * @param \Illuminate\Http\Request $request
184
     * @param string $uuid
185
     * @param int $status_code
186
     * @param array $data
187
     *
188
     * @return \Illuminate\Http\JsonResponse
189
     * @return \Illuminate\Http\RedirectResponse
190
     * @return \Illuminate\Http\Response
191
     */
192
    public function errorResponseForPinRequired(Request $request, string $uuid, int $status_code, array $data): JsonResponse|RedirectResponse|Response
193
    {
194
        if (Helpers::shouldResponseBeJson($request)) {
195
            return $this->httpResponse($request,
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->httpRespon...), $status_code, $data) also could return the type Illuminate\Http\RedirectResponse which is incompatible with the documented return type Illuminate\Http\JsonResponse.
Loading history...
196
                trans('requirepin::general.fail'), $status_code, $data);
197
        }
198
199
        $requirePin = $this->getRequirePin($uuid);
200
201
        if (isset($requirePin->pin_validation_url)) {
202
            return back()->with('pin_validation',
0 ignored issues
show
Bug Best Practice introduced by
The expression return back()->with('pin...on_url, $status_code))) returns the type Illuminate\Http\RedirectResponse which is incompatible with the documented return type Illuminate\Http\JsonResponse.
Loading history...
203
                json_encode([$data['message'],
204
                $requirePin->pin_validation_url, $status_code]));
205
        }
206
207
        return back()->with('pin_validation',
0 ignored issues
show
Bug Best Practice introduced by
The expression return back()->with('pin...ript:void(0)', '500'))) returns the type Illuminate\Http\RedirectResponse which is incompatible with the documented return type Illuminate\Http\JsonResponse.
Loading history...
208
            json_encode([trans('requirepin::pin.unknown_error'),
209
            'javascript:void(0)', '500']));
210
    }
211
212
    /**
213
     * Pin Request Terminated.
214
     *
215
     * @return array
216
     */
217
    public static function pinRequestTerminated(): array
218
    {
219
        return [trans('requirepin::general.fail'), 401,
220
            ['message' => trans('requirepin::pin.terminated')]];
221
    }
222
223
    /**
224
     * Error Response For Pin Authentication.
225
     *
226
     * @param \Illuminate\Http\Request $request
227
     *
228
     * @return bool
229
     */
230
    public static function isArrestedRequestValid(Request $request): bool
231
    {
232
        $param = config('requirepin.param', '_uuid');
233
        $requirePin = RequirePin::where('user_id', Auth::user()->id)
0 ignored issues
show
Bug introduced by
Accessing id on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
234
                        ->where('route_arrested', $request->path())
235
                        ->where('uuid', $request->{$param})
236
                        ->whereNull('approved_at')
237
                        ->whereNull('cancelled_at')
238
                        ->first();
239
240
        if (!isset($requirePin->id)) {
241
            return false;
242
        }
243
244
        PinService::checkMaxTrial($requirePin);
245
246
        return true;
247
    }
248
249
    /**
250
     * Cancel Unprocessed Arrested Request.
251
     *
252
     * @return void
253
     */
254
    public static function cancelAllOpenArrestedRequests(): void
255
    {
256
        RequirePin::where('user_id', Auth::user()->id)
0 ignored issues
show
Bug introduced by
Accessing id on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
257
            ->whereNull('approved_at')
258
            ->whereNull('cancelled_at')
259
            ->update(['cancelled_at' => now()]);
260
    }
261
262
    /**
263
     * Pin Validation For RequirePin Middleware.
264
     *
265
     * @param \Illuminate\Http\Request $request
266
     * @param string $ip
267
     *
268
     * @return \Illuminate\Http\JsonResponse
269
     * @return \Illuminate\Http\RedirectResponse
270
     * @return \Illuminate\Http\Response
271
     */
272
    public static function requirePinValidationForRequest(Request $request, string $ip): JsonResponse|RedirectResponse|Response
273
    {
274
        $arrestRouteData = PinService::arrestRequest($request, $ip);
275
        [$status, $status_code, $data] = PinService::pinValidationURL(...$arrestRouteData);
276
277
        if (Helpers::shouldResponseBeJson($request))
278
        {
279
            return ResponseFacade::json([
280
                'status' => $status,
281
                'status_code' => $status_code,
282
                'data' => $data
283
            ]);
284
        }
285
286
        return redirect(route('requirePinView'))->with('pin_validation',
0 ignored issues
show
Bug Best Practice introduced by
The expression return redirect(route('r...'url'], $status_code))) returns the type Illuminate\Http\RedirectResponse which is incompatible with the documented return type Illuminate\Http\JsonResponse.
Loading history...
287
            json_encode([$data['message'], $data['url'], $status_code]));
288
    }
289
290
    /**
291
     * Arrest Request.
292
     *
293
     * @param \Illuminate\Http\Request $request
294
     * @param string $ip
295
     *
296
     * @return array
297
     */
298
    private static function arrestRequest(Request $request, string $ip): array
299
    {
300
        $redirect_to = url()->previous() ?? '/';
301
        $uuid = (string) Str::uuid();
302
        $expires_at = now()->addSeconds(config('requirepin.duration',
303
            null));
304
        $pin_validation_url = URL::temporarySignedRoute(
305
            Helpers::pinRequiredRoute($request), $expires_at, ['uuid' => $uuid]);
306
307
        RequirePin::create([
308
            "user_id" => Auth::user()->id,
0 ignored issues
show
Bug introduced by
Accessing id on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
309
            "uuid" => $uuid,
310
            "ip" => $ip,
311
            "device" => $request->userAgent(),
312
            "method" => $request->method(),
313
            "route_arrested" => $request->path(),
314
            "payload" => Crypt::encryptString(serialize($request->all())),
315
            "redirect_to" => $redirect_to,
316
            "pin_validation_url" => $pin_validation_url,
317
            "expires_at" => $expires_at
318
        ]);
319
320
        return [$pin_validation_url, $redirect_to];
321
    }
322
323
    /**
324
     * Pin Validation URL.
325
     *
326
     * @param string $url
327
     * @param null|string $redirect
328
     *
329
     * @return array
330
     */
331
    private static function pinValidationURL(string $url, null|string $redirect): array
332
    {
333
        return [trans('requirepin::general.success'), 200,
334
            [
335
                'message' => trans('requirepin::pin.require_pin'),
336
                'url' => $url,
337
                'redirect' => $redirect
338
            ]];
339
    }
340
341
    /**
342
     * Dispatch Arrested Request.
343
     *
344
     * @param \Illuminate\Http\Request $request
345
     * @param \Ikechukwukalu\Requirepin\Models\RequirePin $requirePin
346
     * @param string $uuid
347
     *
348
     * @return \Illuminate\Http\JsonResponse
349
     * @return \Illuminate\Http\RedirectResponse
350
     * @return \Illuminate\Http\Response
351
     */
352
    private function dispatchArrestedRequest(Request $request, RequirePin $requirePin, string $uuid): JsonResponse|RedirectResponse|Response
353
    {
354
        $this->arrestedRequest = Request::create($requirePin->route_arrested,
355
                        $requirePin->method, ['_uuid' => $uuid] + $this->payload);
356
357
        if (Helpers::shouldResponseBeJson($request)) {
358
            $this->arrestedRequest->headers->set('Accept', 'application/json');
359
        }
360
361
        return Route::dispatch($this->arrestedRequest);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Illuminate\Suppor...$this->arrestedRequest) returns the type Symfony\Component\HttpFoundation\Response which includes types incompatible with the type-hinted return Illuminate\Http\JsonResp...lluminate\Http\Response.
Loading history...
362
    }
363
364
    /**
365
     * Transfer Sessions To New Request.
366
     *
367
     * @param \Illuminate\Http\Request $request
368
     *
369
     * @return void
370
     */
371
    private function transferSessionsToNewRequest(Request $request): void
372
    {
373
        if (Helpers::shouldResponseBeJson($request))
374
        {
375
            return;
376
        }
377
378
        foreach ($this->arrestedRequest->session()->all() as $key => $session) {
379
            if (!in_array($key, ['_old_input', '_previous', 'errors'])) {
380
                continue;
381
            }
382
383
            $request->session()->flash($key, $session);
384
        }
385
    }
386
387
    /**
388
     * Update Current Request.
389
     *
390
     * @param \Illuminate\Http\Request $request
391
     * @param \Ikechukwukalu\Requirepin\Models\RequirePin $requirePin
392
     *
393
     * @return void
394
     */
395
    private function updateCurrentRequest(Request $request, RequirePin $requirePin): void
396
    {
397
        $this->payload = unserialize(Crypt::decryptString($requirePin->payload));
398
399
        $request->merge([
400
            'expires' => null,
401
            'signature' => null,
402
            config('requirepin.input', '_pin') => null
403
        ]);
404
405
        foreach($this->payload as $key => $item) {
406
            $request->merge([$key => $item]);
407
        }
408
    }
409
410
    /**
411
     * Save User's New Pin.
412
     *
413
     * @param array $validated
414
     *
415
     * @return null
416
     * @return \App\Models\User
417
     */
418
    private function saveNewPin(array $validated)
419
    {
420
        $user = Auth::user();
421
        $user->pin = Hash::make($validated['pin']);
0 ignored issues
show
Bug introduced by
Accessing pin on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
422
        $user->default_pin = (string) $validated['pin'] === (string) config('requirepin.default', '0000');
0 ignored issues
show
Bug introduced by
Accessing default_pin on the interface Illuminate\Contracts\Auth\Authenticatable suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
423
424
        if ($user->save()) {
425
            return $user;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $user also could return the type Illuminate\Contracts\Auth\Authenticatable which is incompatible with the documented return type null.
Loading history...
426
        }
427
428
        return null;
429
    }
430
431
    /**
432
     * Save User's Old Pin.
433
     *
434
     * @param \App\Models\User $user
435
     * @param array $validated
436
     *
437
     * @return void
438
     */
439
    private function saveOldPin(User|TestUser $user, array $validated): void
440
    {
441
        OldPin::create([
442
            'user_id' => $user->id,
443
            'pin' => Hash::make($validated['current_pin'])
444
        ]);
445
    }
446
447
    /**
448
     * Send Pin change Notification.
449
     *
450
     * @param \App\Models\User $user
451
     *
452
     * @return void
453
     */
454
    private function sendPinChangeNotification(User|TestUser $user): void
455
    {
456
        if (config('requirepin.notify.change', true)
457
            && env('APP_ENV') !== 'package_testing') {
458
            $user->notify(new PinChange());
459
        }
460
    }
461
462
    /**
463
     * Max Route Dispatch For Arrested Request.
464
     *
465
     * @param \Ikechukwukalu\Requirepin\Models\RequirePin $requirePin
466
     *
467
     * @return void
468
     */
469
    private static function checkMaxTrial(RequirePin $requirePin): void
470
    {
471
        $maxTrial = $requirePin->retry;
472
        $maxTrial ++;
473
474
        if ($maxTrial >= config('requirepin.max_trial', 3)) {
475
            $requirePin->approved_at = now();
476
        }
477
478
        $requirePin->retry = $maxTrial;
479
        $requirePin->save();
480
    }
481
}
482