Implement basic database access and login
This commit is contained in:
parent
8be7988d97
commit
b24fe56c8a
|
|
@ -17,8 +17,11 @@ services:
|
||||||
image: mariadb
|
image: mariadb
|
||||||
env_file:
|
env_file:
|
||||||
- .env.develop
|
- .env.develop
|
||||||
|
ports:
|
||||||
|
- 13306:3306
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/mysql
|
- db_data:/var/lib/mysql
|
||||||
|
- ./sql/init_tables.sql:/docker-entrypoint-initdb.d/init_tables.sql:ro
|
||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer
|
image: adminer
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
-- Create tables
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
-- TODO create on prod
|
||||||
|
CREATE TABLE `admin_users`
|
||||||
|
(
|
||||||
|
`admin_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(255) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`is_active` tinyint(3) unsigned NOT NULL DEFAULT 1,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT NOW(),
|
||||||
|
`modified_at` datetime NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (`admin_id`),
|
||||||
|
UNIQUE KEY `username` (`username`)
|
||||||
|
) DEFAULT CHARSET = utf8mb4;
|
||||||
|
|
||||||
|
-- Create initial admin user for development (password is 'admin')
|
||||||
|
INSERT INTO `admin_users`
|
||||||
|
(`username`, `password`, `is_active`, `created_at`, `modified_at`)
|
||||||
|
VALUES ('admin', '$2y$10$zaNOBUk4PBlhDZD40h35CeyUxiqixi9LTrxlAxnrckXd95hcCctl6', '1', NOW(), NOW());
|
||||||
|
|
||||||
|
-- TODO rename on prod `users` -> `mail_users`
|
||||||
|
CREATE TABLE `mail_users`
|
||||||
|
(
|
||||||
|
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(255) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`is_active` tinyint(3) unsigned NOT NULL DEFAULT 1,
|
||||||
|
`home_dir` varchar(255) NOT NULL,
|
||||||
|
`memo` text DEFAULT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT NOW(),
|
||||||
|
`modified_at` datetime NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (`user_id`),
|
||||||
|
UNIQUE KEY `username` (`username`)
|
||||||
|
) DEFAULT CHARSET = utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE `mail_aliases`
|
||||||
|
(
|
||||||
|
`alias_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(10) unsigned NOT NULL,
|
||||||
|
`mail_address` varchar(255) NOT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT NOW(),
|
||||||
|
`modified_at` datetime NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (`alias_id`),
|
||||||
|
UNIQUE KEY `mail_address` (`mail_address`),
|
||||||
|
KEY `user_id` (`user_id`),
|
||||||
|
-- TODO rename on prod `users` -> `mail_users`
|
||||||
|
CONSTRAINT `mail_aliases_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `mail_users` (`user_id`)
|
||||||
|
) DEFAULT CHARSET = utf8mb4;
|
||||||
|
|
@ -21,6 +21,7 @@ class AuthMiddleware implements MiddlewareInterface
|
||||||
// TODO: Lots of stuff. Session middleware, auth handler class, etc...
|
// TODO: Lots of stuff. Session middleware, auth handler class, etc...
|
||||||
if ($uri->getPath() !== '/login') {
|
if ($uri->getPath() !== '/login') {
|
||||||
// Check authorization via session
|
// Check authorization via session
|
||||||
|
// TODO username or user ID?
|
||||||
if (empty($_SESSION['username'])) {
|
if (empty($_SESSION['username'])) {
|
||||||
// Not logged in -> Redirect to /login
|
// Not logged in -> Redirect to /login
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MailAccountAdmin\Common;
|
||||||
|
|
||||||
|
use http\Exception\RuntimeException;
|
||||||
|
use MailAccountAdmin\Models\AdminUser;
|
||||||
|
use MailAccountAdmin\Repositories\AdminUserRepository;
|
||||||
|
|
||||||
|
class UserHelper
|
||||||
|
{
|
||||||
|
/** @var AdminUserRepository */
|
||||||
|
private $adminUserRepository;
|
||||||
|
|
||||||
|
public function __construct(AdminUserRepository $adminUserRepository)
|
||||||
|
{
|
||||||
|
$this->adminUserRepository = $adminUserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLoggedIn(): bool
|
||||||
|
{
|
||||||
|
return !empty($_SESSION['username']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentUser(): AdminUser
|
||||||
|
{
|
||||||
|
$username = $_SESSION['username'] ?? null;
|
||||||
|
if (empty($username)) {
|
||||||
|
throw new RuntimeException('Not logged in!');
|
||||||
|
}
|
||||||
|
return $this->adminUserRepository->getUserByName($username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||||
namespace MailAccountAdmin;
|
namespace MailAccountAdmin;
|
||||||
|
|
||||||
use DI\Container;
|
use DI\Container;
|
||||||
|
use MailAccountAdmin\Common\UserHelper;
|
||||||
use MailAccountAdmin\Frontend\Login\LoginController;
|
use MailAccountAdmin\Frontend\Login\LoginController;
|
||||||
use MailAccountAdmin\Frontend\Dashboard\DashboardController;
|
use MailAccountAdmin\Frontend\Dashboard\DashboardController;
|
||||||
|
use MailAccountAdmin\Repositories\AdminUserRepository;
|
||||||
use PDO;
|
use PDO;
|
||||||
use Psr\Container\ContainerInterface;
|
use Psr\Container\ContainerInterface;
|
||||||
use Slim\Views\Twig;
|
use Slim\Views\Twig;
|
||||||
|
|
@ -48,17 +50,34 @@ class Dependencies
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
$container->set(AdminUserRepository::class, function (ContainerInterface $c) {
|
||||||
|
return new AdminUserRepository(
|
||||||
|
$c->get(self::DATABASE),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper classes
|
||||||
|
$container->set(UserHelper::class, function (ContainerInterface $c) {
|
||||||
|
return new UserHelper(
|
||||||
|
$c->get(AdminUserRepository::class),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Login page
|
// Login page
|
||||||
$container->set(LoginController::class, function (ContainerInterface $c) {
|
$container->set(LoginController::class, function (ContainerInterface $c) {
|
||||||
return new LoginController(
|
return new LoginController(
|
||||||
$c->get(self::TWIG)
|
$c->get(self::TWIG),
|
||||||
|
$c->get(UserHelper::class),
|
||||||
|
$c->get(AdminUserRepository::class),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
$container->set(DashboardController::class, function (ContainerInterface $c) {
|
$container->set(DashboardController::class, function (ContainerInterface $c) {
|
||||||
return new DashboardController(
|
return new DashboardController(
|
||||||
$c->get(self::TWIG)
|
$c->get(self::TWIG),
|
||||||
|
$c->get(UserHelper::class),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MailAccountAdmin\Exceptions;
|
||||||
|
|
||||||
|
class AdminUserNotFoundException extends AppException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MailAccountAdmin\Exceptions;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class AppException extends RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MailAccountAdmin\Frontend;
|
||||||
|
|
||||||
|
use MailAccountAdmin\Common\UserHelper;
|
||||||
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
|
class BaseController
|
||||||
|
{
|
||||||
|
/** @var Twig */
|
||||||
|
protected $view;
|
||||||
|
/** @var UserHelper */
|
||||||
|
protected $userHelper;
|
||||||
|
|
||||||
|
public function __construct(Twig $view, UserHelper $userHelper)
|
||||||
|
{
|
||||||
|
$this->view = $view;
|
||||||
|
$this->userHelper = $userHelper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,24 +3,19 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace MailAccountAdmin\Frontend\Dashboard;
|
namespace MailAccountAdmin\Frontend\Dashboard;
|
||||||
|
|
||||||
|
use MailAccountAdmin\Frontend\BaseController;
|
||||||
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 DashboardController
|
class DashboardController extends BaseController
|
||||||
{
|
{
|
||||||
/** @var Twig */
|
|
||||||
private $view;
|
|
||||||
|
|
||||||
public function __construct(Twig $view)
|
|
||||||
{
|
|
||||||
$this->view = $view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function showDashboard(Request $request, Response $response): Response
|
public function showDashboard(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
|
$currentUser = $this->userHelper->getCurrentUser();
|
||||||
|
|
||||||
$renderData = [
|
$renderData = [
|
||||||
'username' => $_SESSION['username'],
|
'username' => $currentUser->getUsername(),
|
||||||
|
'user' => $currentUser,
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->view->render($response, 'dashboard.html.twig', $renderData);
|
return $this->view->render($response, 'dashboard.html.twig', $renderData);
|
||||||
|
|
|
||||||
|
|
@ -3,52 +3,69 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace MailAccountAdmin\Frontend\Login;
|
namespace MailAccountAdmin\Frontend\Login;
|
||||||
|
|
||||||
|
use MailAccountAdmin\Common\UserHelper;
|
||||||
|
use MailAccountAdmin\Exceptions\AdminUserNotFoundException;
|
||||||
|
use MailAccountAdmin\Frontend\BaseController;
|
||||||
|
use MailAccountAdmin\Repositories\AdminUserRepository;
|
||||||
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\Exception\HttpBadRequestException;
|
|
||||||
use Slim\Views\Twig;
|
use Slim\Views\Twig;
|
||||||
|
|
||||||
class LoginController
|
class LoginController extends BaseController
|
||||||
{
|
{
|
||||||
/** @var Twig */
|
/** @var AdminUserRepository */
|
||||||
private $view;
|
private $adminUserRepository;
|
||||||
|
|
||||||
public function __construct(Twig $view)
|
public function __construct(Twig $view, UserHelper $userHelper, AdminUserRepository $adminUserRepository)
|
||||||
{
|
{
|
||||||
$this->view = $view;
|
parent::__construct($view, $userHelper);
|
||||||
|
$this->adminUserRepository = $adminUserRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderLoginPage(Response $response, array $renderData = []): Response
|
||||||
|
{
|
||||||
|
return $this->view->render($response, 'login.html.twig', $renderData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function showLoginPage(Request $request, Response $response): Response
|
public function showLoginPage(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
if (!empty($_SESSION['username'])) {
|
if ($this->userHelper->isLoggedIn()) {
|
||||||
// Already logged in, redirect to dashboard
|
// Already logged in, redirect to dashboard
|
||||||
return $response
|
return $response
|
||||||
->withHeader('Location', '/')
|
->withHeader('Location', '/')
|
||||||
->withStatus(303);
|
->withStatus(303);
|
||||||
}
|
}
|
||||||
|
|
||||||
$renderData = [
|
return $this->renderLoginPage($response);
|
||||||
];
|
|
||||||
|
|
||||||
return $this->view->render($response, 'login.html.twig', $renderData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function authenticateUser(Request $request, Response $response): Response
|
public function authenticateUser(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
$params = (array)$request->getParsedBody();
|
$params = (array)$request->getParsedBody();
|
||||||
|
|
||||||
if (empty($params['username']) || empty($params['password'])) {
|
if (empty($params['username'])) {
|
||||||
throw new HttpBadRequestException($request, 'Missing parameters');
|
return $this->renderLoginPage($response, ['error' => 'Missing username!']);
|
||||||
|
} elseif (empty($params['password'])) {
|
||||||
|
return $this->renderLoginPage($response, ['error' => 'Missing password!']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: only for testing, obviously
|
$loginUsername = $params['username'];
|
||||||
if ($params['username'] === 'lexi' && $params['password'] === 'testpw') {
|
$loginPassword = $params['password'];
|
||||||
$_SESSION['username'] = $params['username'];
|
|
||||||
|
try {
|
||||||
|
$user = $this->adminUserRepository->getUserByName($loginUsername);
|
||||||
|
}
|
||||||
|
catch (AdminUserNotFoundException $e) {
|
||||||
|
$user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user !== null && password_verify($loginPassword, $user->getPasswordHash())) {
|
||||||
|
$_SESSION['username'] = $user->getUsername();
|
||||||
return $response
|
return $response
|
||||||
->withHeader('Location', '/')
|
->withHeader('Location', '/')
|
||||||
->withStatus(303);
|
->withStatus(303);
|
||||||
} else {
|
} else {
|
||||||
return $this->view->render($response, 'login.html.twig', ['error' => 'Wrong username or password!']);
|
return $this->renderLoginPage($response, ['error' => 'Wrong username or password!']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MailAccountAdmin\Models;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
class AdminUser
|
||||||
|
{
|
||||||
|
/** @var int */
|
||||||
|
private $id;
|
||||||
|
/** @var string */
|
||||||
|
private $username;
|
||||||
|
/** @var string */
|
||||||
|
private $passwordHash;
|
||||||
|
/** @var bool */
|
||||||
|
private $active;
|
||||||
|
/** @var DateTimeImmutable */
|
||||||
|
private $createdAt;
|
||||||
|
/** @var DateTimeImmutable */
|
||||||
|
private $modifiedAt;
|
||||||
|
|
||||||
|
private function __construct(int $id, string $username, string $passwordHash, bool $isActive,
|
||||||
|
DateTimeImmutable $createdAt, DateTimeImmutable $modifiedAt)
|
||||||
|
{
|
||||||
|
$this->id = $id;
|
||||||
|
$this->username = $username;
|
||||||
|
$this->passwordHash = $passwordHash;
|
||||||
|
$this->active = $isActive;
|
||||||
|
$this->createdAt = $createdAt;
|
||||||
|
$this->modifiedAt = $modifiedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createFromArray(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
(int)$data['admin_id'],
|
||||||
|
$data['username'],
|
||||||
|
$data['password'],
|
||||||
|
$data['is_active'] === '1',
|
||||||
|
new DateTimeImmutable($data['created_at']),
|
||||||
|
new DateTimeImmutable($data['modified_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModifiedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->modifiedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MailAccountAdmin\Repositories;
|
||||||
|
|
||||||
|
use MailAccountAdmin\Exceptions\AdminUserNotFoundException;
|
||||||
|
use MailAccountAdmin\Models\AdminUser;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class AdminUserRepository
|
||||||
|
{
|
||||||
|
/** @var PDO */
|
||||||
|
private $pdo;
|
||||||
|
|
||||||
|
public function __construct(PDO $pdo)
|
||||||
|
{
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws AdminUserNotFoundException
|
||||||
|
*/
|
||||||
|
public function getUserByName(string $username): AdminUser
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->prepare('SELECT * FROM admin_users WHERE username = :username LIMIT 1');
|
||||||
|
$statement->execute(['username' => $username]);
|
||||||
|
|
||||||
|
if ($statement->rowCount() < 1) {
|
||||||
|
throw new AdminUserNotFoundException("Admin with username '$username' was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return AdminUser::createFromArray($row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ class Settings
|
||||||
'host' => getenv('DB_HOST') ?: 'localhost',
|
'host' => getenv('DB_HOST') ?: 'localhost',
|
||||||
'port' => getenv('DB_PORT') ?: 3306,
|
'port' => getenv('DB_PORT') ?: 3306,
|
||||||
'dbname' => getenv('DB_DATABASE') ?: '',
|
'dbname' => getenv('DB_DATABASE') ?: '',
|
||||||
'username' => getenv('DB_USERNAME') ?: '',
|
'username' => getenv('DB_USER') ?: '',
|
||||||
'password' => getenv('DB_PASSWORD') ?: '',
|
'password' => getenv('DB_PASSWORD') ?: '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,14 @@
|
||||||
|
|
||||||
<p>Hello, {{ username }}!</p>
|
<p>Hello, {{ username }}!</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
ID: {{ user.getId() }}
|
||||||
|
username: {{ user.getUsername() }}
|
||||||
|
password: {{ user.getPasswordHash() }}
|
||||||
|
is_active: {{ user.isActive() }}
|
||||||
|
created_at: {{ user.getCreatedAt() | date() }}
|
||||||
|
modified_at: {{ user.getModifiedAt() | date() }}
|
||||||
|
</pre>
|
||||||
|
|
||||||
<a href="/logout">Logout.</a>
|
<a href="/logout">Logout.</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue