Issues (78)

src/Services/PinService.php (23 issues)

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