implermented key import
This commit is contained in:
parent
ace0de4063
commit
74e1b25fcf
20 changed files with 1035 additions and 12 deletions
|
@ -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
346
composer.lock
generated
|
@ -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",
|
||||
|
|
40
deploy/Vagrantfile
vendored
40
deploy/Vagrantfile
vendored
|
@ -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.synced_folder "../../games-shop", "/var/www/html"
|
||||
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
sudo ln -s /var/www/html/deploy/nginx-file /etc/nginx/sites-enabled/default
|
||||
SHELL
|
||||
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
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue