Add incredibly simple authentication to the app.

TODO: Manage feeds
This commit is contained in:
Lewis Dale 2024-12-31 16:17:13 +00:00
parent 49ac6f34bc
commit 2e07b29941
9 changed files with 197 additions and 3 deletions

View File

@ -16,6 +16,7 @@ global $container;
$commands = [
$container->get(TestFeed::class),
$container->get(\Lewisdale\App\Tools\Console\CreateUser::class),
];
ConsoleRunner::run(

View File

@ -30,6 +30,11 @@ class FeedController
{
$this->logger->info("FeedController::get_feed() called");
$feed = $this->feedRepository->find($request->getAttribute('id'));
if (empty($feed)) {
return $response->withStatus(404);
}
$filtered = $feed->get_filtered_feed();
$body = $response->getBody();

View File

@ -0,0 +1,37 @@
<?php
namespace Lewisdale\App\Controllers;
use Lewisdale\App\Models\Repositories\UserRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Views\Twig;
class LoginController {
function __construct(
private readonly Twig $view,
private readonly UserRepository $users,
)
{
}
public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
return $this->view->render($response, 'login/index.twig.html', []);
}
public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
$body = $request->getParsedBody();
$email = $body['email'];
$password = $body['password'];
$user = $this->users->validateCredentials($email, $password);
if (!$user) {
return $this->view->render($response, 'login/index.twig.html', ['error' => 'Invalid email or password']);
}
$_SESSION['user'] = $user->id;
return $response->withStatus(302)->withHeader('Location', '/feed');
}
}

View File

@ -18,7 +18,7 @@ class User
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private ?int $id;
public ?int $id;
#[ORM\Column(type: 'string')]
private string $password;
#[ORM\Column(type: 'string')]

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Lewisdale\App\Models\Repositories;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Lewisdale\App\Models\Data\User;
use Psr\Log\LoggerInterface;
use function DI\string;
/** @extends EntityRepository<User> */
class UserRepository extends EntityRepository {
private readonly LoggerInterface $logger;
public function __construct(EntityManager $em, LoggerInterface $logger)
{
parent::__construct($em, $em->getClassMetadata(User::class));
$this->logger = $logger;
}
public function findByEmail(string $email): ?User {
return $this->findOneBy(['email' => $email]);
}
public function validateCredentials(string $email, string $password): ?User {
$user = $this->findByEmail($email);
if (!$user || !$user->validate($password)) {
$this->logger->warning('Invalid credentials.', ['email' => $email, 'password' => $password, 'user' => $user, 'valid' => $user?->validate($password)]);
return null;
}
return $user;
}
public function save(User $user): void {
$this->_em->persist($user);
$this->_em->flush();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Lewisdale\App\Session;
use Lewisdale\App\Models\Repositories\UserRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
class LoginMiddleware implements MiddlewareInterface {
function __construct(
private readonly UserRepository $users,
) {}
private function redirectToLogin() {
$response = new Response(302);
return $response->withHeader('Location', '/account/login');
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!isset($_SESSION['user'])) {
return $this->redirectToLogin();
}
$user = $this->users->find($_SESSION['user']);
if (!$user) {
return $this->redirectToLogin();
}
return $handler->handle($request->withAttribute('user', $user));
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Lewisdale\App\Tools\Console;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\Command\SchemaTool\AbstractCommand;
use Lewisdale\App\Models\Data\User;
use Lewisdale\App\Models\Repositories\FeedRepository;
use Lewisdale\App\Models\Repositories\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'user:create', description: 'Create a user')]
class CreateUser extends Command {
public function __construct(private readonly EntityManager $em,
private readonly UserRepository $userRepository)
{
parent::__construct();
}
protected function configure(): void {
$this->addArgument('email', InputArgument::REQUIRED, "The user's email address")
->addArgument('password', InputArgument::REQUIRED, "The user's password");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$email = $input->getArgument('email');
$password = $input->getArgument('password');
$user = $this->userRepository->findOneBy(['email' => $email]);
if ($user !== null) {
$output->writeln('<error>User with email ' . $email . ' already exists.</error>');
return 1;
}
$user = new User('', $email);
$user->updatePassword($password);
$this->userRepository->save($user);
$output->writeln("<info>User $email created!</info>");
return 0;
}
}

View File

@ -2,9 +2,12 @@
use Lewisdale\App\Controllers\FeedController;
use Lewisdale\App\Controllers\HomeController;
use Lewisdale\App\Controllers\LoginController;
use Slim\Views\TwigMiddleware;
ini_set('user_agent', 'Baleen/1.0 (https://baleen.lewisdale.dev)');
ini_set('session.name', 'sessid');
ini_set('session.cookie_samesite', 'Lax');
require_once __DIR__ . "/dependencies.php";
@ -21,10 +24,16 @@ $app->add('csrf');
$app->get("/", [HomeController::class, 'get']);
$app->get('/feed', [FeedController::class, 'get']);
$app->get('/feed', [FeedController::class, 'get'])
->add($container->get(\Lewisdale\App\Session\LoginMiddleware::class));
$app->get('/feed/{id}', [FeedController::class, 'get_feed']);
$app->group('/account', function (\Slim\Routing\RouteCollectorProxy $group) use ($app, $container) {
$group->get('/login', [LoginController::class, 'index']);
$group->post('/login', [LoginController::class, 'login']);
});
$app->addErrorMiddleware(true, true, true);
$app->run();

View File

@ -0,0 +1,16 @@
<h1>Login</h1>
<form method="post">
{{ csrf() | raw }}
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<label for="email">Email address</label>
<input id="email" type="email" name="email" placeholder="Email" required />
<label for="password">Password</label>
<input id="password" type="password" name="password" placeholder="Password" required minlength="8" />
<button type="submit">Login</button>
</form>