From b24fe56c8ae6fc3e3f7a9b153fc4e5a6f3c7cb38 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Tue, 27 Jul 2021 01:25:03 +0200 Subject: [PATCH] Implement basic database access and login --- docker-compose.yml | 3 + sql/init_tables.sql | 50 +++++++++++++ src/Auth/AuthMiddleware.php | 1 + src/Common/UserHelper.php | 33 ++++++++ src/Dependencies.php | 23 +++++- src/Exceptions/AdminUserNotFoundException.php | 8 ++ src/Exceptions/AppException.php | 10 +++ src/Frontend/BaseController.php | 21 ++++++ .../Dashboard/DashboardController.php | 17 ++--- src/Frontend/Login/LoginController.php | 51 ++++++++----- src/Models/AdminUser.php | 75 +++++++++++++++++++ src/Repositories/AdminUserRepository.php | 35 +++++++++ src/Settings.php | 2 +- templates/dashboard.html.twig | 9 +++ 14 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 sql/init_tables.sql create mode 100644 src/Common/UserHelper.php create mode 100644 src/Exceptions/AdminUserNotFoundException.php create mode 100644 src/Exceptions/AppException.php create mode 100644 src/Frontend/BaseController.php create mode 100644 src/Models/AdminUser.php create mode 100644 src/Repositories/AdminUserRepository.php diff --git a/docker-compose.yml b/docker-compose.yml index f59e2d4..f742930 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,11 @@ services: image: mariadb env_file: - .env.develop + ports: + - 13306:3306 volumes: - db_data:/var/lib/mysql + - ./sql/init_tables.sql:/docker-entrypoint-initdb.d/init_tables.sql:ro adminer: image: adminer diff --git a/sql/init_tables.sql b/sql/init_tables.sql new file mode 100644 index 0000000..f45577d --- /dev/null +++ b/sql/init_tables.sql @@ -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; diff --git a/src/Auth/AuthMiddleware.php b/src/Auth/AuthMiddleware.php index 2a8d7eb..6738f6e 100644 --- a/src/Auth/AuthMiddleware.php +++ b/src/Auth/AuthMiddleware.php @@ -21,6 +21,7 @@ class AuthMiddleware implements MiddlewareInterface // TODO: Lots of stuff. Session middleware, auth handler class, etc... if ($uri->getPath() !== '/login') { // Check authorization via session + // TODO username or user ID? if (empty($_SESSION['username'])) { // Not logged in -> Redirect to /login $response = new Response(); diff --git a/src/Common/UserHelper.php b/src/Common/UserHelper.php new file mode 100644 index 0000000..e67f56b --- /dev/null +++ b/src/Common/UserHelper.php @@ -0,0 +1,33 @@ +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); + } +} diff --git a/src/Dependencies.php b/src/Dependencies.php index cd8d3e3..9f2e901 100644 --- a/src/Dependencies.php +++ b/src/Dependencies.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace MailAccountAdmin; use DI\Container; +use MailAccountAdmin\Common\UserHelper; use MailAccountAdmin\Frontend\Login\LoginController; use MailAccountAdmin\Frontend\Dashboard\DashboardController; +use MailAccountAdmin\Repositories\AdminUserRepository; use PDO; use Psr\Container\ContainerInterface; 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 $container->set(LoginController::class, function (ContainerInterface $c) { return new LoginController( - $c->get(self::TWIG) + $c->get(self::TWIG), + $c->get(UserHelper::class), + $c->get(AdminUserRepository::class), ); }); // Dashboard $container->set(DashboardController::class, function (ContainerInterface $c) { return new DashboardController( - $c->get(self::TWIG) + $c->get(self::TWIG), + $c->get(UserHelper::class), ); }); diff --git a/src/Exceptions/AdminUserNotFoundException.php b/src/Exceptions/AdminUserNotFoundException.php new file mode 100644 index 0000000..413b5ff --- /dev/null +++ b/src/Exceptions/AdminUserNotFoundException.php @@ -0,0 +1,8 @@ +view = $view; + $this->userHelper = $userHelper; + } +} diff --git a/src/Frontend/Dashboard/DashboardController.php b/src/Frontend/Dashboard/DashboardController.php index bb7218b..7538686 100644 --- a/src/Frontend/Dashboard/DashboardController.php +++ b/src/Frontend/Dashboard/DashboardController.php @@ -3,24 +3,19 @@ declare(strict_types=1); namespace MailAccountAdmin\Frontend\Dashboard; +use MailAccountAdmin\Frontend\BaseController; use Psr\Http\Message\ResponseInterface as Response; 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 { + $currentUser = $this->userHelper->getCurrentUser(); + $renderData = [ - 'username' => $_SESSION['username'], + 'username' => $currentUser->getUsername(), + 'user' => $currentUser, ]; return $this->view->render($response, 'dashboard.html.twig', $renderData); diff --git a/src/Frontend/Login/LoginController.php b/src/Frontend/Login/LoginController.php index 9935f63..f486c44 100644 --- a/src/Frontend/Login/LoginController.php +++ b/src/Frontend/Login/LoginController.php @@ -3,52 +3,69 @@ declare(strict_types=1); 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\ServerRequestInterface as Request; -use Slim\Exception\HttpBadRequestException; use Slim\Views\Twig; -class LoginController +class LoginController extends BaseController { - /** @var Twig */ - private $view; + /** @var AdminUserRepository */ + 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 { - if (!empty($_SESSION['username'])) { + if ($this->userHelper->isLoggedIn()) { // Already logged in, redirect to dashboard return $response ->withHeader('Location', '/') ->withStatus(303); } - $renderData = [ - ]; - - return $this->view->render($response, 'login.html.twig', $renderData); + return $this->renderLoginPage($response); } public function authenticateUser(Request $request, Response $response): Response { $params = (array)$request->getParsedBody(); - if (empty($params['username']) || empty($params['password'])) { - throw new HttpBadRequestException($request, 'Missing parameters'); + if (empty($params['username'])) { + return $this->renderLoginPage($response, ['error' => 'Missing username!']); + } elseif (empty($params['password'])) { + return $this->renderLoginPage($response, ['error' => 'Missing password!']); } - // TODO: only for testing, obviously - if ($params['username'] === 'lexi' && $params['password'] === 'testpw') { - $_SESSION['username'] = $params['username']; + $loginUsername = $params['username']; + $loginPassword = $params['password']; + + 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 ->withHeader('Location', '/') ->withStatus(303); } else { - return $this->view->render($response, 'login.html.twig', ['error' => 'Wrong username or password!']); + return $this->renderLoginPage($response, ['error' => 'Wrong username or password!']); } } diff --git a/src/Models/AdminUser.php b/src/Models/AdminUser.php new file mode 100644 index 0000000..cef9c7b --- /dev/null +++ b/src/Models/AdminUser.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/src/Repositories/AdminUserRepository.php b/src/Repositories/AdminUserRepository.php new file mode 100644 index 0000000..31e2a41 --- /dev/null +++ b/src/Repositories/AdminUserRepository.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/src/Settings.php b/src/Settings.php index af0e385..ca111a0 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -24,7 +24,7 @@ class Settings 'host' => getenv('DB_HOST') ?: 'localhost', 'port' => getenv('DB_PORT') ?: 3306, 'dbname' => getenv('DB_DATABASE') ?: '', - 'username' => getenv('DB_USERNAME') ?: '', + 'username' => getenv('DB_USER') ?: '', 'password' => getenv('DB_PASSWORD') ?: '', ]; } diff --git a/templates/dashboard.html.twig b/templates/dashboard.html.twig index 92cad0f..cf4cb89 100644 --- a/templates/dashboard.html.twig +++ b/templates/dashboard.html.twig @@ -7,5 +7,14 @@

Hello, {{ username }}!

+
+        ID: {{ user.getId() }}
+        username: {{ user.getUsername() }}
+        password: {{ user.getPasswordHash() }}
+        is_active: {{ user.isActive() }}
+        created_at: {{ user.getCreatedAt() | date() }}
+        modified_at: {{ user.getModifiedAt() | date() }}
+    
+ Logout. {% endblock %}