Passed
Push — main ( 9c1f0d...e780b1 )
by ikechukwu
05:56
created

PinService   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 43
eloc 147
c 5
b 0
f 0
dl 0
loc 468
rs 8.96

22 Methods

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