Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
87 / 87
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
AngelTypesController
100.00% covered (success)
100.00%
87 / 87
100.00% covered (success)
100.00%
7 / 7
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPermission
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 about
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 qrCode
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
2
 join
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
6
 qrJoinEnabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getAngelType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Controllers;
6
7use Engelsystem\Config\Config;
8use Engelsystem\Helpers\Authenticator;
9use Engelsystem\Helpers\Carbon;
10use Engelsystem\Http\Exceptions\HttpNotFound;
11use Engelsystem\Http\Request;
12use Engelsystem\Http\Response;
13use Engelsystem\Models\AngelType;
14use Engelsystem\Models\User\User;
15use Engelsystem\Models\UserAngelType;
16use Exception;
17use Firebase\JWT\BeforeValidException;
18use Firebase\JWT\ExpiredException;
19use Firebase\JWT\JWT;
20use Firebase\JWT\Key;
21use Illuminate\Support\Str;
22use Psr\Http\Message\ServerRequestInterface;
23use Psr\Log\LoggerInterface;
24
25class AngelTypesController extends BaseController
26{
27    use HasUserNotifications;
28
29    public function __construct(
30        protected Response $response,
31        protected Config $config,
32        protected Authenticator $auth,
33        protected LoggerInterface $log,
34    ) {
35    }
36
37    public function hasPermission(ServerRequestInterface $request, string $method): ?bool
38    {
39        return match ($method) {
40            'qrCode' =>
41                $this->auth->user()?->isAngelTypeSupporter($this->getAngelType($request))
42                || $this->auth->can('admin_user_angeltypes'),
43            'join' => (bool) $this->auth->user(),
44            default => parent::hasPermission($request, $method),
45        };
46    }
47
48    public function about(): Response
49    {
50        $angeltypes = AngelType::all();
51
52        return $this->response->withView(
53            'pages/angeltypes/about',
54            ['angeltypes' => $angeltypes]
55        );
56    }
57
58    public function qrCode(Request $request): Response
59    {
60        $this->qrJoinEnabled();
61        $angelType = $this->getAngelType($request);
62        $jwtExpirationMin = $this->config->get('jwt_expiration_time');
63        $qrData = null;
64        $data = [];
65
66        if ($request->isMethod('post')) {
67            $data = $this->validate($request, [
68                'minutes' => 'required|int|min:1|max:' . $jwtExpirationMin,
69            ]);
70            $minutes = (int) $data['minutes'];
71            $time = Carbon::now();
72
73            $key = $this->config->get('app_key');
74            $alg = $this->config->get('jwt_algorithm');
75            $jti = Str::random();
76
77            $iat = $time->timestamp;
78            $exp = $time->addMinutes($minutes)->timestamp;
79            $data['expires'] = $time;
80
81            $payload = [
82                'sub' => 'join_angel_type',
83                'iat' => $iat,
84                'exp' => $exp,
85                'jti' => $jti,
86                'id' => $angelType->id,
87                'by' => $this->auth->user()->id,
88            ];
89            $jwt = JWT::encode($payload, $key, $alg);
90            $qrData = url('/angeltypes/' . $angelType->id . '/join', ['token' => $jwt]);
91        }
92
93        return $this->response->withInput($data)->withView(
94            'pages/angeltypes/qr',
95            ['angel_type' => $angelType, 'qr_data' => $qrData, 'qr_max_expiration_minutes' => $jwtExpirationMin],
96        );
97    }
98
99    public function join(Request $request): Response
100    {
101        $this->qrJoinEnabled();
102        $angelType = $this->getAngelType($request);
103
104        $jwt = $request->get('token', '');
105
106        $key = $this->config->get('app_key');
107        $alg = $this->config->get('jwt_algorithm');
108
109        try {
110            $decoded = JWT::decode($jwt, new Key($key, $alg));
111        } catch (BeforeValidException | ExpiredException) {
112            throw new HttpNotFound();
113        } catch (Exception $e) {
114            $this->log->error('JWT Error', ['exception' => $e]);
115            throw new HttpNotFound();
116        }
117
118        $type = $decoded->sub ?? null;
119        $id = $decoded->id ?? null;
120        $jti = $decoded->jti ?? null;
121        if ($type !== 'join_angel_type' || $id !== $angelType->id) {
122            throw new HttpNotFound();
123        }
124
125        /** @var User $confirmingUser */
126        $confirmingUser = User::findOrFail($decoded->by ?? null);
127        /** @var UserAngelType $userAngelType */
128        $userAngelType = UserAngelType::firstOrNew([
129            'user_id' => $this->auth->user()->id,
130            'angel_type_id' => $angelType->id,
131        ]);
132
133        if (!$userAngelType->confirmUser) {
134            $userAngelType->confirmUser()->associate($confirmingUser);
135
136            $this->log->info(
137                'Joined angel type {type} ({type_id}) via QR token {token_id} '
138                . 'created by {confirming_user} ({confirming_id})',
139                [
140                    'type' => $angelType->name,
141                    'type_id' => $angelType->id,
142                    'token_id' => $jti,
143                    'confirming_user' => $confirmingUser->name,
144                    'confirming_id' => $confirmingUser->id,
145                ]
146            );
147
148            $userAngelType->save();
149        }
150
151        $this->addNotification('angeltype.add.success');
152
153        return redirect(url('/angeltypes', ['action' => 'view', 'angeltype_id' => $angelType->id]));
154    }
155
156    protected function qrJoinEnabled(): void
157    {
158        if ($this->config->get('app_key') && $this->config->get('join_qr_code', true)) {
159            return;
160        }
161
162        throw new HttpNotFound();
163    }
164
165    protected function getAngelType(ServerRequestInterface $request): AngelType
166    {
167        $angelTypeId = (int) $request->getAttribute('angel_type_id');
168        /** @var AngelType $angelType */
169        $angelType = AngelType::findOrFail($angelTypeId);
170        return $angelType;
171    }
172}