Passed
Push — main ( 7db786...5c2953 )
by Daniel
04:12
created

AbstractUser::getEmailAddressVerifyToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Entity\User;
15
16
use ApiPlatform\Metadata\ApiProperty;
17
use DateTime;
18
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserInterface;
19
use Ramsey\Uuid\Uuid;
20
use Silverback\ApiComponentsBundle\Annotation as Silverback;
21
use Silverback\ApiComponentsBundle\Entity\Utility\IdTrait;
22
use Silverback\ApiComponentsBundle\Entity\Utility\TimestampedTrait;
23
use Silverback\ApiComponentsBundle\Validator\Constraints as AcbAssert;
24
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
25
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
26
use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface;
27
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
28
use Symfony\Component\Serializer\Annotation\Groups;
29
use Symfony\Component\Validator\Constraints as Assert;
30
31
/**
32
 * @author Daniel West <[email protected]>
33
 */
34
#[Silverback\Timestamped]
35
#[UniqueEntity(fields: ['username'], message: 'Sorry, that user already exists in the database.', errorPath: 'username')]
36
#[UniqueEntity(fields: ['emailAddress'], message: 'Sorry, that email address already exists in the database.', errorPath: 'emailAddress')]
37
#[AcbAssert\NewEmailAddress(groups: ['User:emailAddress', 'Default'])]
38
abstract class AbstractUser implements SymfonyUserInterface, PasswordAuthenticatedUserInterface, JWTUserInterface
39
{
40
    use IdTrait;
41
    use TimestampedTrait;
42
43
    #[Assert\NotBlank(message: 'Please enter a username.', groups: ['Default'])]
44
    #[Groups(['User:superAdmin', 'User:output', 'Form:cwa_resource:read'])]
45
    protected ?string $username;
46
47
    #[Assert\NotBlank(groups: ['Default'])]
48
    #[Assert\Email]
49
    #[Groups(['User:superAdmin', 'User:output', 'Form:cwa_resource:read'])]
50
    protected ?string $emailAddress;
51
52
    #[Groups(['User:superAdmin', 'User:output', 'Form:cwa_resource:read'])]
53
    protected array $roles;
54
55
    #[Groups(['User:superAdmin'])]
56
    protected bool $enabled;
57
58
    #[ApiProperty(readable: false, writable: false)]
59
    protected string $password;
60
61
    #[Assert\NotBlank(message: 'Please enter your desired password.', groups: ['User:password:create'])]
62
    #[Assert\Length(min: 6, max: 4096, minMessage: 'Your password must be more than 6 characters long.', maxMessage: 'Your password cannot be over 4096 characters', groups: ['User:password:create'])]
63
    #[ApiProperty(readable: false)]
64
    #[Groups(['User:input'])]
65
    protected ?string $plainPassword = null;
66
67
    /**
68
     * Random string sent to the user email address in order to verify it.
69
     */
70
    #[ApiProperty(readable: false, writable: false)]
71
    protected ?string $newPasswordConfirmationToken = null;
72
73
    #[ApiProperty(readable: false, writable: false)]
74
    public ?string $plainNewPasswordConfirmationToken = null;
75
76
    #[ApiProperty(readable: false, writable: false)]
77
    protected ?DateTime $passwordRequestedAt = null;
78
79
    #[UserPassword(message: 'You have not entered your current password correctly. Please try again.', groups: ['User:password:change'])]
80
    #[ApiProperty(readable: false)]
81
    #[Groups(['User:input'])]
82
    protected ?string $oldPassword = null;
83
84
    #[ApiProperty(readable: false, writable: false)]
85
    protected ?DateTime $passwordUpdatedAt = null;
86
87
    #[Assert\NotBlank(allowNull: true, groups: ['User:emailAddress', 'Default'])]
88
    #[Assert\Email]
89
    #[Groups(['User:input', 'User:output', 'User:emailAddress', 'Form:cwa_resource:read:role_user'])]
90
    protected ?string $newEmailAddress = null;
91
92
    /**
93
     * Random string sent to the user's new email address in order to verify it.
94
     */
95
    #[ApiProperty(readable: false, writable: false)]
96
    protected ?string $newEmailConfirmationToken = null;
97
98
    #[ApiProperty(readable: false, writable: false)]
99
    #[Groups(['User:output'])]
100
    protected ?DateTime $newEmailAddressChangeRequestedAt = null;
101
102
    #[ApiProperty(readable: false, writable: false)]
103
    public ?string $plainNewEmailConfirmationToken = null;
104
105
    #[ApiProperty(readable: false, writable: false)]
106
    protected bool $emailAddressVerified = false;
107
108
    /**
109
     * Random string sent to previous email address when email is changed to permit email restore and password change.
110
     */
111
    #[ApiProperty(readable: false, writable: false)]
112
    protected ?string $emailAddressVerifyToken = null;
113
114
    #[ApiProperty(readable: false, writable: false)]
115
    public ?string $plainEmailAddressVerifyToken = null;
116
117
    #[ApiProperty(readable: false, writable: false)]
118
    protected ?DateTime $emailLastUpdatedAt = null;
119
120
    /**
121
     * `final` to make `createFromPayload` safe. Could instead make an interface? Or abstract and force child to define constructor?
122
     */
123 49
    public function __construct(string $username = '', string $emailAddress = '', bool $emailAddressVerified = false, array $roles = ['ROLE_USER'], string $password = '', bool $enabled = true)
124
    {
125 49
        $this->username = $username;
126 49
        $this->emailAddress = $emailAddress;
127 49
        $this->emailAddressVerified = $emailAddressVerified;
128 49
        $this->roles = $roles;
129 49
        $this->password = $password;
130 49
        $this->enabled = $enabled;
131
    }
132
133 23
    public function getUsername(): string
134
    {
135 23
        return $this->username;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->username could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
136
    }
137
138 16
    public function setUsername(string $username): self
139
    {
140 16
        $this->username = $username;
141
142 16
        return $this;
143
    }
144
145 18
    public function getEmailAddress(): ?string
146
    {
147 18
        return $this->emailAddress;
148
    }
149
150 18
    public function setEmailAddress(?string $emailAddress): self
151
    {
152 18
        $this->emailAddress = $emailAddress;
153 18
        if ($emailAddress) {
154 18
            $this->emailLastUpdatedAt = new \DateTime();
155
        }
156
157 18
        return $this;
158
    }
159
160 4
    public function getRoles(): array
161
    {
162 4
        return $this->roles;
163
    }
164
165 1
    public function setRoles(array $roles): self
166
    {
167 1
        $this->roles = $roles;
168
169 1
        return $this;
170
    }
171
172 6
    public function isEnabled(): bool
173
    {
174 6
        return $this->enabled;
175
    }
176
177 4
    public function setEnabled(bool $enabled): self
178
    {
179 4
        $this->enabled = $enabled;
180
181 4
        return $this;
182
    }
183
184 3
    public function getPassword(): ?string
185
    {
186 3
        return $this->password;
187
    }
188
189 2
    public function setPassword(string $password): self
190
    {
191 2
        $this->password = $password;
192
193 2
        return $this;
194
    }
195
196 1
    public function getPlainPassword(): ?string
197
    {
198 1
        return $this->plainPassword;
199
    }
200
201 1
    public function setPlainPassword(?string $plainPassword): self
202
    {
203 1
        $this->plainPassword = $plainPassword;
204 1
        if ($plainPassword) {
205
            // Needs to update mapped field to trigger update event which will encode the plain password
206 1
            $this->passwordUpdatedAt = new \DateTime();
207
        }
208
209 1
        return $this;
210
    }
211
212 1
    public function getNewPasswordConfirmationToken(): ?string
213
    {
214 1
        return $this->newPasswordConfirmationToken;
215
    }
216
217 1
    public function setNewPasswordConfirmationToken(?string $newPasswordConfirmationToken): self
218
    {
219 1
        $this->newPasswordConfirmationToken = $newPasswordConfirmationToken;
220
221 1
        return $this;
222
    }
223
224 2
    public function getPasswordRequestedAt(): ?DateTime
225
    {
226 2
        return $this->passwordRequestedAt;
227
    }
228
229 2
    public function setPasswordRequestedAt(?DateTime $passwordRequestedAt): self
230
    {
231 2
        $this->passwordRequestedAt = $passwordRequestedAt;
232
233 2
        return $this;
234
    }
235
236 1
    public function getOldPassword(): ?string
237
    {
238 1
        return $this->oldPassword;
239
    }
240
241 1
    public function setOldPassword(?string $oldPassword): self
242
    {
243 1
        $this->oldPassword = $oldPassword;
244
245 1
        return $this;
246
    }
247
248 6
    public function getNewEmailAddress(): ?string
249
    {
250 6
        return $this->newEmailAddress;
251
    }
252
253 4
    public function setNewEmailAddress(?string $newEmailAddress): self
254
    {
255 4
        $this->newEmailAddress = $newEmailAddress;
256 4
        if ($newEmailAddress) {
257 4
            $this->newEmailAddressChangeRequestedAt = new \DateTime();
258
        }
259
260 4
        return $this;
261
    }
262
263 1
    public function getNewEmailConfirmationToken(): ?string
264
    {
265 1
        return $this->newEmailConfirmationToken;
266
    }
267
268 1
    public function setNewEmailConfirmationToken(?string $newEmailConfirmationToken): self
269
    {
270 1
        $this->newEmailConfirmationToken = $newEmailConfirmationToken;
271
272 1
        return $this;
273
    }
274
275
    public function getNewEmailAddressChangeRequestedAt(): ?DateTime
276
    {
277
        return $this->newEmailAddressChangeRequestedAt;
278
    }
279
280 6
    public function isEmailAddressVerified(): bool
281
    {
282 6
        return $this->emailAddressVerified;
283
    }
284
285 5
    public function setEmailAddressVerified(bool $emailAddressVerified): self
286
    {
287 5
        $this->emailAddressVerified = $emailAddressVerified;
288
289 5
        return $this;
290
    }
291
292
    public function getEmailAddressVerifyToken(): ?string
293
    {
294
        return $this->emailAddressVerifyToken;
295
    }
296
297
    public function setEmailAddressVerifyToken(?string $emailAddressVerifyToken): void
298
    {
299
        $this->emailAddressVerifyToken = $emailAddressVerifyToken;
300
    }
301
302 1
    public function isPasswordRequestLimitReached($ttl): bool
303
    {
304 1
        $lastRequest = $this->getPasswordRequestedAt();
305
306
        return $lastRequest instanceof DateTime &&
307 1
            $lastRequest->getTimestamp() + $ttl > time();
308
    }
309
310
    /** @see \Serializable::serialize() */
311 1
    public function serialize(): string
312
    {
313 1
        return serialize(
314
            [
315 1
                (string) $this->id,
316 1
                $this->username,
317 1
                $this->emailAddress,
318 1
                $this->password,
319 1
                $this->enabled,
320 1
                $this->roles,
321
            ]
322
        );
323
    }
324
325
    /**
326
     * @see \Serializable::unserialize()
327
     */
328 2
    public function unserialize(string $serialized): self
329
    {
330 2
        $id = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $id is dead and can be removed.
Loading history...
331
        [
332
            $id,
333 2
            $this->username,
334 2
            $this->emailAddress,
335 2
            $this->password,
336 2
            $this->enabled,
337 2
            $this->roles,
338 2
        ] = unserialize($serialized, ['allowed_classes' => false]);
339 2
        $this->id = Uuid::fromString($id);
340
341 2
        return $this;
342
    }
343
344
    /**
345
     * Not needed - we use bcrypt.
346
     */
347
    #[ApiProperty(readable: false, writable: false)]
348
    public function getSalt(): ?string
349
    {
350
        return null;
351
    }
352
353
    /**
354
     * Remove sensitive data - e.g. plain passwords etc.
355
     */
356 1
    public function eraseCredentials(): void
357
    {
358 1
        $this->plainPassword = null;
359
    }
360
361 1
    public function __toString()
362
    {
363 1
        return (string) $this->id;
364
    }
365
366
    public static function createFromPayload($username, array $payload): JWTUserInterface
367
    {
368
        $newUser = new static(
369
            $username,
370
            $payload['emailAddress'],
371
            $payload['emailAddressVerified'],
372
            $payload['roles']
373
        );
374
375
        $newUser->setNewEmailAddress($payload['newEmailAddress']);
376
377
        $reflection = new \ReflectionClass(static::class);
378
        $idProperty = $reflection->getProperty('id');
379
        $idProperty->setAccessible(true);
380
        $idProperty->setValue($newUser, Uuid::fromString($payload['id']));
381
382
        return $newUser;
383
    }
384
385
    public function getUserIdentifier(): string
386
    {
387
        return $this->getUsername();
388
    }
389
}
390