diff --git a/public/static/style.css b/public/static/style.css index ab04854..f4cf7bd 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -86,12 +86,8 @@ main.login_page { border: 1px solid #666666; } -main.login_page table td { - padding: 0.2em; -} - /* --- Text and other styling --- */ -h2 { +h2, h4 { margin: 0 0 0.5em 0; } @@ -105,9 +101,9 @@ a:hover, a:focus { text-decoration: underline; } -.error { +.error_box { background: #ff4444; - width: 30em; + max-width: 50em; margin: 1em 0; padding: 1em; } @@ -116,31 +112,50 @@ a:hover, a:focus { color: gray; } -button { - padding: 0.2em 1em; +.green { + color: green; } -table, tr, td, th { - border: 1px solid #999999; - border-collapse: collapse; - padding: 0.25em 0.5em; +.red { + color: red; +} + +button { + padding: 0.2em 1em; } .inactive { color: gray; } -/* --- Filter options --- */ -.filter_options { +/* --- Tables --- */ + +table td, table th { + padding: 0.25em 0.5em; +} + +table.bordered_table, +table.bordered_table tr, +table.bordered_table td, +table.bordered_table th { + border: 1px solid #999999; + border-collapse: collapse; + padding: 0.25em 0.5em; +} + +.vertical_table_headers th { + text-align: left; +} + +/* --- Boxes --- */ +.filter_options, +.edit_box, +.confirmation_box { border: 1px solid #999999; padding: 1em; margin: 1em 0; } -.filter_options h4 { - margin: 0 0 0.5em 0; -} - /* --- Detail columns --- */ input#show_details_checkbox { margin-bottom: 1em; @@ -149,3 +164,13 @@ input#show_details_checkbox { input#show_details_checkbox:not(:checked) ~ table .detail_column { display: none; } + +/* --- Edit box --- */ +.edit_box p:last-child { + margin-bottom: 0; +} + +/* --- Confirmation box --- */ +.confirmation_box p:first-child { + margin-top: 0; +} diff --git a/src/Dependencies.php b/src/Dependencies.php index f7f9742..50c82e1 100644 --- a/src/Dependencies.php +++ b/src/Dependencies.php @@ -11,6 +11,7 @@ use MailAccountAdmin\Frontend\Login\LoginController; use MailAccountAdmin\Frontend\Dashboard\DashboardController; use MailAccountAdmin\Repositories\AccountRepository; use MailAccountAdmin\Repositories\AdminUserRepository; +use MailAccountAdmin\Repositories\AliasRepository; use MailAccountAdmin\Repositories\DomainRepository; use PDO; use Psr\Container\ContainerInterface; @@ -70,6 +71,11 @@ class Dependencies $c->get(self::DATABASE), ); }); + $container->set(AliasRepository::class, function (ContainerInterface $c) { + return new AliasRepository( + $c->get(self::DATABASE), + ); + }); // Helper classes $container->set(UserHelper::class, function (ContainerInterface $c) { @@ -104,6 +110,7 @@ class Dependencies $c->get(self::TWIG), $c->get(UserHelper::class), $c->get(AccountRepository::class), + $c->get(AliasRepository::class), ); }); diff --git a/src/Exceptions/AccountNotFoundException.php b/src/Exceptions/AccountNotFoundException.php new file mode 100644 index 0000000..4c9833b --- /dev/null +++ b/src/Exceptions/AccountNotFoundException.php @@ -0,0 +1,8 @@ +accountRepository = $accountRepository; + $this->aliasRepository = $aliasRepository; } public function showAccounts(Request $request, Response $response): Response @@ -34,4 +38,90 @@ class AccountController extends BaseController return $this->view->render($response, 'accounts.html.twig', $renderData); } + + 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]; + + // Don't display the password hash, but at least the type of hash (used hash algorithm) + if ($accountData['password'] === '') { + $passwordHashType = 'empty'; + } else { + $passwordHashInfo = password_get_info($accountData['password']); + $passwordHashType = $passwordHashInfo['algoName'] ?? 'unknown'; + } + + // Get list of aliases for this account + $aliases = $this->aliasRepository->fetchAliasesForUserId($accountId); + + $renderData = [ + 'id' => $accountId, + 'accountUsername' => $accountUsername, + 'accountData' => $accountData, + 'passwordHashType' => $passwordHashType, + 'aliases' => $aliases, + ]; + + return $this->view->render($response, 'account_details.html.twig', $renderData); + } + + public function showAccountCreate(Request $request, Response $response): Response + { + return $this->showAccounts($request, $response); + } + + 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); + + $renderData = [ + 'id' => $accountId, + 'accountUsername' => $accountData['username'], + 'accountData' => $accountData, + ]; + + return $this->view->render($response, 'account_edit.html.twig', $renderData); + } + + 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); + } + + 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); + $aliases = $this->aliasRepository->fetchAliasesForUserId($accountId); + + $renderData = [ + 'id' => $accountId, + 'accountUsername' => $accountData['username'], + 'aliases' => $aliases, + ]; + + 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); + } } diff --git a/src/Repositories/AccountRepository.php b/src/Repositories/AccountRepository.php index a97e3cd..a506447 100644 --- a/src/Repositories/AccountRepository.php +++ b/src/Repositories/AccountRepository.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace MailAccountAdmin\Repositories; +use MailAccountAdmin\Exceptions\AccountNotFoundException; use PDO; class AccountRepository extends BaseRepository @@ -32,4 +33,16 @@ class AccountRepository extends BaseRepository $statement->execute($queryParams); return $statement->fetchAll(PDO::FETCH_ASSOC); } + + public function fetchAccountById(int $accountId): array + { + $statement = $this->pdo->prepare('SELECT * FROM mail_users WHERE user_id = :user_id LIMIT 1'); + $statement->execute(['user_id' => $accountId]); + + if ($statement->rowCount() < 1) { + throw new AccountNotFoundException("Account with user ID '$accountId' was not found."); + } + + return $statement->fetch(PDO::FETCH_ASSOC); + } } diff --git a/src/Repositories/AliasRepository.php b/src/Repositories/AliasRepository.php new file mode 100644 index 0000000..ac212c2 --- /dev/null +++ b/src/Repositories/AliasRepository.php @@ -0,0 +1,16 @@ +pdo->prepare('SELECT * FROM mail_aliases WHERE user_id = :user_id ORDER BY mail_address'); + $statement->execute(['user_id' => $userId]); + return $statement->fetchAll(PDO::FETCH_ASSOC); + } +} diff --git a/src/Routes.php b/src/Routes.php index 03e0b4a..728e70e 100644 --- a/src/Routes.php +++ b/src/Routes.php @@ -23,10 +23,14 @@ class Routes // Domains $app->get('/domains', DomainController::class . ':showDomains'); - $app->get('/domains/{foo}', DomainController::class . ':showDomains'); // Accounts $app->get('/accounts', AccountController::class . ':showAccounts'); - $app->get('/accounts/{foo}', AccountController::class . ':showAccounts'); + $app->get('/accounts/new', AccountController::class . ':showAccountCreate'); + $app->get('/accounts/{id:[1-9][0-9]*}', AccountController::class . ':showAccountDetails'); + $app->get('/accounts/{id:[1-9][0-9]*}/edit', AccountController::class . ':showAccountEdit'); + $app->post('/accounts/{id:[1-9][0-9]*}/edit', AccountController::class . ':editAccount'); + $app->get('/accounts/{id:[1-9][0-9]*}/delete', AccountController::class . ':showAccountDelete'); + $app->post('/accounts/{id:[1-9][0-9]*}/delete', AccountController::class . ':deleteAccount'); } } diff --git a/templates/account_delete.html.twig b/templates/account_delete.html.twig new file mode 100644 index 0000000..7a4840a --- /dev/null +++ b/templates/account_delete.html.twig @@ -0,0 +1,44 @@ +{% extends "base.html.twig" %} + +{% block title %}Delete account{% endblock %} + +{% block content %} +

Account: {{ accountUsername }}

+ +

+ Actions: + View | + Edit | + Delete | + Send mail +

+ +

Delete account

+ +

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!

+ + {% if error is defined %} +
+

Error:

+ {{ error }} +
+ {% endif %} + +
+

Please confirm that you want to delete this account by entering your password.

+
+

+ + +

+ +
+
+{% endblock %} diff --git a/templates/account_details.html.twig b/templates/account_details.html.twig new file mode 100644 index 0000000..ec702fe --- /dev/null +++ b/templates/account_details.html.twig @@ -0,0 +1,85 @@ +{% extends "base.html.twig" %} + +{% block title %}View account{% endblock %} + +{% block content %} +

Account: {{ accountUsername }}

+ +

+ Actions: + View | + Edit | + Delete | + Send mail +

+ +

View account details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User ID{{ accountData['user_id'] }}
Username{{ accountData['username'] }}
Domain{{ accountData['domain'] }}
Password + {% if passwordHashType == 'empty' %} + [No password set] + {% elseif passwordHashType == 'unknown' %} + [Not hashed / unknown hash algorithm] + {% else %} + [{{ passwordHashType }} hash] + {% endif %} +
Status{{ accountData['is_active'] == '1' ? 'Active' : 'Inactive' }}
Home directory/srv/vmail/{{ accountData['home_dir'] }}
Admin memo{{ accountData['memo'] | nl2br }}
Created at{{ accountData['created_at'] }}
Last modified at{{ accountData['modified_at'] }}
+ +

Aliases

+ + {% if aliases %} + + + + + + + {% for alias in aliases %} + + + + {# TODO #} + + {% endfor %} +
AddressCreated atActions
{{ alias['mail_address'] }}{{ alias['created_at'] }}Delete
+ {% else %} +

No aliases.

+ {% endif %} +{% endblock %} diff --git a/templates/account_edit.html.twig b/templates/account_edit.html.twig new file mode 100644 index 0000000..4667eef --- /dev/null +++ b/templates/account_edit.html.twig @@ -0,0 +1,115 @@ +{% extends "base.html.twig" %} + +{% block title %}Edit account{% endblock %} + +{% block content %} +

Account: {{ accountUsername }}

+ +

+ Actions: + View | + Edit | + Delete | + Send mail +

+ +

Edit account data

+ +
+ {% if error is defined %} +
+

Error

+ {{ error }} +
+ {% endif %} + +
+

Username

+ + + + + + + + + + + + + + + +
Current username:{{ accountData['username'] }}
+ +
+ +
+
+ +
+

Password

+

The new password will be hashed using the current default hash algorithm.

+ + + + + + + + + +
+
+ +
+

Account status

+ + + + + + + + + +
Current status:{{ accountData['is_active'] == '1' ? 'Active' : 'Inactive' }}
New status: + +
+
+ +
+

Home directory

+

Important: Changing the home directory here will NOT move any existing mail data, this needs to be done + manually!

+ + + + + + + + + +
Current home directory:/srv/vmail/{{ accountData['home_dir'] }}
+ /srv/vmail/ +
+
+ +
+

+

This field is only readable by admins.

+ + + + + +
+
+ + + +
+{% endblock %} diff --git a/templates/accounts.html.twig b/templates/accounts.html.twig index b920b3b..3c9ae7f 100644 --- a/templates/accounts.html.twig +++ b/templates/accounts.html.twig @@ -5,7 +5,12 @@ {% block content %}

Accounts

-

List of all mail accounts.

+

+ Actions: + Create account +

+ +

List of accounts

@@ -19,7 +24,7 @@ - +
diff --git a/templates/domains.html.twig b/templates/domains.html.twig index 79d0044..998e7ab 100644 --- a/templates/domains.html.twig +++ b/templates/domains.html.twig @@ -7,7 +7,7 @@

This is a list of all domains auto-generated from the existing mail accounts and aliases. As such it is read-only.

-
Username Domain
+
diff --git a/templates/login.html.twig b/templates/login.html.twig index 07fafb6..ffe297e 100644 --- a/templates/login.html.twig +++ b/templates/login.html.twig @@ -18,7 +18,7 @@ {% if error is defined %} -
{{ error }}
+
{{ error }}
{% endif %}
Domain Accounts