Compare commits

..

3 Commits

10 changed files with 248 additions and 14 deletions

View File

@ -54,12 +54,13 @@ nav a {
nav a:link, nav a:visited { nav a:link, nav a:visited {
background-color: #eeeeee; background-color: #eeeeee;
color: blue; color: #ff00e6;
text-decoration: none; text-decoration: none;
} }
nav a:hover, nav a:focus { nav a:hover, nav a:focus {
background-color: #ffffff; background-color: #ffffff;
color: #0066ff;
text-decoration: underline; text-decoration: underline;
} }
@ -94,6 +95,16 @@ h2 {
margin: 0 0 0.5em 0; margin: 0 0 0.5em 0;
} }
a:link, a:visited {
color: #ff00e6;
text-decoration: none;
}
a:hover, a:focus {
color: #0066ff;
text-decoration: underline;
}
.error { .error {
background: #ff4444; background: #ff4444;
width: 30em; width: 30em;
@ -101,6 +112,40 @@ h2 {
padding: 1em; padding: 1em;
} }
.gray {
color: gray;
}
button { button {
padding: 0.2em 1em; padding: 0.2em 1em;
} }
table, tr, td, th {
border: 1px solid #999999;
border-collapse: collapse;
padding: 0.25em 0.5em;
}
.inactive {
color: gray;
}
/* --- Filter options --- */
.filter_options {
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;
}
input#show_details_checkbox:not(:checked) ~ table .detail_column {
display: none;
}

View File

@ -9,7 +9,9 @@ use MailAccountAdmin\Frontend\Accounts\AccountController;
use MailAccountAdmin\Frontend\Domains\DomainController; use MailAccountAdmin\Frontend\Domains\DomainController;
use MailAccountAdmin\Frontend\Login\LoginController; use MailAccountAdmin\Frontend\Login\LoginController;
use MailAccountAdmin\Frontend\Dashboard\DashboardController; use MailAccountAdmin\Frontend\Dashboard\DashboardController;
use MailAccountAdmin\Repositories\AccountRepository;
use MailAccountAdmin\Repositories\AdminUserRepository; use MailAccountAdmin\Repositories\AdminUserRepository;
use MailAccountAdmin\Repositories\DomainRepository;
use PDO; use PDO;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Slim\Views\Twig; use Slim\Views\Twig;
@ -58,6 +60,16 @@ class Dependencies
$c->get(self::DATABASE), $c->get(self::DATABASE),
); );
}); });
$container->set(DomainRepository::class, function (ContainerInterface $c) {
return new DomainRepository(
$c->get(self::DATABASE),
);
});
$container->set(AccountRepository::class, function (ContainerInterface $c) {
return new AccountRepository(
$c->get(self::DATABASE),
);
});
// Helper classes // Helper classes
$container->set(UserHelper::class, function (ContainerInterface $c) { $container->set(UserHelper::class, function (ContainerInterface $c) {
@ -84,12 +96,14 @@ class Dependencies
return new DomainController( return new DomainController(
$c->get(self::TWIG), $c->get(self::TWIG),
$c->get(UserHelper::class), $c->get(UserHelper::class),
$c->get(DomainRepository::class),
); );
}); });
$container->set(AccountController::class, function (ContainerInterface $c) { $container->set(AccountController::class, function (ContainerInterface $c) {
return new AccountController( return new AccountController(
$c->get(self::TWIG), $c->get(self::TWIG),
$c->get(UserHelper::class), $c->get(UserHelper::class),
$c->get(AccountRepository::class),
); );
}); });

View File

