Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add the status "deleted" to the user table #529

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions config/autoload/authentication.global.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@
'invalid_credential' => Message::INVALID_CREDENTIALS,
],
'options' => [
'status' => [
'status' => [
'value' => UserStatusEnum::Active,
'message' => Message::USER_NOT_ACTIVATED,
],
'isDeleted' => [
'value' => false,
'message' => Message::ACCOUNT_NOT_FOUND,
],
],
],
],
Expand Down
2 changes: 1 addition & 1 deletion data/doctrine/migrations/Version20241120160406.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE contact_message (uuid BINARY(16) NOT NULL, email VARCHAR(150) NOT NULL, name VARCHAR(150) NOT NULL, subject LONGTEXT NOT NULL, message LONGTEXT NOT NULL, platform LONGTEXT NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE user (uuid BINARY(16) NOT NULL, identity VARCHAR(191) NOT NULL, password VARCHAR(191) NOT NULL, status ENUM(\'active\', \'pending\') DEFAULT \'pending\' NOT NULL, isDeleted TINYINT(1) NOT NULL, hash VARCHAR(64) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D6496A95E9C4 (identity), UNIQUE INDEX UNIQ_8D93D649D1B862B8 (hash), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE user (uuid BINARY(16) NOT NULL, identity VARCHAR(191) NOT NULL, password VARCHAR(191) NOT NULL, status ENUM(\'active\', \'pending\', \'deleted\') DEFAULT \'pending\' NOT NULL, hash VARCHAR(64) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D6496A95E9C4 (identity), UNIQUE INDEX UNIQ_8D93D649D1B862B8 (hash), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE user_roles (userUuid BINARY(16) NOT NULL, roleUuid BINARY(16) NOT NULL, INDEX IDX_54FCD59FD73087E9 (userUuid), INDEX IDX_54FCD59F88446210 (roleUuid), PRIMARY KEY(userUuid, roleUuid)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE user_avatar (uuid BINARY(16) NOT NULL, name VARCHAR(191) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_73256912D73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE user_detail (uuid BINARY(16) NOT NULL, firstName VARCHAR(191) DEFAULT NULL, lastName VARCHAR(191) DEFAULT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, userUuid BINARY(16) NOT NULL, UNIQUE INDEX UNIQ_4B5464AED73087E9 (userUuid), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4');
Expand Down
2 changes: 1 addition & 1 deletion src/App/src/Middleware/RememberMeMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$hash === $rememberUser->getRememberMeToken() &&
$rememberUser->getUserAgent() === $deviceType &&
$rememberUser->getExpireDate() > new DateTimeImmutable('now') &&
$user->getIsDeleted() === false
$user->isDeleted() === false
) {
$userIdentity = UserIdentity::fromEntity($user);
$this->authenticationService->getStorage()->write($userIdentity);
Expand Down
11 changes: 11 additions & 0 deletions src/User/src/Adapter/AuthenticationAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Frontend\User\Adapter;

use Doctrine\ORM\EntityRepository;
use Frontend\App\Common\Message;
use Frontend\User\Entity\UserIdentity;
use Frontend\User\Exception\AuthenticationAdapterException;
use Laminas\Authentication\Adapter\AbstractAdapter;
Expand Down Expand Up @@ -39,6 +40,16 @@ public function authenticate(): Result
);
}

$methodName = 'isDeleted';
$this->checkMethod($identityClass, $methodName);
if ($identityClass->$methodName()) {
return new Result(
Result::FAILURE_IDENTITY_NOT_FOUND,
null,
[Message::ACCOUNT_NOT_FOUND]
);
}

$getCredential = 'get' . ucfirst($this->config['credential_property']);

$this->checkMethod($identityClass, $getCredential);
Expand Down
11 changes: 2 additions & 9 deletions src/User/src/Controller/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,13 @@ public function unregisterAction(): ResponseInterface
return new RedirectResponse($this->router->generateUri('user', ['action' => 'login']));
}

if ($user->getIsDeleted() === User::IS_DELETED_YES) {
$this->messenger->addError(Message::USER_ALREADY_DEACTIVATED, 'user-login');
return new RedirectResponse($this->router->generateUri('user', ['action' => 'login']));
}

if (! $user->isPending()) {
$this->messenger->addError(Message::USER_UNREGISTER_STATUS, 'user-login');
return new RedirectResponse($this->router->generateUri('user', ['action' => 'login']));
}

