diff --git a/src/Common/ActionResult.php b/src/Common/ActionResult.php index d9adf77..21a125f 100644 --- a/src/Common/ActionResult.php +++ b/src/Common/ActionResult.php @@ -24,7 +24,7 @@ class ActionResult return new self(self::STATUS_SUCCESS, $successMessage); } - public static function createErrorResult(string $errorMessage, array $inputData): self + public static function createErrorResult(string $errorMessage, ?array $inputData = null): self { return new self(self::STATUS_ERROR, $errorMessage, $inputData); } diff --git a/src/Common/UserHelper.php b/src/Common/UserHelper.php index f156459..0607fe3 100644 --- a/src/Common/UserHelper.php +++ b/src/Common/UserHelper.php @@ -3,19 +3,21 @@ declare(strict_types=1); namespace MailAccountAdmin\Common; -use RuntimeException; +use MailAccountAdmin\Exceptions\UnauthenticatedException; use MailAccountAdmin\Models\AdminUser; use MailAccountAdmin\Repositories\AdminUserRepository; class UserHelper { - private SessionHelper $sessionHelper; private AdminUserRepository $adminUserRepository; + private SessionHelper $sessionHelper; + private PasswordHelper $passwordHelper; - public function __construct(SessionHelper $sessionHelper, AdminUserRepository $adminUserRepository) + public function __construct(AdminUserRepository $adminUserRepository, SessionHelper $sessionHelper, PasswordHelper $passwordHelper) { - $this->sessionHelper = $sessionHelper; $this->adminUserRepository = $adminUserRepository; + $this->sessionHelper = $sessionHelper; + $this->passwordHelper = $passwordHelper; } public function isLoggedIn(): bool @@ -27,8 +29,17 @@ class UserHelper { $userId = $this->sessionHelper->getUserId(); if (empty($userId)) { - throw new RuntimeException('Not logged in!'); + throw new UnauthenticatedException('Not logged in!'); } return $this->adminUserRepository->getUserById($userId); } + + public function confirmActionByAdminPassword(string $enteredPassword): void + { + $currentUser = $this->getCurrentUser(); + + if (!$this->passwordHelper->verifyPassword($enteredPassword, $currentUser->getPasswordHash())) { + throw new UnauthenticatedException('Admin password wrong.'); + } + } } diff --git a/src/Dependencies.php b/src/Dependencies.php index 654bbed..53d21f1 100644 --- a/src/Dependencies.php +++ b/src/Dependencies.php @@ -98,8 +98,9 @@ class Dependencies $container->set(UserHelper::class, function (ContainerInterface $c) { return new UserHelper( - $c->get(SessionHelper::class), $c->get(AdminUserRepository::class), + $c->get(SessionHelper::class), + $c->get(PasswordHelper::class), ); }); diff --git a/src/Exceptions/UnauthenticatedException.php b/src/Exceptions/UnauthenticatedException.php new file mode 100644 index 0000000..3dda3ca --- /dev/null +++ b/src/Exceptions/UnauthenticatedException.php @@ -0,0 +1,8 @@ +getQueryParams(); $filterByDomain = $queryParams['domain'] ?? ''; + // Get list of all accounts $renderData = $this->accountHandler->listAccounts($filterByDomain); + + // If the form has been submitted, add the result message to the render data array + $renderData = $this->addLastActionResultToRenderData($renderData); + return $this->view->render($response, 'accounts.html.twig', $renderData); } @@ -55,10 +61,7 @@ class AccountController extends BaseController $renderData = $this->accountHandler->getPageDataForCreate(); // If the form has been submitted, add the result message and form input data to the render data array - $lastActionResult = $this->sessionHelper->getLastActionResult(); - if ($lastActionResult !== null) { - $renderData = array_merge($renderData, $lastActionResult->getRenderData()); - } + $renderData = $this->addLastActionResultToRenderData($renderData); return $this->view->render($response, 'account_create.html.twig', $renderData); } @@ -100,10 +103,7 @@ class AccountController extends BaseController $renderData = $this->accountHandler->getAccountDataForEdit($accountId); // If the form has been submitted, add the result message and form input data to the render data array - $lastActionResult = $this->sessionHelper->getLastActionResult(); - if ($lastActionResult !== null) { - $renderData = array_merge($renderData, $lastActionResult->getRenderData()); - } + $renderData = $this->addLastActionResultToRenderData($renderData); return $this->view->render($response, 'account_edit.html.twig', $renderData); } @@ -145,13 +145,47 @@ class AccountController extends BaseController // Get account data and list of aliases from database $renderData = $this->accountHandler->getAccountDataForDelete($accountId); + // If the form has been submitted, add the result message to the render data array + $renderData = $this->addLastActionResultToRenderData($renderData); + return $this->view->render($response, 'account_delete.html.twig', $renderData); } public function deleteAccount(Request $request, Response $response, array $args): Response { - // TODO: just a placeholder - $this->view->getEnvironment()->addGlobal('error', 'Not implemented yet!'); - return $this->showAccountDelete($request, $response, $args); + // Parse URL arguments and form data + $accountId = (int)$args['id']; + $formData = $request->getParsedBody(); + + try { + // Confirm action by entering the admin password + $this->userHelper->confirmActionByAdminPassword($formData['admin_password'] ?? ''); + + // Delete account + $deleteResult = $this->accountHandler->deleteAccount($accountId); + + // Save success result + $successMessage = "Account {$deleteResult['username']} "; + $deletedAliasCount = $deleteResult['deleted_alias_count']; + if ($deletedAliasCount > 0) { + $aliasWordPlural = $deletedAliasCount > 1 ? 'aliases' : 'alias'; + $successMessage .= "and {$deletedAliasCount} {$aliasWordPlural} were deleted."; + } else { + $successMessage .= "was deleted."; + } + $this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult($successMessage)); + + // Redirect to account list (where the success message will be displayed) + $redirectTarget = '/accounts'; + } catch (AppException $e) { + // Save error result + $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage())); + + // Stay on delete page + $redirectTarget = '/accounts/' . $accountId . '/delete'; + } + + // Redirect to edit form page via GET (PRG) + return $response->withHeader('Location', $redirectTarget)->withStatus(303); } } diff --git a/src/Frontend/Accounts/AccountHandler.php b/src/Frontend/Accounts/AccountHandler.php index 4e58e5a..0556e6a 100644 --- a/src/Frontend/Accounts/AccountHandler.php +++ b/src/Frontend/Accounts/AccountHandler.php @@ -212,7 +212,7 @@ class AccountHandler // Remove existing alias for new username (if wanted) if ($editData->getUsernameReplaceAlias() && $aliasNeedsToBeReplaced) { - $this->aliasRepository->removeAlias($accountId, $newUsername); + $this->aliasRepository->deleteAlias($accountId, $newUsername); } // Commit database transaction @@ -238,4 +238,31 @@ class AccountHandler 'aliases' => $aliases, ]; } + + 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!'); + } + + // Start database transaction + $this->accountRepository->beginTransaction(); + + // Delete all aliases associated with this account + $deleteAliasCount = $this->aliasRepository->deleteAllAliasesForUserId($accountId); + + // Delete account from database + $this->accountRepository->deleteAccountWithId($accountId); + + // Commit database transaction + $this->accountRepository->commitTransaction(); + + return [ + 'username' => $account->getUsername(), + 'deleted_alias_count' => $deleteAliasCount, + ]; + } } diff --git a/src/Frontend/BaseController.php b/src/Frontend/BaseController.php index 278fb2e..eadc069 100644 --- a/src/Frontend/BaseController.php +++ b/src/Frontend/BaseController.php @@ -23,4 +23,13 @@ class BaseController $twigEnv = $view->getEnvironment(); $twigEnv->addGlobal('current_user_name', $userHelper->isLoggedIn() ? $userHelper->getCurrentUser()->getUsername() : null); } + + protected function addLastActionResultToRenderData(array $renderData): array + { + $lastActionResult = $this->sessionHelper->getLastActionResult(); + if ($lastActionResult !== null) { + $renderData = array_merge($renderData, $lastActionResult->getRenderData()); + } + return $renderData; + } } diff --git a/src/Repositories/AccountRepository.php b/src/Repositories/AccountRepository.php index 318ada1..f66a5ca 100644 --- a/src/Repositories/AccountRepository.php +++ b/src/Repositories/AccountRepository.php @@ -124,4 +124,10 @@ class AccountRepository extends BaseRepository $statement = $this->pdo->prepare($query); $statement->execute($queryParams); } + + public function deleteAccountWithId(int $accountId): void + { + $statement = $this->pdo->prepare('DELETE FROM mail_users WHERE user_id = :user_id LIMIT 1'); + $statement->execute(['user_id' => $accountId]); + } } diff --git a/src/Repositories/AliasRepository.php b/src/Repositories/AliasRepository.php index 91bea5b..86b9caf 100644 --- a/src/Repositories/AliasRepository.php +++ b/src/Repositories/AliasRepository.php @@ -52,7 +52,7 @@ class AliasRepository extends BaseRepository ]); } - public function removeAlias(int $userId, string $mailAddress): void + public function deleteAlias(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'); @@ -61,4 +61,13 @@ class AliasRepository extends BaseRepository 'mail_address' => $mailAddress, ]); } + + public function deleteAllAliasesForUserId(int $userId): int + { + $statement = $this->pdo->prepare('DELETE FROM mail_aliases WHERE user_id = :user_id'); + $statement->execute([ + 'user_id' => $userId, + ]); + return $statement->rowCount(); + } } diff --git a/templates/account_delete.html.twig b/templates/account_delete.html.twig index 6b0f949..927698e 100644 --- a/templates/account_delete.html.twig +++ b/templates/account_delete.html.twig @@ -15,21 +15,16 @@
You are about to delete the mail account "{{ accountUsername }}" including the following aliases:
+You are about to delete the mail account "{{ accountUsername }}" including the following aliases:
Note: This will only delete the user entry from the database. Mail data will not be deleted!
+Note: This will only delete the account entry from the database. Mail data will not be deleted!
- {% if error is defined %} -Please confirm that you want to delete this account by entering your password.
diff --git a/templates/accounts.html.twig b/templates/accounts.html.twig index ae99225..9f5086c 100644 --- a/templates/accounts.html.twig +++ b/templates/accounts.html.twig @@ -13,6 +13,8 @@