From 36b8cfe8b17312dc7b9daf4af123a91c17e6aa5c Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Fri, 20 Aug 2021 22:48:15 +0200 Subject: [PATCH] Implement option to replace existing alias when renaming an account; use database transactions when editing accounts --- src/Exceptions/AliasNotFoundException.php | 8 +++ src/Frontend/Accounts/AccountHandler.php | 45 ++++++++++++---- src/Models/Alias.php | 65 +++++++++++++++++++++++ src/Repositories/AliasRepository.php | 34 +++++++++++- src/Repositories/BaseRepository.php | 20 +++++++ templates/account_delete.html.twig | 2 +- templates/account_details.html.twig | 4 +- 7 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 src/Exceptions/AliasNotFoundException.php create mode 100644 src/Models/Alias.php diff --git a/src/Exceptions/AliasNotFoundException.php b/src/Exceptions/AliasNotFoundException.php new file mode 100644 index 0000000..f86f8ec --- /dev/null +++ b/src/Exceptions/AliasNotFoundException.php @@ -0,0 +1,8 @@ +getUsername(); if ($newUsername === $account->getUsername()) { // Username is unchanged $newUsername = null; } - // TODO: This feature is not supported yet. If the alias exists, the availability check (next) would fail anyway. - if ($editData->getUsernameReplaceAlias()) { - throw new InputValidationError('Replace alias: Not implemented yet.'); - } + // This variable will be set to true if the user wants to change their username, has an existing alias with this username, + // and checked the "replace existing alias" option. + $aliasNeedsToBeReplaced = false; // Check if new username is still available if ($newUsername !== null) { - if (!$this->accountRepository->checkUsernameAvailable($newUsername) || !$this->aliasRepository->checkAliasAvailable($newUsername)) { + $newUsernameAvailable = true; + + // Check if account with this username already exists + if (!$this->accountRepository->checkUsernameAvailable($newUsername)) { + $newUsernameAvailable = false; + } + + // Check if alias with this username/address already exists + if (!$this->aliasRepository->checkAliasAvailable($newUsername)) { + $newUsernameAvailable = false; + + // Alias already exists. If user wants to replace an existing alias, check if the alias belongs to this user. + if ($editData->getUsernameReplaceAlias()) { + $existingAlias = $this->aliasRepository->fetchAliasByAddress($newUsername); + if ($existingAlias->getUserId() === $accountId) { + $aliasNeedsToBeReplaced = true; + $newUsernameAvailable = true; + } + } + } + + if (!$newUsernameAvailable) { throw new InputValidationError("Username \"$newUsername\" is not available."); } } + // Start database transaction + $this->accountRepository->beginTransaction(); + // Create alias for old username (if wanted) if ($editData->getUsernameCreateAlias()) { $oldUsername = $account->getUsername(); @@ -119,9 +140,11 @@ class AccountHandler ); // Remove existing alias for new username (if wanted) - // TODO: See above. This is the point where the alias should be deleted. - // if ($editData->getUsernameReplaceAlias()) { - // throw new InputValidationError('Replace alias: Not implemented yet.'); - // } + if ($editData->getUsernameReplaceAlias() && $aliasNeedsToBeReplaced) { + $this->aliasRepository->removeAlias($accountId, $newUsername); + } + + // Commit database transaction + $this->accountRepository->commitTransaction(); } } diff --git a/src/Models/Alias.php b/src/Models/Alias.php new file mode 100644 index 0000000..94e8052 --- /dev/null +++ b/src/Models/Alias.php @@ -0,0 +1,65 @@ +id = $id; + $this->userId = $userId; + $this->mailAddress = $mailAddress; + $this->createdAt = $createdAt; + $this->modifiedAt = $modifiedAt; + } + + public static function createFromArray(array $data): self + { + return new self( + (int)$data['alias_id'], + (int)$data['user_id'], + $data['mail_address'], + new DateTimeImmutable($data['created_at']), + new DateTimeImmutable($data['modified_at']), + ); + } + + public function getId(): int + { + return $this->id; + } + + public function getUserId(): int + { + return $this->userId; + } + + public function getMailAddress(): string + { + return $this->mailAddress; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getModifiedAt(): DateTimeImmutable + { + return $this->modifiedAt; + } +} diff --git a/src/Repositories/AliasRepository.php b/src/Repositories/AliasRepository.php index 0c779e5..91bea5b 100644 --- a/src/Repositories/AliasRepository.php +++ b/src/Repositories/AliasRepository.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace MailAccountAdmin\Repositories; +use MailAccountAdmin\Exceptions\AliasNotFoundException; +use MailAccountAdmin\Models\Alias; use PDO; class AliasRepository extends BaseRepository @@ -11,7 +13,27 @@ class AliasRepository extends BaseRepository { $statement = $this->pdo->prepare('SELECT * FROM mail_aliases WHERE user_id = :user_id ORDER BY mail_address'); $statement->execute(['user_id' => $userId]); - return $statement->fetchAll(PDO::FETCH_ASSOC); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + + // Create Account models from rows + $aliasList = []; + foreach ($rows as $row) { + $aliasList[] = Alias::createFromArray($row); + } + return $aliasList; + } + + public function fetchAliasByAddress(string $mailAddress): Alias + { + $statement = $this->pdo->prepare('SELECT * FROM mail_aliases WHERE mail_address = :mail_address LIMIT 1'); + $statement->execute(['mail_address' => $mailAddress]); + + if ($statement->rowCount() < 1) { + throw new AliasNotFoundException("Alias '$mailAddress' was not found."); + } + + $row = $statement->fetch(PDO::FETCH_ASSOC); + return Alias::createFromArray($row); } public function checkAliasAvailable(string $mailAddress): bool @@ -29,4 +51,14 @@ class AliasRepository extends BaseRepository 'mail_address' => $mailAddress, ]); } + + public function removeAlias(int $userId, string $mailAddress): void + { + // Check user ID in WHERE clause to make sure we don't accidentally delete someone else's alias + $statement = $this->pdo->prepare('DELETE FROM mail_aliases WHERE user_id = :user_id AND mail_address = :mail_address LIMIT 1'); + $statement->execute([ + 'user_id' => $userId, + 'mail_address' => $mailAddress, + ]); + } } diff --git a/src/Repositories/BaseRepository.php b/src/Repositories/BaseRepository.php index 2108442..158c55d 100644 --- a/src/Repositories/BaseRepository.php +++ b/src/Repositories/BaseRepository.php @@ -14,4 +14,24 @@ class BaseRepository { $this->pdo = $pdo; } + + public function beginTransaction(): void + { + $this->pdo->beginTransaction(); + } + + public function commitTransaction(): void + { + $this->pdo->commit(); + } + + public function rollBackTransaction(): void + { + $this->pdo->rollBack(); + } + + public function inTransaction(): bool + { + return $this->pdo->inTransaction(); + } } diff --git a/templates/account_delete.html.twig b/templates/account_delete.html.twig index 7a4840a..6b0f949 100644 --- a/templates/account_delete.html.twig +++ b/templates/account_delete.html.twig @@ -18,7 +18,7 @@

You are about to delete the mail account "{{ accountUsername }}" including the following aliases:

diff --git a/templates/account_details.html.twig b/templates/account_details.html.twig index 642f3aa..5021452 100644 --- a/templates/account_details.html.twig +++ b/templates/account_details.html.twig @@ -73,8 +73,8 @@ {% for alias in aliases %} - {{ alias['mail_address'] }} - {{ alias['created_at'] }} + {{ alias.getMailAddress() }} + {{ alias.getCreatedAt() | date }} Delete {# TODO #} {% endfor %}