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