Compare commits

..

No commits in common. "186dcdc0cb9d9485ecf5d25d239aad8c0e0ccf27" and "57b712300f9fa20a85b4f3e2ecc5bed7f4953b72" have entirely different histories.

10 changed files with 46 additions and 164 deletions

View File

@ -9,7 +9,7 @@ abstract class FormData
{ {
// Abstract methods // Abstract methods
abstract public static function createFromArray(array $raw): self; abstract public static function createFromArray($raw): self;
// Input validation - Base types // Input validation - Base types
@ -26,16 +26,6 @@ abstract class FormData
return $raw; 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 protected static function validateBoolOption(string $raw): bool
{ {
return $raw !== ''; return $raw !== '';

View File

@ -14,7 +14,7 @@ class AccountAddAliasData extends FormData
$this->aliasAddress = $aliasAddress; $this->aliasAddress = $aliasAddress;
} }
public static function createFromArray(array $raw): self public static function createFromArray($raw): self
{ {
return new self( return new self(
self::validateAliasAddress(trim($raw['alias_address'] ?? '')), self::validateAliasAddress(trim($raw['alias_address'] ?? '')),

View File

@ -236,33 +236,7 @@ class AccountController extends BaseController
$this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $addAliasData)); $this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($e->getMessage(), $addAliasData));
} }
// Redirect to account details page via GET (PRG) // Redirect to edit form page via GET (PRG)
return $response->withHeader('Location', '/accounts/' . $accountId . '#aliases')->withStatus(303); return $response->withHeader('Location', '/accounts/' . $accountId)->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);
} }
} }

View File

@ -22,7 +22,7 @@ class AccountCreateData extends FormData
$this->memo = $memo; $this->memo = $memo;
} }
public static function createFromArray(array $raw): self public static function createFromArray($raw): self
{ {
return new self( return new self(
self::validateUsername(trim($raw['username'] ?? '')), self::validateUsername(trim($raw['username'] ?? '')),

View File

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Frontend\Accounts;
use MailAccountAdmin\Common\FormData;
use MailAccountAdmin\Exceptions\InputValidationError;
class AccountDeleteAliasesData extends FormData
{
/** @var array[int] */
private array $selectedAliasIds;
private function __construct(array $selectedAliasIds)
{
$this->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;
}
}

View File

@ -29,7 +29,7 @@ class AccountEditData extends FormData
$this->memo = $memo; $this->memo = $memo;
} }
public static function createFromArray(array $raw): self public static function createFromArray($raw): self
{ {
return new self( return new self(
self::validateUsername(trim($raw['username'] ?? ''), false), self::validateUsername(trim($raw['username'] ?? ''), false),

View File

@ -6,7 +6,6 @@ namespace MailAccountAdmin\Frontend\Accounts;
use MailAccountAdmin\Common\PasswordHelper; use MailAccountAdmin\Common\PasswordHelper;
use MailAccountAdmin\Exceptions\AccountNotFoundException; use MailAccountAdmin\Exceptions\AccountNotFoundException;
use MailAccountAdmin\Exceptions\InputValidationError; use MailAccountAdmin\Exceptions\InputValidationError;
use MailAccountAdmin\Models\Account;
use MailAccountAdmin\Repositories\AccountRepository; use MailAccountAdmin\Repositories\AccountRepository;
use MailAccountAdmin\Repositories\AliasRepository; use MailAccountAdmin\Repositories\AliasRepository;
use MailAccountAdmin\Repositories\DomainRepository; use MailAccountAdmin\Repositories\DomainRepository;
@ -28,17 +27,6 @@ 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 // -- /accounts - List all accounts
public function listAccounts(string $filterByDomain): array public function listAccounts(string $filterByDomain): array
@ -146,7 +134,11 @@ class AccountHandler
$returnData = []; $returnData = [];
// Check if account exists // Check if account exists
$account = $this->ensureAccountExists($accountId); try {
$account = $this->accountRepository->fetchAccountById($accountId);
} catch (AccountNotFoundException $e) {
throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!');
}
$newUsername = $editData->getUsername(); $newUsername = $editData->getUsername();
if ($newUsername === $account->getUsername()) { if ($newUsername === $account->getUsername()) {
@ -154,9 +146,9 @@ class AccountHandler
$newUsername = null; $newUsername = null;
} }
// If the user wants to change their username, has an existing alias with this username and checked the "replace existing alias" // This variable will be set to true if the user wants to change their username, has an existing alias with this username,
// option, this variable will be set to the ID of the alias that needs to be deleted. // and checked the "replace existing alias" option.
$aliasNeedsToBeReplaced = null; $aliasNeedsToBeReplaced = false;
// Check if new username is still available // Check if new username is still available
if ($newUsername !== null) { if ($newUsername !== null) {
@ -175,7 +167,7 @@ class AccountHandler
if ($editData->getUsernameReplaceAlias()) { if ($editData->getUsernameReplaceAlias()) {
$existingAlias = $this->aliasRepository->fetchAliasByAddress($newUsername); $existingAlias = $this->aliasRepository->fetchAliasByAddress($newUsername);
if ($existingAlias->getUserId() === $accountId) { if ($existingAlias->getUserId() === $accountId) {
$aliasNeedsToBeReplaced = $existingAlias->getId(); $aliasNeedsToBeReplaced = true;
$newUsernameAvailable = true; $newUsernameAvailable = true;
} }
} }
@ -219,8 +211,8 @@ class AccountHandler
); );
// Remove existing alias for new username (if wanted) // Remove existing alias for new username (if wanted)
if ($editData->getUsernameReplaceAlias() && $aliasNeedsToBeReplaced !== null) { if ($editData->getUsernameReplaceAlias() && $aliasNeedsToBeReplaced) {
$this->aliasRepository->deleteAliasById($accountId, $aliasNeedsToBeReplaced); $this->aliasRepository->deleteAlias($accountId, $newUsername);
} }
// Commit database transaction // Commit database transaction
@ -250,7 +242,11 @@ class AccountHandler
public function deleteAccount(int $accountId): array public function deleteAccount(int $accountId): array
{ {
// Check if account exists // Check if account exists
$account = $this->ensureAccountExists($accountId); try {
$account = $this->accountRepository->fetchAccountById($accountId);
} catch (AccountNotFoundException $e) {
throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!');
}
// Start database transaction // Start database transaction
$this->accountRepository->beginTransaction(); $this->accountRepository->beginTransaction();
@ -276,7 +272,11 @@ class AccountHandler
public function addAliasToAccount(int $accountId, AccountAddAliasData $aliasAddData): void public function addAliasToAccount(int $accountId, AccountAddAliasData $aliasAddData): void
{ {
// Check if account exists // Check if account exists
$this->ensureAccountExists($accountId); try {
$this->accountRepository->fetchAccountById($accountId);
} catch (AccountNotFoundException $e) {
throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!');
}
// Check if alias address is still available // Check if alias address is still available
$address = $aliasAddData->getAliasAddress(); $address = $aliasAddData->getAliasAddress();
@ -287,29 +287,4 @@ class AccountHandler
// Create alias in database // Create alias in database
$this->aliasRepository->createNewAlias($accountId, $address); $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;
}
} }

View File

@ -52,15 +52,14 @@ class AliasRepository extends BaseRepository
]); ]);
} }
public function deleteAliasById(int $userId, int $aliasId): bool 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 // 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 alias_id = :alias_id LIMIT 1'); $statement = $this->pdo->prepare('DELETE FROM mail_aliases WHERE user_id = :user_id AND mail_address = :mail_address LIMIT 1');
$statement->execute([ $statement->execute([
'user_id' => $userId, 'user_id' => $userId,
'alias_id' => $aliasId, 'mail_address' => $mailAddress,
]); ]);
return $statement->rowCount() > 0;
} }
public function deleteAllAliasesForUserId(int $userId): int public function deleteAllAliasesForUserId(int $userId): int

