Implemented account system
This commit is contained in:
parent
51c20b55a0
commit
ace0de4063
25 changed files with 1543 additions and 40 deletions
1025
package-lock.json
generated
1025
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,12 +13,15 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@types/bootstrap": "^5.2.10",
|
"@types/bootstrap": "^5.2.10",
|
||||||
"bootstrap": "^5.3.3"
|
"bootstrap": "^5.3.3",
|
||||||
|
"datatables.net-bs5": "^2.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jquery": "^3.5.30",
|
||||||
"assets-webpack-plugin": "^7.1.1",
|
"assets-webpack-plugin": "^7.1.1",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
"mini-css-extract-plugin": "^2.9.0",
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
"resolve-url-loader": "^5.0.0",
|
"resolve-url-loader": "^5.0.0",
|
||||||
"sass": "^1.77.5",
|
"sass": "^1.77.5",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"index":{"js":["js/runtime.js","js/index.js"],"css":"css/common.css"}}
|
{"index":{"js":["js/runtime.js","js/vendors.js","js/index.js"],"css":["css/vendors.css","css/common.css"]},"admin/accounts":{"js":["js/runtime.js","js/vendors.js","js/admin/accounts.js"],"css":["css/vendors.css","css/common.css"]}}
|
102
src/js/pages/admin/accounts.ts
Normal file
102
src/js/pages/admin/accounts.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import '../../../css/common/index.scss';
|
||||||
|
import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
|
||||||
|
import '../../common/index';
|
||||||
|
|
||||||
|
import DataTable from 'datatables.net-bs5';
|
||||||
|
import {Modal} from "bootstrap";
|
||||||
|
|
||||||
|
const DT_SERVERSIDE_PROCESSING_URL = '/api/dt/accounts';
|
||||||
|
const TABLE = document.querySelector('#user-table');
|
||||||
|
|
||||||
|
function displayEdit(data: any) {
|
||||||
|
|
||||||
|
const modalElem = document.querySelector('#edit-modal');
|
||||||
|
// @ts-ignore
|
||||||
|
modalElem.querySelector('.name-input').textContent = data.name;
|
||||||
|
// @ts-ignore
|
||||||
|
modalElem.querySelector('.login-method').textContent = data.loginMethod;
|
||||||
|
// @ts-ignore
|
||||||
|
modalElem.querySelector('.permission-editor').value = data.permissionIndex;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
modalElem?.querySelector('.js--save').dataset.userid = data.userid;
|
||||||
|
|
||||||
|
Modal.getOrCreateInstance('#edit-modal').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const dt = new DataTable('#user-table', {
|
||||||
|
ajax: {
|
||||||
|
url: DT_SERVERSIDE_PROCESSING_URL,
|
||||||
|
},
|
||||||
|
serverSide: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
data: 'profilePictureUrl',
|
||||||
|
render(data, type) {
|
||||||
|
if (type !== 'display') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<img src="${ data }" alt="Profile Picture" class="ratio-1 rounded-circle w-100" />`
|
||||||
|
},
|
||||||
|
orderable: false,
|
||||||
|
searchable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Name',
|
||||||
|
data: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Permission',
|
||||||
|
data: 'permission'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Login-Method',
|
||||||
|
data: 'loginMethod'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [ [1, 'asc'] ],
|
||||||
|
drawCallback: function (settings) {
|
||||||
|
const api = new DataTable.Api(settings);
|
||||||
|
api.rows().every(function (row) {
|
||||||
|
const node = this.node();
|
||||||
|
const data = this.data();
|
||||||
|
|
||||||
|
node.addEventListener('click', (e) => {
|
||||||
|
displayEdit(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const button = <HTMLButtonElement>document.querySelector('#edit-modal .js--save');
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', async (e) => {
|
||||||
|
const permissionEditor = <HTMLSelectElement>document.querySelector('#edit-modal .permission-editor');
|
||||||
|
if (!permissionEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.set('permission', permissionEditor.value);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/web/users/${button.dataset.userid}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.getOrCreateInstance('#edit-modal').hide();
|
||||||
|
dt.draw();
|
||||||
|
})
|
||||||
|
})
|
|
@ -13,4 +13,11 @@ enum LoginMethod: int
|
||||||
self::DISCORD => 'fa-discord',
|
self::DISCORD => 'fa-discord',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getHumanReadableName(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::DISCORD => 'Discord',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,21 @@ namespace GamesShop\Login;
|
||||||
|
|
||||||
enum UserPermission : int
|
enum UserPermission : int
|
||||||
{
|
{
|
||||||
|
case NONE = 0;
|
||||||
case VIEWER = 1;
|
case VIEWER = 1;
|
||||||
case PROVIDER = 10;
|
case PROVIDER = 10;
|
||||||
case ADMIN = 100;
|
case ADMIN = 100;
|
||||||
|
|
||||||
|
public function hasLevel(UserPermission $userPermission): bool {
|
||||||
|
return $this->value >= $userPermission->value;
|
||||||
|
}
|
||||||
|
|
||||||
public function getHumanReadableName() {
|
public function getHumanReadableName() {
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::VIEWER => "Claimer",
|
self::VIEWER => "Claimer",
|
||||||
self::PROVIDER => "Provider",
|
self::PROVIDER => "Provider",
|
||||||
self::ADMIN => "Admin",
|
self::ADMIN => "Admin",
|
||||||
|
default => "None",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
src/php/Routing/AdminAccountConfigRoute.php
Normal file
49
src/php/Routing/AdminAccountConfigRoute.php
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
src/php/Routing/Api/APIRoutes.php
Normal file
16
src/php/Routing/Api/APIRoutes.php
Normal 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(...));
|
||||||
|
}
|
||||||
|
}
|
80
src/php/Routing/Api/DataTables/AccountsEndpoint.php
Normal file
80
src/php/Routing/Api/DataTables/AccountsEndpoint.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
14
src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php
Normal file
14
src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
46
src/php/Routing/Api/Web/UserModifyRoute.php
Normal file
46
src/php/Routing/Api/Web/UserModifyRoute.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
13
src/php/Routing/Api/Web/WebAPIRoutes.php
Normal file
13
src/php/Routing/Api/Web/WebAPIRoutes.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||||
final class ErrorRoute
|
final class ErrorRoute
|
||||||
{
|
{
|
||||||
public function renderErrorPage(int $errorCode): ResponseInterface {
|
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 = new Response;
|
||||||
$response->getBody()->write($pageContent);
|
$response->getBody()->write($pageContent);
|
||||||
|
|
|
@ -9,6 +9,7 @@ use Doctrine\ORM\OptimisticLockException;
|
||||||
use GamesShop\ContainerHandler;
|
use GamesShop\ContainerHandler;
|
||||||
use GamesShop\Environment\EnvironmentHandler;
|
use GamesShop\Environment\EnvironmentHandler;
|
||||||
use GamesShop\Login\LoginHandler;
|
use GamesShop\Login\LoginHandler;
|
||||||
|
use GamesShop\Routing\Responses\TemplateResponse;
|
||||||
use GamesShop\Templates\TemplateEngine;
|
use GamesShop\Templates\TemplateEngine;
|
||||||
use Laminas\Diactoros\Response;
|
use Laminas\Diactoros\Response;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
@ -24,16 +25,10 @@ final class LoginRoutes
|
||||||
|
|
||||||
public function login(ServerRequestInterface $request) {
|
public function login(ServerRequestInterface $request) {
|
||||||
$discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment();
|
$discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment();
|
||||||
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage(
|
|
||||||
'login',
|
|
||||||
[
|
|
||||||
'discordUrl' => $discordEnv->loginUrl
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = new Response;
|
return new TemplateResponse('login', [
|
||||||
$response->getBody()->write($pageContent);
|
'discordUrl' => $discordEnv->loginUrl
|
||||||
return $response;
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,9 +6,9 @@ namespace GamesShop\Routing;
|
||||||
use GamesShop\ContainerHandler;
|
use GamesShop\ContainerHandler;
|
||||||
use GamesShop\Paths;
|
use GamesShop\Paths;
|
||||||
use Laminas\Diactoros\Response;
|
use Laminas\Diactoros\Response;
|
||||||
|
use Laminas\Diactoros\Uri;
|
||||||
use Mimey\MimeTypes;
|
use Mimey\MimeTypes;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
|
|
||||||
final class ResourceRoute
|
final class ResourceRoute
|
||||||
{
|
{
|
||||||
|
@ -19,9 +19,20 @@ final class ResourceRoute
|
||||||
'gif', 'svg', 'png', 'jpg'
|
'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)) {
|
if (!file_exists($filePath)) {
|
||||||
$response = new Response(status: 404);
|
$response = new Response(status: 404);
|
||||||
|
@ -41,11 +52,4 @@ final class ResourceRoute
|
||||||
|
|
||||||
return $response;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
20
src/php/Routing/Responses/TemplateResponse.php
Normal file
20
src/php/Routing/Responses/TemplateResponse.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,22 +5,36 @@ namespace GamesShop\Routing;
|
||||||
|
|
||||||
use GamesShop\ContainerHandler;
|
use GamesShop\ContainerHandler;
|
||||||
use GamesShop\Login\LoginHandler;
|
use GamesShop\Login\LoginHandler;
|
||||||
|
use GamesShop\Routing\Api\APIRoutes;
|
||||||
use GamesShop\Templates\TemplateEngine;
|
use GamesShop\Templates\TemplateEngine;
|
||||||
use Laminas\Diactoros\Response;
|
use Laminas\Diactoros\Response;
|
||||||
use Laminas\Diactoros\ServerRequestFactory;
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
use League\Container\Container;
|
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\NotFoundException;
|
||||||
|
use League\Route\Http\Exception\UnauthorizedException;
|
||||||
use League\Route\Strategy\ApplicationStrategy;
|
use League\Route\Strategy\ApplicationStrategy;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
final class Router
|
final class Router
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ResourceRoute $resourceRoute
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function route(): ResponseInterface
|
public function route(): ResponseInterface
|
||||||
{
|
{
|
||||||
$request = ServerRequestFactory::fromGlobals(
|
$request = ServerRequestFactory::fromGlobals(
|
||||||
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
|
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
|
||||||
);
|
);
|
||||||
|
if ($this->resourceRoute->isValid($request->getUri())) {
|
||||||
|
return $this->resourceRoute->getResponse($request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
$router = new \League\Route\Router;
|
$router = new \League\Route\Router;
|
||||||
$strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance());
|
$strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance());
|
||||||
$router->setStrategy($strategy);
|
$router->setStrategy($strategy);
|
||||||
|
@ -28,12 +42,18 @@ final class Router
|
||||||
IndexRoute::applyRoutes($router);
|
IndexRoute::applyRoutes($router);
|
||||||
LoginRoutes::addRoutes($router);
|
LoginRoutes::addRoutes($router);
|
||||||
SetupRoute::applyRoutes($router);
|
SetupRoute::applyRoutes($router);
|
||||||
ResourceRoute::addRouteEntry($router);
|
AdminAccountConfigRoute::applyRoutes($router);
|
||||||
|
|
||||||
|
APIRoutes::applyRoutes($router);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $router->dispatch($request);
|
return $router->dispatch($request);
|
||||||
} catch (NotFoundException $e) {
|
} catch (NotFoundException $e) {
|
||||||
return (new ErrorRoute())->renderErrorPage(404);
|
return (new ErrorRoute())->renderErrorPage(404);
|
||||||
|
} catch (UnauthorizedException) {
|
||||||
|
return (new ErrorRoute())->renderErrorPage(401);
|
||||||
|
} catch (ForbiddenException) {
|
||||||
|
return (new ErrorRoute())->renderErrorPage(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -29,7 +29,6 @@ final class SetupRoute
|
||||||
|
|
||||||
$attribute = $repo->find('ADMIN_SETUP_COMPLETED');
|
$attribute = $repo->find('ADMIN_SETUP_COMPLETED');
|
||||||
if ($attribute) {
|
if ($attribute) {
|
||||||
|
|
||||||
return new RedirectResponse('/');
|
return new RedirectResponse('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
src/php/Templates/NavigationHeader.php
Normal file
17
src/php/Templates/NavigationHeader.php
Normal 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,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace GamesShop\Templates;
|
namespace GamesShop\Templates;
|
||||||
|
|
||||||
|
use GamesShop\Login\LoginHandler;
|
||||||
use GamesShop\Paths;
|
use GamesShop\Paths;
|
||||||
use League\Plates\Engine;
|
use League\Plates\Engine;
|
||||||
|
|
||||||
|
@ -12,11 +13,13 @@ final class TemplateEngine extends Engine
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ResourceIndex $resourceIndex,
|
private ResourceIndex $resourceIndex,
|
||||||
|
LoginHandler $loginHandler,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
parent::__construct(self::TEMPLATES_PATH, 'php');
|
parent::__construct(self::TEMPLATES_PATH, 'php');
|
||||||
$this->addData([
|
$this->addData([
|
||||||
'resources' => $this->resourceIndex,
|
'resources' => $this->resourceIndex,
|
||||||
|
'activeUser' => $loginHandler->isLoggedIn() ? $loginHandler->getCurrentUser() : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,4 +27,8 @@ final class TemplateEngine extends Engine
|
||||||
{
|
{
|
||||||
return parent::render("pages/$page", $data);
|
return parent::render("pages/$page", $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function renderErrorPage(int $error) {
|
||||||
|
return self::renderPage('error', [ 'errorCode' => $error ]);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,31 +1,29 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use GamesShop\ContainerHandler;
|
use GamesShop\Entities\Account\User;
|
||||||
use GamesShop\Login\LoginHandler;
|
|
||||||
|
/** @var User|null $activeUser */
|
||||||
|
|
||||||
$loginHandler = ContainerHandler::get(LoginHandler::class);
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if ($loginHandler->isLoggedIn()):
|
<?php if ($activeUser !== null): ?>
|
||||||
$user = $loginHandler->getCurrentUser();
|
|
||||||
?>
|
|
||||||
<div class="d-flex avatar justify-content-center">
|
<div class="d-flex avatar justify-content-center">
|
||||||
|
|
||||||
<div class="avatar-icon h-100 position-relative me-2 ratio-1">
|
<div class="avatar-icon h-100 position-relative me-2 ratio-1">
|
||||||
<img src="<?= $user->getProfilePictureUrl(); ?>" class="rounded-circle h-100" alt="User Profile Picture" />
|
<img src="<?= $activeUser->getProfilePictureUrl(); ?>" class="rounded-circle h-100" alt="User Profile Picture" />
|
||||||
<div class="position-absolute bottom-0 end-0 ratio-1 d-flex align-items-center z-1 avatar-login-method">
|
<div class="position-absolute bottom-0 end-0 ratio-1 d-flex align-items-center z-1 avatar-login-method">
|
||||||
<i class="fa-brands <?= $user->getLoginMethod()->getIconClass() ?>"></i>
|
<i class="fa-brands <?= $activeUser->getLoginMethod()->getIconClass() ?>"></i>
|
||||||
<span class="position-absolute w-100 h-100 bottom-0 end-0 ratio-1 bg-body rounded-circle z-n1 avatar-login-method-icon"></span>
|
<span class="position-absolute w-100 h-100 bottom-0 end-0 ratio-1 bg-body rounded-circle z-n1 avatar-login-method-icon"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<span class="me-2 h-3">
|
<span class="me-2 h-3">
|
||||||
<?= $user->getName() ?>
|
<?= $activeUser->getName() ?>
|
||||||
</span>
|
</span>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<?= $user->getPermission()->getHumanReadableName() ?>
|
<?= $activeUser->getPermission()->getHumanReadableName() ?>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-100 d-flex align-items-center ms-2">
|
<div class="h-100 d-flex align-items-center ms-2">
|
||||||
|
|
|
@ -39,5 +39,7 @@ $resource = $resources->getResource($resourceEntry);
|
||||||
<?= $this->section('content'); ?>
|
<?= $this->section('content'); ?>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<?= $this->section('modal') ?>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use GamesShop\ContainerHandler;
|
use GamesShop\Entities\Account\User;
|
||||||
use GamesShop\Login\LoginHandler;
|
use GamesShop\Login\UserPermission;
|
||||||
|
use GamesShop\Templates\NavigationHeader;
|
||||||
|
|
||||||
ContainerHandler::get(LoginHandler::class);
|
$headers = [
|
||||||
|
new NavigationHeader('My Keys', '/keys', UserPermission::PROVIDER),
|
||||||
|
new NavigationHeader('Accounts', '/accounts', UserPermission::ADMIN)
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var User|null $activeUser */
|
||||||
|
$currentPermission = $activeUser === null ? UserPermission::NONE : $activeUser->getPermission();
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
@ -15,7 +22,19 @@ ContainerHandler::get(LoginHandler::class);
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbar-content">
|
<div class="collapse navbar-collapse" id="navbar-content">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<?php foreach ($headers as $header):
|
||||||
|
if (!$currentPermission->hasLevel($header->minimumPermission)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<li class="nav-link">
|
||||||
|
<a href="<?= $header->link ?>" class="nav-link"><?= $header->title ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<?= $this->insert('layout/accountDisplay'); ?>
|
<?= $this->insert('layout/accountDisplay'); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
57
src/templates/pages/admin/accounts.php
Normal file
57
src/templates/pages/admin/accounts.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use GamesShop\Login\UserPermission;
|
||||||
|
|
||||||
|
$this->layout('layout/main', [ 'resourceEntry' => 'admin/accounts' ]);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h1>Users</h1>
|
||||||
|
|
||||||
|
<table id="user-table" class="table table-striped w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="2.4rem"></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Permission</th>
|
||||||
|
<th>Login-Method</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php $this->start('modal') ?>
|
||||||
|
<div class="modal" id="edit-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title h3">
|
||||||
|
Edit User
|
||||||
|
</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
Name: <span class="name-input"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
Login Method: <span class="login-method"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="permissions">Permissions:</label>
|
||||||
|
<select name="" id="permissions" class="form-select permission-editor">
|
||||||
|
<?php foreach (UserPermission::cases() as $userPermission):?>
|
||||||
|
<option value="<?= $userPermission->value ?>"><?= $userPermission->getHumanReadableName() ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary js--save">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php $this->end() ?>
|
|
@ -2,6 +2,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'),
|
||||||
Path = require('path'),
|
Path = require('path'),
|
||||||
AssetsPlugin = require('assets-webpack-plugin'),
|
AssetsPlugin = require('assets-webpack-plugin'),
|
||||||
CopyPlugin = require('copy-webpack-plugin');
|
CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
||||||
|
|
||||||
const PUBLIC_FOLDER = Path.resolve(__dirname, 'public'),
|
const PUBLIC_FOLDER = Path.resolve(__dirname, 'public'),
|
||||||
SOURCE_FOLDER = Path.resolve(__dirname, 'src'),
|
SOURCE_FOLDER = Path.resolve(__dirname, 'src'),
|
||||||
|
@ -38,6 +39,10 @@ module.exports = {
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
optimization: {
|
optimization: {
|
||||||
runtimeChunk: "single",
|
runtimeChunk: "single",
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [
|
||||||
|
new CssMinimizerPlugin()
|
||||||
|
],
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
cacheGroups: {
|
cacheGroups: {
|
||||||
vendor: {
|
vendor: {
|
||||||
|
@ -86,7 +91,8 @@ module.exports = {
|
||||||
extensions: ['.js', '.ts'],
|
extensions: ['.js', '.ts'],
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
index: JS_FOLDER + "/pages/index"
|
index: JS_FOLDER + "/pages/index",
|
||||||
|
'admin/accounts': JS_FOLDER + "/pages/admin/accounts",
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: PUBLIC_FOLDER,
|
path: PUBLIC_FOLDER,
|
||||||
|
|
Loading…
Reference in a new issue