Implement basic database access and login
This commit is contained in:
parent
8be7988d97
commit
b24fe56c8a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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!']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
'port' => getenv('DB_PORT') ?: 3306,
|
||||
'dbname' => getenv('DB_DATABASE') ?: '',
|
||||
'username' => getenv('DB_USERNAME') ?: '',
|
||||
'username' => getenv('DB_USER') ?: '',
|
||||
'password' => getenv('DB_PASSWORD') ?: '',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,14 @@
|
|||
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue