Implement basic database access and login

This commit is contained in:
Lexi / Zoe 2021-07-27 01:25:03 +02:00
parent 8be7988d97
commit b24fe56c8a
Signed by: binaryDiv
GPG Key ID: F8D4956E224DA232
14 changed files with 307 additions and 31 deletions

View File

@ -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

50
sql/init_tables.sql Normal file
View File

@ -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;

View File

@ -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();

33
src/Common/UserHelper.php Normal file
View File

@ -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);
}
}

View File

@ -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),
); );
}); });

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Exceptions;
class AdminUserNotFoundException extends AppException
{
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace MailAccountAdmin\Exceptions;
use RuntimeException;
class AppException extends RuntimeException
{
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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!']);
} }
} }

75
src/Models/AdminUser.php Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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') ?: '',
]; ];
} }

View File

@ -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 %}