Implemented account system

This commit is contained in:
Michel Fedde 2024-07-05 16:50:53 +02:00
parent 51c20b55a0
commit ace0de4063
25 changed files with 1543 additions and 40 deletions

View file

@ -13,4 +13,11 @@ enum LoginMethod: int
self::DISCORD => 'fa-discord',
};
}
public function getHumanReadableName(): string
{
return match ($this) {
self::DISCORD => 'Discord',
};
}
}

View file

@ -5,15 +5,21 @@ namespace GamesShop\Login;
enum UserPermission : int
{
case NONE = 0;
case VIEWER = 1;
case PROVIDER = 10;
case ADMIN = 100;
public function hasLevel(UserPermission $userPermission): bool {
return $this->value >= $userPermission->value;
}
public function getHumanReadableName() {
return match ($this) {
self::VIEWER => "Claimer",
self::PROVIDER => "Provider",
self::ADMIN => "Admin",
default => "None",
};
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing;
use GamesShop\ContainerHandler;
use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission;
use GamesShop\Routing\Responses\TemplateResponse;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\RedirectResponse;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class AdminAccountConfigRoute
{
public function __construct(
private readonly LoginHandler $loginHandler
)
{
}
/**
* @throws ForbiddenException
* @throws UnauthorizedException
*/
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException();
}
$user = $this->loginHandler->getCurrentUser();
if ($user->getPermission()->value < UserPermission::ADMIN->value) {
throw new ForbiddenException();
}
return new TemplateResponse('admin/accounts');
}
public static function applyRoutes(\League\Route\Router $router) {
$router->get('/accounts', self::class);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api;
use GamesShop\Routing\Api\DataTables\DataTablesAPIRoutes;
use GamesShop\Routing\Api\Web\WebAPIRoutes;
use League\Route\Router;
final class APIRoutes
{
public static function applyRoutes(Router $router) {
$router->group('/api/dt', DataTablesAPIRoutes::setupRoutes(...));
$router->group('/api/web', WebAPIRoutes::applyRoutes(...));
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api\DataTables;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\ORM\EntityManager;
use GamesShop\Entities\Account\User;
use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\JsonResponse;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException;
use League\Route\RouteGroup;
use League\Route\Router;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class AccountsEndpoint
{
public function __construct(
private readonly LoginHandler $loginHandler,
private readonly EntityManager $entityManager,
)
{
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException();
}
$user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::ADMIN)) {
throw new ForbiddenException();
}
$params = $request->getQueryParams();
$draw = $params['draw'];
$start = $params['start'];
$length = $params['length'];
$searchValue = $params['search']['value'];
$repo = $this->entityManager->getRepository(User::class);
$total = $repo->count();
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->contains('name', $searchValue));
$criteria->setFirstResult((int)$start);
$criteria->setMaxResults((int)$length);
$values = $repo->matching($criteria);
$filteredCount = $values->count();
return new JsonResponse([
'draw' => $draw,
'recordsTotal' => $total,
'recordsFiltered' => $filteredCount,
'data' =>
$values->map(function (User $user) {
return [
'userid' => $user->getId(),
'name' => $user->getName(),
'profilePictureUrl' => $user->getProfilePictureUrl(),
'permission' => $user->getPermission()->getHumanReadableName(),
'permissionIndex' => $user->getPermission()->value,
'loginMethod' => $user->getLoginMethod()->getHumanReadableName(),
];
})->toArray()
]);
}
public static function applyRoutes(RouteGroup $router) {
$router->get('/accounts', AccountsEndpoint::class);
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api\DataTables;
use League\Route\RouteGroup;
use League\Route\Router;
final class DataTablesAPIRoutes
{
public static function setupRoutes(RouteGroup $group): void {
AccountsEndpoint::applyRoutes($group);
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api\Web;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use GamesShop\Entities\Account\User;
use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\EmptyResponse;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final readonly class UserModifyRoute
{
public function __construct(
private LoginHandler $loginHandler,
private EntityManager $entityManager,
)
{
}
public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface
{
if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException();
}
$user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::ADMIN)) {
throw new ForbiddenException();
}
$permissions = $request->getParsedBody()['permission'];
$toChangeUser = $this->entityManager->getRepository(User::class)->find((int)$args['id']);
$toChangeUser->setPermission(UserPermission::from((int)$permissions));
$this->entityManager->flush();
return new EmptyResponse(200);
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api\Web;
use League\Route\RouteGroup;
final class WebAPIRoutes
{
public static function applyRoutes(RouteGroup $group): void {
$group->post('/users/{id:number}', UserModifyRoute::class);
}
}

View file

@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface;
final class ErrorRoute
{
public function renderErrorPage(int $errorCode): ResponseInterface {
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage('error', [ 'errorCode' => $errorCode ]);
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderErrorPage($errorCode);
$response = new Response;
$response->getBody()->write($pageContent);

View file

@ -9,6 +9,7 @@ use Doctrine\ORM\OptimisticLockException;
use GamesShop\ContainerHandler;
use GamesShop\Environment\EnvironmentHandler;
use GamesShop\Login\LoginHandler;
use GamesShop\Routing\Responses\TemplateResponse;
use GamesShop\Templates\TemplateEngine;
use Laminas\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
@ -24,16 +25,10 @@ final class LoginRoutes
public function login(ServerRequestInterface $request) {
$discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment();
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage(
'login',
[
'discordUrl' => $discordEnv->loginUrl
]
);
$response = new Response;
$response->getBody()->write($pageContent);
return $response;
return new TemplateResponse('login', [
'discordUrl' => $discordEnv->loginUrl
]);
}
/**

View file

@ -6,9 +6,9 @@ namespace GamesShop\Routing;
use GamesShop\ContainerHandler;
use GamesShop\Paths;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Uri;
use Mimey\MimeTypes;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class ResourceRoute
{
@ -19,9 +19,20 @@ final class ResourceRoute
'gif', 'svg', 'png', 'jpg'
];
public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface {
public function isValid(Uri $uri): bool {
$path = $uri->getPath();
foreach (self::RESOURCE_EXTENSIONS as $extension) {
if (!str_ends_with($path, $extension)) {
continue;
}
$filePath = Paths::PUBLIC_PATH . $request->getUri()->getPath();
return true;
}
return false;
}
public function getResponse(Uri $uri): ResponseInterface {
$filePath = Paths::PUBLIC_PATH . $uri->getPath();
if (!file_exists($filePath)) {
$response = new Response(status: 404);
@ -41,11 +52,4 @@ final class ResourceRoute
return $response;
}
public static function addRouteEntry(\League\Route\Router $router): void {
$joinedResourceExtensions = implode('|', self::RESOURCE_EXTENSIONS);
$router->addPatternMatcher('resource', ".+[{$joinedResourceExtensions}]");
$router->get('/{resource:resource}', self::class);
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Responses;
use GamesShop\ContainerHandler;
use GamesShop\Templates\TemplateEngine;
use Laminas\Diactoros\Response;
final class TemplateResponse extends Response
{
public function __construct(string $templateName, array $data = [], array $headers = [])
{
parent::__construct('php://memory', 200, $headers);
$templateEngine = ContainerHandler::get(TemplateEngine::class);
$body = $templateEngine->renderPage($templateName, $data);
$this->getBody()->write($body);
}
}

View file

@ -5,22 +5,36 @@ namespace GamesShop\Routing;
use GamesShop\ContainerHandler;
use GamesShop\Login\LoginHandler;
use GamesShop\Routing\Api\APIRoutes;
use GamesShop\Templates\TemplateEngine;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use League\Container\Container;
use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\NotFoundException;
use League\Route\Http\Exception\UnauthorizedException;
use League\Route\Strategy\ApplicationStrategy;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class Router
{
public function __construct(
private ResourceRoute $resourceRoute
)
{
}
public function route(): ResponseInterface
{
$request = ServerRequestFactory::fromGlobals(
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
);
if ($this->resourceRoute->isValid($request->getUri())) {
return $this->resourceRoute->getResponse($request->getUri());
}
$router = new \League\Route\Router;
$strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance());
$router->setStrategy($strategy);
@ -28,12 +42,18 @@ final class Router
IndexRoute::applyRoutes($router);
LoginRoutes::addRoutes($router);
SetupRoute::applyRoutes($router);
ResourceRoute::addRouteEntry($router);
AdminAccountConfigRoute::applyRoutes($router);
APIRoutes::applyRoutes($router);
try {
return $router->dispatch($request);
} catch (NotFoundException $e) {
return (new ErrorRoute())->renderErrorPage(404);
} catch (UnauthorizedException) {
return (new ErrorRoute())->renderErrorPage(401);
} catch (ForbiddenException) {
return (new ErrorRoute())->renderErrorPage(403);
}
}
}

View file

@ -29,7 +29,6 @@ final class SetupRoute
$attribute = $repo->find('ADMIN_SETUP_COMPLETED');
if ($attribute) {
return new RedirectResponse('/');
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace GamesShop\Templates;
use GamesShop\Login\UserPermission;
final class NavigationHeader
{
public function __construct(
public readonly string $title,
public readonly string $link,
public readonly UserPermission $minimumPermission,
)
{
}
}

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace GamesShop\Templates;
use GamesShop\Login\LoginHandler;
use GamesShop\Paths;
use League\Plates\Engine;
@ -12,11 +13,13 @@ final class TemplateEngine extends Engine
public function __construct(
private ResourceIndex $resourceIndex,
LoginHandler $loginHandler,
)
{
parent::__construct(self::TEMPLATES_PATH, 'php');
$this->addData([
'resources' => $this->resourceIndex,
'activeUser' => $loginHandler->isLoggedIn() ? $loginHandler->getCurrentUser() : null,
]);
}
@ -24,4 +27,8 @@ final class TemplateEngine extends Engine
{
return parent::render("pages/$page", $data);
}
public function renderErrorPage(int $error) {
return self::renderPage('error', [ 'errorCode' => $error ]);
}
}