diff --git a/public/static/style.css b/public/static/style.css
index f4cf7bd..4424a71 100644
--- a/public/static/style.css
+++ b/public/static/style.css
@@ -14,7 +14,8 @@ body {
/* --- Header --- */
header {
- margin: 1em;
+ margin: 1rem;
+ padding: 0 1rem;
display: flex;
justify-content: space-between;
align-content: center;
@@ -22,7 +23,7 @@ header {
}
header h1 {
- margin: 1em;
+ margin: 1rem;
}
/* --- Navigation bar --- */
@@ -76,14 +77,15 @@ nav li.nav_current_page a {
/* --- Main section --- */
main {
- margin: 2em;
+ margin: 2rem;
}
/* --- Login page --- */
main.login_page {
- margin: 2em;
- padding: 1em;
+ margin: 2rem;
+ padding: 1rem;
border: 1px solid #666666;
+ width: 40rem;
}
/* --- Text and other styling --- */
@@ -101,11 +103,18 @@ a:hover, a:focus {
text-decoration: underline;
}
+.error_box, .success_box {
+ max-width: 50rem;
+ margin: 1rem 0;
+ padding: 1rem;
+}
+
.error_box {
background: #ff4444;
- max-width: 50em;
- margin: 1em 0;
- padding: 1em;
+}
+
+.success_box {
+ background: #00cc00;
}
.gray {
@@ -131,7 +140,7 @@ button {
/* --- Tables --- */
table td, table th {
- padding: 0.25em 0.5em;
+ padding: 0.25rem 0.5rem;
}
table.bordered_table,
@@ -140,7 +149,7 @@ table.bordered_table td,
table.bordered_table th {
border: 1px solid #999999;
border-collapse: collapse;
- padding: 0.25em 0.5em;
+ padding: 0.25rem 0.5rem;
}
.vertical_table_headers th {
@@ -152,13 +161,13 @@ table.bordered_table th {
.edit_box,
.confirmation_box {
border: 1px solid #999999;
- padding: 1em;
- margin: 1em 0;
+ padding: 1rem;
+ margin: 1rem 0;
}
/* --- Detail columns --- */
input#show_details_checkbox {
- margin-bottom: 1em;
+ margin-bottom: 1rem;
}
input#show_details_checkbox:not(:checked) ~ table .detail_column {
diff --git a/src/Dependencies.php b/src/Dependencies.php
index 50c82e1..fc3d767 100644
--- a/src/Dependencies.php
+++ b/src/Dependencies.php
@@ -16,6 +16,7 @@ use MailAccountAdmin\Repositories\DomainRepository;
use PDO;
use Psr\Container\ContainerInterface;
use Slim\Views\Twig;
+use Twig\Extension\CoreExtension as TwigCoreExtension;
class Dependencies
{
@@ -36,7 +37,17 @@ class Dependencies
/** @var Settings $settings */
$settings = $c->get(self::SETTINGS);
- return Twig::create(self::TWIG_TEMPLATE_DIR, $settings->getTwigSettings());
+ // Create Twig view
+ $twig = Twig::create(self::TWIG_TEMPLATE_DIR, $settings->getTwigSettings());
+
+ // Set default date format
+ /** @var TwigCoreExtension $coreExtension */
+ $coreExtension = $twig->getEnvironment()->getExtension(TwigCoreExtension::class);
+ $coreExtension->setDateFormat($settings->getDateFormat());
+ $coreExtension->setTimezone($settings->getTimezone());
+
+ // Return Twig view
+ return $twig;
});
// Database connection
diff --git a/src/Exceptions/InformationUnknownException.php b/src/Exceptions/InformationUnknownException.php
new file mode 100644
index 0000000..0889af8
--- /dev/null
+++ b/src/Exceptions/InformationUnknownException.php
@@ -0,0 +1,8 @@
+aliasRepository = $aliasRepository;
}
+
+ // -- /accounts - List all accounts
+
public function showAccounts(Request $request, Response $response): Response
{
// Parse query parameters for filters
@@ -39,21 +43,22 @@ class AccountController extends BaseController
return $this->view->render($response, 'accounts.html.twig', $renderData);
}
+
+ // -- /accounts/{id} - Show account details
+
public function showAccountDetails(Request $request, Response $response, array $args): Response
{
// Parse URL arguments
$accountId = (int)$args['id'];
// Get account data from database
- $accountData = $this->accountRepository->fetchAccountById($accountId);
- $accountUsername = $accountData['username'];
- $accountData['domain'] = explode('@', $accountUsername, 2)[1];
+ $account = $this->accountRepository->fetchAccountById($accountId);
// Don't display the password hash, but at least the type of hash (used hash algorithm)
- if ($accountData['password'] === '') {
+ if ($account->getPasswordHash() === '') {
$passwordHashType = 'empty';
} else {
- $passwordHashInfo = password_get_info($accountData['password']);
+ $passwordHashInfo = password_get_info($account->getPasswordHash());
$passwordHashType = $passwordHashInfo['algoName'] ?? 'unknown';
}
@@ -62,8 +67,8 @@ class AccountController extends BaseController
$renderData = [
'id' => $accountId,
- 'accountUsername' => $accountUsername,
- 'accountData' => $accountData,
+ 'accountUsername' => $account->getUsername(),
+ 'account' => $account,
'passwordHashType' => $passwordHashType,
'aliases' => $aliases,
];
@@ -71,26 +76,27 @@ class AccountController extends BaseController
return $this->view->render($response, 'account_details.html.twig', $renderData);
}
+
+ // -- /accounts/new - Create new account
+
public function showAccountCreate(Request $request, Response $response): Response
{
return $this->showAccounts($request, $response);
}
+
+ // -- /accounts/{id}/edit - Edit account
+
public function showAccountEdit(Request $request, Response $response, array $args): Response
{
// Parse URL arguments
$accountId = (int)$args['id'];
// Get account data from database
- $accountData = $this->accountRepository->fetchAccountById($accountId);
+ $account = $this->accountRepository->fetchAccountById($accountId);
- $renderData = [
- 'id' => $accountId,
- 'accountUsername' => $accountData['username'],
- 'accountData' => $accountData,
- ];
-
- return $this->view->render($response, 'account_edit.html.twig', $renderData);
+ // Render page
+ return $this->renderEditPage($response, $account);
}
public function editAccount(Request $request, Response $response, array $args): Response
@@ -100,18 +106,31 @@ class AccountController extends BaseController
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));
+ }
+
+ // -- /accounts/{id}/delete - Delete account
+
public function showAccountDelete(Request $request, Response $response, array $args): Response
{
// Parse URL arguments
$accountId = (int)$args['id'];
// Get account data and list of aliases from database
- $accountData = $this->accountRepository->fetchAccountById($accountId);
+ $account = $this->accountRepository->fetchAccountById($accountId);
$aliases = $this->aliasRepository->fetchAliasesForUserId($accountId);
$renderData = [
'id' => $accountId,
- 'accountUsername' => $accountData['username'],
+ 'accountUsername' => $account->getUsername(),
'aliases' => $aliases,
];
diff --git a/src/Models/Account.php b/src/Models/Account.php
new file mode 100644
index 0000000..472e576
--- /dev/null
+++ b/src/Models/Account.php
@@ -0,0 +1,138 @@
+id = $id;
+ $this->username = $username;
+ $this->passwordHash = $passwordHash;
+ $this->active = $active;
+ $this->homeDir = $homeDir;
+ $this->memo = $memo;
+ $this->createdAt = $createdAt;
+ $this->modifiedAt = $modifiedAt;
+
+ // Extract domain from username
+ $this->domain = explode('@', $this->username, 2)[1];
+ }
+
+ public static function createFromArray(array $data): self
+ {
+ $account = new self(
+ (int)$data['user_id'],
+ $data['username'],
+ $data['password'],
+ $data['is_active'] === '1',
+ $data['home_dir'],
+ $data['memo'] !== null ? $data['memo'] : '',
+ new DateTimeImmutable($data['created_at']),
+ new DateTimeImmutable($data['modified_at']),
+ );
+
+ if (isset($data['alias_count'])) {
+ $account->setAliasCount((int)$data['alias_count']);
+ }
+ return $account;
+ }
+
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ public function getUsername(): string
+ {
+ return $this->username;
+ }
+
+ public function getPasswordHash(): string
+ {
+ return $this->passwordHash;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->active;
+ }
+
+ public function getHomeDir(): string
+ {
+ return $this->homeDir;
+ }
+
+ public function getMemo(): string
+ {
+ return $this->memo;
+ }
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getModifiedAt(): DateTimeImmutable
+ {
+ return $this->modifiedAt;
+ }
+
+
+ // Getters and setters for extra data that is not part of the data model
+
+ public function getDomain(): string
+ {
+ return $this->domain;
+ }
+
+ public function hasAliasCount(): bool
+ {
+ return $this->aliasCount !== null;
+ }
+
+ public function getAliasCount(): int
+ {
+ if ($this->aliasCount === null) {
+ throw new InformationUnknownException('Alias count is not set on this object.');
+ }
+ return $this->aliasCount;
+ }
+
+ public function setAliasCount(int $newAliasCount): void
+ {
+ $this->aliasCount = $newAliasCount;
+ }
+}
diff --git a/src/Repositories/AccountRepository.php b/src/Repositories/AccountRepository.php
index a506447..03a6066 100644
--- a/src/Repositories/AccountRepository.php
+++ b/src/Repositories/AccountRepository.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace MailAccountAdmin\Repositories;
use MailAccountAdmin\Exceptions\AccountNotFoundException;
+use MailAccountAdmin\Models\Account;
use PDO;
class AccountRepository extends BaseRepository
@@ -31,10 +32,17 @@ class AccountRepository extends BaseRepository
$statement = $this->pdo->prepare($query);
$statement->execute($queryParams);
- return $statement->fetchAll(PDO::FETCH_ASSOC);
+ $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
+
+ // Create Account models from rows
+ $accountList = [];
+ foreach ($rows as $row) {
+ $accountList[] = Account::createFromArray($row);
+ }
+ return $accountList;
}
- public function fetchAccountById(int $accountId): array
+ public function fetchAccountById(int $accountId): Account
{
$statement = $this->pdo->prepare('SELECT * FROM mail_users WHERE user_id = :user_id LIMIT 1');
$statement->execute(['user_id' => $accountId]);
@@ -43,6 +51,7 @@ class AccountRepository extends BaseRepository
throw new AccountNotFoundException("Account with user ID '$accountId' was not found.");
}
- return $statement->fetch(PDO::FETCH_ASSOC);
+ $row = $statement->fetch(PDO::FETCH_ASSOC);
+ return Account::createFromArray($row);
}
}
diff --git a/src/Settings.php b/src/Settings.php
index ca111a0..1aecf4b 100644
--- a/src/Settings.php
+++ b/src/Settings.php
@@ -10,10 +10,23 @@ class Settings
return getenv('APP_DEBUG') === 'true';
}
+ public function getTimezone(): string
+ {
+ return 'Europe/Berlin';
+ }
+
+ public function getDateFormat(): string
+ {
+ // Default date format (https://www.php.net/manual/en/datetime.format.php)
+ // RFC 2822 formatted date (Thu, 21 Dec 2000 16:01:07 +0200)
+ return 'r';
+ }
+
public function getTwigSettings(): array
{
return [
'cache' => getenv('TWIG_CACHE_DIR') ?: false,
+ 'debug' => $this->isDebugMode(),
'strict_variables' => getenv('TWIG_STRICT') === 'true',
];
}
diff --git a/templates/account_details.html.twig b/templates/account_details.html.twig
index ec702fe..642f3aa 100644
--- a/templates/account_details.html.twig
+++ b/templates/account_details.html.twig
@@ -18,15 +18,15 @@
diff --git a/templates/account_edit.html.twig b/templates/account_edit.html.twig
index 4667eef..3d48be6 100644
--- a/templates/account_edit.html.twig
+++ b/templates/account_edit.html.twig
@@ -28,7 +28,7 @@
| Current username: |
- {{ accountData['username'] }} |
+ {{ account.getUsername() }} |
|
@@ -67,13 +67,13 @@