AuthTrait::sendMail()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 11
rs 9.6111
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 3.0.0
13
 */
14
15
namespace Quantum\Libraries\Auth\Traits;
16
17
use Quantum\Libraries\Auth\Contracts\AuthServiceInterface;
18
use Quantum\Libraries\Auth\Exceptions\AuthException;
19
use Quantum\Libraries\Jwt\Exceptions\JwtException;
20
use Quantum\Config\Exceptions\ConfigException;
21
use Quantum\Libraries\Auth\Enums\AuthKeys;
22
use Quantum\App\Exceptions\BaseException;
23
use Quantum\Di\Exceptions\DiException;
24
use Quantum\Libraries\Mailer\Mailer;
25
use Quantum\Libraries\Hasher\Hasher;
26
use Quantum\Libraries\Jwt\JwtToken;
27
use Quantum\Libraries\Auth\User;
28
use ReflectionException;
29
use ReflectionClass;
30
use DateInterval;
31
use Exception;
32
use DateTime;
33
34
/**
35
 * Trait AuthTrait
36
 * @package Quantum\Libraries\Auth
37
 */
38
trait AuthTrait
39
{
40
    /**
41
     * @var Mailer
42
     */
43
    protected $mailer;
44
45
    /**
46
     * @var Hasher
47
     */
48
    protected $hasher;
49
50
    /**
51
     * @var JwtToken
52
     */
53
    protected $jwt;
54
55
    /**
56
     * @var AuthServiceInterface
57
     */
58
    protected $authService;
59
60
    /**
61
     * @var int
62
     */
63
    protected $otpLength = 6;
64
65
    /**
66
     * @var array
67
     */
68
    protected $keyFields = [];
69
70
    /**
71
     * @var array
72
     */
73
    protected $visibleFields = [];
74
75
    /**
76
     * @inheritDoc
77
     * @throws ConfigException
78
     * @throws DiException
79
     * @throws JwtException
80
     * @throws ReflectionException
81
     * @throws BaseException
82
     */
83
    public function check(): bool
84
    {
85
        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

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

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

346
        return base64_encode($this->hasher->hash(/** @scrutinizer ignore-type */ $val ?: config()->get('app.key')));
Loading history...
347
    }
348
349
    /**
350
     * Send email
351
     * @param User $user
352
     * @param array $body
353
     */
354
    protected function sendMail(User $user, array $body)
355
    {
356
        $fullName = ($user->hasField('firstname') && $user->hasField('lastname')) ? $user->getFieldValue('firstname') . ' ' . $user->getFieldValue('lastname') : '';
357
358
        $appEmail = config()->get('app.email') ?: '';
359
        $appName = config()->get('app.name') ?: '';
360
361
        $this->mailer->setFrom($appEmail, $appName)
362
            ->setAddress($user->getFieldValue($this->keyFields[AuthKeys::USERNAME]), $fullName)
0 ignored issues
show
Bug introduced by
It seems like $user->getFieldValue($th...ms\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

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