Passed
Pull Request — master (#190)
by Arman
02:55
created

AuthTrait::getVisibleFields()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 13
rs 10
1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.5
13
 */
14
15
namespace Quantum\Libraries\Auth\Traits;
16
17
use Quantum\Libraries\Encryption\Exceptions\CryptorException;
18
use Quantum\Libraries\Database\Exceptions\DatabaseException;
19
use Quantum\Libraries\Auth\Contracts\AuthServiceInterface;
20
use Quantum\Libraries\Session\Exceptions\SessionException;
21
use Quantum\Libraries\Config\Exceptions\ConfigException;
22
use Quantum\Libraries\Mailer\Contracts\MailerInterface;
23
use Quantum\Libraries\Auth\Exceptions\AuthException;
24
use Quantum\Libraries\Jwt\Exceptions\JwtException;
25
use Quantum\Libraries\Auth\Constants\AuthKeys;
26
use Quantum\Di\Exceptions\DiException;
27
use Quantum\Libraries\Hasher\Hasher;
28
use Quantum\Libraries\Jwt\JwtToken;
29
use Quantum\Libraries\Auth\User;
30
use ReflectionException;
31
use ReflectionClass;
32
use DateInterval;
33
use Exception;
34
use DateTime;
35
36
/**
37
 * Trait AuthTrait
38
 * @package Quantum\Libraries\Auth
39
 */
40
trait AuthTrait
41
{
42
43
    /**
44
     * @var MailerInterface
45
     */
46
    protected $mailer;
47
48
    /**
49
     * @var Hasher
50
     */
51
    protected $hasher;
52
53
    /**
54
     * @var JwtToken
55
     */
56
    protected $jwt;
57
58
    /**
59
     * @var AuthServiceInterface
60
     */
61
    protected $authService;
62
63
    /**
64
     * @var int
65
     */
66
    protected $otpLength = 6;
67
68
    /**
69
     * @var array
70
     */
71
    protected $keyFields = [];
72
73
    /**
74
     * @var array
75
     */
76
    protected $visibleFields = [];
77
78
    /**
79
     * @inheritDoc
80
     * @return bool
81
     * @throws ConfigException
82
     * @throws CryptorException
83
     * @throws DatabaseException
84
     * @throws DiException
85
     * @throws ReflectionException
86
     * @throws SessionException
87
     * @throws JwtException
88
     */
89
    public function check(): bool
90
    {
91
        return !is_null($this->user());
0 ignored issues
show
Bug introduced by
It seems like user() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
        return !is_null($this->/** @scrutinizer ignore-call */ user());
Loading history...
92
    }
93
94
    /**
95
     * Sign Up
96
     * @param array $userData
97
     * @param array|null $customData
98
     * @return User
99
     */
100
    public function signup(array $userData, array $customData = null): User
101
    {
102
        $activationToken = $this->generateToken();
103
104
        $userData[$this->keyFields[AuthKeys::PASSWORD]] = $this->hasher->hash($userData[$this->keyFields[AuthKeys::PASSWORD]]);
105
        $userData[$this->keyFields[AuthKeys::ACTIVATION_TOKEN]] = $activationToken;
106
107
        $user = $this->authService->add($userData);
108
109
        $body = [
110
            'user' => $user,
111
            'activationToken' => $activationToken
112
        ];
113
114
        if ($customData) {
115
            $body = array_merge($body, $customData);
116
        }
117
118
        $this->mailer->setSubject(t('common.activate_account'));
119
        $this->mailer->setTemplate(base_dir() . DS . 'shared' . DS . 'views' . DS . 'email' . DS . 'activate');
120
121
        $this->sendMail($user, $body);
122
123
        return $user;
124
    }
125
126
    /**
127
     * Activate
128
     * @param string $token
129
     */
130
    public function activate(string $token)
131
    {
132
        $this->authService->update(
133
            $this->keyFields[AuthKeys::ACTIVATION_TOKEN],
134
            $token,
135
            [$this->keyFields[AuthKeys::ACTIVATION_TOKEN] => '']
136
        );
137
    }
138
139
    /**
140
     * Forget
141
     * @param string $username
142
     * @return string|null
143
     */
144
    public function forget(string $username): ?string
145
    {
146
        $user = $this->authService->get($this->keyFields[AuthKeys::USERNAME], $username);
147
148
        if (!$user) {
149
            return null;
150
        }
151
152
        $resetToken = $this->generateToken();
153
154
        $this->authService->update(
155
            $this->keyFields[AuthKeys::USERNAME],
156
            $username,
157
            [$this->keyFields[AuthKeys::RESET_TOKEN] => $resetToken]
158
        );
159
160
        $body = [
161
            'user' => $user,
162
            'resetToken' => $resetToken
163
        ];
164
165
        $this->mailer->setSubject(t('common.reset_password'));
166
        $this->mailer->setTemplate(base_dir() . DS . 'shared' . DS . 'views' . DS . 'email' . DS . 'reset');
167
168
        $this->sendMail($user, $body);
169
170
        return $resetToken;
171
    }
172
173
    /**
174
     * Reset
175
     * @param string $token
176
     * @param string $password
177
     */
178
    public function reset(string $token, string $password)
179
    {
180
        $user = $this->authService->get($this->keyFields[AuthKeys::RESET_TOKEN], $token);
181
182
        if ($user) {
183
            if (!$this->isActivated($user)) {
184
                $this->activate($token);
185
            }
186
187
            $this->authService->update(
188
                $this->keyFields[AuthKeys::RESET_TOKEN],
189
                $token,
190
                [
191
                    $this->keyFields[AuthKeys::PASSWORD] => $this->hasher->hash($password),
192
                    $this->keyFields[AuthKeys::RESET_TOKEN] => ''
193
                ]
194
            );
195
        }
196
    }
197
198
    /**
199
     * Resend OTP
200
     * @param string $otpToken
201
     * @return string
202
     * @throws AuthException
203
     * @throws Exception
204
     */
205
    public function resendOtp(string $otpToken): string
206
    {
207
        $user = $this->authService->get($this->keyFields[AuthKeys::OTP_TOKEN], $otpToken);
208
209
        if (!$user) {
210
            throw AuthException::incorrectCredentials();
211
        }
212
213
        return $this->twoStepVerification($user);
214
215
    }
216
217
    /**
218
     * Gets the user by username and password
219
     * @param string $username
220
     * @param string $password
221
     * @return User
222
     * @throws AuthException
223
     */
224
    protected function getUser(string $username, string $password): User
225
    {
226
        $user = $this->authService->get($this->keyFields[AuthKeys::USERNAME], $username);
227
228
        if (!$user) {
229
            throw AuthException::incorrectCredentials();
230
        }
231
232
        if (!$this->hasher->check($password, $user->getFieldValue($this->keyFields[AuthKeys::PASSWORD]))) {
0 ignored issues
show
Bug introduced by
It seems like $user->getFieldValue($th...ts\AuthKeys::PASSWORD]) can also be of type null; however, parameter $hash of Quantum\Libraries\Hasher\Hasher::check() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
        if (!$this->hasher->check($password, /** @scrutinizer ignore-type */ $user->getFieldValue($this->keyFields[AuthKeys::PASSWORD]))) {
Loading history...
233
            throw AuthException::incorrectCredentials();
234
        }
235
236
        if (!$this->isActivated($user)) {
237
            throw AuthException::inactiveAccount();
238
        }
239
240
        return $user;
241
    }
242
243
    /**
244
     * Two-Step Verification
245
     * @param User $user
246
     * @return string
247
     * @throws Exception
248
     */
249
    protected function twoStepVerification(User $user): string
250
    {
251
        $otp = random_number($this->otpLength);
252
253
        $otpToken = $this->generateToken($user->getFieldValue($this->keyFields[AuthKeys::USERNAME]));
254
255
        $time = new DateTime();
256
257
        $time->add(new DateInterval('PT' . config()->get('otp_expires') . 'M'));
258
259
        $this->authService->update(
260
            $this->keyFields[AuthKeys::USERNAME],
261
            $user->getFieldValue($this->keyFields[AuthKeys::USERNAME]),
262
            [
263
                $this->keyFields[AuthKeys::OTP] => $otp,
264
                $this->keyFields[AuthKeys::OTP_EXPIRY] => $time->format('Y-m-d H:i'),
265
                $this->keyFields[AuthKeys::OTP_TOKEN] => $otpToken,
266
            ]
267
        );
268
269
        $body = [
270
            'user' => $user,
271
            'code' => $otp
272
        ];
273
274
        $this->mailer->setSubject(t('common.otp'));
275
        $this->mailer->setTemplate(base_dir() . DS . 'shared' . DS . 'views' . DS . 'email' . DS . 'verification');
276
277
        $this->sendMail($user, $body);
278
279
        return $otpToken;
280
    }
281
282
    /**
283
     * Verify and update OTP
284
     * @param int $otp
285
     * @param string $otpToken
286
     * @return User
287
     * @throws AuthException
288
     * @throws Exception
289
     */
290
    protected function verifyAndUpdateOtp(int $otp, string $otpToken): User
291
    {
292
        $user = $this->authService->get($this->keyFields[AuthKeys::OTP_TOKEN], $otpToken);
293
294
        if (!$user || $otp != $user->getFieldValue($this->keyFields[AuthKeys::OTP])) {
295
            throw AuthException::incorrectVerificationCode();
296
        }
297
298
        if (new DateTime() >= new DateTime($user->getFieldValue($this->keyFields[AuthKeys::OTP_EXPIRY]))) {
299
            throw AuthException::verificationCodeExpired();
300
        }
301
302
        $this->authService->update(
303
            $this->keyFields[AuthKeys::USERNAME],
304
            $user->getFieldValue($this->keyFields[AuthKeys::USERNAME]),
305
            [
306
                $this->keyFields[AuthKeys::OTP] => null,
307
                $this->keyFields[AuthKeys::OTP_EXPIRY] => null,
308
                $this->keyFields[AuthKeys::OTP_TOKEN] => null,
309
            ]
310
        );
311
312
        return $user;
313
    }
314
315
    /**
316
     * Filters and gets the visible fields
317
     * @param User $user
318
     * @return array
319
     */
320
    protected function getVisibleFields(User $user): array
321
    {
322
        $userData = $user->getData();
323
324
        if (count($this->visibleFields)) {
325
            foreach ($userData as $field => $value) {
326
                if (!in_array($field, $this->visibleFields)) {
327
                    unset($userData[$field]);
328
                }
329
            }
330
        }
331
332
        return $userData;
333
    }
334
335
    /**
336
     * Is user account activated
337
     * @param User $user
338
     * @return bool
339
     */
340
    protected function isActivated(User $user): bool
341
    {
342
        return empty($user->getFieldValue($this->keyFields[AuthKeys::ACTIVATION_TOKEN]));
343
    }
344
345
    /**
346
     * Generate Token
347
     * @param string|null $val
348
     * @return string
349
     */
350
    protected function generateToken(string $val = null): string
351
    {
352
        return base64_encode($this->hasher->hash($val ?: config()->get('app_key')));
0 ignored issues
show
Bug introduced by
It seems like $val ?: config()->get('app_key') can also be of type null; however, parameter $password of Quantum\Libraries\Hasher\Hasher::hash() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

352
        return base64_encode($this->hasher->hash(/** @scrutinizer ignore-type */ $val ?: config()->get('app_key')));
Loading history...
353
    }
354
355
    /**
356
     * Send email
357
     * @param User $user
358
     * @param array $body
359
     */
360
    protected function sendMail(User $user, array $body)
361
    {
362
        $fullName = ($user->hasField('firstname') && $user->hasField('lastname')) ? $user->getFieldValue('firstname') . ' ' . $user->getFieldValue('lastname') : '';
363
364
        $appEmail = config()->get('app_email') ?: '';
365
        $appName = config()->get('app_name') ?: '';
366
367
        $this->mailer->setFrom($appEmail, $appName)
368
            ->setAddress($user->getFieldValue($this->keyFields[AuthKeys::USERNAME]), $fullName)
0 ignored issues
show
Bug introduced by
It seems like $user->getFieldValue($th...ts\AuthKeys::USERNAME]) can also be of type null; however, parameter $email of Quantum\Libraries\Mailer...Interface::setAddress() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

368
            ->setAddress(/** @scrutinizer ignore-type */ $user->getFieldValue($this->keyFields[AuthKeys::USERNAME]), $fullName)
Loading history...
369
            ->setBody($body)
370
            ->send();
371
    }
372
373
    /**
374
     * Verify user schema
375
     * @param array $schema
376
     * @throws AuthException
377
     */
378
    protected function verifySchema(array $schema)
379
    {
380
        $constants = (new ReflectionClass(AuthKeys::class))->getConstants();
381
382
        foreach ($constants as $constant) {
383
            if (!isset($schema[$constant]) || !isset($schema[$constant]['name'])) {
384
                throw AuthException::incorrectUserSchema();
385
            }
386
387
            $this->keyFields[$constant] = $schema[$constant]['name'];
388
        }
389
390
        foreach ($schema as $field) {
391
            if (isset($field['visible']) && $field['visible']) {
392
                $this->visibleFields[] = $field['name'];
393
            }
394
        }
395
    }
396
397
    /**
398
     * @return bool
399
     */
400
    protected function isTwoFactorEnabled(): bool
401
    {
402
        return filter_var(config()->get('2FA'), FILTER_VALIDATE_BOOLEAN);
403
    }
404
}