Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
16 / 17
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
SignatureOtpService
94.12% covered (success)
94.12%
16 / 17
50.00% covered (danger)
50.00%
1 / 2
7.01
0.00% covered (danger)
0.00%
0 / 1
 issue
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 verify
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Signatures\Application\Services;
6
7use ImovelZapAi\Signatures\Domain\Exceptions\SignatureException;
8use ImovelZapAi\Signatures\Infrastructure\Persistence\Eloquent\SignerModel;
9
10final class SignatureOtpService
11{
12    private const MAX_ATTEMPTS = 5;
13
14    public function issue(SignerModel $signer): string
15    {
16        $length = (int) config('signatures.otp_length', 6);
17        $max = (10 ** $length) - 1;
18        $plainOtp = str_pad((string) random_int(0, $max), $length, '0', STR_PAD_LEFT);
19
20        $signer->forceFill([
21            'otp_hash' => hash('sha256', $plainOtp),
22            'otp_expires_at' => now()->addMinutes((int) config('signatures.otp_ttl_minutes', 15)),
23            'otp_attempts' => 0,
24        ])->save();
25
26        return $plainOtp;
27    }
28
29    public function verify(SignerModel $signer, string $otp): bool
30    {
31        if ($signer->otp_hash === null || $signer->otp_expires_at === null || $signer->otp_expires_at->isPast()) {
32            throw SignatureException::otpInvalid();
33        }
34
35        if ($signer->otp_attempts >= self::MAX_ATTEMPTS) {
36            throw SignatureException::otpInvalid();
37        }
38
39        if (! hash_equals($signer->otp_hash, hash('sha256', $otp))) {
40            $signer->forceFill(['otp_attempts' => $signer->otp_attempts + 1])->save();
41
42            return false;
43        }
44
45        return true;
46    }
47}