Test Failed
Push — master ( edf801...e89e67 )
by Daniel
10:19
created

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