Implement account deletion

This commit is contained in:
Lexi / Zoe 2021-09-16 20:54:21 +02:00
parent 2ccee2169b
commit 930726432e
Signed by: binaryDiv
GPG Key ID: F8D4956E224DA232
11 changed files with 130 additions and 28 deletions

View File

@ -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);
}

View File

@ -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.');
}
}
}

View File

@ -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),
);
});

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Exceptions;
class UnauthenticatedException extends AppException
{
}

View File

@ -6,6 +6,7 @@ namespace MailAccountAdmin\Frontend\Accounts;
use MailAccountAdmin\Common\ActionResult;
use MailAccountAdmin\Common\SessionHelper;
use MailAccountAdmin\Common\UserHelper;
use MailAccountAdmin\Exceptions\AppException;
use MailAccountAdmin\Exceptions\InputValidationError;
use MailAccountAdmin\Frontend\BaseController;
use Psr\Http\Message\ResponseInterface as Response;
@ -31,7 +32,12 @@ class AccountController extends BaseController
$queryParams = $request->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 <i>{$deleteResult['username']}</i> ";
$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);
}
}

View File

@ -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,
];
}
}

View File

@ -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;
}
}

View File

@ -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]);
}
}

View File

@ -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();
}
}

View File

@ -15,21 +15,16 @@
<h3>Delete account</h3>
<p>You are about to delete the mail account "{{ accountUsername }}" including the following aliases:</p>
<p>You are about to delete the mail account "{{ accountUsername }}" <b>including</b> the following aliases:</p>
<ul>
{% for alias in aliases %}
<li>{{ alias.getMailAddress() }}</li>
{% endfor %}
</ul>
<p><b>Note:</b> This will only delete the user entry from the database. Mail data will not be deleted!</p>
<p><b>Note:</b> This will only delete the account entry from the database. Mail data will not be deleted!</p>
{% if error is defined %}
<div class="error_box">
<h4>Error:</h4>
{{ error }}
</div>
{% endif %}
{{ include('includes/form_result_box.html.twig') }}
<div class="confirmation_box">
<p><b>Please confirm that you want to delete this account by entering your password.</b></p>

View File

@ -13,6 +13,8 @@
<h3>List of accounts</h3>
{{ include('includes/form_result_box.html.twig') }}
<div class="filter_options">
<form action="/accounts" method="GET">
<h4>Filter:</h4>