implermented key import

This commit is contained in:
Michel Fedde 2024-07-05 22:29:35 +02:00
parent ace0de4063
commit 74e1b25fcf
20 changed files with 1035 additions and 12 deletions

View file

@ -18,8 +18,6 @@
"symfony/uid": "^7.1",
"php-curl-class/php-curl-class": "^9.19",
"symfony/cache": "^7.1",
},
"require-dev": {
"ext-xdebug": "*"
"phpoffice/phpspreadsheet": "^2.1"
}
}

346
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3dbd3cfcf406f3c8c63570303d3d7942",
"content-hash": "89e0df3a5a9ac052a0102b007850e150",
"packages": [
{
"name": "doctrine/collections",
@ -1334,6 +1334,194 @@
],
"time": "2021-07-30T08:33:09+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/b8174494eda667f7d13876b4a7bfef0f62a7c0d1",
"reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.1"
},
"require-dev": {
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^10.0",
"vimeo/psalm": "^5.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.0"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
},
{
"url": "https://opencollective.com/zipstream",
"type": "open_collective"
}
],
"time": "2023-06-21T14:59:35+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
@ -1529,6 +1717,110 @@
},
"time": "2024-04-09T18:03:13+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e",
"reference": "dbed77bd3a0f68f96c0dd68ad4499d5674fecc3e",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.1.0"
},
"time": "2024-05-11T04:17:56+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.2",
@ -1706,6 +1998,58 @@
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
"version": "1.1.0",

30
deploy/Vagrantfile vendored
View file

@ -1,13 +1,39 @@
Vagrant.configure("2") do |config|
config.vm.boot_timeout = 600
config.vm.box = "vagrant-php83-mysql"
config.vm.box = "ubuntu/focal64"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network :forwarded_port, guest: 80, host: 1111
config.vm.synced_folder "../../games-shop", "/var/www/html"
config.vm.provision "shell", inline: <<-SHELL
sudo apt update
# Install dependencies
sudo apt install ca-certificates apt-transport-https software-properties-common lsb-release debian-keyring debian-archive-keyring curl -y
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install nginx php8.3 php8.3-{cli,fpm,curl,gd,zip,fileinfo,common,sqlite3,dom,mbstring,simplexml,xml,xmlreader,xmlwriter} -y
sudo ufw allow "OpenSSH"
sudo ufw allow "Nginx HTTP"
sudo ufw --force enable
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /var/www/html/deploy/nginx-file /etc/nginx/sites-enabled/default
sudo systemctl enable --now nginx
sudo systemctl restart nginx
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer
SHELL
end

View file

@ -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
View 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);
})

View file

@ -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);
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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';
}

View 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;
}
}

View 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
];
}
}

View 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);
}
}

View 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 ]);
}
}

View file

@ -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);
}
}

View 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);
}
}

View file

@ -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);

View file

@ -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>

View 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>

View file

@ -93,6 +93,7 @@ module.exports = {
entry: {
index: JS_FOLDER + "/pages/index",
'admin/accounts': JS_FOLDER + "/pages/admin/accounts",
keys: JS_FOLDER + "/pages/keys",
},
output: {
path: PUBLIC_FOLDER,