Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.43% covered (success)
96.43%
27 / 28
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
DocumentShareTokenService
96.43% covered (success)
96.43%
27 / 28
66.67% covered (warning)
66.67%
2 / 3
11
0.00% covered (danger)
0.00%
0 / 1
 issue
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 publicUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 resolve
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
8.03
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Documents\Application\Services;
6
7use Illuminate\Support\Str;
8use ImovelZapAi\Documents\Domain\Exceptions\DocumentException;
9use ImovelZapAi\Documents\Infrastructure\Persistence\Eloquent\BusinessDocumentModel;
10use ImovelZapAi\Documents\Infrastructure\Persistence\Eloquent\DocumentShareModel;
11
12final class DocumentShareTokenService
13{
14    /** @return array{plain: string, model: DocumentShareModel, url: string} */
15    public function issue(BusinessDocumentModel $document, string $versionId, ?\DateTimeInterface $expiresAt = null): array
16    {
17        $plain = Str::lower(Str::random(40));
18        $signature = hash_hmac('sha256', $document->id.'|'.$plain, (string) config('app.key'));
19        $expires = $expiresAt ?? now()->addDays((int) config('documents.token_ttl_days', 14));
20
21        $model = DocumentShareModel::query()->create([
22            'tenant_id' => $document->tenant_id,
23            'business_document_id' => $document->id,
24            'document_version_id' => $versionId,
25            'token_prefix' => substr($plain, 0, 8),
26            'token_hash' => hash('sha256', $plain),
27            'signature' => $signature,
28            'expires_at' => $expires,
29        ]);
30
31        return ['plain' => $plain, 'model' => $model, 'url' => $this->publicUrl($plain)];
32    }
33
34    public function publicUrl(string $plain): string
35    {
36        $base = rtrim((string) (config('documents.public_base_url') ?: config('app.url')), '/');
37
38        return $base.'/d/'.$plain;
39    }
40
41    /** @return array{share: DocumentShareModel, document: BusinessDocumentModel} */
42    public function resolve(string $plain): array
43    {
44        $share = DocumentShareModel::query()->where('token_hash', hash('sha256', $plain))->first();
45        if ($share === null) {
46            throw DocumentException::tokenInvalid();
47        }
48
49        $expected = hash_hmac('sha256', $share->business_document_id.'|'.$plain, (string) config('app.key'));
50        if (! hash_equals($share->signature, $expected)) {
51            throw DocumentException::tokenInvalid();
52        }
53        if ($share->revoked_at !== null || ($share->expires_at !== null && $share->expires_at->isPast())) {
54            throw DocumentException::tokenInvalid();
55        }
56
57        $document = BusinessDocumentModel::query()->whereKey($share->business_document_id)->first();
58        if ($document === null || ! $document->isPubliclyAccessible()) {
59            throw DocumentException::tokenInvalid();
60        }
61
62        $share->forceFill(['last_used_at' => now()])->save();
63
64        return ['share' => $share, 'document' => $document];
65    }
66}