Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
21 / 21
CRAP
100.00% covered (success)
100.00%
1 / 1
Authenticator
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
21 / 21
47
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
 user
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 userFromSession
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 userFromApi
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 can
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 canAny
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 authenticate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 verifyPassword
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 userByHeaders
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 userByQueryParam
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 resetApiKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 userByApiKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 isApiRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPasswordAlgorithm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPasswordAlgorithm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGuestRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setGuestRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadPermissions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Helpers;
6
7use Carbon\Carbon;
8use Engelsystem\Models\Group;
9use Engelsystem\Models\User\User;
10use Engelsystem\Models\User\User as UserRepository;
11use Illuminate\Support\Str;
12use Psr\Http\Message\ServerRequestInterface;
13use Symfony\Component\HttpFoundation\Session\Session;
14
15class Authenticator
16{
17    protected ?User $user = null;
18
19    /** @var string[] */
20    protected array $permissions = [];
21
22    protected int|string|null $passwordAlgorithm = PASSWORD_DEFAULT;
23
24    protected int $defaultRole = 20;
25
26    protected int $guestRole = 10;
27
28    public function __construct(
29        protected ServerRequestInterface $request,
30        protected Session $session,
31        protected UserRepository $userRepository
32    ) {
33    }
34
35    /**
36     * Load the user from session or api auth
37     */
38    public function user(): ?User
39    {
40        if ($this->user) {
41            return $this->user;
42        }
43
44        $this->user = $this->userFromSession();
45        if (!$this->user && $this->isApiRequest()) {
46            $this->user = $this->userFromApi();
47        }
48
49        return $this->user;
50    }
51
52    /**
53     * Load the user from session
54     */
55    public function userFromSession(): ?User
56    {
57        if ($this->user) {
58            return $this->user;
59        }
60
61        $userId = $this->session->get('user_id');
62        if (!$userId) {
63            return null;
64        }
65
66        $this->user = $this
67            ->userRepository
68            ->find($userId);
69
70        return $this->user;
71    }
72
73    /**
74     * Get the user by its api key
75     */
76    public function userFromApi(): ?User
77    {
78        if ($this->user) {
79            return $this->user;
80        }
81
82        $this->user = $this->userByHeaders();
83        if ($this->user) {
84            return $this->user;
85        }
86
87        $this->user = $this->userByQueryParam();
88
89        return $this->user;
90    }
91
92    /**
93     * @param string[]|string $abilities
94     */
95    public function can(array|string $abilities): bool
96    {
97        $abilities = (array) $abilities;
98
99        if (empty($this->permissions)) {
100            $this->loadPermissions();
101        }
102
103        foreach ($abilities as $ability) {
104            if (!in_array($ability, $this->permissions)) {
105                return false;
106            }
107        }
108
109        return true;
110    }
111
112    /**
113     * @param string[]|string $abilities
114     */
115    public function canAny(array|string $abilities): bool
116    {
117        $abilities = (array) $abilities;
118
119        foreach ($abilities as $ability) {
120            if ($this->can($ability)) {
121                return true;
122            }
123        }
124
125        return false;
126    }
127
128    public function authenticate(string $login, string $password): ?User
129    {
130        /** @var User $user */
131        $user = $this->userRepository->whereName($login)->first();
132        if (!$user) {
133            $user = $this->userRepository->whereEmail($login)->first();
134        }
135
136        if (!$user) {
137            return null;
138        }
139
140        if (!$this->verifyPassword($user, $password)) {
141            return null;
142        }
143
144        return $user;
145    }
146
147    public function verifyPassword(User $user, string $password): bool
148    {
149        if (!password_verify($password, $user->password)) {
150            return false;
151        }
152
153        if (password_needs_rehash($user->password, $this->passwordAlgorithm)) {
154            $this->setPassword($user, $password);
155        }
156
157        return true;
158    }
159
160    /**
161     * Get the user by authorization bearer or x-api-key headers
162     */
163    protected function userByHeaders(): ?User
164    {
165        $header = $this->request->getHeader('authorization');
166        if (!empty($header) && Str::startsWith(Str::lower($header[0]), 'bearer ')) {
167            return $this->userByApiKey(trim(Str::substr($header[0], 7)));
168        }
169
170        $header = $this->request->getHeader('x-api-key');
171        if (!empty($header)) {
172            return $this->userByApiKey($header[0]);
173        }
174
175        return null;
176    }
177
178    /**
179     * Get the user by query parameters
180     */
181    protected function userByQueryParam(): ?User
182    {
183        $params = $this->request->getQueryParams();
184        if (!empty($params['key'])) {
185            $this->user = $this->userByApiKey($params['key']);
186        }
187
188        return $this->user;
189    }
190
191    public function resetApiKey(User $user): void
192    {
193        $user->api_key = bin2hex(random_bytes(32));
194        $user->save();
195    }
196
197    /**
198     * Get the user by its api key
199     */
200    protected function userByApiKey(string $key): ?User
201    {
202        $this->user = $this
203            ->userRepository
204            ->whereApiKey($key)
205            ->first();
206
207        return $this->user;
208    }
209
210    protected function isApiRequest(): bool
211    {
212        return (bool) request()->getAttribute('route-api-accessible', false);
213    }
214
215    public function setPassword(User $user, string $password): void
216    {
217        $user->password = password_hash($password, $this->passwordAlgorithm);
218        $user->save();
219    }
220
221    public function getPasswordAlgorithm(): int|string|null
222    {
223        return $this->passwordAlgorithm;
224    }
225
226    public function setPasswordAlgorithm(int|string|null $passwordAlgorithm): void
227    {
228        $this->passwordAlgorithm = $passwordAlgorithm;
229    }
230
231    public function getDefaultRole(): int
232    {
233        return $this->defaultRole;
234    }
235
236    public function setDefaultRole(int $defaultRole): void
237    {
238        $this->defaultRole = $defaultRole;
239    }
240
241    public function getGuestRole(): int
242    {
243        return $this->guestRole;
244    }
245
246    public function setGuestRole(int $guestRole): void
247    {
248        $this->guestRole = $guestRole;
249    }
250
251    protected function loadPermissions(): void
252    {
253        $user = $this->user();
254
255        if ($user) {
256            $this->permissions = $user->privileges->pluck('name')->toArray();
257
258            if ($user->last_login_at < Carbon::now()->subMinutes(5) && !$this->isApiRequest()) {
259                $user->last_login_at = Carbon::now();
260                $user->save(['touch' => false]);
261            }
262        } elseif ($this->session->get('user_id')) {
263            $this->session->remove('user_id');
264        }
265
266        if (empty($this->permissions)) {
267            /** @var Group $group */
268            $group = Group::find($this->guestRole);
269            $this->permissions = $group->privileges->pluck('name')->toArray();
270        }
271    }
272}