View File

@ -34,12 +34,5 @@ class Routes
$app->get('/accounts/{id:[1-9][0-9]*}/delete', AccountController::class . ':showAccountDelete'); $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]*}/delete', AccountController::class . ':deleteAccount');
$app->post('/accounts/{id:[1-9][0-9]*}/addalias', AccountController::class . ':addAliasToAccount'); $app->post('/accounts/{id:[1-9][0-9]*}/addalias', AccountController::class . ':addAliasToAccount');
$app->post('/accounts/{id:[1-9][0-9]*}/deletealiases', AccountController::class . ':deleteAliasesFromAccount');
// Redirect URLs with trailing slashes to correct URLs
$app->redirect('/login/', '/login');
$app->redirect('/logout/', '/logout');
$app->redirect('/domains/', '/domains');
$app->redirect('/accounts/', '/accounts');
} }
} }

View File

@ -62,41 +62,34 @@
</tr> </tr>
</table> </table>
<h3 id="aliases">Aliases</h3> <h3>Aliases</h3>
{{ include('includes/form_result_box.html.twig') }}
{% if aliases %} {% if aliases %}
<form action="/accounts/{{ id }}/deletealiases" method="POST">
<table class="bordered_table"> <table class="bordered_table">
<tr> <tr>
<th></th>
<th>Address</th> <th>Address</th>
<th>Created at</th> <th>Created at</th>
<th>Actions</th>
</tr> </tr>
{% for alias in aliases %} {% for alias in aliases %}
<tr> <tr>
<td><input type="checkbox" id="delete_selected_{{ alias.getId() }}" name="selected_aliases[]" value="{{ alias.getId() }}"/></td> <td>{{ alias.getMailAddress() }}</td>
<td><label for="delete_selected_{{ alias.getId() }}">{{ alias.getMailAddress() }}</label></td>
<td>{{ alias.getCreatedAt() | date }}</td> <td>{{ alias.getCreatedAt() | date }}</td>
<td><a href="#">Delete</a></td> {# TODO #}
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<p>
<button type="submit">Delete selected aliases</button>
</p>
</form>
{% else %} {% else %}
<p>No aliases.</p> <p>No aliases.</p>
{% endif %} {% endif %}
<form action="/accounts/{{ id }}/addalias" method="POST"> <form action="/accounts/{{ id }}/addalias" method="POST">
{{ include('includes/form_result_box.html.twig') }}
<table class="margin_vertical_1rem"> <table class="margin_vertical_1rem">
<tr> <tr>
<td><label for="add_alias_address">New alias:</label></td> <td><label for="add_alias_address">New alias:</label></td>
<td> <td><input id="add_alias_address" name="alias_address" value="{{ formData['new_alias'] | default('') }}"/></td>
<input id="add_alias_address" name="alias_address" value="{{ formData['new_alias'] | default('') }}" {{ success is defined or error is defined ? 'autofocus': '' }}/>
</td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">