try {
$this->userService->updateUser($user, ['isDeleted' => User::IS_DELETED_YES]);
$this->userService->deleteUser($user);
} catch (Exception $exception) {
$this->messenger->addError($exception->getMessage(), 'user-login');
return new RedirectResponse($this->router->generateUri('user', ['action' => 'login']));
Expand Down Expand Up @@ -423,10 +418,8 @@ public function deleteAccountAction(): ResponseInterface
if (RequestMethodInterface::METHOD_POST === $this->request->getMethod()) {
$form->setData($this->request->getParsedBody());
if ($form->isValid()) {
/** @var array $userData */
$userData = $form->getData();
try {
$this->userService->updateUser($user, $userData);
$this->userService->deleteUser($user);
$this->userService->deleteAvatar($user);
} catch (Exception $e) {
$this->messenger->addData('shouldRebind', true);
Expand Down
21 changes: 2 additions & 19 deletions src/User/src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ class User extends AbstractEntity implements UserInterface
#[ORM\Column(type: 'user_status_enum', options: ['default' => UserStatusEnum::Pending])]
protected UserStatusEnum $status = UserStatusEnum::Pending;

#[ORM\Column(name: 'isDeleted', type: 'boolean')]
protected bool $isDeleted = self::IS_DELETED_NO;

#[ORM\Column(name: 'hash', type: 'string', length: 64, unique: true, nullable: false)]
protected string $hash;

Expand Down Expand Up @@ -138,18 +135,6 @@ public function setStatus(UserStatusEnum $status): self
return $this;
}

public function getIsDeleted(): bool
{
return $this->isDeleted;
}

public function setIsDeleted(bool $isDeleted): self
{
$this->isDeleted = $isDeleted;

return $this;
}

public function getHash(): string
{
return $this->hash;
Expand Down Expand Up @@ -213,11 +198,9 @@ public function isPending(): bool
return $this->status === UserStatusEnum::Pending;
}

public function markAsDeleted(): self
public function isDeleted(): bool
{
$this->isDeleted = self::IS_DELETED_YES;

return $this;
return $this->status === UserStatusEnum::Deleted;
}

public function getName(): string
Expand Down
2 changes: 0 additions & 2 deletions src/User/src/Entity/UserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ public function addRole(UserRole $role): UserInterface;

public function removeRole(UserRole $role): UserInterface;

public function getIsDeleted(): bool;

public function getArrayCopy(): array;

public function activate(): UserInterface;
Expand Down
1 change: 1 addition & 0 deletions src/User/src/Enum/UserStatusEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum UserStatusEnum: string
{
case Active = 'active';
case Pending = 'pending';
case Deleted = 'deleted';
}
2 changes: 1 addition & 1 deletion src/User/src/Form/ProfileDeleteForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function init(): void
'title' => 'Delete account',
],
'options' => [
'label' => 'I want to delete account',
'label' => 'I want to delete my account',
'use_hidden_element' => false,
'checked_value' => (string) User::IS_DELETED_YES,
'unchecked_value' => (string) User::IS_DELETED_NO,
Expand Down
17 changes: 14 additions & 3 deletions src/User/src/Repository/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Exception;
use Frontend\User\Entity\User;
use Frontend\User\Entity\UserRememberMe;
use Frontend\User\Enum\UserStatusEnum;
use Ramsey\Uuid\Uuid;

use function is_string;
Expand All @@ -36,6 +37,9 @@ public function findByUuid(string $uuid): ?User
->where("user.uuid = :uuid")
->setParameter('uuid', $uuid)
->setMaxResults(1);

//ignore deleted users
$qb->andWhere('user.status != :status')->setParameter('status', UserStatusEnum::Deleted);
return $qb->getQuery()->useQueryCache(true)->getOneOrNullResult();
}

Expand Down Expand Up @@ -72,7 +76,10 @@ public function findByResetPasswordHash(string $hash): ?User
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select(['user', 'resetPasswords'])->from(User::class, 'user')
->leftJoin('user.resetPasswords', 'resetPasswords')
->andWhere('resetPasswords.hash = :hash')->setParameter('hash', $hash);
->andWhere('resetPasswords.hash = :hash')
->setParameter('hash', $hash)
->andWhere('user.status != :deleted')
->setParameter('deleted', UserStatusEnum::Deleted);

return $qb->getQuery()->useQueryCache(true)->getSingleResult();
} catch (Exception) {
Expand All @@ -95,7 +102,9 @@ public function getRememberUser(string $token): ?UserRememberMe
$qb->select('user_remember_me')
->from(UserRememberMe::class, 'user_remember_me')
->where('user_remember_me.rememberMeToken = :token')
->setParameter('token', $token);
->setParameter('token', $token)
->andWhere('user.status != :deleted')
->setParameter('deleted', UserStatusEnum::Deleted);

return $qb->getQuery()->useQueryCache(true)->getOneOrNullResult();
}
Expand All @@ -111,7 +120,9 @@ public function findRememberMeUser(User $user, string $userAgent): ?UserRemember
->where('user_remember_me.user = :uuid')
->setParameter('uuid', $user->getUuid()->getBytes())
->andWhere('user_remember_me.userAgent = :userAgent')
->setParameter('userAgent', $userAgent);
->setParameter('userAgent', $userAgent)
->andWhere('user.status != :deleted')
->setParameter('deleted', UserStatusEnum::Deleted);

return $qb->getQuery()->useQueryCache(true)->getOneOrNullResult();
}
Expand Down
45 changes: 28 additions & 17 deletions src/User/src/Service/UserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Laminas\Diactoros\UploadedFile;
use Mezzio\Template\TemplateRendererInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;

