Implement account edit; add SessionHelper, PasswordHelper and ActionResult
This commit is contained in:
parent
32add30c9d
commit
6ace072841
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MailAccountAdmin\Common;
|
||||
|
||||
class ActionResult
|
||||
{
|
||||
private const STATUS_SUCCESS = 'success';
|
||||
private const STATUS_ERROR = 'error';
|
||||
|
||||
/** @var string */
|
||||
private $status;
|
||||
/** @var string */
|
||||
private $message;
|
||||
/** @var null|array */
|
||||
private $inputData;
|
||||
|
||||
private function __construct(string $status, string $message, ?array $inputData = null)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->message = $message;
|
||||
$this->inputData = $inputData;
|
||||
}
|
||||
|
||||
public static function createSuccessResult(string $successMessage): self
|
||||
{
|
||||
return new self(self::STATUS_SUCCESS, $successMessage);
|
||||
}
|
||||
|
||||
public static function createErrorResult(string $errorMessage, array $inputData): self
|
||||
{
|
||||
return new self(self::STATUS_ERROR, $errorMessage, $inputData);
|
||||
}
|
||||
|
||||
|
||||
// Array serialization for session usage
|
||||
|
||||
public static function createFromArray(array $array): self
|
||||
{
|
||||
return new self(
|
||||
$array['status'],
|
||||
$array['message'],
|
||||
$array['input_data'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'message' => $this->message,
|
||||
'input_data' => $this->inputData,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Getters
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
public function isError(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ERROR;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getInputData(): ?array
|
||||
{
|
||||
return $this->inputData;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MailAccountAdmin\Common;
|
||||
|
||||
class PasswordHelper
|
||||
{
|
||||
public function hashPassword(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
public function verifyPassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
public function getPasswordHashType(string $passwordHash): string
|
||||
{
|
||||
if ($passwordHash === '') {
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
$passwordHashInfo = password_get_info($passwordHash);
|
||||
return $passwordHashInfo['algoName'] ?? 'unknown';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MailAccountAdmin\Common;
|
||||
|
||||
class SessionHelper
|
||||
{
|
||||
// Generic get and set methods
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
return $_SESSION[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function set(string $key, $value): void
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
|
||||
public function unset(string $key): void
|
||||
{
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
// Login related
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->get('user_id');
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->set('user_id', $userId);
|
||||
}
|
||||
|
||||
// Form processing
|
||||
public function setLastActionResult(ActionResult $actionResult): void
|
||||
{
|
||||
$this->set('last_action_result', $actionResult->toArray());
|
||||
}
|
||||
|
||||
public function getLastActionResult(bool $delete = true): ?ActionResult
|
||||
{
|
||||
$resultArray = $this->get('last_action_result');
|
||||
if ($resultArray) {
|
||||
if ($delete) {
|
||||
$this->unset('last_action_result');
|
||||
}
|
||||
return ActionResult::createFromArray($resultArray);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,28 +3,31 @@ declare(strict_types=1);
|
|||
|
||||
namespace MailAccountAdmin\Common;
|
||||
|
||||
use http\Exception\RuntimeException;
|
||||
use RuntimeException;
|
||||
use MailAccountAdmin\Models\AdminUser;
|
||||
use MailAccountAdmin\Repositories\AdminUserRepository;
|
||||
|
||||
class UserHelper
|
||||
{
|
||||
/** @var SessionHelper */
|
||||
private $sessionHelper;
|
||||
/** @var AdminUserRepository */
|
||||
private $adminUserRepository;
|
||||
|
||||
public function __construct(AdminUserRepository $adminUserRepository)
|
||||
public function __construct(SessionHelper $sessionHelper, AdminUserRepository $adminUserRepository)
|
||||
{
|
||||
$this->sessionHelper = $sessionHelper;
|
||||
$this->adminUserRepository = $adminUserRepository;
|
||||
}
|
||||
|
||||
public function isLoggedIn(): bool
|
||||
{
|
||||
return !empty($_SESSION['user_id']);
|
||||
return $this->sessionHelper->getUserId() !== null;
|
||||
}
|
||||
|
||||
public function getCurrentUser(): AdminUser
|
||||
{
|
||||
$userId = $_SESSION['user_id'] ?? null;
|
||||
$userId = $this->sessionHelper->getUserId();
|
||||
if (empty($userId)) {
|
||||
throw new RuntimeException('Not logged in!');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ declare(strict_types=1);
|
|||
namespace MailAccountAdmin;
|
||||
|
||||
use DI\Container;
|
||||
use MailAccountAdmin\Common\PasswordHelper;
|
||||
use MailAccountAdmin\Common\SessionHelper;
|
||||
use MailAccountAdmin\Common\UserHelper;
|
||||
use MailAccountAdmin\Frontend\Accounts\AccountController;
|
||||
use MailAccountAdmin\Frontend\Accounts\AccountHandler;
|
||||
use MailAccountAdmin\Frontend\Domains\DomainController;
|
||||
use MailAccountAdmin\Frontend\Login\LoginController;
|
||||
use MailAccountAdmin\Frontend\Dashboard\DashboardController;
|
||||
|
|
@ -89,41 +92,70 @@ class Dependencies
|
|||
});
|
||||
|
||||
// Helper classes
|
||||
$container->set(SessionHelper::class, function (ContainerInterface $c) {
|
||||
return new SessionHelper();
|
||||
});
|
||||
|
||||
$container->set(UserHelper::class, function (ContainerInterface $c) {
|
||||
return new UserHelper(
|
||||
$c->get(SessionHelper::class),
|
||||
$c->get(AdminUserRepository::class),
|
||||
);
|
||||
});
|
||||
|
||||
// Frontend controllers
|
||||
$container->set(PasswordHelper::class, function (ContainerInterface $c) {
|
||||
return new PasswordHelper();
|
||||
});
|
||||
|
||||
// Frontend controllers and handlers
|
||||
// -> Login page
|
||||
$container->set(LoginController::class, function (ContainerInterface $c) {
|
||||
return new LoginController(
|
||||
$c->get(self::TWIG),
|
||||
$c->get(SessionHelper::class),
|
||||
$c->get(UserHelper::class),
|
||||
$c->get(AdminUserRepository::class),
|
||||
$c->get(PasswordHelper::class),
|
||||
);
|
||||
});
|
||||
|
||||
// -> Dashboard
|
||||
$container->set(DashboardController::class, function (ContainerInterface $c) {
|
||||
return new DashboardController(
|
||||
$c->get(self::TWIG),
|
||||
$c->get(SessionHelper::class),
|
||||
$c->get(UserHelper::class),
|
||||
);
|
||||
});
|
||||
|
||||
// -> Domains
|
||||
$container->set(DomainController::class, function (ContainerInterface $c) {
|
||||
return new DomainController(
|
||||
$c->get(self::TWIG),
|
||||
$c->get(SessionHelper::class),
|
||||
$c->get(UserHelper::class),
|
||||
$c->get(DomainRepository::class),
|
||||
);
|
||||
});
|
||||
|
||||
// -> Accounts
|
||||
$container->set(AccountController::class, function (ContainerInterface $c) {
|
||||
return new AccountController(
|
||||
$c->get(self::TWIG),
|
||||
$c->get(SessionHelper::class),
|
||||
$c->get(UserHelper::class),
|
||||
$c->get(AccountHandler::class),
|
||||
$c->get(AccountRepository::class),
|
||||
$c->get(AliasRepository::class),
|
||||
);
|
||||
});
|
||||
$container->set(AccountHandler::class, function (ContainerInterface $c) {
|
||||
return new AccountHandler(
|
||||
$c->get(AccountRepository::class),
|
||||
$c->get(AliasRepository::class),
|
||||
$c->get(PasswordHelper::class),
|
||||
);
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MailAccountAdmin\Exceptions;
|
||||
|
||||
class InputValidationError extends AppException
|
||||
{
|
||||
}
|
||||
|
|
@ -3,9 +3,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace MailAccountAdmin\Frontend\Accounts;
|
||||
|
||||
use MailAccountAdmin\Common\ActionResult;
|
||||
use MailAccountAdmin\Common\SessionHelper;
|
||||
use MailAccountAdmin\Common\UserHelper;
|
||||
use MailAccountAdmin\Exceptions\InputValidationError;
|
||||
use MailAccountAdmin\Frontend\BaseController;
|
||||
use MailAccountAdmin\Models\Account;
|
||||
use MailAccountAdmin\Repositories\AccountRepository;
|
||||
use MailAccountAdmin\Repositories\AliasRepository;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
|
|
@ -14,14 +16,17 @@ use Slim\Views\Twig;
|
|||
|
||||
class AccountController extends BaseController
|
||||
{
|
||||
/** @var AccountHandler */
|
||||
private $accountHandler;
|
||||
/** @var AccountRepository */
|
||||
private $accountRepository;
|
||||
/** @var AliasRepository */
|
||||
private $aliasRepository;
|
||||
|
||||
public function __construct(Twig $view, UserHelper $userHelper, AccountRepository $accountRepository, AliasRepository $aliasRepository)
|
||||
public function __construct(Twig $view, SessionHelper $sessionHelper, UserHelper $userHelper, AccountHandler $accountHandler, AccountRepository $accountRepository, AliasRepository $aliasRepository)
|
||||
{
|
||||
parent::__construct($view, $userHelper);
|
||||
parent::__construct($view, $sessionHelper, $userHelper);
|
||||
$this->accountHandler = $accountHandler;
|
||||
$this->accountRepository = $accountRepository;
|
||||
$this->aliasRepository = $aliasRepository;
|
||||
}
|
||||
|
|
@ -33,13 +38,9 @@ class AccountController extends BaseController
|
|||
{
|
||||
// Parse query parameters for filters
|
||||
$queryParams = $request->getQueryParams();
|
||||
$filterByDomain = $queryParams['domain'] ?? null;
|
||||
|
||||
$renderData = [
|
||||
'filterDomain' => $filterByDomain,
|
||||
'accountList' => $this->accountRepository->fetchAccountList($filterByDomain),
|
||||
];
|
||||
$filterByDomain = $queryParams['domain'] ?? '';
|
||||
|
||||
$renderData = $this->accountHandler->listAccounts($filterByDomain);
|
||||
return $this->view->render($response, 'accounts.html.twig', $renderData);
|
||||
}
|
||||
|
||||
|
|
@ -51,28 +52,7 @@ class AccountController extends BaseController
|
|||
// Parse URL arguments
|
||||
$accountId = (int)$args['id'];
|
||||
|
||||
// Get account data from database
|
||||
$account = $this->accountRepository->fetchAccountById($accountId);
|
||||
|
||||
// Don't display the password hash, but at least the type of hash (used hash algorithm)
|
||||
if ($account->getPasswordHash() === '') {
|
||||
$passwordHashType = 'empty';
|
||||
} else {
|
||||
$passwordHashInfo = password_get_info($account->getPasswordHash());
|
||||
$passwordHashType = $passwordHashInfo['algoName'] ?? 'unknown';
|
||||
}
|
||||
|
||||
// Get list of aliases for this account
|
||||
$aliases = $this->aliasRepository->fetchAliasesForUserId($accountId);
|
||||
|
||||
$renderData = [
|
||||
'id' => $accountId,
|
||||
'accountUsername' => $account->getUsername(),
|
||||
'account' => $account,
|
||||
'passwordHashType' => $passwordHashType,
|
||||
'aliases' => $aliases,
|
||||
];
|
||||
|
||||
$renderData = $this->accountHandler->getAccountDetails($accountId);
|
||||
return $this->view->render($response, 'account_details.html.twig', $renderData);
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +61,7 @@ class AccountController extends BaseController
|
|||
|
||||
public function showAccountCreate(Request $request, Response $response): Response
|
||||
{
|
||||
// TODO: just a placeholder
|
||||
return $this->showAccounts($request, $response);
|
||||
}
|
||||
|
||||
|
|
@ -95,28 +76,52 @@ class AccountController extends BaseController
|
|||
// Get account data from database
|
||||
$account = $this->accountRepository->fetchAccountById($accountId);
|
||||
|
||||
// Render page
|
||||
return $this->renderEditPage($response, $account);
|
||||
}
|
||||
|
||||
public function editAccount(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
// TODO: just a placeholder
|
||||
$this->view->getEnvironment()->addGlobal('error', 'Not implemented yet!');
|
||||
return $this->showAccountEdit($request, $response, $args);
|
||||
}
|
||||
|
||||
private function renderEditPage(Response $response, Account $account, array $extraRenderData = []): Response
|
||||
{
|
||||
$renderData = [
|
||||
'id' => $account->getId(),
|
||||
'accountUsername' => $account->getUsername(),
|
||||
'account' => $account,
|
||||
];
|
||||
|
||||
return $this->view->render($response, 'account_edit.html.twig', array_merge($renderData, $extraRenderData));
|
||||
$lastActionResult = $this->sessionHelper->getLastActionResult();
|
||||
if ($lastActionResult !== null) {
|
||||
$resultData = $lastActionResult->isSuccess()
|
||||
? ['success' => $lastActionResult->getMessage()]
|
||||
: ['error' => $lastActionResult->getMessage()];
|
||||
$resultData['editData'] = $lastActionResult->getInputData();
|
||||
$renderData = array_merge($renderData, $resultData);
|
||||
}
|
||||
|
||||
return $this->view->render($response, 'account_edit.html.twig', $renderData);
|
||||
}
|
||||
|
||||
public function editAccount(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
// Parse URL arguments
|
||||
$accountId = (int)$args['id'];
|
||||
|
||||
// Parse form data
|
||||
$editData = $request->getParsedBody();
|
||||
$errorMessage = null;
|
||||
|
||||
try {
|
||||
// Validate input
|
||||
$validatedEditData = AccountEditData::createFromArray($editData);
|
||||
$this->accountHandler->editAccountData($accountId, $validatedEditData);
|
||||
} catch (InputValidationError $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
}
|
||||
|
||||
if (empty($errorMessage)) {
|
||||
$this->sessionHelper->setLastActionResult(ActionResult::createSuccessResult('Account data was saved.'));
|
||||
} else {
|
||||
$this->sessionHelper->setLastActionResult(ActionResult::createErrorResult($errorMessage, $editData));
|
||||
}
|
||||
|
||||
// Redirect to edit form page via GET (PRG)
|
||||
return $response->withHeader('Location', '/accounts/' . $accountId . '/edit')->withStatus(303);
|
||||
}
|
||||
|
||||
|
||||
// -- /accounts/{id}/delete - Delete account
|
||||
|
||||
public function showAccountDelete(Request $request, Response $response, array $args): Response
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MailAccountAdmin\Frontend\Accounts;
|
||||
|
||||
use MailAccountAdmin\Exceptions\InputValidationError;
|
||||
|
||||
class AccountEditData
|
||||
{
|
||||
/** @var null|string */
|
||||
private $username;
|
||||
/** @var bool */
|
||||
private $usernameCreateAlias;
|
||||
/** @var bool */
|
||||
private $usernameReplaceAlias;
|
||||
/** @var null|string */
|
||||
private $password;
|
||||
/** @var bool */
|
||||
private $active;
|
||||
/** @var null|string */
|
||||
private $homeDir;
|
||||
/** @var null|string */
|
||||
private $memo;
|
||||
|
||||
private function __construct(?string $username, bool $usernameCreateAlias, bool $usernameReplaceAlias, ?string $password, bool $active,
|
||||
?string $homeDir, ?string $memo)
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->usernameCreateAlias = $usernameCreateAlias;
|
||||
$this->usernameReplaceAlias = $usernameReplaceAlias;
|
||||
$this->password = $password;
|
||||
$this->active = $active;
|
||||
$this->homeDir = $homeDir;
|
||||
$this->memo = $memo;
|
||||
}
|
||||
|
||||
public static function createFromArray($raw): self
|
||||
{
|
||||
return new self(
|
||||
self::validateUsername(trim($raw['username'] ?? '')),
|
||||
self::validateBoolOption(trim($raw['username_create_alias'] ?? '')),
|
||||
self::validateBoolOption(trim($raw['username_replace_alias'] ?? '')),
|
||||
self::validatePassword(trim($raw['password'] ?? ''), trim($raw['password_repeat'] ?? '')),
|
||||
self::validateBoolOption(trim($raw['is_active'] ?? '')),
|
||||
self::validateHomeDir(trim($raw['home_dir'] ?? '')),
|
||||
trim($raw['memo'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Input validation
|
||||
|
||||
private static function validateUsername(string $username): ?string
|
||||
{
|
||||
if ($username === '') {
|
||||
return null;
|
||||
}
|
||||
if (strlen($username) > 100) {
|
||||
throw new InputValidationError('Username is too long.');
|
||||
}
|
||||
|
||||
$username = strtolower($username);
|
||||
if (!preg_match('/^[a-z0-9._+-]+@[a-z0-9.-]+$/', $username) || preg_match('/^\\.|\\.\\.|\\.@|@\\.|\\.$/', $username)) {
|
||||
throw new InputValidationError('Username is not valid (must be a valid mail address).');
|
||||
}
|
||||
return $username;
|
||||
}
|
||||
|
||||
private static function validatePassword(string $password, string $passwordRepeat): ?string
|
||||
{
|
||||
if ($password === '') {
|
||||
return null;
|
||||
}
|
||||
if ($password !== $passwordRepeat) {
|
||||
throw new InputValidationError('Passwords do not match.');
|
||||
}
|
||||
return $password;
|
||||
}
|
||||
|
||||
private static function validateHomeDir(string $homeDir): ?string
|
||||
{
|
||||
if ($homeDir === '') {
|
||||
return null;
|
||||
}
|
||||
if (strlen($homeDir) > 100) {
|
||||
throw new InputValidationError('Home directory is too long.');
|
||||
}
|
||||
|
||||
$homeDir = trim($homeDir, '/');
|
||||
if (!preg_match('!^[a-z0-9._+-]+(/[a-z0-9._+-]+)*$!i', $homeDir)) {
|
||||
throw new InputValidationError('Home directory is not a valid path.');
|
||||
}
|
||||
return $homeDir;
|
||||
}
|
||||
|
||||
private static function validateBoolOption(string $raw): bool
|
||||
{
|
||||
return $raw !== '';
|
||||
}
|
||||
|
||||
|
||||
// Getters
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getUsernameCreateAlias(): bool
|
||||
{
|
||||
return $this->usernameCreateAlias;
|
||||
}
|
||||
|
||||
public function getUsernameReplaceAlias(): bool
|
||||
{
|
||||
return $this->usernameReplaceAlias;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function getHomeDir(): ?string
|
||||
{
|
||||
return $this->homeDir;
|
||||
}
|
||||
|
||||
public function getMemo(): string
|
||||
{
|
||||
return $this->memo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MailAccountAdmin\Frontend\Accounts;
|
||||
|
||||
use MailAccountAdmin\Common\PasswordHelper;
|
||||
use MailAccountAdmin\Exceptions\AccountNotFoundException;
|
||||
use MailAccountAdmin\Exceptions\InputValidationError;
|
||||
use MailAccountAdmin\Repositories\AccountRepository;
|
||||
use MailAccountAdmin\Repositories\AliasRepository;
|
||||
|
||||
class AccountHandler
|
||||
{
|
||||
/** @var AccountRepository */
|
||||
private $accountRepository;
|
||||
/** @var AliasRepository */
|
||||
private $aliasRepository;
|
||||
/** @var PasswordHelper */
|
||||
private $passwordHelper;
|
||||
|
||||
public function __construct(AccountRepository $accountRepository, AliasRepository $aliasRepository, PasswordHelper $passwordHelper)
|
||||
{
|
||||
$this->accountRepository = $accountRepository;
|
||||
$this->aliasRepository = $aliasRepository;
|
||||
$this->passwordHelper = $passwordHelper;
|
||||
}
|
||||
|
||||
|
||||
// -- /accounts - List all accounts
|
||||
|
||||
public function listAccounts(string $filterByDomain): array
|
||||
{
|
||||
$accountList = $this->accountRepository->fetchAccountList($filterByDomain);
|
||||
|
||||
return [
|
||||
'filterDomain' => $filterByDomain,
|
||||
'accountList' => $accountList,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// -- /accounts/{id} - Show account details
|
||||
|
||||
public function getAccountDetails(int $accountId): array
|
||||
{
|
||||
// Get account data from database
|
||||
$account = $this->accountRepository->fetchAccountById($accountId);
|
||||
|
||||
// Don't display the password hash, but at least the type of hash (used hash algorithm)
|
||||
$passwordHashType = $this->passwordHelper->getPasswordHashType($account->getPasswordHash());
|
||||
|
||||
// Get list of aliases for this account
|
||||
$aliases = $this->aliasRepository->fetchAliasesForUserId($accountId);
|
||||
|
||||
return [
|
||||
'id' => $accountId,
|
||||
'accountUsername' => $account->getUsername(),
|
||||
'account' => $account,
|
||||
'passwordHashType' => $passwordHashType,
|
||||
'aliases' => $aliases,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// -- /accounts/{id}/edit - Edit account data
|
||||
|
||||
public function editAccountData(int $accountId, AccountEditData $editData): void
|
||||
{
|
||||
// Check if account exists
|
||||
try {
|
||||
$account = $this->accountRepository->fetchAccountById($accountId);
|
||||
} catch (AccountNotFoundException $e) {
|
||||
throw new InputValidationError('Account with ID ' . $accountId . ' does not exist!');
|
||||
}
|
||||
|
||||
// TODO: Use database transactions (beginTransaction/commit/rollBack in BaseRepository maybe?)
|
||||
|
||||
$newUsername = $editData->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.');
|
||||
}
|
||||
|
||||
// Check if new username is still available
|
||||
if ($newUsername !== null) {
|
||||
if (!$this->accountRepository->checkUsernameAvailable($newUsername) || !$this->aliasRepository->checkAliasAvailable($newUsername)) {
|
||||
throw new InputValidationError("Username \"$newUsername\" is not available.");
|
||||
}
|
||||
}
|
||||
|
||||
// Create alias for old username (if wanted)
|
||||
if ($editData->getUsernameCreateAlias()) {
|
||||
$oldUsername = $account->getUsername();
|
||||
if (!$this->aliasRepository->checkAliasAvailable($oldUsername)) {
|
||||
throw new InputValidationError("Alias \"$oldUsername\" cannot be created: Alias already exists.");
|
||||
}
|
||||
$this->aliasRepository->createNewAlias($accountId, $oldUsername);
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
$newPasswordHash = null;
|
||||
if ($editData->getPassword() !== null) {
|
||||
$newPasswordHash = $this->passwordHelper->hashPassword($editData->getPassword());
|
||||
}
|
||||
|
||||
// Update account in database
|
||||
$this->accountRepository->updateAccountWithId(
|
||||
$accountId,
|
||||
$newUsername,
|
||||
$newPasswordHash,
|
||||
$editData->getActive(),
|
||||
$editData->getHomeDir(),
|
||||
$editData->getMemo()
|
||||
);
|
||||
|
||||
// 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.');
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace MailAccountAdmin\Frontend;
|
||||
|
||||
use MailAccountAdmin\Common\SessionHelper;
|
||||
use MailAccountAdmin\Common\UserHelper;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
|
|
@ -10,12 +11,15 @@ class BaseController
|
|||
{
|
||||
/** @var Twig */
|
||||
protected $view;
|
||||
/** @var SessionHelper */
|
||||
protected $sessionHelper;
|
||||
/** @var UserHelper */
|
||||
protected $userHelper;
|
||||
|
||||
public function __construct(Twig $view, UserHelper $userHelper)
|
||||
public function __construct(Twig $view, SessionHelper $sessionHelper, UserHelper $userHelper)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->sessionHelper = $sessionHelper;
|
||||
$this->userHelper = $userHelper;
|
||||
|
||||
// Register globals
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace MailAccountAdmin\Frontend\Domains;
|
||||
|
||||
use MailAccountAdmin\Common\SessionHelper;
|
||||
use MailAccountAdmin\Common\UserHelper;
|
||||
use MailAccountAdmin\Frontend\BaseController;
|
||||
use MailAccountAdmin\Repositories\DomainRepository;
|
||||
|
|
@ -15,9 +16,9 @@ class DomainController extends BaseController
|
|||
/** @var DomainRepository */
|
||||
private $domainRepository;
|
||||
|
||||
public function __construct(Twig $view, UserHelper $userHelper, DomainRepository $domainRepository)
|
||||
public function __construct(Twig $view, SessionHelper $sessionHelper, UserHelper $userHelper, DomainRepository $domainRepository)
|
||||
{
|
||||
parent::__construct($view, $userHelper);
|
||||
parent::__construct($view, $sessionHelper, $userHelper);
|
||||
$this->domainRepository = $domainRepository;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace MailAccountAdmin\Frontend\Login;
|
||||
|
||||
use MailAccountAdmin\Common\PasswordHelper;
|
||||
use MailAccountAdmin\Common\SessionHelper;
|
||||
use MailAccountAdmin\Common\UserHelper;
|
||||
use MailAccountAdmin\Exceptions\AdminUserNotFoundException;
|
||||
use MailAccountAdmin\Frontend\BaseController;
|
||||
|
|
@ -15,11 +17,15 @@ class LoginController extends BaseController
|
|||
{
|
||||
/** @var AdminUserRepository */
|
||||
private $adminUserRepository;
|
||||
/** @var PasswordHelper */
|
||||
private $passwordHelper;
|
||||
|
||||
public function __construct(Twig $view, UserHelper $userHelper, AdminUserRepository $adminUserRepository)
|
||||
public function __construct(Twig $view, SessionHelper $sessionHelper, UserHelper $userHelper, AdminUserRepository $adminUserRepository,
|
||||
PasswordHelper $passwordHelper)
|
||||
{
|
||||
parent::__construct($view, $userHelper);
|
||||
parent::__construct($view, $sessionHelper, $userHelper);
|
||||
$this->adminUserRepository = $adminUserRepository;
|
||||
$this->passwordHelper = $passwordHelper;
|
||||
}
|
||||
|
||||
private function renderLoginPage(Response $response, array $renderData = []): Response
|
||||
|
|
@ -54,19 +60,18 @@ class LoginController extends BaseController
|
|||
|
||||
try {
|
||||
$user = $this->adminUserRepository->getUserByName($loginUsername);
|
||||
}
|
||||
catch (AdminUserNotFoundException $e) {
|
||||
} catch (AdminUserNotFoundException $e) {
|
||||
$user = null;
|
||||
}
|
||||
|
||||
if ($user === null || !password_verify($loginPassword, $user->getPasswordHash())) {
|
||||
if ($user === null || !$this->passwordHelper->verifyPassword($loginPassword, $user->getPasswordHash())) {
|
||||
return $this->renderLoginPage($response, ['error' => 'Wrong username or password!']);
|
||||
} elseif (!$user->isActive()) {
|
||||
return $this->renderLoginPage($response, ['error' => 'User is inactive!']);
|
||||
}
|
||||
|
||||
// Set login session
|
||||
$_SESSION['user_id'] = $user->getId();
|
||||
$this->sessionHelper->setUserId($user->getId());
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(303);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ class AccountRepository extends BaseRepository
|
|||
return $accountList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AccountNotFoundException
|
||||
*/
|
||||
public function fetchAccountById(int $accountId): Account
|
||||
{
|
||||
$statement = $this->pdo->prepare('SELECT * FROM mail_users WHERE user_id = :user_id LIMIT 1');
|
||||
|
|
@ -54,4 +57,50 @@ class AccountRepository extends BaseRepository
|
|||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
return Account::createFromArray($row);
|
||||
}
|
||||
|
||||
public function checkUsernameAvailable(string $username): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare('SELECT 1 FROM mail_users WHERE username = :username LIMIT 1');
|
||||
$statement->execute(['username' => $username]);
|
||||
return $statement->rowCount() === 0;
|
||||
}
|
||||
|
||||
public function updateAccountWithId(int $accountId, ?string $newUsername, ?string $newPasswordHash, bool $newActive,
|
||||
?string $newHomeDir, string $newMemo): void
|
||||
{
|
||||
$queryParams = [
|
||||
'user_id' => $accountId,
|
||||
'new_memo' => $newMemo,
|
||||
'new_active' => $newActive ? '1' : '0',
|
||||
];
|
||||
|
||||
$querySet = '';
|
||||
if (isset($newUsername)) {
|
||||
$querySet .= 'username = :new_username, ';
|
||||
$queryParams['new_username'] = $newUsername;
|
||||
}
|
||||
if (isset($newPasswordHash)) {
|
||||
$querySet .= 'password = :new_password, ';
|
||||
$queryParams['new_password'] = $newPasswordHash;
|
||||
}
|
||||
if (isset($newHomeDir)) {
|
||||
$querySet .= 'home_dir = :new_home_dir, ';
|
||||
$queryParams['new_home_dir'] = $newHomeDir;
|
||||
}
|
||||
|
||||
$query = '
|
||||
UPDATE mail_users
|
||||
SET
|
||||
' . $querySet . '
|
||||
memo = :new_memo,
|
||||
is_active = :new_active,
|
||||
modified_at = CURRENT_TIMESTAMP()
|
||||
WHERE
|
||||
user_id = :user_id
|
||||
LIMIT 1
|
||||
';
|
||||
|
||||
$statement = $this->pdo->prepare($query);
|
||||
$statement->execute($queryParams);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,20 @@ class AliasRepository extends BaseRepository
|
|||
$statement->execute(['user_id' => $userId]);
|
||||
return $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function checkAliasAvailable(string $mailAddress): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare('SELECT 1 FROM mail_aliases WHERE mail_address = :mail_address LIMIT 1');
|
||||
$statement->execute(['mail_address' => $mailAddress]);
|
||||
return $statement->rowCount() === 0;
|
||||
}
|
||||
|
||||
public function createNewAlias(int $userId, string $mailAddress): void
|
||||
{
|
||||
$statement = $this->pdo->prepare('INSERT INTO mail_aliases (user_id, mail_address) VALUES (:user_id, :mail_address)');
|
||||
$statement->execute([
|
||||
'user_id' => $userId,
|
||||
'mail_address' => $mailAddress,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,20 @@
|
|||
<h3>Edit account data</h3>
|
||||
|
||||
<form action="/accounts/{{ id }}/edit" method="POST">
|
||||
{% if error is defined %}
|
||||
{% if success is defined %}
|
||||
<div class="success_box">
|
||||
<h4>Success</h4>
|
||||
{{ success }}
|
||||
</div>
|
||||
{% elseif error is defined %}
|
||||
<div class="error_box">
|
||||
<h4>Error</h4>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="hidden" name="user_id" value="{{ id }}"/>
|
||||
|
||||
<div class="edit_box">
|
||||
<h4>Username</h4>
|
||||
<table>
|
||||
|
|
@ -32,16 +39,22 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td><label for="edit_username">New username:</label></td>
|
||||
<td><input id="edit_username" name="username"/></td>
|
||||
<td><input id="edit_username" name="username" value="{{ editData['username'] | default('') }}"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<label><input type="checkbox" name="username_create_alias"> Create alias for old username</label>
|
||||
<label>
|
||||
<input type="checkbox" name="username_create_alias" {{ editData['username_create_alias'] | default('') ? 'checked' : '' }}/>
|
||||
Create alias for old username
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<label><input type="checkbox" name="username_replace_alias"> Replace existing alias</label>
|
||||
<label>
|
||||
<input type="checkbox" name="username_replace_alias" {{ editData['username_replace_alias'] | default('') ? 'checked' : '' }}/>
|
||||
Replace existing alias
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -53,11 +66,11 @@
|
|||
<table>
|
||||
<tr>
|
||||
<td><label for="edit_password">New password:</label></td>
|
||||
<td><input type="password" id="edit_password" name="password"/></td>
|
||||
<td><input type="password" id="edit_password" name="password" value="{{ editData['password'] | default('') }}"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="edit_password_repeat">Repeat password:</label></td>
|
||||
<td><input type="password" id="edit_password_repeat" name="password_repeat"/></td>
|
||||
<td><input type="password" id="edit_password_repeat" name="password_repeat" value="{{ editData['password_repeat'] | default('') }}"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -73,7 +86,13 @@
|
|||
<td>New status:</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" {{ account.isActive() ? 'checked' : '' }}/> Active
|
||||
<input type="checkbox" name="is_active"
|
||||
{%- if editData | default() -%}
|
||||
{{ editData['is_active'] ? ' checked' : '' }}
|
||||
{%- else -%}
|
||||
{{ account.isActive() ? ' checked' : '' }}
|
||||
{%- endif -%}
|
||||
/> Active
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -92,7 +111,7 @@
|
|||
<tr>
|
||||
<td><label for="edit_home_dir">New home directory:</label></td>
|
||||
<td>
|
||||
<span class="gray">/srv/vmail/</span><input id="edit_home_dir" name="home_dir"/>
|
||||
<span class="gray">/srv/vmail/</span><input id="edit_home_dir" name="home_dir" value="{{ editData['home_dir'] | default('') }}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -104,7 +123,7 @@
|
|||
<table>
|
||||
<tr>
|
||||
<td><label for="edit_memo">Admin memo:</label></td>
|
||||
<td><textarea id="edit_memo" name="memo" style="min-width: 40em;">{{ account.getMemo() }}</textarea></td>
|
||||
<td><textarea id="edit_memo" name="memo" style="min-width: 40em;">{{ editData | default() ? editData['memo'] : account.getMemo() }}</textarea></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue