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