diff --git a/src/Common/FormData.php b/src/Common/FormData.php index 2756aba..fde1281 100644 --- a/src/Common/FormData.php +++ b/src/Common/FormData.php @@ -9,7 +9,7 @@ abstract class FormData { // Abstract methods - abstract public static function createFromArray($raw): self; + abstract public static function createFromArray(array $raw): self; // Input validation - Base types @@ -26,6 +26,16 @@ abstract class FormData return $raw; } + protected static function validateInteger(string $raw, bool $required = true, string $fieldName = 'Field'): int + { + if ($raw === '' && $required) { + throw new InputValidationError("$fieldName is required."); + } elseif (!is_numeric($raw)) { + throw new InputValidationError("$fieldName is not a number."); + } + return (int)$raw; + } + protected static function validateBoolOption(string $raw): bool { return $raw !== ''; diff --git a/src/Frontend/Accounts/AccountAddAliasData.php b/src/Frontend/Accounts/AccountAddAliasData.php index 306ca90..6e5a3c6 100644 --- a/src/Frontend/Accounts/AccountAddAliasData.php +++ b/src/Frontend/Accounts/AccountAddAliasData.php @@ -14,7 +14,7 @@ class AccountAddAliasData extends FormData $this->aliasAddress = $aliasAddress; } - public static function createFromArray($raw): self + public static function createFromArray(array $raw): self { return new self( self::validateAliasAddress(trim($raw['alias_address'] ?? '')), diff --git a/src/Frontend/Accounts/AccountController.php b/src/Frontend/Accounts/AccountController.php index 1f9d6ac..42ee4bb 100644 --- a/src/Frontend/Accounts/AccountController.php +++ b/src/Frontend/Accounts/AccountController.php @@ -236,7 +236,33 @@ class AccountController extends BaseController $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $addAliasData)); } - // Redirect to edit form page via GET (PRG) - return $response->withHeader('Location', '/accounts/' . $accountId)->withStatus(303); + // Redirect to account details page via GET (PRG) + return $response->withHeader('Location', '/accounts/' . $accountId . '#aliases')->withStatus(303); + } + + + // -- /accounts/{id}/deletealiases - Deletes a list of aliases + + public function deleteAliasesFromAccount(Request $request, Response $response, array $args): Response + { + // Parse URL arguments and form data + $accountId = (int)$args['id']; + $deleteAliasesData = $request->getParsedBody(); + + try { + // Validate input + $validatedDeleteAliasesData = AccountDeleteAliasesData::createFromArray($deleteAliasesData); + $deletedCount = $this->accountHandler->deleteAliasesFromAccount($accountId, $validatedDeleteAliasesData); + + // Save success result + $successMessage = $deletedCount === 1 ? "1 alias was deleted." : "{$deletedCount} aliases were deleted."; + $this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult($successMessage)); + } catch (AppException $e) { + // Save error result + $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage())); + } + + // Redirect to account details page via GET (PRG) + return $response->withHeader('Location', '/accounts/' . $accountId . '#aliases')->withStatus(303); } } diff --git a/src/Frontend/Accounts/AccountCreateData.php b/src/Frontend/Accounts/AccountCreateData.php index 265e5ed..166fa6e 100644 --- a/src/Frontend/Accounts/AccountCreateData.php +++ b/src/Frontend/Accounts/AccountCreateData.php @@ -22,7 +22,7 @@ class AccountCreateData extends FormData $this->memo = $memo; } - public static function createFromArray($raw): self + public static function createFromArray(array $raw): self { return new self( self::validateUsername(trim($raw['username'] ?? '')), diff --git a/src/Frontend/Accounts/AccountDeleteAliasesData.php b/src/Frontend/Accounts/AccountDeleteAliasesData.php new file mode 100644 index 0000000..2f828f3 --- /dev/null +++ b/src/Frontend/Accounts/AccountDeleteAliasesData.php @@ -0,0 +1,42 @@ +selectedAliasIds = $selectedAliasIds; + } + + public static function createFromArray(array $raw): self + { + $rawAliasIds = $raw['selected_aliases'] ?? []; + + if (!is_array($rawAliasIds)) { + throw new InputValidationError('selected_aliases is not an array.'); + } + if (empty($rawAliasIds)) { + throw new InputValidationError('No aliases were selected.'); + } + + $selectedAliasIds = []; + foreach ($rawAliasIds as $i => $id) { + $selectedAliasIds[] = self::validateInteger($id, true, "selected_aliases[$i]"); + } + + return new self($selectedAliasIds); + } + + public function getSelectedAliasIds(): array + { + return $this->selectedAliasIds; + } +} diff --git a/src/Frontend/Accounts/AccountEditData.php b/src/Frontend/Accounts/AccountEditData.php index a48a88e..499b6f8 100644 --- a/src/Frontend/Accounts/AccountEditData.php +++ b/src/Frontend/Accounts/AccountEditData.php @@ -29,7 +29,7 @@ class AccountEditData extends FormData $this->memo = $memo; } - public static function createFromArray($raw): self + public static function createFromArray(array $raw): self { return new self( self::validateUsername(trim($raw['username'] ?? ''), false), diff --git a/src/Frontend/Accounts/AccountHandler.php b/src/Frontend/Accounts/AccountHandler.php index 32dce32..4a71fc0 100644 --- a/src/Frontend/Accounts/AccountHandler.php +++ b/src/Frontend/Accounts/AccountHandler.php @@ -6,6 +6,7 @@ namespace MailAccountAdmin\Frontend\Accounts; use MailAccountAdmin\Common\PasswordHelper; use MailAccountAdmin\Exceptions\AccountNotFoundException; use MailAccountAdmin\Exceptions\InputValidationError; +use MailAccountAdmin\Models\Account; use MailAccountAdmin\Repositories\AccountRepository; use MailAccountAdmin\Repositories\AliasRepository; use MailAccountAdmin\Repositories\DomainRepository; @@ -27,6 +28,17 @@ class AccountHandler } + // -- Helper methods + + private function ensureAccountExists(int $accountId): Account + { + try { + return $this->accountRepository->fetchAccountById($accountId); + } catch (AccountNotFoundException $e) { + throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!'); + } + } + // -- /accounts - List all accounts public function listAccounts(string $filterByDomain): array @@ -134,11 +146,7 @@ class AccountHandler $returnData = []; // Check if account exists - try { - $account = $this->accountRepository->fetchAccountById($accountId); - } catch (AccountNotFoundException $e) { - throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!'); - } + $account = $this->ensureAccountExists($accountId); $newUsername = $editData->getUsername(); if ($newUsername === $account->getUsername()) { @@ -146,9 +154,9 @@ class AccountHandler $newUsername = null; } - // 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; + // If the user wants to change their username, has an existing alias with this username and checked the "replace existing alias" + // option, this variable will be set to the ID of the alias that needs to be deleted. + $aliasNeedsToBeReplaced = null; // Check if new username is still available if ($newUsername !== null) { @@ -167,7 +175,7 @@ class AccountHandler if ($editData->getUsernameReplaceAlias()) { $existingAlias = $this->aliasRepository->fetchAliasByAddress($newUsername); if ($existingAlias->getUserId() === $accountId) { - $aliasNeedsToBeReplaced = true; + $aliasNeedsToBeReplaced = $existingAlias->getId(); $newUsernameAvailable = true; } } @@ -211,8 +219,8 @@ class AccountHandler ); // Remove existing alias for new username (if wanted) - if ($editData->getUsernameReplaceAlias() && $aliasNeedsToBeReplaced) { - $this->aliasRepository->deleteAlias($accountId, $newUsername); + if ($editData->getUsernameReplaceAlias() && $aliasNeedsToBeReplaced !== null) { + $this->aliasRepository->deleteAliasById($accountId, $aliasNeedsToBeReplaced); } // Commit database transaction @@ -242,11 +250,7 @@ class AccountHandler public function deleteAccount(int $accountId): array { // Check if account exists - try { - $account = $this->accountRepository->fetchAccountById($accountId); - } catch (AccountNotFoundException $e) { - throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!'); - } + $account = $this->ensureAccountExists($accountId); // Start database transaction $this->accountRepository->beginTransaction(); @@ -272,11 +276,7 @@ class AccountHandler public function addAliasToAccount(int $accountId, AccountAddAliasData $aliasAddData): void { // Check if account exists - try { - $this->accountRepository->fetchAccountById($accountId); - } catch (AccountNotFoundException $e) { - throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!'); - } + $this->ensureAccountExists($accountId); // Check if alias address is still available $address = $aliasAddData->getAliasAddress(); @@ -287,4 +287,29 @@ class AccountHandler // Create alias in database $this->aliasRepository->createNewAlias($accountId, $address); } + + + // -- /accounts/{id}/deletealiases - Deletes a list of aliases from the account + + public function deleteAliasesFromAccount(int $accountId, AccountDeleteAliasesData $aliasesDeleteData): int + { + // Check if account exists + $this->ensureAccountExists($accountId); + + // Start database transaction + $this->aliasRepository->beginTransaction(); + + // Delete aliases + $deletedCount = 0; + foreach ($aliasesDeleteData->getSelectedAliasIds() as $aliasId) { + if ($this->aliasRepository->deleteAliasById($accountId, $aliasId)) { + $deletedCount++; + } + } + + // Commit database transaction + $this->aliasRepository->commitTransaction(); + + return $deletedCount; + } } diff --git a/src/Repositories/AliasRepository.php b/src/Repositories/AliasRepository.php index 86b9caf..45ed640 100644 --- a/src/Repositories/AliasRepository.php +++ b/src/Repositories/AliasRepository.php @@ -52,14 +52,15 @@ class AliasRepository extends BaseRepository ]); } - public function deleteAlias(int $userId, string $mailAddress): void + public function deleteAliasById(int $userId, int $aliasId): bool { // 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 = $this->pdo->prepare('DELETE FROM mail_aliases WHERE user_id = :user_id AND alias_id = :alias_id LIMIT 1'); $statement->execute([ 'user_id' => $userId, - 'mail_address' => $mailAddress, + 'alias_id' => $aliasId, ]); + return $statement->rowCount() > 0; } public function deleteAllAliasesForUserId(int $userId): int diff --git a/src/Routes.php b/src/Routes.php index 00611ab..c8b7a92 100644 --- a/src/Routes.php +++ b/src/Routes.php @@ -34,5 +34,6 @@ class Routes $app->get('/accounts/{id:[1-9][0-9]*}/delete', AccountController::class . ':showAccountDelete'); $app->post('/accounts/{id:[1-9][0-9]*}/delete', AccountController::class . ':deleteAccount'); $app->post('/accounts/{id:[1-9][0-9]*}/addalias', AccountController::class . ':addAliasToAccount'); + $app->post('/accounts/{id:[1-9][0-9]*}/deletealiases', AccountController::class . ':deleteAliasesFromAccount'); } } diff --git a/templates/account_details.html.twig b/templates/account_details.html.twig index 4e00c14..0c092cc 100644 --- a/templates/account_details.html.twig +++ b/templates/account_details.html.twig @@ -62,34 +62,41 @@ -

Aliases

+

Aliases

+ + {{ include('includes/form_result_box.html.twig') }} {% if aliases %} - - - - - - - {% for alias in aliases %} + +
AddressCreated atActions
- - - {# TODO #} + + + - {% endfor %} -
{{ alias.getMailAddress() }}{{ alias.getCreatedAt() | date }}DeleteAddressCreated at
+ {% for alias in aliases %} + + + + {{ alias.getCreatedAt() | date }} + + {% endfor %} + +

+ +

+ {% else %}

No aliases.

{% endif %}
- {{ include('includes/form_result_box.html.twig') }} - - +
+ +