@ -3,15 +3,33 @@ declare(strict_types=1);
namespace MailAccountAdmin\Frontend\Accounts; namespace MailAccountAdmin\Frontend\Accounts;
use MailAccountAdmin\Common\UserHelper;
use MailAccountAdmin\Frontend\BaseController; use MailAccountAdmin\Frontend\BaseController;
use MailAccountAdmin\Repositories\AccountRepository;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
class AccountController extends BaseController class AccountController extends BaseController
{ {
/** @var AccountRepository */
private $accountRepository;
public function __construct(Twig $view, UserHelper $userHelper, AccountRepository $accountRepository)
{
parent::__construct($view, $userHelper);
$this->accountRepository = $accountRepository;
}
public function showAccounts(Request $request, Response $response): Response public function showAccounts(Request $request, Response $response): Response
{ {
// Parse query parameters for filters
$queryParams = $request->getQueryParams();
$filterByDomain = $queryParams['domain'] ?? null;
$renderData = [ $renderData = [
'filterDomain' => $filterByDomain,
'accountList' => $this->accountRepository->fetchAccountList($filterByDomain),
]; ];
return $this->view->render($response, 'accounts.html.twig', $renderData); return $this->view->render($response, 'accounts.html.twig', $renderData);

View File

@ -3,15 +3,28 @@ declare(strict_types=1);
namespace MailAccountAdmin\Frontend\Domains; namespace MailAccountAdmin\Frontend\Domains;
use MailAccountAdmin\Common\UserHelper;
use MailAccountAdmin\Frontend\BaseController; use MailAccountAdmin\Frontend\BaseController;
use MailAccountAdmin\Repositories\DomainRepository;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
class DomainController extends BaseController class DomainController extends BaseController
{ {
/** @var DomainRepository */
private $domainRepository;
public function __construct(Twig $view, UserHelper $userHelper, DomainRepository $domainRepository)
{
parent::__construct($view, $userHelper);
$this->domainRepository = $domainRepository;
}
public function showDomains(Request $request, Response $response): Response public function showDomains(Request $request, Response $response): Response
{ {
$renderData = [ $renderData = [
'domainList' => $this->domainRepository->fetchDomainList(),
]; ];
return $this->view->render($response, 'domains.html.twig', $renderData); return $this->view->render($response, 'domains.html.twig', $renderData);

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Repositories;
use PDO;
class AccountRepository extends BaseRepository
{
public function fetchAccountList(string $filterByDomain = null): array
{
$queryWhere = '';
$queryParams = [];
if (!empty($filterByDomain)) {
$queryWhere = 'WHERE REGEXP_REPLACE(username, "^.*@", "") LIKE :domain';
$queryParams['domain'] = str_replace('*', '%', $filterByDomain);
}
$query = '
SELECT
mail_users.*,
REGEXP_REPLACE(username, "^.*@", "") AS domain,
COUNT(alias_id) AS alias_count
FROM mail_users
LEFT JOIN mail_aliases ON mail_users.user_id = mail_aliases.user_id
' . $queryWhere . '
GROUP BY username
ORDER BY domain, username
';
$statement = $this->pdo->prepare($query);
$statement->execute($queryParams);
return $statement->fetchAll(PDO::FETCH_ASSOC);
}
}

View File

@ -7,16 +7,8 @@ use MailAccountAdmin\Exceptions\AdminUserNotFoundException;
use MailAccountAdmin\Models\AdminUser; use MailAccountAdmin\Models\AdminUser;
use PDO; use PDO;
class AdminUserRepository class AdminUserRepository extends BaseRepository
{ {
/** @var PDO */
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/** /**
* @throws AdminUserNotFoundException * @throws AdminUserNotFoundException
*/ */

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Repositories;
use PDO;
class BaseRepository
{
/** @var PDO */
protected $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Repositories;
use PDO;
class DomainRepository extends BaseRepository
{
public function fetchDomainList(): array
{
// Fetch domains with account/alias count from database
$statement = $this->pdo->query('
SELECT
REGEXP_REPLACE(address, "^.*@", "") AS domain,
COUNT(is_account) AS account_count,
COUNT(is_alias) AS alias_count
FROM (
SELECT username AS address, 1 AS is_account, NULL AS is_alias FROM mail_users
UNION
SELECT mail_address AS address, NULL AS is_account, 1 AS is_alias FROM mail_aliases
) AS addresses
GROUP BY domain
');
$domainRows = $statement->fetchAll(PDO::FETCH_ASSOC);
$domainsCounted = [];
foreach ($domainRows as $row) {
$domainsCounted[$row['domain']] = [
'accounts' => $row['account_count'],
'aliases' => $row['alias_count'],
];
}
ksort($domainsCounted);
return $domainsCounted;
}
}

View File

@ -5,6 +5,48 @@
{% block content %} {% block content %}
<h2>Accounts</h2> <h2>Accounts</h2>
<p>List of accounts ... <b>TODO</b></p> <p>List of all mail accounts.</p>
<p><a href="/accounts/42">Test</a></p>
<div class="filter_options">
<form action="/accounts" method="GET">
<h4>Filter:</h4>
<label for="filter_domain">Domain: </label>
<input name="domain" id="filter_domain" value="{{ filterDomain | default('') }}"/>
<button type="submit">Apply</button>
<button type="reset" onclick="location.href='/accounts'">Clear</button>
</form>
</div>
<input type="checkbox" id="show_details_checkbox"><label for="show_details_checkbox"> Show detail columns</label>
<table>
<tr>
<th style="min-width: 15em">Username</th>
<th style="min-width: 10em">Domain</th>
<th>Aliases</th>
<th>Active</th>
<th class="detail_column">Home directory</th>
<th class="detail_column" style="min-width: 10em">Memo</th>
<th>Created</th>
<th class="detail_column">Last modified</th>
</tr>
{% if accountList %}
{% for account in accountList -%}
<tr{% if account['is_active'] != 1 %} class="inactive"{% endif %}>
<td><a href="/accounts/{{ account['user_id'] }}">{{ account['username'] }}</a></td>
<td><a href="/accounts?domain={{ account['domain'] }}">{{ account['domain'] }}</a></td>
<td>{{ account['alias_count'] }}</td>
<td>{{ account['is_active'] == 1 ? 'Yes' : 'No' }}</td>
<td class="detail_column"><span class="gray">vmail/</span>{{ account['home_dir'] }}</td>
<td class="detail_column">{{ account['memo'] }}</td>
<td>{{ account['created_at'] }}</td>
<td class="detail_column">{{ account['modified_at'] }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="100" style="text-align: center">No accounts found.</td>
</tr>
{% endif %}
</table>
{% endblock %} {% endblock %}

View File

@ -5,6 +5,26 @@
{% block content %} {% block content %}
<h2>Domains</h2> <h2>Domains</h2>
<p>List of domains ... <b>TODO</b></p> <p>This is a list of all domains auto-generated from the existing mail accounts and aliases. As such it is read-only.</p>
<p><a href="/domains/42">Test</a></p>
<table>
<tr>
<th style="min-width: 12em">Domain</th>
<th>Accounts</th>
<th>Aliases</th>
</tr>
{% if domainList %}
{% for domain, domainCounts in domainList -%}
<tr>
<td><a href="/accounts?domain={{ domain | escape('url') }}">{{ domain }}</a></td>
<td>{{ domainCounts['accounts'] }}</td>
<td>{{ domainCounts['aliases'] }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="100" style="text-align: center">No domains found.</td>
</tr>
{% endif %}
</table>
{% endblock %} {% endblock %}