use function date;
use function file_exists;
Expand Down Expand Up @@ -138,22 +139,6 @@ public function updateUser(User $user, array $data = []): User
$user->setStatus($data['status']);
}

if (isset($data['isDeleted'])) {
$user->setIsDeleted((bool) $data['isDeleted']);

if ((bool) $data['isDeleted'] === true) {
// make user anonymous
$user->setIdentity(
sprintf('anonymous%s@%s', date('dmYHis'), $this->config['userAnonymizeAppend'])
);
$userDetails = $user->getDetail();
$userDetails->setFirstName('anonymous' . date('dmYHis'));
$userDetails->setLastName('anonymous' . date('dmYHis'));

$user->setDetail($userDetails);
}
}

if (isset($data['hash'])) {
$user->setHash($data['hash']);
}
Expand Down Expand Up @@ -189,6 +174,25 @@ public function updateUser(User $user, array $data = []): User
return $this->userRepository->saveUser($user);
}

public function deleteUser(User $user): User
{
$placeholder = $this->getAnonymousPlaceholder();

// make user anonymous
$user
bidi47 marked this conversation as resolved.
Show resolved Hide resolved
->setStatus(UserStatusEnum::Deleted)
->setIdentity($placeholder . $this->config['userAnonymizeAppend'])
->getDetail()
->setFirstName($placeholder)
->setLastName($placeholder);
return $this->userRepository->saveUser($user);
}

private function getAnonymousPlaceholder(): string
{
return 'anonymous' . date('dmYHis');
}

protected function createAvatar(User $user, UploadedFile $uploadedFile): UserAvatar
{
$path = sprintf('%s/%s/', $this->config['uploads']['user']['path'], $user->getUuid()->toString());
Expand Down Expand Up @@ -282,7 +286,13 @@ public function findOneBy(array $params = []): ?User
return null;
}

return $this->userRepository->findOneBy($params);
$user = $this->userRepository->findOneBy($params);

if (! $user instanceof User || $user->isDeleted()) {
return null;
}

return $user;
}

public function activateUser(User $user): User
Expand Down Expand Up @@ -320,6 +330,7 @@ public function findByResetPasswordHash(?string $hash): ?User

/**
* @throws MailException
* @throws TransportExceptionInterface
*/
public function sendResetPasswordCompletedMail(User $user): bool
{
Expand Down
2 changes: 2 additions & 0 deletions src/User/src/Service/UserServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function deleteExpiredRememberMeTokens(): void;

public function updateUser(User $user, array $data = []): UserInterface;

public function deleteUser(User $user): UserInterface;

public function deleteAvatar(User $user): void;

public function deleteAvatarFile(string $path): bool;
Expand Down
2 changes: 0 additions & 2 deletions test/Unit/App/Middleware/RememberMeMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public function testAutologin(): void

$user = (new User())
->setIdentity('[email protected]')
->setIsDeleted(false)
->activate();

$detail = (new UserDetail())
Expand Down Expand Up @@ -126,7 +125,6 @@ public function testAutologinExpired(): void

$user = (new User())
->setIdentity('[email protected]')
->setIsDeleted(false)
->activate();

$detail = (new UserDetail())
Expand Down
3 changes: 3 additions & 0 deletions test/Unit/User/Adapter/AuthenticationAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ public function testCheckMethodThrowsException(): void
$this->expectExceptionMessage(
(AuthenticationAdapterException::methodNotExists('getPassword', $class::class))->getMessage()
);
$this->expectExceptionMessage(
(AuthenticationAdapterException::methodNotExists('isDeleted', $class::class))->getMessage()
);

$adapter = new AuthenticationAdapter($repository, $this->getConfig($class));

Expand Down
Loading