implermented key import
This commit is contained in:
parent
ace0de4063
commit
74e1b25fcf
20 changed files with 1035 additions and 12 deletions
|
@ -1 +1 @@
|
|||
{"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"]}}
|
||||
{"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"]},"keys":{"js":["js/runtime.js","js/vendors.js","js/keys.js"],"css":["css/vendors.css","css/common.css"]}}
|
155
src/js/pages/keys.ts
Normal file
155
src/js/pages/keys.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import '../../css/common/index.scss';
|
||||
import '../common/index';
|
||||
|
||||
import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
|
||||
|
||||
import { Tab } from 'bootstrap';
|
||||
|
||||
async function checkImportFile() {
|
||||
const fileInput = document.querySelector<HTMLInputElement>('#import');
|
||||
|
||||
if (!fileInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileInput.files || !fileInput.files[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.set('file', fileInput.files[0]);
|
||||
|
||||
const response = await fetch(
|
||||
`/api/web/keys/import/prepare`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const jsonData = await response.json();
|
||||
|
||||
const container = document.getElementById('import-info-container');
|
||||
if (!container) {
|
||||
return
|
||||
}
|
||||
|
||||
container.classList.remove('d-none');
|
||||
createTableContents(jsonData);
|
||||
}
|
||||
|
||||
function createTableContents(data: {index: string, displayName: string, guessedAttribute: string}[]) {
|
||||
const table = document.querySelector<HTMLTableElement>('#import-attribute-table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tbody = table.querySelector<HTMLTableSectionElement>('tbody');
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let child of tbody.children) {
|
||||
child.remove();
|
||||
}
|
||||
|
||||
const keyAttributeMeta = document.querySelector<HTMLMetaElement>('meta[name="key-attributes"]')?.content;
|
||||
const attributes = JSON.parse(keyAttributeMeta ?? '');
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.classList.add('form-select', 'w-100');
|
||||
for (let attributeName in attributes) {
|
||||
const attributeValue = attributes[attributeName];
|
||||
const option = document.createElement('option');
|
||||
option.value = attributeValue;
|
||||
option.textContent = attributeName;
|
||||
|
||||
select.add(option);
|
||||
}
|
||||
|
||||
data.forEach((datum) => {
|
||||
const row = tbody.insertRow();
|
||||
const indexCell = row.insertCell();
|
||||
indexCell.classList.add('index-cell');
|
||||
indexCell.innerText = datum.index;
|
||||
|
||||
const displayNameCEll = row.insertCell();
|
||||
displayNameCEll.classList.add('name-cell');
|
||||
displayNameCEll.innerText = datum.displayName;
|
||||
|
||||
const attributeCell = row.insertCell();
|
||||
attributeCell.classList.add('attribute-cell');
|
||||
const rowSelect = <HTMLSelectElement>select.cloneNode(true);
|
||||
rowSelect.value = datum.guessedAttribute;
|
||||
attributeCell.appendChild(rowSelect);
|
||||
})
|
||||
}
|
||||
|
||||
async function doImport() {
|
||||
const fileInput = document.querySelector<HTMLInputElement>('#import');
|
||||
|
||||
if (!fileInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileInput.files || !fileInput.files[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.set('file', fileInput.files[0]);
|
||||
|
||||
const tbody = document.querySelector<HTMLTableSectionElement>('#import-attribute-table tbody');
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let row of tbody.querySelectorAll('tr')) {
|
||||
const columnIndex = row.querySelector('.index-cell')?.textContent;
|
||||
const attribute = row.querySelector<HTMLSelectElement>('.attribute-cell select')?.value ?? 'none';
|
||||
|
||||
formData.set(`columns[${columnIndex}]`, attribute);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/web/keys/import/perform`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const container = document.getElementById('import-info-container');
|
||||
if (!container) {
|
||||
return
|
||||
}
|
||||
|
||||
container.classList.add('d-none');
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const triggerTabList = document.querySelectorAll('#key-tab button')
|
||||
triggerTabList.forEach(triggerEl => {
|
||||
const tabTrigger = new Tab(triggerEl)
|
||||
|
||||
triggerEl.addEventListener('click', event => {
|
||||
event.preventDefault()
|
||||
tabTrigger.show()
|
||||
})
|
||||
})
|
||||
|
||||
const importButton = document.querySelector('.js--send-import');
|
||||
importButton?.addEventListener('click', checkImportFile);
|
||||
|
||||
const doImportButton = document.querySelector('.js--do-import');
|
||||
doImportButton?.addEventListener('click', doImport);
|
||||
})
|
|
@ -6,6 +6,7 @@ namespace GamesShop;
|
|||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\ORMSetup;
|
||||
use GamesShop\Environment\EnvironmentHandler;
|
||||
|
||||
|
@ -27,6 +28,7 @@ final class DoctrineManager
|
|||
$entityManager = new EntityManager($connection, $config);
|
||||
|
||||
$container->addShared(EntityManager::class, $entityManager);
|
||||
$container->addShared(EntityManagerInterface::class, $entityManager);
|
||||
$container->addShared(Connection::class, $connection);
|
||||
}
|
||||
|
||||
|
|
26
src/php/Entities/Games/Game.php
Normal file
26
src/php/Entities/Games/Game.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Entities\Games;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'games')]
|
||||
final class Game
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer', options: ['unsigned' => true])]
|
||||
#[ORM\GeneratedValue]
|
||||
private int|null $id;
|
||||
#[ORM\Column]
|
||||
private string $name;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
39
src/php/Entities/Games/Key.php
Normal file
39
src/php/Entities/Games/Key.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Entities\Games;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use GamesShop\Entities\Account\User;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'keys')]
|
||||
final class Key
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer', options: ['unsigned' => true])]
|
||||
#[ORM\GeneratedValue]
|
||||
private int|null $id;
|
||||
#[ORM\ManyToOne]
|
||||
private Game $game;
|
||||
#[ORM\ManyToOne]
|
||||
private User $contributedUser;
|
||||
#[ORM\Column]
|
||||
private string $key;
|
||||
#[ORM\Column(type: 'string', enumType: Store::class)]
|
||||
private Store $store;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private string|null $storeLink;
|
||||
#[ORM\Column]
|
||||
private string|null $fromWhere;
|
||||
|
||||
public function __construct(Game $game, User $contributedUser, string $key, Store $store, ?string $storeLink, ?string $fromWhere)
|
||||
{
|
||||
$this->game = $game;
|
||||
$this->contributedUser = $contributedUser;
|
||||
$this->key = $key;
|
||||
$this->store = $store;
|
||||
$this->storeLink = $storeLink;
|
||||
$this->fromWhere = $fromWhere;
|
||||
}
|
||||
}
|
22
src/php/Entities/Games/KeyAttribute.php
Normal file
22
src/php/Entities/Games/KeyAttribute.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Entities\Games;
|
||||
|
||||
enum KeyAttribute: string
|
||||
{
|
||||
case NONE = 'none';
|
||||
case GAME_NAME = "game_name";
|
||||
case KEY = "key";
|
||||
case STORE = 'store';
|
||||
case FROM = 'from';
|
||||
|
||||
public static function casesAsAssociative(): array {
|
||||
$result = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$result[$case->name] = $case->value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
15
src/php/Entities/Games/Store.php
Normal file
15
src/php/Entities/Games/Store.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Entities\Games;
|
||||
|
||||
enum Store: string
|
||||
{
|
||||
case STEAM = 'steam';
|
||||
case GOG = 'gog';
|
||||
case EPICGAMES = 'epicgames';
|
||||
case ORIGIN = 'origin';
|
||||
case UPLAY = 'uplay';
|
||||
case BATTLENET = 'battlenet';
|
||||
case EXTERNAL = 'external';
|
||||
}
|
165
src/php/Importer/GameImporter.php
Normal file
165
src/php/Importer/GameImporter.php
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Importer;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use GamesShop\Entities\Account\User;
|
||||
use GamesShop\Entities\Games\Game;
|
||||
use GamesShop\Entities\Games\Key;
|
||||
use GamesShop\Entities\Games\KeyAttribute;
|
||||
use GamesShop\Entities\Games\Store;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
|
||||
final class GameImporter
|
||||
{
|
||||
private const HEADER_ROW_INDEX = 1;
|
||||
|
||||
private const STORE_ADDITIONAL_CASES = [
|
||||
'epic' => Store::EPICGAMES,
|
||||
'ea' => Store::ORIGIN,
|
||||
'eaplay' => Store::ORIGIN,
|
||||
'ubisoft' => Store::UPLAY,
|
||||
'activision' => Store::BATTLENET
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return ImportColumnInterpretation[]
|
||||
*/
|
||||
public function interpret(string $path): array {
|
||||
$spreadsheet = IOFactory::load($path);
|
||||
|
||||
$worksheet = $spreadsheet->getSheet(0);
|
||||
$result = [];
|
||||
foreach ($worksheet->getColumnIterator() as $column) {
|
||||
$columnIndex = $column->getColumnIndex();
|
||||
|
||||
$value = $worksheet->getCell(sprintf('%s%d', $columnIndex, self::HEADER_ROW_INDEX))->getValueString();
|
||||
|
||||
if (empty(trim($value))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$guessedAttribute = $this->guessAttribute($value);
|
||||
$result[] = new ImportColumnInterpretation(
|
||||
$columnIndex,
|
||||
$value,
|
||||
$guessedAttribute
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function guessAttribute(string $value): KeyAttribute|null {
|
||||
$value = trim($value);
|
||||
$value = strtolower($value);
|
||||
$value = str_replace(' ', '_', $value);
|
||||
|
||||
$attribute = match($value) {
|
||||
'key' => KeyAttribute::KEY,
|
||||
'name', 'game_name', 'game' => KeyAttribute::GAME_NAME,
|
||||
'from' => KeyAttribute::FROM,
|
||||
'store', 'for' => KeyAttribute::STORE,
|
||||
default => null
|
||||
};
|
||||
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columnDefinitions
|
||||
*/
|
||||
public function import(string $path, array $columnDefinitions, User $contributedUser): array {
|
||||
$spreadsheet = IOFactory::load($path);
|
||||
|
||||
$worksheet = $spreadsheet->getSheet(0);
|
||||
|
||||
$totalRows = 0;
|
||||
$addedAmount = 0;
|
||||
|
||||
foreach ($worksheet->getRowIterator(self::HEADER_ROW_INDEX + 1) as $row) {
|
||||
$totalRows++;
|
||||
$values = [
|
||||
'name' => null,
|
||||
'key' => null,
|
||||
'from' => null,
|
||||
'store' => null,
|
||||
'store_link' => null
|
||||
];
|
||||
|
||||
foreach ($columnDefinitions as $columnIndex => $attribute) {
|
||||
$value = $worksheet->getCell(sprintf('%s%d', $columnIndex, $row->getRowIndex()))->getValueString();
|
||||
|
||||
switch(KeyAttribute::from($attribute)) {
|
||||
case KeyAttribute::NONE:
|
||||
break;
|
||||
case KeyAttribute::GAME_NAME:
|
||||
$values['name'] = $value;
|
||||
break;
|
||||
case KeyAttribute::KEY:
|
||||
$values['key'] = $value;
|
||||
break;
|
||||
case KeyAttribute::FROM:
|
||||
$values['from'] = $value;
|
||||
break;
|
||||
case KeyAttribute::STORE:
|
||||
$store = $this->interpretStore($value);
|
||||
$values['store'] = $store;
|
||||
if ($store === Store::EXTERNAL) {
|
||||
$values['store_link'] = $value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($values['key'] === null || $values['name'] === null || $values['store'] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$game = $this->entityManager->getRepository(Game::class)->findOneBy([ 'name' => $values['name'] ]);
|
||||
if ($game === null) {
|
||||
$game = new Game($values['name']);
|
||||
}
|
||||
|
||||
$key = new Key(
|
||||
$game,
|
||||
$contributedUser,
|
||||
$values['key'],
|
||||
$values['store'],
|
||||
$values['store_link'],
|
||||
$values['from'],
|
||||
);
|
||||
|
||||
$this->entityManager->persist($game);
|
||||
$this->entityManager->persist($key);
|
||||
$addedAmount++;
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return [$totalRows, $addedAmount];
|
||||
}
|
||||
|
||||
private function interpretStore(string $storeString): Store {
|
||||
$storeString = trim($storeString);
|
||||
$storeString = strtolower($storeString);
|
||||
$storeString = str_replace(' ', '', $storeString);
|
||||
|
||||
$triedConversion = Store::tryFrom($storeString);
|
||||
if ($triedConversion !== null) {
|
||||
return $triedConversion;
|
||||
}
|
||||
|
||||
if (array_key_exists($storeString, self::STORE_ADDITIONAL_CASES)) {
|
||||
return self::STORE_ADDITIONAL_CASES[$storeString];
|
||||
}
|
||||
|
||||
return Store::EXTERNAL;
|
||||
}
|
||||
}
|
26
src/php/Importer/ImportColumnInterpretation.php
Normal file
26
src/php/Importer/ImportColumnInterpretation.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Importer;
|
||||
|
||||
use GamesShop\Entities\Games\KeyAttribute;
|
||||
use JsonSerializable;
|
||||
|
||||
final readonly class ImportColumnInterpretation implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public string $index,
|
||||
public string $displayName,
|
||||
public KeyAttribute|null $guessedAttribute
|
||||
)
|
||||
{ }
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'index' => $this->index,
|
||||
'displayName' => $this->displayName,
|
||||
'guessedAttribute' => $this->guessedAttribute ?? null
|
||||
];
|
||||
}
|
||||
}
|
50
src/php/Routing/Api/Web/ImportKeysPrepareRoute.php
Normal file
50
src/php/Routing/Api/Web/ImportKeysPrepareRoute.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Routing\Api\Web;
|
||||
|
||||
use GamesShop\Importer\GameImporter;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Laminas\Diactoros\UploadedFile;
|
||||
use League\Route\Http\Exception\BadRequestException;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class ImportKeysPrepareRoute
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler,
|
||||
private readonly GameImporter $importer,
|
||||
) { }
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @var UploadedFile $file
|
||||
*/
|
||||
$file = $request->getUploadedFiles()['file'] ?? null;
|
||||
if (!$file === null) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
$fileName = tempnam(sys_get_temp_dir(), 'ImportKeys');
|
||||
$file->moveTo($fileName);
|
||||
$results = $this->importer->interpret($fileName);
|
||||
unlink($fileName);
|
||||
|
||||
return new JsonResponse($results);
|
||||
}
|
||||
}
|
53
src/php/Routing/Api/Web/ImportKeysRoute.php
Normal file
53
src/php/Routing/Api/Web/ImportKeysRoute.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Routing\Api\Web;
|
||||
|
||||
use GamesShop\Importer\GameImporter;
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Laminas\Diactoros\UploadedFile;
|
||||
use League\Route\Http\Exception\BadRequestException;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class ImportKeysRoute
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler,
|
||||
private readonly GameImporter $importer,
|
||||
) { }
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @var UploadedFile $file
|
||||
*/
|
||||
$file = $request->getUploadedFiles()['file'] ?? null;
|
||||
if (!$file === null) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
$fileName = tempnam(sys_get_temp_dir(), 'ImportKeys');
|
||||
$file->moveTo($fileName);
|
||||
|
||||
$columnDefs = $request->getParsedBody()['columns'];
|
||||
|
||||
[$total, $imported] = $this->importer->import($fileName, $columnDefs, $user);
|
||||
unlink($fileName);
|
||||
|
||||
return new JsonResponse([ 'success' => true, 'total' => $total, 'imported' => $imported ]);
|
||||
}
|
||||
}
|
|
@ -9,5 +9,8 @@ final class WebAPIRoutes
|
|||
{
|
||||
public static function applyRoutes(RouteGroup $group): void {
|
||||
$group->post('/users/{id:number}', UserModifyRoute::class);
|
||||
|
||||
$group->post('/keys/import/prepare', ImportKeysPrepareRoute::class);
|
||||
$group->post('/keys/import/perform', ImportKeysRoute::class);
|
||||
}
|
||||
}
|
41
src/php/Routing/KeysRoute.php
Normal file
41
src/php/Routing/KeysRoute.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GamesShop\Routing;
|
||||
|
||||
use GamesShop\Login\LoginHandler;
|
||||
use GamesShop\Login\UserPermission;
|
||||
use GamesShop\Routing\Responses\TemplateResponse;
|
||||
use League\Route\Http\Exception\ForbiddenException;
|
||||
use League\Route\Http\Exception\UnauthorizedException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
final class KeysRoute
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LoginHandler $loginHandler
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->loginHandler->isLoggedIn()) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
$user = $this->loginHandler->getCurrentUser();
|
||||
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
|
||||
return new TemplateResponse('key-manager');
|
||||
}
|
||||
|
||||
public static function applyRoutes(\League\Route\Router $router): void
|
||||
{
|
||||
$router->get('/keys', KeysRoute::class);
|
||||
}
|
||||
}
|
|
@ -42,6 +42,8 @@ final class Router
|
|||
IndexRoute::applyRoutes($router);
|
||||
LoginRoutes::addRoutes($router);
|
||||
SetupRoute::applyRoutes($router);
|
||||
|
||||
KeysRoute::applyRoutes($router);
|
||||
AdminAccountConfigRoute::applyRoutes($router);
|
||||
|
||||
APIRoutes::applyRoutes($router);
|
||||
|
|
|
@ -41,5 +41,9 @@ $resource = $resources->getResource($resourceEntry);
|
|||
|
||||
<?= $this->section('modal') ?>
|
||||
|
||||
<div class="position-absolute bottom-0 start-50 opacity-25 text-center translate-middle-x">
|
||||
<span class="h1">PROTOTYPE / POC</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
51
src/templates/pages/key-manager.php
Normal file
51
src/templates/pages/key-manager.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use GamesShop\Entities\Games\KeyAttribute;
|
||||
|
||||
$this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
|
||||
?>
|
||||
<meta name="key-attributes" content="<?= htmlspecialchars(json_encode(KeyAttribute::casesAsAssociative())) ?>" />
|
||||
|
||||
<ul id="key-tab" class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keys-tab-pane" role="tab">
|
||||
Keys
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#import-tab-pane" role="tab">
|
||||
Import
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="keys-tab-pane" role="tabpanel">
|
||||
Key Table
|
||||
</div>
|
||||
<div class="tab-pane fade" id="import-tab-pane" role="tabpanel">
|
||||
<label for="formFile" class="form-label mt-3">Insert import file:</label>
|
||||
|
||||
<div class="mb-3 input-group">
|
||||
<input class="form-control" type="file" id="import" accept="text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
|
||||
<button class="btn btn-primary js--send-import">Send</button>
|
||||
</div>
|
||||
|
||||
<div class="d-none" id="import-info-container">
|
||||
<table class="table table-striped w-100" id="import-attribute-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Column</td>
|
||||
<td>Header</td>
|
||||
<td>Attribute</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button class="btn btn-primary js--do-import">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue