I rightly do not know what this all is

This commit is contained in:
Michel 2024-10-30 19:40:26 +01:00
parent af6b2b752e
commit 287c1f67c5
78 changed files with 3484 additions and 3365 deletions

254
.gitignore vendored
View file

@ -1,127 +1,127 @@
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# .gitignore # .gitignore
# Bare Minimum Git # Bare Minimum Git
# https://salferrarello.com/starter-gitignore-file/ # https://salferrarello.com/starter-gitignore-file/
# ver 20221125 # ver 20221125
# #
# From the root of your project run # From the root of your project run
# curl -O https://gist.githubusercontent.com/salcode/10017553/raw/.gitignore # curl -O https://gist.githubusercontent.com/salcode/10017553/raw/.gitignore
# to download this file # to download this file
# #
# This file is tailored for a general web project, it # This file is tailored for a general web project, it
# is NOT optimized for a WordPress project. See # is NOT optimized for a WordPress project. See
# https://gist.github.com/salcode/b515f520d3f8207ecd04 # https://gist.github.com/salcode/b515f520d3f8207ecd04
# for a WordPress specific .gitignore # for a WordPress specific .gitignore
# #
# This file specifies intentionally untracked files to ignore # This file specifies intentionally untracked files to ignore
# http://git-scm.com/docs/gitignore # http://git-scm.com/docs/gitignore
# #
# NOTES: # NOTES:
# The purpose of gitignore files is to ensure that certain files not # The purpose of gitignore files is to ensure that certain files not
# tracked by Git remain untracked. # tracked by Git remain untracked.
# #
# To ignore uncommitted changes in a file that is already tracked, # To ignore uncommitted changes in a file that is already tracked,
# use `git update-index --assume-unchanged`. # use `git update-index --assume-unchanged`.
# #
# To stop tracking a file that is currently tracked, # To stop tracking a file that is currently tracked,
# use `git rm --cached` # use `git rm --cached`
# #
# Change Log: # Change Log:
# 20221125 ignore /dist directory # 20221125 ignore /dist directory
# unignore /.git-blame-ignore-revs # unignore /.git-blame-ignore-revs
# 20220720 ignore /build directory # 20220720 ignore /build directory
# 20220128 unignore .nvmrc # 20220128 unignore .nvmrc
# 20210211 unignore .env.example # 20210211 unignore .env.example
# 20190705 ignore private/secret files # 20190705 ignore private/secret files
# 20181206 remove trailing whitespaces # 20181206 remove trailing whitespaces
# 20180714 unignore .phpcs.xml.dist # 20180714 unignore .phpcs.xml.dist
# 20170502 unignore composer.lock # 20170502 unignore composer.lock
# 20170502 ignore components loaded via Bower # 20170502 ignore components loaded via Bower
# 20150326 ignore jekyll build directory `/_site` # 20150326 ignore jekyll build directory `/_site`
# 20150324 Reorganized file to list ignores first and whitelisted last, # 20150324 Reorganized file to list ignores first and whitelisted last,
# change WordPress .gitignore link to preferred gist, # change WordPress .gitignore link to preferred gist,
# add curl line for quick installation # add curl line for quick installation
# ignore composer files (vendor directory and lock file) # ignore composer files (vendor directory and lock file)
# 20140606 Add .editorconfig as a tracked file # 20140606 Add .editorconfig as a tracked file
# 20140418 remove explicit inclusion # 20140418 remove explicit inclusion
# of readme.md (this is not an ignored file by default) # of readme.md (this is not an ignored file by default)
# 20140407 Initially Published # 20140407 Initially Published
# #
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# ignore all files starting with . or ~ # ignore all files starting with . or ~
.* .*
~* ~*
# ignore node/grunt dependency directories # ignore node/grunt dependency directories
node_modules/ node_modules/
# Ignore build directories. # Ignore build directories.
/build /build
/dist /dist
# ignore composer vendor directory # ignore composer vendor directory
/vendor /vendor
# ignore components loaded via Bower # ignore components loaded via Bower
/bower_components /bower_components
# ignore jekyll build directory # ignore jekyll build directory
/_site /_site
# ignore OS generated files # ignore OS generated files
ehthumbs.db ehthumbs.db
Thumbs.db Thumbs.db
# ignore Editor files # ignore Editor files
*.sublime-project *.sublime-project
*.sublime-workspace *.sublime-workspace
*.komodoproject *.komodoproject
# ignore log files and databases # ignore log files and databases
*.log *.log
*.sql *.sql
*.sqlite *.sqlite
# ignore compiled files # ignore compiled files
*.com *.com
*.class *.class
*.dll *.dll
*.exe *.exe
*.o *.o
*.so *.so
# ignore packaged files # ignore packaged files
*.7z *.7z
*.dmg *.dmg
*.gz *.gz
*.iso *.iso
*.jar *.jar
*.rar *.rar
*.tar *.tar
*.zip *.zip
# ignore private/secret files # ignore private/secret files
*.der *.der
*.key *.key
*.pem *.pem
# -------------------------------------------------------- # --------------------------------------------------------
# BEGIN Explictly Allowed Files (i.e. do NOT ignore these) # BEGIN Explictly Allowed Files (i.e. do NOT ignore these)
# -------------------------------------------------------- # --------------------------------------------------------
# track these files, if they exist # track these files, if they exist
!.editorconfig !.editorconfig
!.env.example !.env.example
!.git-blame-ignore-revs !.git-blame-ignore-revs
!.gitignore !.gitignore
!.nvmrc !.nvmrc
!.phpcs.xml.dist !.phpcs.xml.dist
# ------------ # ------------
# USER CHANGES # USER CHANGES
# ------------ # ------------
# ignore public directory (we build it on server) # ignore public directory (we build it on server)
/public /public
/data/ /data/

View file

@ -18,6 +18,7 @@
"symfony/uid": "^7.1", "symfony/uid": "^7.1",
"php-curl-class/php-curl-class": "^9.19", "php-curl-class/php-curl-class": "^9.19",
"symfony/cache": "^7.1", "symfony/cache": "^7.1",
"phpoffice/phpspreadsheet": "^2.1" "phpoffice/phpspreadsheet": "^2.1",
"symfony/polyfill-iconv": "^1.31"
} }
} }

452
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
@import "@fortawesome/fontawesome-free/scss/fontawesome"; @import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/solid"; @import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/brands"; @import "@fortawesome/fontawesome-free/scss/brands";

View file

@ -1,44 +1,44 @@
@import "bootstrap"; @import "bootstrap";
@import "fonts"; @import "fonts";
$main-container-max-width: 992px; $main-container-max-width: 992px;
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.navigation-container, .navigation-container,
main { main {
width: 100%; width: 100%;
max-width: $main-container-max-width; max-width: $main-container-max-width;
margin: 0 auto; margin: 0 auto;
} }
.ratio-1 { .ratio-1 {
aspect-ratio: 1; aspect-ratio: 1;
} }
.avatar { .avatar {
height: 3rem; height: 3rem;
.avatar-login-method-icon { .avatar-login-method-icon {
scale: 1.5; scale: 1.5;
} }
} }
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
.mode-switch { .mode-switch {
text-align: center; text-align: center;
padding-top: 0.5rem; padding-top: 0.5rem;
margin-left: auto; margin-left: auto;
} }
.navigation-container { .navigation-container {
.navbar-brand { .navbar-brand {
margin: 0 auto !important; margin: 0 auto !important;
} }
} }
} }

View file

@ -1,3 +1,3 @@
import "./theme"; import "./theme";
import "bootstrap/js/src/collapse"; import "bootstrap/js/src/collapse";

View file

@ -1,39 +1,39 @@
(() => { (() => {
function setTheme (mode = 'auto') { function setTheme (mode = 'auto') {
const userMode = localStorage.getItem('bs-theme'); const userMode = localStorage.getItem('bs-theme');
const sysMode = window.matchMedia('(prefers-color-scheme: light)').matches; const sysMode = window.matchMedia('(prefers-color-scheme: light)').matches;
const useSystem = mode === 'system' || (!userMode && mode === 'auto'); const useSystem = mode === 'system' || (!userMode && mode === 'auto');
const modeChosen = selectMode(mode); const modeChosen = selectMode(mode);
if (useSystem) { if (useSystem) {
localStorage.removeItem('bs-theme'); localStorage.removeItem('bs-theme');
} else { } else {
localStorage.setItem('bs-theme', modeChosen); localStorage.setItem('bs-theme', modeChosen);
} }
document.documentElement.setAttribute('data-bs-theme', useSystem ? (sysMode ? 'light' : 'dark') : modeChosen); document.documentElement.setAttribute('data-bs-theme', useSystem ? (sysMode ? 'light' : 'dark') : modeChosen);
document.querySelectorAll('.mode-switch .btn').forEach(e => e.classList.remove('text-body')); document.querySelectorAll('.mode-switch .btn').forEach(e => e.classList.remove('text-body'));
document.getElementById(modeChosen)?.classList.add('text-body'); document.getElementById(modeChosen)?.classList.add('text-body');
} }
function selectMode(mode: string): string { function selectMode(mode: string): string {
const userMode = <string>localStorage.getItem('bs-theme'); const userMode = <string>localStorage.getItem('bs-theme');
const useSystem = mode === 'system' || (!userMode && mode === 'auto'); const useSystem = mode === 'system' || (!userMode && mode === 'auto');
if (useSystem) { if (useSystem) {
return 'system'; return 'system';
} }
if (mode === 'dark' || mode === 'light') { if (mode === 'dark' || mode === 'light') {
return mode; return mode;
} }
return userMode; return userMode;
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
setTheme(); setTheme();
document.querySelectorAll('.mode-switch .btn').forEach(e => e.addEventListener('click', () => setTheme(e.id))); document.querySelectorAll('.mode-switch .btn').forEach(e => e.addEventListener('click', () => setTheme(e.id)));
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => setTheme()); window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => setTheme());
}); });
})() })()

View file

@ -1,102 +1,102 @@
import '../../../css/common/index.scss'; import '../../../css/common/index.scss';
import 'datatables.net-bs5/css/dataTables.bootstrap5.css'; import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
import '../../common/index'; import '../../common/index';
import DataTable from 'datatables.net-bs5'; import DataTable from 'datatables.net-bs5';
import {Modal} from "bootstrap"; import {Modal} from "bootstrap";
const DT_SERVERSIDE_PROCESSING_URL = '/api/dt/accounts'; const DT_SERVERSIDE_PROCESSING_URL = '/api/dt/accounts';
const TABLE = document.querySelector('#user-table'); const TABLE = document.querySelector('#user-table');
function displayEdit(data: any) { function displayEdit(data: any) {
const modalElem = document.querySelector('#edit-modal'); const modalElem = document.querySelector('#edit-modal');
// @ts-ignore // @ts-ignore
modalElem.querySelector('.name-input').textContent = data.name; modalElem.querySelector('.name-input').textContent = data.name;
// @ts-ignore // @ts-ignore
modalElem.querySelector('.login-method').textContent = data.loginMethod; modalElem.querySelector('.login-method').textContent = data.loginMethod;
// @ts-ignore // @ts-ignore
modalElem.querySelector('.permission-editor').value = data.permissionIndex; modalElem.querySelector('.permission-editor').value = data.permissionIndex;
// @ts-ignore // @ts-ignore
modalElem?.querySelector('.js--save').dataset.userid = data.userid; modalElem?.querySelector('.js--save').dataset.userid = data.userid;
Modal.getOrCreateInstance('#edit-modal').show(); Modal.getOrCreateInstance('#edit-modal').show();
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const dt = new DataTable('#user-table', { const dt = new DataTable('#user-table', {
ajax: { ajax: {
url: DT_SERVERSIDE_PROCESSING_URL, url: DT_SERVERSIDE_PROCESSING_URL,
}, },
serverSide: true, serverSide: true,
columns: [ columns: [
{ {
data: 'profilePictureUrl', data: 'profilePictureUrl',
render(data, type) { render(data, type) {
if (type !== 'display') { if (type !== 'display') {
return data; return data;
} }
return `<img src="${ data }" alt="Profile Picture" class="ratio-1 rounded-circle w-100" />` return `<img src="${ data }" alt="Profile Picture" class="ratio-1 rounded-circle w-100" />`
}, },
orderable: false, orderable: false,
searchable: false searchable: false
}, },
{ {
name: 'Name', name: 'Name',
data: 'name' data: 'name'
}, },
{ {
name: 'Permission', name: 'Permission',
data: 'permission' data: 'permission'
}, },
{ {
name: 'Login-Method', name: 'Login-Method',
data: 'loginMethod' data: 'loginMethod'
} }
], ],
order: [ [1, 'asc'] ], order: [ [1, 'asc'] ],
drawCallback: function (settings) { drawCallback: function (settings) {
const api = new DataTable.Api(settings); const api = new DataTable.Api(settings);
api.rows().every(function (row) { api.rows().every(function (row) {
const node = this.node(); const node = this.node();
const data = this.data(); const data = this.data();
node.addEventListener('click', (e) => { node.addEventListener('click', (e) => {
displayEdit(data); displayEdit(data);
}); });
}); });
} }
}); });
const button = <HTMLButtonElement>document.querySelector('#edit-modal .js--save'); const button = <HTMLButtonElement>document.querySelector('#edit-modal .js--save');
if (!button) { if (!button) {
return; return;
} }
button.addEventListener('click', async (e) => { button.addEventListener('click', async (e) => {
const permissionEditor = <HTMLSelectElement>document.querySelector('#edit-modal .permission-editor'); const permissionEditor = <HTMLSelectElement>document.querySelector('#edit-modal .permission-editor');
if (!permissionEditor) { if (!permissionEditor) {
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.set('permission', permissionEditor.value); formData.set('permission', permissionEditor.value);
const response = await fetch( const response = await fetch(
`/api/web/users/${button.dataset.userid}`, `/api/web/users/${button.dataset.userid}`,
{ {
method: 'POST', method: 'POST',
body: formData body: formData
} }
); );
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
Modal.getOrCreateInstance('#edit-modal').hide(); Modal.getOrCreateInstance('#edit-modal').hide();
dt.draw(); dt.draw();
}) })
}) })

View file

@ -1,2 +1,2 @@
import '../../css/common/index.scss'; import '../../css/common/index.scss';
import '../common/index'; import '../common/index';

View file

@ -1,148 +1,148 @@
import {getCurrentlySelectedList} from "./userlists"; import {getCurrentlySelectedList} from "./userlists";
import {getTableAPI} from "./table"; import {getTableAPI} from "./table";
async function checkImportFile() { async function checkImportFile() {
const fileInput = document.querySelector<HTMLInputElement>('#import'); const fileInput = document.querySelector<HTMLInputElement>('#import');
if (!fileInput) { if (!fileInput) {
return; return;
} }
if (!fileInput.files || !fileInput.files[0]) { if (!fileInput.files || !fileInput.files[0]) {
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.set('file', fileInput.files[0]); formData.set('file', fileInput.files[0]);
const response = await fetch( const response = await fetch(
`/api/web/keys/import/prepare`, `/api/web/keys/import/prepare`,
{ {
method: 'POST', method: 'POST',
body: formData body: formData
} }
); );
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
const jsonData = await response.json(); const jsonData = await response.json();
const container = document.getElementById('import-info-container'); const container = document.getElementById('import-info-container');
if (!container) { if (!container) {
return return
} }
container.classList.remove('d-none'); container.classList.remove('d-none');
createTableContents(jsonData); createTableContents(jsonData);
} }
function createTableContents(data: {index: string, displayName: string, guessedAttribute: string}[]) { function createTableContents(data: {index: string, displayName: string, guessedAttribute: string}[]) {
const table = document.querySelector<HTMLTableElement>('#import-attribute-table'); const table = document.querySelector<HTMLTableElement>('#import-attribute-table');
if (!table) { if (!table) {
return; return;
} }
const tbody = table.querySelector<HTMLTableSectionElement>('tbody'); const tbody = table.querySelector<HTMLTableSectionElement>('tbody');
if (!tbody) { if (!tbody) {
return; return;
} }
for (let child of tbody.children) { for (let child of tbody.children) {
child.remove(); child.remove();
} }
const keyAttributeMeta = document.querySelector<HTMLMetaElement>('meta[name="key-attributes"]')?.content; const keyAttributeMeta = document.querySelector<HTMLMetaElement>('meta[name="key-attributes"]')?.content;
const attributes = JSON.parse(keyAttributeMeta ?? ''); const attributes = JSON.parse(keyAttributeMeta ?? '');
const select = document.createElement('select'); const select = document.createElement('select');
select.classList.add('form-select', 'w-100'); select.classList.add('form-select', 'w-100');
for (let attributeName in attributes) { for (let attributeName in attributes) {
const attributeValue = attributes[attributeName]; const attributeValue = attributes[attributeName];
const option = document.createElement('option'); const option = document.createElement('option');
option.value = attributeValue; option.value = attributeValue;
option.textContent = attributeName; option.textContent = attributeName;
select.add(option); select.add(option);
} }
data.forEach((datum) => { data.forEach((datum) => {
const row = tbody.insertRow(); const row = tbody.insertRow();
const indexCell = row.insertCell(); const indexCell = row.insertCell();
indexCell.classList.add('index-cell'); indexCell.classList.add('index-cell');
indexCell.innerText = datum.index; indexCell.innerText = datum.index;
const displayNameCEll = row.insertCell(); const displayNameCEll = row.insertCell();
displayNameCEll.classList.add('name-cell'); displayNameCEll.classList.add('name-cell');
displayNameCEll.innerText = datum.displayName; displayNameCEll.innerText = datum.displayName;
const attributeCell = row.insertCell(); const attributeCell = row.insertCell();
attributeCell.classList.add('attribute-cell'); attributeCell.classList.add('attribute-cell');
const rowSelect = <HTMLSelectElement>select.cloneNode(true); const rowSelect = <HTMLSelectElement>select.cloneNode(true);
rowSelect.value = datum.guessedAttribute; rowSelect.value = datum.guessedAttribute;
attributeCell.appendChild(rowSelect); attributeCell.appendChild(rowSelect);
}) })
} }
async function doImport() { async function doImport() {
const fileInput = document.querySelector<HTMLInputElement>('#import'); const fileInput = document.querySelector<HTMLInputElement>('#import');
if (!fileInput) { if (!fileInput) {
return; return;
} }
if (!fileInput.files || !fileInput.files[0]) { if (!fileInput.files || !fileInput.files[0]) {
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.set('file', fileInput.files[0]); formData.set('file', fileInput.files[0]);
const tbody = document.querySelector<HTMLTableSectionElement>('#import-attribute-table tbody'); const tbody = document.querySelector<HTMLTableSectionElement>('#import-attribute-table tbody');
if (!tbody) { if (!tbody) {
return; return;
} }
for (let row of tbody.querySelectorAll('tr')) { for (let row of tbody.querySelectorAll('tr')) {
const columnIndex = row.querySelector('.index-cell')?.textContent; const columnIndex = row.querySelector('.index-cell')?.textContent;
const attribute = row.querySelector<HTMLSelectElement>('.attribute-cell select')?.value ?? 'none'; const attribute = row.querySelector<HTMLSelectElement>('.attribute-cell select')?.value ?? 'none';
formData.set(`columns[${columnIndex}]`, attribute); formData.set(`columns[${columnIndex}]`, attribute);
} }
const listId = getCurrentlySelectedList() ?? 0; const listId = getCurrentlySelectedList() ?? 0;
formData.set('listid', listId.toString()); formData.set('listid', listId.toString());
const response = await fetch( const response = await fetch(
`/api/web/keys/import/perform`, `/api/web/keys/import/perform`,
{ {
method: 'POST', method: 'POST',
body: formData body: formData
} }
); );
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
const container = document.getElementById('import-info-container'); const container = document.getElementById('import-info-container');
if (!container) { if (!container) {
return return
} }
container.classList.add('d-none'); container.classList.add('d-none');
fileInput.value = ''; fileInput.value = '';
getTableAPI().ajax.reload(); getTableAPI().ajax.reload();
} }
export function init() { export function init() {
const importButton = document.querySelector('.js--send-import'); const importButton = document.querySelector('.js--send-import');
importButton?.addEventListener('click', checkImportFile); importButton?.addEventListener('click', checkImportFile);
const doImportButton = document.querySelector('.js--do-import'); const doImportButton = document.querySelector('.js--do-import');
doImportButton?.addEventListener('click', doImport); doImportButton?.addEventListener('click', doImport);
} }

View file

@ -1,24 +1,26 @@
import '../../../css/common/index.scss'; import '../../../css/common/index.scss';
import '../../common/index'; import '../../common/index';
import { Tab } from 'bootstrap'; import { Tab } from 'bootstrap';
import {init as initImport} from "./import"; import {init as initImport} from "./import";
import {init as initTable} from "./table"; import {init as initTable} from "./table";
import {init as initUserLists} from "./userlists"; import {init as initUserLists} from "./userlists";
import {initShare} from "./share";
document.addEventListener('DOMContentLoaded', () => {
const triggerTabList = document.querySelectorAll('#key-tab button') document.addEventListener('DOMContentLoaded', () => {
triggerTabList.forEach(triggerEl => { const triggerTabList = document.querySelectorAll('#key-tab button')
const tabTrigger = new Tab(triggerEl) triggerTabList.forEach(triggerEl => {
const tabTrigger = new Tab(triggerEl)
triggerEl.addEventListener('click', event => {
event.preventDefault() triggerEl.addEventListener('click', event => {
tabTrigger.show() event.preventDefault()
}) tabTrigger.show()
}) })
})
initImport();
initTable(); initImport();
initUserLists(); initTable();
initUserLists();
initShare();
}) })

View file

@ -0,0 +1,24 @@
import DataTable from "datatables.net-bs5";
export function initShare() {
const table = document.querySelector<HTMLTableElement>('#shared-users-table');
if (!table) {
return;
}
new DataTable(table, {
columns: [
{
name: '',
data: 'icon',
searchable: false,
orderable: false,
},
{
name: 'Name',
data: 'name',
}
],
order: [ [1, "desc"] ]
});
}

View file

@ -1,108 +1,108 @@
import DataTable, {Api} from "datatables.net-bs5"; import DataTable, {Api} from "datatables.net-bs5";
import 'datatables.net-bs5/css/dataTables.bootstrap5.css'; import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
import {Key} from "../types/entities"; import {Key} from "../types/entities";
import {getIconForKeyState, getKeyStateExplanation} from "../types/keyState"; import {getIconForKeyState, getKeyStateExplanation} from "../types/keyState";
import {Dropdown, Tooltip} from "bootstrap"; import {Dropdown, Tooltip} from "bootstrap";
import {getCurrentlySelectedList} from "./userlists"; import {getCurrentlySelectedList} from "./userlists";
const TABLE_AJAX_URL = '/api/dt/keys/provider'; const TABLE_AJAX_URL = '/api/dt/keys/provider';
let tableAPI: Api; let tableAPI: Api;
export function getTableAPI(): Api { export function getTableAPI(): Api {
return tableAPI; return tableAPI;
} }
function getKeyDisplay(parent: HTMLElement, keys: Key[]) { function getKeyDisplay(parent: HTMLElement, keys: Key[]) {
const header = document.createElement("h1"); const header = document.createElement("h1");
header.innerText = 'Keys'; header.innerText = 'Keys';
header.classList.add('h6'); header.classList.add('h6');
const table = document.createElement("table"); const table = document.createElement("table");
table.classList.add('table', 'table-striped', 'w-100'); table.classList.add('table', 'table-striped', 'w-100');
const body = table.createTBody(); const body = table.createTBody();
for (const {fromWhere, state, key, store, store_link} of keys) { for (const {fromWhere, state, key, store, store_link} of keys) {
const row = body.insertRow(); const row = body.insertRow();
row.classList.add('cursor-pointer'); row.classList.add('cursor-pointer');
const stateCell = row.insertCell(); const stateCell = row.insertCell();
stateCell.classList.add('w-auto', 'align-content-center'); stateCell.classList.add('w-auto', 'align-content-center');
stateCell.innerHTML = `<a data-bs-toggle="tooltip" data-bs-title="${getKeyStateExplanation(state)}"><i class="fa-solid ${ getIconForKeyState(state) }"></i></a>` stateCell.innerHTML = `<a data-bs-toggle="tooltip" data-bs-title="${getKeyStateExplanation(state)}"><i class="fa-solid ${ getIconForKeyState(state) }"></i></a>`
const anchor = stateCell.querySelector('a'); const anchor = stateCell.querySelector('a');
if (!anchor) { if (!anchor) {
return; return;
} }
new Tooltip(anchor); new Tooltip(anchor);
row.insertCell().textContent = key; row.insertCell().textContent = key;
row.insertCell().textContent = store === 'external' ? store_link : store; row.insertCell().textContent = store === 'external' ? store_link : store;
row.insertCell().textContent = fromWhere; row.insertCell().textContent = fromWhere;
row.addEventListener('click', () => { row.addEventListener('click', () => {
console.log('Key options'); console.log('Key options');
}) })
} }
parent.appendChild(header); parent.appendChild(header);
parent.appendChild(table); parent.appendChild(table);
} }
export function init() { export function init() {
const keyTable = document.querySelector<HTMLTableElement>('.key-table'); const keyTable = document.querySelector<HTMLTableElement>('.key-table');
if (!keyTable) { if (!keyTable) {
return; return;
} }
const table = tableAPI = new DataTable(keyTable, { const table = tableAPI = new DataTable(keyTable, {
ajax: { ajax: {
url: TABLE_AJAX_URL, url: TABLE_AJAX_URL,
data: function (d) { data: function (d) {
// @ts-ignore // @ts-ignore
d.listid = getCurrentlySelectedList(); d.listid = getCurrentlySelectedList();
} }
}, },
processing: true, processing: true,
columns: [ columns: [
{ {
data: 'gamePicture', data: 'gamePicture',
searchable: false searchable: false,
}, orderable: false
{ },
data: 'name', {
}, data: 'name',
{ },
data: 'keysAmount', {
searchable: false, data: 'keysAmount',
}, searchable: false,
{ },
data: 'igdbState', {
searchable: false, data: 'igdbState',
} searchable: false,
], }
ordering: false, ],
order: [ [1, 'asc'] ], order: [ [1, 'asc'] ],
createdRow(row: Node, data: any) { createdRow(row: Node, data: any) {
const tableRow = <HTMLTableRowElement>row; const tableRow = <HTMLTableRowElement>row;
tableRow.classList.add('cursor-pointer'); tableRow.classList.add('cursor-pointer');
tableRow.addEventListener('click', () => { tableRow.addEventListener('click', () => {
const rowAPI = table.row(row); const rowAPI = table.row(row);
if (rowAPI.child.isShown()) { if (rowAPI.child.isShown()) {
rowAPI.child.hide(); rowAPI.child.hide();
return; return;
} }
const childRow = document.createElement('tr'); const childRow = document.createElement('tr');
const cell = childRow.insertCell(); const cell = childRow.insertCell();
cell.colSpan = row.childNodes.length; cell.colSpan = row.childNodes.length;
getKeyDisplay(cell, data.keys); getKeyDisplay(cell, data.keys);
rowAPI.child(childRow).show(); rowAPI.child(childRow).show();
}) })
}, },
}); });
} }

View file

@ -1,71 +1,71 @@
import {Modal} from "bootstrap"; import {Modal} from "bootstrap";
import {getTableAPI} from "./table"; import {getTableAPI} from "./table";
export function getCurrentlySelectedList(): number|null { export function getCurrentlySelectedList(): number|null {
const listSelect = document.querySelector<HTMLSelectElement>('#list-select'); const listSelect = document.querySelector<HTMLSelectElement>('#list-select');
if (!listSelect) { if (!listSelect) {
return null; return null;
} }
return parseInt(listSelect.value); return parseInt(listSelect.value);
} }
export function init() { export function init() {
const modal = document.querySelector('#create-list-modal'); const modal = document.querySelector('#create-list-modal');
if (!modal) { if (!modal) {
return; return;
} }
const modalObj = new Modal(modal); const modalObj = new Modal(modal);
modal.addEventListener('show.bs.modal', (e) => { modal.addEventListener('show.bs.modal', (e) => {
const input = modal.querySelector<HTMLInputElement>('#createListName'); const input = modal.querySelector<HTMLInputElement>('#createListName');
if (!input) { if (!input) {
return; return;
} }
input.value = ''; input.value = '';
}) })
modal.querySelector('.js--create-list')?.addEventListener('click', async (e) => { modal.querySelector('.js--create-list')?.addEventListener('click', async (e) => {
const input = modal.querySelector<HTMLInputElement>('#createListName'); const input = modal.querySelector<HTMLInputElement>('#createListName');
if (!input) { if (!input) {
return; return;
} }
const newName = input.value; const newName = input.value;
const formData = new FormData(); const formData = new FormData();
formData.append('name', newName); formData.append('name', newName);
const response = await fetch( const response = await fetch(
`/api/web/keys/list/create`, `/api/web/keys/list/create`,
{ {
method: 'POST', method: 'POST',
body: formData body: formData
} }
); );
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
window.location.reload(); window.location.reload();
}); });
const listSelect = document.querySelector<HTMLSelectElement>('#list-select'); const listSelect = document.querySelector<HTMLSelectElement>('#list-select');
if (listSelect) { if (listSelect) {
listSelect.addEventListener('change', (e) => { listSelect.addEventListener('change', (e) => {
if (listSelect.value === '_create') { if (listSelect.value === '_create') {
modalObj.show() modalObj.show()
return; return;
} }
getTableAPI().ajax.reload(); getTableAPI().ajax.reload();
}) })
} }
const newButton = document.querySelector('.js--create-list-button'); const newButton = document.querySelector('.js--create-list-button');
if (newButton) { if (newButton) {
newButton.addEventListener('click', () => modalObj.show()) newButton.addEventListener('click', () => modalObj.show())
} }
} }

View file

@ -1,25 +1,25 @@
import {KeyState} from "./keyState"; import {KeyState} from "./keyState";
enum Store { enum Store {
STEAM = 'steam', STEAM = 'steam',
GOG = 'gog', GOG = 'gog',
EPICGAMES = 'epicgames', EPICGAMES = 'epicgames',
ORIGIN = 'origin', ORIGIN = 'origin',
UPLAY = 'uplay', UPLAY = 'uplay',
BATTLENET = 'battlenet', BATTLENET = 'battlenet',
EXTERNAL = 'external' EXTERNAL = 'external'
} }
export type Game = { export type Game = {
name: string name: string
} }
export type Key = { export type Key = {
game?: Game, game?: Game,
key: string, key: string,
store: Store, store: Store,
state: KeyState, state: KeyState,
store_link: string|null, store_link: string|null,
fromWhere: string|null, fromWhere: string|null,
} }

View file

@ -1,35 +1,35 @@
export enum KeyState { export enum KeyState {
AVAILABLE = 1, AVAILABLE = 1,
UNKNOWN = 0, UNKNOWN = 0,
RESERVED_FOR_GIFT = -1, RESERVED_FOR_GIFT = -1,
CLAIMED = -10 CLAIMED = -10
} }
export function getIconForKeyState(keyState: KeyState): string { export function getIconForKeyState(keyState: KeyState): string {
switch (keyState) { switch (keyState) {
case KeyState.AVAILABLE: case KeyState.AVAILABLE:
return "fa-check text-success"; return "fa-check text-success";
default: default:
case KeyState.UNKNOWN: case KeyState.UNKNOWN:
return 'fa-question text-info'; return 'fa-question text-info';
case KeyState.RESERVED_FOR_GIFT: case KeyState.RESERVED_FOR_GIFT:
return 'fa-gift text-warning'; return 'fa-gift text-warning';
case KeyState.CLAIMED: case KeyState.CLAIMED:
return 'fa-x text-danger'; return 'fa-x text-danger';
} }
} }
export function getKeyStateExplanation(keyState: KeyState): string { export function getKeyStateExplanation(keyState: KeyState): string {
switch (keyState) { switch (keyState) {
case KeyState.AVAILABLE: case KeyState.AVAILABLE:
return "This key is available"; return "This key is available";
default: default:
case KeyState.UNKNOWN: case KeyState.UNKNOWN:
return 'The state of this key is unknown'; return 'The state of this key is unknown';
case KeyState.RESERVED_FOR_GIFT: case KeyState.RESERVED_FOR_GIFT:
return 'This key is reserved for a gift'; return 'This key is reserved for a gift';
case KeyState.CLAIMED: case KeyState.CLAIMED:
return 'This key was claimed'; return 'This key was claimed';
} }
} }

View file

@ -1,79 +1,79 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Api; namespace GamesShop\Api;
use Curl\Curl; use Curl\Curl;
use Exception; use Exception;
use GamesShop\Environment\EnvironmentHandler; use GamesShop\Environment\EnvironmentHandler;
use GamesShop\Errors\ExtendedException; use GamesShop\Errors\ExtendedException;
final class DiscordAPI final class DiscordAPI
{ {
const string OAUTH_TOKEN_URL = "https://discord.com/api/oauth2/token"; const string OAUTH_TOKEN_URL = "https://discord.com/api/oauth2/token";
const string USER_ME_URL = 'https://discord.com/api/users/@me'; const string USER_ME_URL = 'https://discord.com/api/users/@me';
public function __construct( public function __construct(
private readonly EnvironmentHandler $env private readonly EnvironmentHandler $env
) )
{ {
} }
/** /**
* @return array{id: string, global_name: string, avatar: string, discriminator: int} * @return array{id: string, global_name: string, avatar: string, discriminator: int}
* @throws Exception * @throws Exception
*/ */
public function getUserFromCode(string $code, string $redirectUri): array { public function getUserFromCode(string $code, string $redirectUri): array {
$discordEnv = $this->env->getDiscordEnvironment(); $discordEnv = $this->env->getDiscordEnvironment();
$curl = new Curl(); $curl = new Curl();
$curl->setHeader('Content-Type', 'application/x-www-form-urlencoded'); $curl->setHeader('Content-Type', 'application/x-www-form-urlencoded');
$curl->setBasicAuthentication($discordEnv->clientId, $discordEnv->clientSecret); $curl->setBasicAuthentication($discordEnv->clientId, $discordEnv->clientSecret);
$data =[ $data =[
'grant_type' => 'authorization_code', 'grant_type' => 'authorization_code',
'code' => $code, 'code' => $code,
'redirect_uri' => $redirectUri 'redirect_uri' => $redirectUri
]; ];
$curl->post(self::OAUTH_TOKEN_URL, $data); $curl->post(self::OAUTH_TOKEN_URL, $data);
if ($curl->error) { if ($curl->error) {
$curl->diagnose(); $curl->diagnose();
throw new ExtendedException($curl->errorMessage, [ 'response' => $curl->response, 'data' => $data ]); throw new ExtendedException($curl->errorMessage, [ 'response' => $curl->response, 'data' => $data ]);
} }
$accessToken = $curl->response->access_token; $accessToken = $curl->response->access_token;
$tokenType = $curl->response->token_type; $tokenType = $curl->response->token_type;
$curl = new Curl(); $curl = new Curl();
$curl->setHeader("authorization", "$tokenType $accessToken"); $curl->setHeader("authorization", "$tokenType $accessToken");
$curl->get(self::USER_ME_URL); $curl->get(self::USER_ME_URL);
if ($curl->error) { if ($curl->error) {
$curl->diagnose(); $curl->diagnose();
throw new ExtendedException($curl->errorMessage, [ 'response' => $curl->response, ]); throw new ExtendedException($curl->errorMessage, [ 'response' => $curl->response, ]);
} }
return [ return [
'id' => $curl->response->id, 'id' => $curl->response->id,
'global_name' => $curl->response->global_name, 'global_name' => $curl->response->global_name,
'avatar' => $curl->response->avatar, 'avatar' => $curl->response->avatar,
'discriminator' => (int) $curl->response->discriminator 'discriminator' => (int) $curl->response->discriminator
]; ];
} }
public function getAvatarURL(string $userId, string|int $avatarHash) { public function getAvatarURL(string $userId, string|int $avatarHash) {
if (is_int($avatarHash)) { if (is_int($avatarHash)) {
return "https://cdn.discordapp.com/embed/avatars/{$avatarHash}.png"; return "https://cdn.discordapp.com/embed/avatars/{$avatarHash}.png";
} }
$extension = 'png'; $extension = 'png';
if (str_starts_with($avatarHash, 'a_')) { if (str_starts_with($avatarHash, 'a_')) {
$extension = 'gif'; $extension = 'gif';
} }
return "https://cdn.discordapp.com/avatars/{$userId}/{$avatarHash}.{$extension}"; return "https://cdn.discordapp.com/avatars/{$userId}/{$avatarHash}.{$extension}";
} }
} }

View file

@ -1,39 +1,39 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop; namespace GamesShop;
use League\Container\Container; use League\Container\Container;
use League\Container\ReflectionContainer; use League\Container\ReflectionContainer;
final class ContainerHandler final class ContainerHandler
{ {
private static Container|null $instance = null; private static Container|null $instance = null;
public static function getInstance(): Container public static function getInstance(): Container
{ {
if (self::$instance === null) { if (self::$instance === null) {
self::createInstance(); self::createInstance();
} }
return self::$instance; return self::$instance;
} }
/** /**
* @template RequestedType * @template RequestedType
* *
* @param class-string<RequestedType>|string $id * @param class-string<RequestedType>|string $id
* *
* @return RequestedType|mixed * @return RequestedType|mixed
*/ */
public static function get(string $id) { public static function get(string $id) {
return self::getInstance()->get($id); return self::getInstance()->get($id);
} }
private static function createInstance() private static function createInstance()
{ {
self::$instance = new Container(); self::$instance = new Container();
$reflectionContainer = new ReflectionContainer(true); $reflectionContainer = new ReflectionContainer(true);
self::$instance->delegate($reflectionContainer); self::$instance->delegate($reflectionContainer);
} }
} }

View file

@ -1,35 +1,35 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop; namespace GamesShop;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMSetup; use Doctrine\ORM\ORMSetup;
use GamesShop\Environment\EnvironmentHandler; use GamesShop\Environment\EnvironmentHandler;
final class DoctrineManager final class DoctrineManager
{ {
public function setup() { public function setup() {
$container = ContainerHandler::getInstance(); $container = ContainerHandler::getInstance();
$environmentHandler = $container->get(EnvironmentHandler::class); $environmentHandler = $container->get(EnvironmentHandler::class);
$config = ORMSetup::createAttributeMetadataConfiguration( $config = ORMSetup::createAttributeMetadataConfiguration(
paths: [ Paths::PHP_SOURCE_PATH . '/Entities' ], paths: [ Paths::PHP_SOURCE_PATH . '/Entities' ],
isDevMode: !$environmentHandler->isProduction() isDevMode: !$environmentHandler->isProduction()
); );
$dbEnvironment = $environmentHandler->getDatabaseEnvironment(); $dbEnvironment = $environmentHandler->getDatabaseEnvironment();
$connection = DriverManager::getConnection($dbEnvironment->getDoctrineConfig()); $connection = DriverManager::getConnection($dbEnvironment->getDoctrineConfig());
$entityManager = new EntityManager($connection, $config); $entityManager = new EntityManager($connection, $config);
$container->addShared(EntityManager::class, $entityManager); $container->addShared(EntityManager::class, $entityManager);
$container->addShared(EntityManagerInterface::class, $entityManager); $container->addShared(EntityManagerInterface::class, $entityManager);
$container->addShared(Connection::class, $connection); $container->addShared(Connection::class, $connection);
} }
} }

View file

@ -1,85 +1,85 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities\Account; namespace GamesShop\Entities\Account;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use GamesShop\Login\LoginMethod; use GamesShop\Login\LoginMethod;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
use Symfony\Component\Uid\UuidV4; use Symfony\Component\Uid\UuidV4;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: "users")] #[ORM\Table(name: "users")]
final class User final class User
{ {
#[ORM\Id()] #[ORM\Id()]
#[ORM\Column] #[ORM\Column]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
private int|null $id = null; private int|null $id = null;
#[ORM\Column(type: 'integer', enumType: LoginMethod::class)] #[ORM\Column(type: 'integer', enumType: LoginMethod::class)]
private LoginMethod $loginMethod; private LoginMethod $loginMethod;
#[ORM\Column] #[ORM\Column]
private string|null $foreignLoginId = null; private string|null $foreignLoginId = null;
#[ORM\Column] #[ORM\Column]
private string $name; private string $name;
#[ORM\Column] #[ORM\Column]
private string $profilePictureUrl; private string $profilePictureUrl;
#[ORM\Column] #[ORM\Column]
private UserPermission $permission; private UserPermission $permission;
public function __construct(LoginMethod $loginMethod, ?string $foreignLoginId, string $name, string $profilePictureUrl, UserPermission $permission) public function __construct(LoginMethod $loginMethod, ?string $foreignLoginId, string $name, string $profilePictureUrl, UserPermission $permission)
{ {
$this->loginMethod = $loginMethod; $this->loginMethod = $loginMethod;
$this->foreignLoginId = $foreignLoginId; $this->foreignLoginId = $foreignLoginId;
$this->name = $name; $this->name = $name;
$this->profilePictureUrl = $profilePictureUrl; $this->profilePictureUrl = $profilePictureUrl;
$this->permission = $permission; $this->permission = $permission;
} }
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
public function getLoginMethod(): LoginMethod public function getLoginMethod(): LoginMethod
{ {
return $this->loginMethod; return $this->loginMethod;
} }
public function getForeignLoginId(): ?string public function getForeignLoginId(): ?string
{ {
return $this->foreignLoginId; return $this->foreignLoginId;
} }
public function getName(): string public function getName(): string
{ {
return $this->name; return $this->name;
} }
public function getProfilePictureUrl(): string public function getProfilePictureUrl(): string
{ {
return $this->profilePictureUrl; return $this->profilePictureUrl;
} }
public function getPermission(): UserPermission public function getPermission(): UserPermission
{ {
return $this->permission; return $this->permission;
} }
public function setName(string $name): void public function setName(string $name): void
{ {
$this->name = $name; $this->name = $name;
} }
public function setProfilePictureUrl(string $profilePictureUrl): void public function setProfilePictureUrl(string $profilePictureUrl): void
{ {
$this->profilePictureUrl = $profilePictureUrl; $this->profilePictureUrl = $profilePictureUrl;
} }
public function setPermission(UserPermission $permission): void public function setPermission(UserPermission $permission): void
{ {
$this->permission = $permission; $this->permission = $permission;
} }
} }

View file

@ -1,36 +1,36 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities\Games; namespace GamesShop\Entities\Games;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'games')] #[ORM\Table(name: 'games')]
class Game class Game
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: 'integer', options: ['unsigned' => true])] #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
private int|null $id; private int|null $id;
#[ORM\Column] #[ORM\Column]
private string $name; private string $name;
public function getName(): string public function getName(): string
{ {
return $this->name; return $this->name;
} }
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
/** /**
* @param string $name * @param string $name
*/ */
public function __construct(string $name) public function __construct(string $name)
{ {
$this->name = $name; $this->name = $name;
} }
} }

View file

@ -1,96 +1,96 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities\Games; namespace GamesShop\Entities\Games;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use JsonSerializable; use JsonSerializable;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'keys')] #[ORM\Table(name: 'keys')]
final class Key implements JsonSerializable final class Key implements JsonSerializable
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: 'integer', options: ['unsigned' => true])] #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
private int|null $id; private int|null $id;
#[ORM\ManyToOne] #[ORM\ManyToOne]
private Game $game; private Game $game;
#[ORM\ManyToOne] #[ORM\ManyToOne]
private GamesList $list; private GamesList $list;
#[ORM\Column] #[ORM\Column]
private string $key; private string $key;
#[ORM\Column(type: 'string', enumType: Store::class)] #[ORM\Column(type: 'string', enumType: Store::class)]
private Store $store; private Store $store;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private string|null $storeLink; private string|null $storeLink;
#[ORM\Column] #[ORM\Column]
private string|null $fromWhere; private string|null $fromWhere;
#[ORM\Column(type: 'integer', enumType: KeyState::class)] #[ORM\Column(type: 'integer', enumType: KeyState::class)]
private KeyState $state; private KeyState $state;
public function __construct(Game $game, GamesList $list, string $key, Store $store, ?string $storeLink, ?string $fromWhere) public function __construct(Game $game, GamesList $list, string $key, Store $store, ?string $storeLink, ?string $fromWhere)
{ {
$this->game = $game; $this->game = $game;
$this->list = $list; $this->list = $list;
$this->key = $key; $this->key = $key;
$this->store = $store; $this->store = $store;
$this->storeLink = $storeLink; $this->storeLink = $storeLink;
$this->fromWhere = $fromWhere; $this->fromWhere = $fromWhere;
$this->state = KeyState::AVAILABLE; $this->state = KeyState::AVAILABLE;
} }
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
public function getGame(): Game public function getGame(): Game
{ {
return $this->game; return $this->game;
} }
public function getContributedUser(): User public function getContributedUser(): User
{ {
return $this->contributedUser; return $this->contributedUser;
} }
public function getKey(): string public function getKey(): string
{ {
return $this->key; return $this->key;
} }
public function getStore(): Store public function getStore(): Store
{ {
return $this->store; return $this->store;
} }
public function getStoreLink(): ?string public function getStoreLink(): ?string
{ {
return $this->storeLink; return $this->storeLink;
} }
public function getFromWhere(): ?string public function getFromWhere(): ?string
{ {
return $this->fromWhere; return $this->fromWhere;
} }
public function getState(): KeyState public function getState(): KeyState
{ {
return $this->state; return $this->state;
} }
public function jsonSerialize(): mixed public function jsonSerialize(): mixed
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'key' => $this->key, 'key' => $this->key,
'store' => $this->store->value, 'store' => $this->store->value,
'store_link' => $this->storeLink, 'store_link' => $this->storeLink,
'from_where' => $this->fromWhere, 'from_where' => $this->fromWhere,
'state' => $this->state->value, 'state' => $this->state->value,
]; ];
} }
} }

View file

@ -1,22 +1,22 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities\Games; namespace GamesShop\Entities\Games;
enum KeyAttribute: string enum KeyAttribute: string
{ {
case NONE = 'none'; case NONE = 'none';
case GAME_NAME = "game_name"; case GAME_NAME = "game_name";
case KEY = "key"; case KEY = "key";
case STORE = 'store'; case STORE = 'store';
case FROM = 'from'; case FROM = 'from';
public static function casesAsAssociative(): array { public static function casesAsAssociative(): array {
$result = []; $result = [];
foreach (self::cases() as $case) { foreach (self::cases() as $case) {
$result[$case->name] = $case->value; $result[$case->name] = $case->value;
} }
return $result; return $result;
} }
} }

View file

@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities\Games; namespace GamesShop\Entities\Games;
enum KeyState: int enum KeyState: int
{ {
case AVAILABLE = 1; case AVAILABLE = 1;
case UNKNOWN = 0; case UNKNOWN = 0;
case RESERVED_FOR_GIFT = -1; case RESERVED_FOR_GIFT = -1;
case CLAIMED = -10; case CLAIMED = -10;
} }

View file

@ -1,15 +1,15 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities\Games; namespace GamesShop\Entities\Games;
enum Store: string enum Store: string
{ {
case STEAM = 'steam'; case STEAM = 'steam';
case GOG = 'gog'; case GOG = 'gog';
case EPICGAMES = 'epicgames'; case EPICGAMES = 'epicgames';
case ORIGIN = 'origin'; case ORIGIN = 'origin';
case UPLAY = 'uplay'; case UPLAY = 'uplay';
case BATTLENET = 'battlenet'; case BATTLENET = 'battlenet';
case EXTERNAL = 'external'; case EXTERNAL = 'external';
} }

View file

@ -1,62 +1,62 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities; namespace GamesShop\Entities;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'games_lists')] #[ORM\Table(name: 'games_lists')]
final class GamesList final class GamesList
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: 'integer', options: ['unsigned' => true])] #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
private int|null $id; private int|null $id;
#[ORM\ManyToOne] #[ORM\ManyToOne]
private User $owner; private User $owner;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private string|null $name; private string|null $name;
#[ORM\JoinTable(name: 'games_list_claimer')] #[ORM\JoinTable(name: 'games_list_claimer')]
#[ORM\JoinColumn(name: 'id', referencedColumnName: 'id')] #[ORM\JoinColumn(name: 'id', referencedColumnName: 'id')]
#[ORM\ManyToMany(targetEntity: User::class)] #[ORM\ManyToMany(targetEntity: User::class)]
private Collection $claimer; private Collection $claimer;
/** /**
* @param User $owner * @param User $owner
* @param string|null $name * @param string|null $name
*/ */
public function __construct(User $owner, ?string $name) public function __construct(User $owner, ?string $name)
{ {
$this->owner = $owner; $this->owner = $owner;
$this->name = $name; $this->name = $name;
} }
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
public function getOwner(): User public function getOwner(): User
{ {
return $this->owner; return $this->owner;
} }
public function getName(): ?string public function getName(): ?string
{ {
return $this->name; return $this->name;
} }
public function getClaimer(): array public function getClaimer(): array
{ {
return $this->claimer; return $this->claimer;
} }
} }

View file

@ -1,34 +1,34 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Entities; namespace GamesShop\Entities;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'attributes')] #[ORM\Table(name: 'attributes')]
final class SystemAttribute final class SystemAttribute
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column] #[ORM\Column]
private string $name; private string $name;
#[ORM\Column] #[ORM\Column]
private string $value; private string $value;
public function __construct(string $name, string $value) public function __construct(string $name, string $value)
{ {
$this->name = $name; $this->name = $name;
$this->value = $value; $this->value = $value;
} }
public function getName(): string public function getName(): string
{ {
return $this->name; return $this->name;
} }
public function getValue(): string public function getValue(): string
{ {
return $this->value; return $this->value;
} }
} }

View file

@ -1,21 +1,21 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Environment; namespace GamesShop\Environment;
final readonly class DatabaseEnvironment final readonly class DatabaseEnvironment
{ {
public function __construct( public function __construct(
public string $driver, public string $driver,
public string $path public string $path
) )
{ {
} }
public function getDoctrineConfig() { public function getDoctrineConfig() {
return [ return [
'driver' => $this->driver, 'driver' => $this->driver,
'path' => $this->path 'path' => $this->path
]; ];
} }
} }

View file

@ -1,13 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Environment; namespace GamesShop\Environment;
final readonly class DiscordEnvironment final readonly class DiscordEnvironment
{ {
public function __construct( public function __construct(
public string $clientId, public string $clientId,
public string $clientSecret, public string $clientSecret,
public string $loginUrl, public string $loginUrl,
) {} ) {}
} }

View file

@ -1,40 +1,40 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Environment; namespace GamesShop\Environment;
use DotenvVault\DotenvVault; use DotenvVault\DotenvVault;
use GamesShop\Paths; use GamesShop\Paths;
final class EnvironmentHandler final class EnvironmentHandler
{ {
private const string ENVIRONMENT_PATH = Paths::ROOT_PATH . '/config'; private const string ENVIRONMENT_PATH = Paths::ROOT_PATH . '/config';
public function load() { public function load() {
$dotEnv = DotenvVault::createImmutable( $dotEnv = DotenvVault::createImmutable(
self::ENVIRONMENT_PATH self::ENVIRONMENT_PATH
); );
$dotEnv->safeLoad(); $dotEnv->safeLoad();
} }
public function getDiscordEnvironment(): DiscordEnvironment { public function getDiscordEnvironment(): DiscordEnvironment {
return new DiscordEnvironment( return new DiscordEnvironment(
$_SERVER['DISCORD_CLIENT_ID'], $_SERVER['DISCORD_CLIENT_ID'],
$_SERVER['DISCORD_CLIENT_SECRET'], $_SERVER['DISCORD_CLIENT_SECRET'],
$_SERVER['DISCORD_CLIENT_LOGIN_URI'], $_SERVER['DISCORD_CLIENT_LOGIN_URI'],
); );
} }
public function getDatabaseEnvironment(): DatabaseEnvironment public function getDatabaseEnvironment(): DatabaseEnvironment
{ {
return new DatabaseEnvironment( return new DatabaseEnvironment(
$_SERVER['DB_DRIVER'], $_SERVER['DB_DRIVER'],
$_SERVER['DB_PATH'] $_SERVER['DB_PATH']
); );
} }
public function isProduction(): bool { public function isProduction(): bool {
return $_SERVER['PRODUCTION'] === 'true'; return $_SERVER['PRODUCTION'] === 'true';
} }
} }

View file

@ -1,26 +1,26 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Errors; namespace GamesShop\Errors;
use Exception; use Exception;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use Whoops\Handler\HandlerInterface; use Whoops\Handler\HandlerInterface;
use Whoops\Handler\PrettyPageHandler; use Whoops\Handler\PrettyPageHandler;
final class ExtendedException extends Exception final class ExtendedException extends Exception
{ {
public function __construct(string $message = "", array $additionals = [], int $code = 0, ?Throwable $previous = null) public function __construct(string $message = "", array $additionals = [], int $code = 0, ?Throwable $previous = null)
{ {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
$handler = ContainerHandler::get(HandlerInterface::class); $handler = ContainerHandler::get(HandlerInterface::class);
if (!($handler instanceof PrettyPageHandler)) { if (!($handler instanceof PrettyPageHandler)) {
return; return;
} }
$handler->addDataTable("Additional Info", $additionals); $handler->addDataTable("Additional Info", $additionals);
} }
} }

View file

@ -1,8 +1,8 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Whoops\Run; use Whoops\Run;
final class WhoopsHandler final class WhoopsHandler
{ {
} }

View file

@ -1,166 +1,166 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Importer; namespace GamesShop\Importer;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exception; use Exception;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
use GamesShop\Entities\Games\Game; use GamesShop\Entities\Games\Game;
use GamesShop\Entities\Games\Key; use GamesShop\Entities\Games\Key;
use GamesShop\Entities\Games\KeyAttribute; use GamesShop\Entities\Games\KeyAttribute;
use GamesShop\Entities\Games\Store; use GamesShop\Entities\Games\Store;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\IOFactory;
final class GameImporter final class GameImporter
{ {
private const HEADER_ROW_INDEX = 1; private const HEADER_ROW_INDEX = 1;
private const STORE_ADDITIONAL_CASES = [ private const STORE_ADDITIONAL_CASES = [
'epic' => Store::EPICGAMES, 'epic' => Store::EPICGAMES,
'ea' => Store::ORIGIN, 'ea' => Store::ORIGIN,
'eaplay' => Store::ORIGIN, 'eaplay' => Store::ORIGIN,
'ubisoft' => Store::UPLAY, 'ubisoft' => Store::UPLAY,
'activision' => Store::BATTLENET 'activision' => Store::BATTLENET
]; ];
public function __construct( public function __construct(
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
) { } ) { }
/** /**
* @param string $path * @param string $path
* @return ImportColumnInterpretation[] * @return ImportColumnInterpretation[]
*/ */
public function interpret(string $path): array { public function interpret(string $path): array {
$spreadsheet = IOFactory::load($path); $spreadsheet = IOFactory::load($path);
$worksheet = $spreadsheet->getSheet(0); $worksheet = $spreadsheet->getSheet(0);
$result = []; $result = [];
foreach ($worksheet->getColumnIterator() as $column) { foreach ($worksheet->getColumnIterator() as $column) {
$columnIndex = $column->getColumnIndex(); $columnIndex = $column->getColumnIndex();
$value = $worksheet->getCell(sprintf('%s%d', $columnIndex, self::HEADER_ROW_INDEX))->getValueString(); $value = $worksheet->getCell(sprintf('%s%d', $columnIndex, self::HEADER_ROW_INDEX))->getValueString();
if (empty(trim($value))) { if (empty(trim($value))) {
continue; continue;
} }
$guessedAttribute = $this->guessAttribute($value); $guessedAttribute = $this->guessAttribute($value);
$result[] = new ImportColumnInterpretation( $result[] = new ImportColumnInterpretation(
$columnIndex, $columnIndex,
$value, $value,
$guessedAttribute $guessedAttribute
); );
} }
return $result; return $result;
} }
private function guessAttribute(string $value): KeyAttribute|null { private function guessAttribute(string $value): KeyAttribute|null {
$value = trim($value); $value = trim($value);
$value = strtolower($value); $value = strtolower($value);
$value = str_replace(' ', '_', $value); $value = str_replace(' ', '_', $value);
$attribute = match($value) { $attribute = match($value) {
'key' => KeyAttribute::KEY, 'key' => KeyAttribute::KEY,
'name', 'game_name', 'game' => KeyAttribute::GAME_NAME, 'name', 'game_name', 'game' => KeyAttribute::GAME_NAME,
'from' => KeyAttribute::FROM, 'from' => KeyAttribute::FROM,
'store', 'for' => KeyAttribute::STORE, 'store', 'for' => KeyAttribute::STORE,
default => null default => null
}; };
return $attribute; return $attribute;
} }
/** /**
* @param string[] $columnDefinitions * @param string[] $columnDefinitions
*/ */
public function import(string $path, array $columnDefinitions, GamesList $list): array { public function import(string $path, array $columnDefinitions, GamesList $list): array {
$spreadsheet = IOFactory::load($path); $spreadsheet = IOFactory::load($path);
$worksheet = $spreadsheet->getSheet(0); $worksheet = $spreadsheet->getSheet(0);
$totalRows = 0; $totalRows = 0;
$addedAmount = 0; $addedAmount = 0;
foreach ($worksheet->getRowIterator(self::HEADER_ROW_INDEX + 1) as $row) { foreach ($worksheet->getRowIterator(self::HEADER_ROW_INDEX + 1) as $row) {
$totalRows++; $totalRows++;
$values = [ $values = [
'name' => null, 'name' => null,
'key' => null, 'key' => null,
'from' => null, 'from' => null,
'store' => null, 'store' => null,
'store_link' => null 'store_link' => null
]; ];
foreach ($columnDefinitions as $columnIndex => $attribute) { foreach ($columnDefinitions as $columnIndex => $attribute) {
$value = $worksheet->getCell(sprintf('%s%d', $columnIndex, $row->getRowIndex()))->getValueString(); $value = $worksheet->getCell(sprintf('%s%d', $columnIndex, $row->getRowIndex()))->getValueString();
switch(KeyAttribute::from($attribute)) { switch(KeyAttribute::from($attribute)) {
case KeyAttribute::NONE: case KeyAttribute::NONE:
break; break;
case KeyAttribute::GAME_NAME: case KeyAttribute::GAME_NAME:
$values['name'] = $value; $values['name'] = $value;
break; break;
case KeyAttribute::KEY: case KeyAttribute::KEY:
$values['key'] = $value; $values['key'] = $value;
break; break;
case KeyAttribute::FROM: case KeyAttribute::FROM:
$values['from'] = $value; $values['from'] = $value;
break; break;
case KeyAttribute::STORE: case KeyAttribute::STORE:
$store = $this->interpretStore($value); $store = $this->interpretStore($value);
$values['store'] = $store; $values['store'] = $store;
if ($store === Store::EXTERNAL) { if ($store === Store::EXTERNAL) {
$values['store_link'] = $value; $values['store_link'] = $value;
} }
break; break;
} }
} }
if ($values['key'] === null || $values['name'] === null || $values['store'] === null) { if ($values['key'] === null || $values['name'] === null || $values['store'] === null) {
continue; continue;
} }
$game = $this->entityManager->getRepository(Game::class)->findOneBy([ 'name' => $values['name'] ]); $game = $this->entityManager->getRepository(Game::class)->findOneBy([ 'name' => $values['name'] ]);
if ($game === null) { if ($game === null) {
$game = new Game($values['name']); $game = new Game($values['name']);
} }
$key = new Key( $key = new Key(
$game, $game,
$list, $list,
$values['key'], $values['key'],
$values['store'], $values['store'],
$values['store_link'], $values['store_link'],
$values['from'], $values['from'],
); );
$this->entityManager->persist($game); $this->entityManager->persist($game);
$this->entityManager->persist($key); $this->entityManager->persist($key);
$addedAmount++; $addedAmount++;
} }
$this->entityManager->flush(); $this->entityManager->flush();
return [$totalRows, $addedAmount]; return [$totalRows, $addedAmount];
} }
private function interpretStore(string $storeString): Store { private function interpretStore(string $storeString): Store {
$storeString = trim($storeString); $storeString = trim($storeString);
$storeString = strtolower($storeString); $storeString = strtolower($storeString);
$storeString = str_replace(' ', '', $storeString); $storeString = str_replace(' ', '', $storeString);
$triedConversion = Store::tryFrom($storeString); $triedConversion = Store::tryFrom($storeString);
if ($triedConversion !== null) { if ($triedConversion !== null) {
return $triedConversion; return $triedConversion;
} }
if (array_key_exists($storeString, self::STORE_ADDITIONAL_CASES)) { if (array_key_exists($storeString, self::STORE_ADDITIONAL_CASES)) {
return self::STORE_ADDITIONAL_CASES[$storeString]; return self::STORE_ADDITIONAL_CASES[$storeString];
} }
return Store::EXTERNAL; return Store::EXTERNAL;
} }
} }

View file

@ -1,26 +1,26 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Importer; namespace GamesShop\Importer;
use GamesShop\Entities\Games\KeyAttribute; use GamesShop\Entities\Games\KeyAttribute;
use JsonSerializable; use JsonSerializable;
final readonly class ImportColumnInterpretation implements JsonSerializable final readonly class ImportColumnInterpretation implements JsonSerializable
{ {
public function __construct( public function __construct(
public string $index, public string $index,
public string $displayName, public string $displayName,
public KeyAttribute|null $guessedAttribute public KeyAttribute|null $guessedAttribute
) )
{ } { }
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
return [ return [
'index' => $this->index, 'index' => $this->index,
'displayName' => $this->displayName, 'displayName' => $this->displayName,
'guessedAttribute' => $this->guessedAttribute ?? null 'guessedAttribute' => $this->guessedAttribute ?? null
]; ];
} }
} }

View file

@ -1,47 +1,47 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Login; namespace GamesShop\Login;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use GamesShop\Api\DiscordAPI; use GamesShop\Api\DiscordAPI;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class DiscordLoginProvider implements LoginProvider final class DiscordLoginProvider implements LoginProvider
{ {
public function __construct( public function __construct(
private readonly EntityManager $entityManager private readonly EntityManager $entityManager
) )
{ {
} }
public function getUser(ServerRequestInterface $request): User public function getUser(ServerRequestInterface $request): User
{ {
$discordApiHandler = ContainerHandler::get(DiscordAPI::class); $discordApiHandler = ContainerHandler::get(DiscordAPI::class);
$result = $discordApiHandler->getUserFromCode($request->getQueryParams()['code'], (string)$request->getUri()->withQuery('')); $result = $discordApiHandler->getUserFromCode($request->getQueryParams()['code'], (string)$request->getUri()->withQuery(''));
$repo = $this->entityManager->getRepository(User::class); $repo = $this->entityManager->getRepository(User::class);
$users = $repo->findBy(['loginMethod' => LoginMethod::DISCORD, 'foreignLoginId' => $result['id']]); $users = $repo->findBy(['loginMethod' => LoginMethod::DISCORD, 'foreignLoginId' => $result['id']]);
$profilePictureUrl = $discordApiHandler->getAvatarURL($result['id'], $result['avatar'] ?? $result['discriminator'] % 5); $profilePictureUrl = $discordApiHandler->getAvatarURL($result['id'], $result['avatar'] ?? $result['discriminator'] % 5);
if (!empty($users)) { if (!empty($users)) {
$user = $users[0]; $user = $users[0];
$user->setName($result['global_name']); $user->setName($result['global_name']);
$user->setProfilePictureUrl($profilePictureUrl); $user->setProfilePictureUrl($profilePictureUrl);
return $user; return $user;
} }
$newUser = new User( $newUser = new User(
LoginMethod::DISCORD, LoginMethod::DISCORD,
$result['id'], $result['id'],
$result['global_name'], $result['global_name'],
$profilePictureUrl, $profilePictureUrl,
UserPermission::VIEWER UserPermission::VIEWER
); );
return $newUser; return $newUser;
} }
} }

View file

@ -1,76 +1,76 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Login; namespace GamesShop\Login;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Exception; use Exception;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
final class LoginHandler final class LoginHandler
{ {
/** /**
* @return class-string[] * @return class-string[]
*/ */
private static array $providers; private static array $providers;
public function __construct( public function __construct(
private readonly EntityManager $entityManager private readonly EntityManager $entityManager
) )
{ {
} }
public function isLoggedIn(): bool { public function isLoggedIn(): bool {
$this->ensureSession(); $this->ensureSession();
return isset($_SESSION['accountid']); return isset($_SESSION['accountid']);
} }
/** /**
* @throws Exception * @throws Exception
*/ */
public function getLoginProvider(string $method): LoginProvider { public function getLoginProvider(string $method): LoginProvider {
$providers = self::getProviders(); $providers = self::getProviders();
if (!array_key_exists($method, $providers)) { if (!array_key_exists($method, $providers)) {
throw new Exception("Couldn't find method for login '{$method}'"); throw new Exception("Couldn't find method for login '{$method}'");
} }
return ContainerHandler::get($providers[$method]); return ContainerHandler::get($providers[$method]);
} }
public function setCurrentUser(User $user) { public function setCurrentUser(User $user) {
$this->ensureSession(); $this->ensureSession();
$_SESSION['accountid'] = $user->getId(); $_SESSION['accountid'] = $user->getId();
} }
public function getCurrentUser(): User { public function getCurrentUser(): User {
$this->ensureSession(); $this->ensureSession();
$userid = $_SESSION['accountid']; $userid = $_SESSION['accountid'];
return $this->entityManager->getRepository(User::class)->find($userid); return $this->entityManager->getRepository(User::class)->find($userid);
} }
public function deleteSession() { public function deleteSession() {
$this->ensureSession(); $this->ensureSession();
session_destroy(); session_destroy();
} }
private function ensureSession() private function ensureSession()
{ {
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
session_start(); session_start();
} }
} }
/** /**
* @return class-string[] * @return class-string[]
*/ */
private static function getProviders(): array private static function getProviders(): array
{ {
return self::$providers ??= [ return self::$providers ??= [
'discord' => DiscordLoginProvider::class 'discord' => DiscordLoginProvider::class
]; ];
} }
} }

View file

@ -1,23 +1,23 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Login; namespace GamesShop\Login;
enum LoginMethod: int enum LoginMethod: int
{ {
case DISCORD = 1; case DISCORD = 1;
public function getIconClass(): string public function getIconClass(): string
{ {
return match ($this) { return match ($this) {
self::DISCORD => 'fa-discord', self::DISCORD => 'fa-discord',
}; };
} }
public function getHumanReadableName(): string public function getHumanReadableName(): string
{ {
return match ($this) { return match ($this) {
self::DISCORD => 'Discord', self::DISCORD => 'Discord',
}; };
} }
} }

View file

@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Login; namespace GamesShop\Login;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
interface LoginProvider interface LoginProvider
{ {
public function getUser(ServerRequestInterface $request): User; public function getUser(ServerRequestInterface $request): User;
} }

View file

@ -1,25 +1,25 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Login; namespace GamesShop\Login;
enum UserPermission : int enum UserPermission : int
{ {
case NONE = 0; 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 { public function hasLevel(UserPermission $userPermission): bool {
return $this->value >= $userPermission->value; 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", default => "None",
}; };
} }
} }

View file

@ -1,13 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop; namespace GamesShop;
final class Paths final class Paths
{ {
public const string ROOT_PATH = __DIR__ . '/../..'; public const string ROOT_PATH = __DIR__ . '/../..';
public const string PUBLIC_PATH = self::ROOT_PATH . '/public'; public const string PUBLIC_PATH = self::ROOT_PATH . '/public';
public const string SOURCE_PATH = self::ROOT_PATH . '/src'; public const string SOURCE_PATH = self::ROOT_PATH . '/src';
public const string PHP_SOURCE_PATH = self::SOURCE_PATH . '/php'; public const string PHP_SOURCE_PATH = self::SOURCE_PATH . '/php';
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,78 +1,78 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing\Api\DataTables; namespace GamesShop\Routing\Api\DataTables;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order; use Doctrine\Common\Collections\Order;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
use GamesShop\Entities\Games\Game; use GamesShop\Entities\Games\Game;
use GamesShop\Entities\Games\Key; use GamesShop\Entities\Games\Key;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\JsonResponse; use Laminas\Diactoros\Response\JsonResponse;
use League\Route\Http\Exception\BadRequestException; use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException; use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException; use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class ProviderKeysEndpoint final class ProviderKeysEndpoint
{ {
public function __construct( public function __construct(
private readonly LoginHandler $loginHandler, private readonly LoginHandler $loginHandler,
private readonly EntityManager $entityManager, private readonly EntityManager $entityManager,
) )
{ {
} }
public function __invoke(ServerRequestInterface $request): ResponseInterface public function __invoke(ServerRequestInterface $request): ResponseInterface
{ {
if (!$this->loginHandler->isLoggedIn()) { if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
$user = $this->loginHandler->getCurrentUser(); $user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
$body = $request->getQueryParams(); $body = $request->getQueryParams();
if (!array_key_exists('listid', $body)) { if (!array_key_exists('listid', $body)) {
throw new BadRequestException(); throw new BadRequestException();
} }
$list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]);
if (!$list instanceof GamesList) { if (!$list instanceof GamesList) {
throw new BadRequestException(); throw new BadRequestException();
} }
$keys = $this->entityManager->getRepository(Key::class)->findBy(['list' => $list]); $keys = $this->entityManager->getRepository(Key::class)->findBy(['list' => $list]);
$gameToKeyArray = []; $gameToKeyArray = [];
foreach ($keys as $key) { foreach ($keys as $key) {
$game = $key->getGame(); $game = $key->getGame();
$id = $game->getId(); $id = $game->getId();
if (!array_key_exists($id, $gameToKeyArray)) { if (!array_key_exists($id, $gameToKeyArray)) {
$gameToKeyArray[$id] = [ $game, [] ]; $gameToKeyArray[$id] = [ $game, [] ];
} }
$gameToKeyArray[$id][1][] = $key; $gameToKeyArray[$id][1][] = $key;
} }
$result = []; $result = [];
foreach ($gameToKeyArray as [$game, $keys]) { foreach ($gameToKeyArray as [$game, $keys]) {
$result[] = [ $result[] = [
'gamePicture' => '', 'gamePicture' => '',
'name' => $game->getName(), 'name' => $game->getName(),
'keysAmount' => count($keys), 'keysAmount' => count($keys),
'igdbState' => 'not implermented', 'igdbState' => 'not implermented',
'keys' => $keys, 'keys' => $keys,
]; ];
} }
return new JsonResponse([ 'data' => $result ]); return new JsonResponse([ 'data' => $result ]);
} }
} }

View file

@ -1,49 +1,49 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing\Api\Web; namespace GamesShop\Routing\Api\Web;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response; use Laminas\Diactoros\Response;
use League\Route\Http\Exception\BadRequestException; use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException; use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException; use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class CreateKeyListRoute final class CreateKeyListRoute
{ {
public function __construct( public function __construct(
private readonly LoginHandler $loginHandler, private readonly LoginHandler $loginHandler,
private readonly EntityManager $entityManager private readonly EntityManager $entityManager
) )
{ {
} }
public function __invoke(ServerRequestInterface $request): ResponseInterface public function __invoke(ServerRequestInterface $request): ResponseInterface
{ {
if (!$this->loginHandler->isLoggedIn()) { if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
$user = $this->loginHandler->getCurrentUser(); $user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
$body = $request->getParsedBody(); $body = $request->getParsedBody();
if (!array_key_exists('name', $body)) { if (!array_key_exists('name', $body)) {
throw new BadRequestException(); throw new BadRequestException();
} }
$name = $body['name']; $name = $body['name'];
$list = new GamesList($user, $name); $list = new GamesList($user, $name);
$this->entityManager->persist($list); $this->entityManager->persist($list);
$this->entityManager->flush(); $this->entityManager->flush();
return new Response(); return new Response();
} }
} }

View file

@ -1,50 +1,50 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing\Api\Web; namespace GamesShop\Routing\Api\Web;
use GamesShop\Importer\GameImporter; use GamesShop\Importer\GameImporter;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\JsonResponse; use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\UploadedFile; use Laminas\Diactoros\UploadedFile;
use League\Route\Http\Exception\BadRequestException; use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException; use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException; use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class ImportKeysPrepareRoute final class ImportKeysPrepareRoute
{ {
public function __construct( public function __construct(
private readonly LoginHandler $loginHandler, private readonly LoginHandler $loginHandler,
private readonly GameImporter $importer, private readonly GameImporter $importer,
) { } ) { }
public function __invoke(ServerRequestInterface $request): ResponseInterface public function __invoke(ServerRequestInterface $request): ResponseInterface
{ {
if (!$this->loginHandler->isLoggedIn()) { if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
$user = $this->loginHandler->getCurrentUser(); $user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
/** /**
* @var UploadedFile $file * @var UploadedFile $file
*/ */
$file = $request->getUploadedFiles()['file'] ?? null; $file = $request->getUploadedFiles()['file'] ?? null;
if (!$file === null) { if (!$file === null) {
throw new BadRequestException(); throw new BadRequestException();
} }
$fileName = tempnam(sys_get_temp_dir(), 'ImportKeys'); $fileName = tempnam(sys_get_temp_dir(), 'ImportKeys');
$file->moveTo($fileName); $file->moveTo($fileName);
$results = $this->importer->interpret($fileName); $results = $this->importer->interpret($fileName);
unlink($fileName); unlink($fileName);
return new JsonResponse($results); return new JsonResponse($results);
} }
} }

View file

@ -1,66 +1,66 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing\Api\Web; namespace GamesShop\Routing\Api\Web;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use GamesShop\Importer\GameImporter; use GamesShop\Importer\GameImporter;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\JsonResponse; use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\UploadedFile; use Laminas\Diactoros\UploadedFile;
use League\Route\Http\Exception\BadRequestException; use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException; use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException; use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class ImportKeysRoute final class ImportKeysRoute
{ {
public function __construct( public function __construct(
private readonly LoginHandler $loginHandler, private readonly LoginHandler $loginHandler,
private readonly GameImporter $importer, private readonly GameImporter $importer,
private readonly EntityManager $entityManager, private readonly EntityManager $entityManager,
) { } ) { }
public function __invoke(ServerRequestInterface $request): ResponseInterface public function __invoke(ServerRequestInterface $request): ResponseInterface
{ {
if (!$this->loginHandler->isLoggedIn()) { if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
$user = $this->loginHandler->getCurrentUser(); $user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
$body = $request->getParsedBody(); $body = $request->getParsedBody();
if (!array_key_exists('listid', $body)) { if (!array_key_exists('listid', $body)) {
throw new BadRequestException(); throw new BadRequestException();
} }
$list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]);
if (!$list instanceof GamesList) { if (!$list instanceof GamesList) {
throw new BadRequestException(); throw new BadRequestException();
} }
/** /**
* @var UploadedFile $file * @var UploadedFile $file
*/ */
$file = $request->getUploadedFiles()['file'] ?? null; $file = $request->getUploadedFiles()['file'] ?? null;
if (!$file === null) { if (!$file === null) {
throw new BadRequestException(); throw new BadRequestException();
} }
$fileName = tempnam(sys_get_temp_dir(), 'ImportKeys'); $fileName = tempnam(sys_get_temp_dir(), 'ImportKeys');
$file->moveTo($fileName); $file->moveTo($fileName);
$columnDefs = $request->getParsedBody()['columns']; $columnDefs = $request->getParsedBody()['columns'];
[$total, $imported] = $this->importer->import($fileName, $columnDefs, $list); [$total, $imported] = $this->importer->import($fileName, $columnDefs, $list);
unlink($fileName); unlink($fileName);
return new JsonResponse([ 'success' => true, 'total' => $total, 'imported' => $imported ]); return new JsonResponse([ 'success' => true, 'total' => $total, 'imported' => $imported ]);
} }
} }

View file

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

View file

@ -1,18 +1,18 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing\Api\Web; namespace GamesShop\Routing\Api\Web;
use League\Route\RouteGroup; use League\Route\RouteGroup;
final class WebAPIRoutes final class WebAPIRoutes
{ {
public static function applyRoutes(RouteGroup $group): void { public static function applyRoutes(RouteGroup $group): void {
$group->post('/users/{id:number}', UserModifyRoute::class); $group->post('/users/{id:number}', UserModifyRoute::class);
$group->post('/keys/import/prepare', ImportKeysPrepareRoute::class); $group->post('/keys/import/prepare', ImportKeysPrepareRoute::class);
$group->post('/keys/import/perform', ImportKeysRoute::class); $group->post('/keys/import/perform', ImportKeysRoute::class);
$group->post('/keys/list/create', CreateKeyListRoute::class); $group->post('/keys/list/create', CreateKeyListRoute::class);
} }
} }

View file

@ -1,20 +1,20 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; namespace GamesShop\Routing;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
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;
final class ErrorRoute final class ErrorRoute
{ {
public function renderErrorPage(int $errorCode): ResponseInterface { public function renderErrorPage(int $errorCode): ResponseInterface {
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderErrorPage($errorCode); $pageContent = ContainerHandler::get(TemplateEngine::class)->renderErrorPage($errorCode);
$response = new Response(status: $errorCode); $response = new Response(status: $errorCode);
$response->getBody()->write($pageContent); $response->getBody()->write($pageContent);
return $response; return $response;
} }
} }

View file

@ -1,33 +1,33 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; namespace GamesShop\Routing;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Templates\TemplateEngine; use GamesShop\Templates\TemplateEngine;
use Laminas\Diactoros\Response; use Laminas\Diactoros\Response;
use Laminas\Diactoros\Response\RedirectResponse; use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class IndexRoute final class IndexRoute
{ {
public function __invoke(ServerRequestInterface $request): ResponseInterface { public function __invoke(ServerRequestInterface $request): ResponseInterface {
$loginHandler = ContainerHandler::get(LoginHandler::class); $loginHandler = ContainerHandler::get(LoginHandler::class);
if (!$loginHandler->isLoggedIn()) { if (!$loginHandler->isLoggedIn()) {
return new RedirectResponse('/login'); return new RedirectResponse('/login');
} }
$pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage('index'); $pageContent = ContainerHandler::get(TemplateEngine::class)->renderPage('index');
$response = new Response; $response = new Response;
$response->getBody()->write($pageContent); $response->getBody()->write($pageContent);
return $response; return $response;
} }
public static function applyRoutes(\League\Route\Router $router): void { public static function applyRoutes(\League\Route\Router $router): void {
$router->get('/', self::class); $router->get('/', self::class);
} }
} }

View file

@ -1,45 +1,45 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; namespace GamesShop\Routing;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use GamesShop\Routing\Responses\TemplateResponse; use GamesShop\Routing\Responses\TemplateResponse;
use League\Route\Http\Exception\ForbiddenException; use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException; use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class KeysRoute final class KeysRoute
{ {
public function __construct( public function __construct(
private readonly LoginHandler $loginHandler, private readonly LoginHandler $loginHandler,
private readonly EntityManager $entityManager private readonly EntityManager $entityManager
) )
{ {
} }
public function __invoke(ServerRequestInterface $request): ResponseInterface public function __invoke(ServerRequestInterface $request): ResponseInterface
{ {
if (!$this->loginHandler->isLoggedIn()) { if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
$user = $this->loginHandler->getCurrentUser(); $user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
$entityManager = $this->entityManager->getRepository(GamesList::class); $entityManager = $this->entityManager->getRepository(GamesList::class);
$lists = $entityManager->findBy([ 'owner' => $user ]); $lists = $entityManager->findBy([ 'owner' => $user ]);
return new TemplateResponse('key-manager', [ 'usersLists' => $lists ]); return new TemplateResponse('key-manager', [ 'usersLists' => $lists ]);
} }
public static function applyRoutes(\League\Route\Router $router): void public static function applyRoutes(\League\Route\Router $router): void
{ {
$router->get('/keys', KeysRoute::class); $router->get('/keys', KeysRoute::class);
} }
} }

View file

@ -1,72 +1,72 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; namespace GamesShop\Routing;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\OptimisticLockException; 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\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;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class LoginRoutes final class LoginRoutes
{ {
public function __construct( public function __construct(
private readonly EntityManager $entityManager private readonly EntityManager $entityManager
) )
{ {
} }
public function login(ServerRequestInterface $request) { public function login(ServerRequestInterface $request) {
$discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment(); $discordEnv = ContainerHandler::get(EnvironmentHandler::class)->getDiscordEnvironment();
return new TemplateResponse('login', [ return new TemplateResponse('login', [
'discordUrl' => $discordEnv->loginUrl 'discordUrl' => $discordEnv->loginUrl
]); ]);
} }
/** /**
* @throws OptimisticLockException * @throws OptimisticLockException
* @throws ORMException * @throws ORMException
*/ */
public function loginCallback(ServerRequestInterface $request, array $args): ResponseInterface { public function loginCallback(ServerRequestInterface $request, array $args): ResponseInterface {
if (array_key_exists('error', $request->getQueryParams())) { if (array_key_exists('error', $request->getQueryParams())) {
return new Response\RedirectResponse('/login'); return new Response\RedirectResponse('/login');
} }
$method = $args['method']; $method = $args['method'];
$loginHandler = ContainerHandler::get(LoginHandler::class); $loginHandler = ContainerHandler::get(LoginHandler::class);
$loginProvider = $loginHandler->getLoginProvider($method); $loginProvider = $loginHandler->getLoginProvider($method);
$user = $loginProvider->getUser($request); $user = $loginProvider->getUser($request);
if ($user->getId() === null) { if ($user->getId() === null) {
$this->entityManager->persist($user); $this->entityManager->persist($user);
$this->entityManager->flush(); $this->entityManager->flush();
} }
$loginHandler->setCurrentUser($user); $loginHandler->setCurrentUser($user);
return new Response\RedirectResponse('/'); return new Response\RedirectResponse('/');
} }
public function logout(ServerRequestInterface $request): ResponseInterface public function logout(ServerRequestInterface $request): ResponseInterface
{ {
$loginHandler = ContainerHandler::get(LoginHandler::class); $loginHandler = ContainerHandler::get(LoginHandler::class);
$loginHandler->deleteSession(); $loginHandler->deleteSession();
return new Response\RedirectResponse('/login'); return new Response\RedirectResponse('/login');
} }
public static function addRoutes(\League\Route\Router $router): void { public static function addRoutes(\League\Route\Router $router): void {
$routes = ContainerHandler::get(LoginRoutes::class); $routes = ContainerHandler::get(LoginRoutes::class);
$router->get('/login', $routes->login(...)); $router->get('/login', $routes->login(...));
$router->get('/login-callback/{method:word}', $routes->loginCallback(...)); $router->get('/login-callback/{method:word}', $routes->loginCallback(...));
$router->get('/logout', $routes->logout(...)); $router->get('/logout', $routes->logout(...));
} }
} }

View file

@ -1,55 +1,55 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; 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 Laminas\Diactoros\Uri;
use Mimey\MimeTypes; use Mimey\MimeTypes;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
final class ResourceRoute final class ResourceRoute
{ {
private const array RESOURCE_EXTENSIONS = [ private const array RESOURCE_EXTENSIONS = [
'js', 'js',
'css', 'css',
'ttf', 'woff', 'woff2', 'ttf', 'woff', 'woff2',
'gif', 'svg', 'png', 'jpg' 'gif', 'svg', 'png', 'jpg'
]; ];
public function isValid(Uri $uri): bool { public function isValid(Uri $uri): bool {
$path = $uri->getPath(); $path = $uri->getPath();
foreach (self::RESOURCE_EXTENSIONS as $extension) { foreach (self::RESOURCE_EXTENSIONS as $extension) {
if (!str_ends_with($path, $extension)) { if (!str_ends_with($path, $extension)) {
continue; continue;
} }
return true; return true;
} }
return false; return false;
} }
public function getResponse(Uri $uri): ResponseInterface { public function getResponse(Uri $uri): ResponseInterface {
$filePath = Paths::PUBLIC_PATH . $uri->getPath(); $filePath = Paths::PUBLIC_PATH . $uri->getPath();
if (!file_exists($filePath)) { if (!file_exists($filePath)) {
$response = new Response(status: 404); $response = new Response(status: 404);
$response->getBody()->write('File not found'); $response->getBody()->write('File not found');
return $response; return $response;
} }
$mimey = ContainerHandler::get(MimeTypes::class); $mimey = ContainerHandler::get(MimeTypes::class);
$response = new Response( $response = new Response(
headers: [ headers: [
'Content-Type' => $mimey->getMimeType(pathinfo($filePath, PATHINFO_EXTENSION)), 'Content-Type' => $mimey->getMimeType(pathinfo($filePath, PATHINFO_EXTENSION)),
'Cache-Control' => 'public, max-age=3600, must-revalidate', 'Cache-Control' => 'public, max-age=3600, must-revalidate',
] ]
); );
$response->getBody()->write(file_get_contents($filePath)); $response->getBody()->write(file_get_contents($filePath));
return $response; return $response;
} }
} }

View file

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

View file

@ -1,61 +1,61 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; namespace GamesShop\Routing;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Routing\Api\APIRoutes; 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\BadRequestException;
use League\Route\Http\Exception\ForbiddenException; 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\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( public function __construct(
private ResourceRoute $resourceRoute 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())) { if ($this->resourceRoute->isValid($request->getUri())) {
return $this->resourceRoute->getResponse($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);
IndexRoute::applyRoutes($router); IndexRoute::applyRoutes($router);
LoginRoutes::addRoutes($router); LoginRoutes::addRoutes($router);
SetupRoute::applyRoutes($router); SetupRoute::applyRoutes($router);
KeysRoute::applyRoutes($router); KeysRoute::applyRoutes($router);
AdminAccountConfigRoute::applyRoutes($router); AdminAccountConfigRoute::applyRoutes($router);
APIRoutes::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) { } catch (UnauthorizedException) {
return (new ErrorRoute())->renderErrorPage(401); return (new ErrorRoute())->renderErrorPage(401);
} catch (ForbiddenException) { } catch (ForbiddenException) {
return (new ErrorRoute())->renderErrorPage(403); return (new ErrorRoute())->renderErrorPage(403);
} }
} }
} }

View file

@ -1,52 +1,52 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Routing; namespace GamesShop\Routing;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use GamesShop\Entities\SystemAttribute; use GamesShop\Entities\SystemAttribute;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\RedirectResponse; use Laminas\Diactoros\Response\RedirectResponse;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
final class SetupRoute final class SetupRoute
{ {
public function __construct( public function __construct(
private readonly EntityManager $entityManager, private readonly EntityManager $entityManager,
private readonly LoginHandler $loginHandler private readonly LoginHandler $loginHandler
) )
{ {
} }
public function __invoke(ServerRequestInterface $request): ResponseInterface { public function __invoke(ServerRequestInterface $request): ResponseInterface {
if (!$this->loginHandler->isLoggedIn()) { if (!$this->loginHandler->isLoggedIn()) {
return new RedirectResponse('/login'); return new RedirectResponse('/login');
} }
$repo = $this->entityManager->getRepository(SystemAttribute::class); $repo = $this->entityManager->getRepository(SystemAttribute::class);
$attribute = $repo->find('ADMIN_SETUP_COMPLETED'); $attribute = $repo->find('ADMIN_SETUP_COMPLETED');
if ($attribute) { if ($attribute) {
return new RedirectResponse('/'); return new RedirectResponse('/');
} }
$user = $this->loginHandler->getCurrentUser(); $user = $this->loginHandler->getCurrentUser();
$user->setPermission(UserPermission::ADMIN); $user->setPermission(UserPermission::ADMIN);
$attribute = new SystemAttribute( $attribute = new SystemAttribute(
'ADMIN_SETUP_COMPLETED', 'ADMIN_SETUP_COMPLETED',
'true' 'true'
); );
$this->entityManager->persist($attribute); $this->entityManager->persist($attribute);
$this->entityManager->flush(); $this->entityManager->flush();
return new RedirectResponse('/'); return new RedirectResponse('/');
} }
public static function applyRoutes(\League\Route\Router $router) { public static function applyRoutes(\League\Route\Router $router) {
$router->get('/setup-admin', self::class); $router->get('/setup-admin', self::class);
} }
} }

View file

@ -1,13 +1,13 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop; namespace GamesShop;
final class SetupHandler final class SetupHandler
{ {
public function __construct( public function __construct(
) )
{ {
} }
} }

View file

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

View file

@ -1,16 +1,16 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Templates; namespace GamesShop\Templates;
final class ResourceEntry final class ResourceEntry
{ {
/** /**
* @param string[] $js * @param string[] $js
* @param string[] $css * @param string[] $css
*/ */
public function __construct( public function __construct(
public readonly array $js, public readonly array $js,
public readonly array $css, public readonly array $css,
) { } ) { }
} }

View file

@ -1,51 +1,51 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Templates; namespace GamesShop\Templates;
use Exception; use Exception;
use GamesShop\Paths; use GamesShop\Paths;
final class ResourceIndex final class ResourceIndex
{ {
private const string PATH = Paths::SOURCE_PATH . '/file-index.json'; private const string PATH = Paths::SOURCE_PATH . '/file-index.json';
/** /**
* @var ResourceEntry[] * @var ResourceEntry[]
*/ */
private array $resources = []; private array $resources = [];
public function __construct() public function __construct()
{ {
$fileContents = file_get_contents(self::PATH); $fileContents = file_get_contents(self::PATH);
$index = json_decode($fileContents, true); $index = json_decode($fileContents, true);
foreach ($index as $entryKey => $resource) { foreach ($index as $entryKey => $resource) {
$js = $resource['js']; $js = $resource['js'];
if (is_string($js)) { if (is_string($js)) {
$js = [$js]; $js = [$js];
} }
$css = $resource['css']; $css = $resource['css'];
if (is_string($css)) { if (is_string($css)) {
$css = [$css]; $css = [$css];
} }
$this->resources[$entryKey] = new ResourceEntry( $this->resources[$entryKey] = new ResourceEntry(
$js, $css $js, $css
); );
} }
} }
/** /**
* @throws Exception * @throws Exception
*/ */
public function getResource(string $entry): ResourceEntry public function getResource(string $entry): ResourceEntry
{ {
if (!array_key_exists($entry, $this->resources)) { if (!array_key_exists($entry, $this->resources)) {
throw new Exception("Entry '$entry' not found"); throw new Exception("Entry '$entry' not found");
} }
return $this->resources[$entry]; return $this->resources[$entry];
} }
} }

View file

@ -1,34 +1,34 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace GamesShop\Templates; namespace GamesShop\Templates;
use GamesShop\Login\LoginHandler; use GamesShop\Login\LoginHandler;
use GamesShop\Paths; use GamesShop\Paths;
use League\Plates\Engine; use League\Plates\Engine;
final class TemplateEngine extends Engine final class TemplateEngine extends Engine
{ {
private const string TEMPLATES_PATH = Paths::SOURCE_PATH . '/templates'; private const string TEMPLATES_PATH = Paths::SOURCE_PATH . '/templates';
public function __construct( public function __construct(
private ResourceIndex $resourceIndex, private ResourceIndex $resourceIndex,
LoginHandler $loginHandler, 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, 'activeUser' => $loginHandler->isLoggedIn() ? $loginHandler->getCurrentUser() : null,
]); ]);
} }
public function renderPage(string $page, array $data = array()) public function renderPage(string $page, array $data = array())
{ {
return parent::render("pages/$page", $data); return parent::render("pages/$page", $data);
} }
public function renderErrorPage(int $error) { public function renderErrorPage(int $error) {
return self::renderPage('error', [ 'errorCode' => $error ]); return self::renderPage('error', [ 'errorCode' => $error ]);
} }
} }

View file

@ -1,15 +1,15 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
require_once __DIR__ . '/../bootstrap.php'; require_once __DIR__ . '/../bootstrap.php';
$entityManager = ContainerHandler::get(EntityManager::class); $entityManager = ContainerHandler::get(EntityManager::class);
ConsoleRunner::run( ConsoleRunner::run(
new SingleManagerProvider($entityManager) new SingleManagerProvider($entityManager)
); );

View file

@ -1,11 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use GamesShop\DoctrineManager; use GamesShop\DoctrineManager;
use GamesShop\Environment\EnvironmentHandler; use GamesShop\Environment\EnvironmentHandler;
require_once __DIR__ . '/../../vendor/autoload.php'; require_once __DIR__ . '/../../vendor/autoload.php';
ContainerHandler::get(EnvironmentHandler::class)->load(); ContainerHandler::get(EnvironmentHandler::class)->load();
ContainerHandler::get(DoctrineManager::class)->setup(); ContainerHandler::get(DoctrineManager::class)->setup();

View file

@ -1,25 +1,25 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\ContainerHandler; use GamesShop\ContainerHandler;
use GamesShop\DoctrineManager; use GamesShop\DoctrineManager;
use GamesShop\Environment\EnvironmentHandler; use GamesShop\Environment\EnvironmentHandler;
use GamesShop\Routing\Router; use GamesShop\Routing\Router;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Whoops\Handler\HandlerInterface; use Whoops\Handler\HandlerInterface;
use Whoops\Handler\PrettyPageHandler; use Whoops\Handler\PrettyPageHandler;
use Whoops\Run; use Whoops\Run;
require_once __DIR__ . '/../src/php/bootstrap.php'; require_once __DIR__ . '/../src/php/bootstrap.php';
$whoops = new Run(); $whoops = new Run();
$prettyPageHandler = new PrettyPageHandler(); $prettyPageHandler = new PrettyPageHandler();
$whoops->pushHandler($prettyPageHandler); $whoops->pushHandler($prettyPageHandler);
$whoops->register(); $whoops->register();
ContainerHandler::getInstance()->addShared(HandlerInterface::class, $prettyPageHandler); ContainerHandler::getInstance()->addShared(HandlerInterface::class, $prettyPageHandler);
$router = ContainerHandler::getInstance()->get(Router::class); $router = ContainerHandler::getInstance()->get(Router::class);
$result = $router->route(); $result = $router->route();
(new SapiEmitter)->emit($result); (new SapiEmitter)->emit($result);

View file

@ -1,2 +1,2 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);

View file

@ -1,35 +1,35 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
/** @var User|null $activeUser */ /** @var User|null $activeUser */
?> ?>
<?php if ($activeUser !== null): ?> <?php if ($activeUser !== null): ?>
<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="<?= $activeUser->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 <?= $activeUser->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">
<?= $activeUser->getName() ?> <?= $activeUser->getName() ?>
</span> </span>
<small class="text-muted"> <small class="text-muted">
<?= $activeUser->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">
<a href="/logout"> <a href="/logout">
<i class="fa-solid fa-arrow-right-to-bracket fa-xl text-danger"></i> <i class="fa-solid fa-arrow-right-to-bracket fa-xl text-danger"></i>
</a> </a>
</div> </div>
</div> </div>
<?php endif ?> <?php endif ?>

View file

@ -1,5 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
?> ?>

View file

@ -1,49 +1,49 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\Templates\ResourceIndex; use GamesShop\Templates\ResourceIndex;
use League\Plates\Template\Template; use League\Plates\Template\Template;
assert($this instanceof Template); assert($this instanceof Template);
/** @var ResourceIndex $resources */ /** @var ResourceIndex $resources */
assert($resources instanceof ResourceIndex); assert($resources instanceof ResourceIndex);
if (!isset($resourceEntry)) { if (!isset($resourceEntry)) {
throw new Exception("Resource entry not set"); throw new Exception("Resource entry not set");
} }
/** /**
* @var string $resourceEntry * @var string $resourceEntry
*/ */
$resource = $resources->getResource($resourceEntry); $resource = $resources->getResource($resourceEntry);
?> ?>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Game Shop</title> <title>Game Shop</title>
<?php foreach ($resource->js as $js): ?> <?php foreach ($resource->js as $js): ?>
<script src="/<?= $js ?>"></script> <script src="/<?= $js ?>"></script>
<?php endforeach; ?> <?php endforeach; ?>
<?php foreach ($resource->css as $css): ?> <?php foreach ($resource->css as $css): ?>
<link rel="stylesheet" href="/<?= $css ?>"> <link rel="stylesheet" href="/<?= $css ?>">
<?php endforeach; ?> <?php endforeach; ?>
</head> </head>
<body class="vh-100 d-flex flex-column"> <body class="vh-100 d-flex flex-column">
<?= $this->insert('layout/navbar') ?> <?= $this->insert('layout/navbar') ?>
<main class="mt-2 position-relative flex-grow-1"> <main class="mt-2 position-relative flex-grow-1">
<?= $this->section('content'); ?> <?= $this->section('content'); ?>
</main> </main>
<?= $this->section('modal') ?> <?= $this->section('modal') ?>
<div class="position-absolute bottom-0 start-50 opacity-25 text-center translate-middle-x"> <div class="position-absolute bottom-0 start-50 opacity-25 text-center translate-middle-x">
<span class="h1">PROTOTYPE / POC</span> <span class="h1">PROTOTYPE / POC</span>
</div> </div>
</body> </body>
</html> </html>

View file

@ -1,53 +1,53 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\Entities\Account\User; use GamesShop\Entities\Account\User;
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
use GamesShop\Templates\NavigationHeader; use GamesShop\Templates\NavigationHeader;
$headers = [ $headers = [
new NavigationHeader('My Keys', '/keys', UserPermission::PROVIDER), new NavigationHeader('My Keys', '/keys', UserPermission::PROVIDER),
new NavigationHeader('Accounts', '/accounts', UserPermission::ADMIN) new NavigationHeader('Accounts', '/accounts', UserPermission::ADMIN)
]; ];
/** @var User|null $activeUser */ /** @var User|null $activeUser */
$currentPermission = $activeUser === null ? UserPermission::NONE : $activeUser->getPermission(); $currentPermission = $activeUser === null ? UserPermission::NONE : $activeUser->getPermission();
?> ?>
<nav class="navbar navbar-expand-lg bg-body-tertiary main-navigation"> <nav class="navbar navbar-expand-lg bg-body-tertiary main-navigation">
<div class="container-fluid navigation-container"> <div class="container-fluid navigation-container">
<a class="navbar-brand" href="/">Game Shop</a> <a class="navbar-brand" href="/">Game Shop</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-content" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-content" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<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 class="navbar-nav me-auto mb-2 mb-lg-0">
<?php foreach ($headers as $header): <?php foreach ($headers as $header):
if (!$currentPermission->hasLevel($header->minimumPermission)) { if (!$currentPermission->hasLevel($header->minimumPermission)) {
continue; continue;
} }
?> ?>
<li class="nav-link"> <li class="nav-link">
<a href="<?= $header->link ?>" class="nav-link"><?= $header->title ?></a> <a href="<?= $header->link ?>" class="nav-link"><?= $header->title ?></a>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?= $this->insert('layout/accountDisplay'); ?> <?= $this->insert('layout/accountDisplay'); ?>
</div> </div>
</div> </div>
<div class="d-flex mode-switch me-3"> <div class="d-flex mode-switch me-3">
<button title="Use dark mode" id="dark" class="btn btn-sm btn-default text-secondary"> <button title="Use dark mode" id="dark" class="btn btn-sm btn-default text-secondary">
<i class="fa-regular fa-moon"></i> <i class="fa-regular fa-moon"></i>
</button> </button>
<button title="Use light mode" id="light" class="btn btn-sm btn-default text-secondary"> <button title="Use light mode" id="light" class="btn btn-sm btn-default text-secondary">
<i class="fa-regular fa-sun"></i> <i class="fa-regular fa-sun"></i>
</button> </button>
<button title="Use system preferred mode" id="system" class="btn btn-sm btn-default text-secondary"> <button title="Use system preferred mode" id="system" class="btn btn-sm btn-default text-secondary">
<i class="fa-solid fa-display"></i> <i class="fa-solid fa-display"></i>
</button> </button>
</div> </div>
</nav> </nav>

View file

@ -1,57 +1,57 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\Login\UserPermission; use GamesShop\Login\UserPermission;
$this->layout('layout/main', [ 'resourceEntry' => 'admin/accounts' ]); $this->layout('layout/main', [ 'resourceEntry' => 'admin/accounts' ]);
?> ?>
<h1>Users</h1> <h1>Users</h1>
<table id="user-table" class="table table-striped w-100"> <table id="user-table" class="table table-striped w-100">
<thead> <thead>
<tr> <tr>
<th width="2.4rem"></th> <th width="2.4rem"></th>
<th>Name</th> <th>Name</th>
<th>Permission</th> <th>Permission</th>
<th>Login-Method</th> <th>Login-Method</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<?php $this->start('modal') ?> <?php $this->start('modal') ?>
<div class="modal" id="edit-modal"> <div class="modal" id="edit-modal">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title h3"> <h1 class="modal-title h3">
Edit User Edit User
</h1> </h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-2"> <div class="mb-2">
Name: <span class="name-input"></span> Name: <span class="name-input"></span>
</div> </div>
<div class="mb-2"> <div class="mb-2">
Login Method: <span class="login-method"></span> Login Method: <span class="login-method"></span>
</div> </div>
<div> <div>
<label for="permissions">Permissions:</label> <label for="permissions">Permissions:</label>
<select name="" id="permissions" class="form-select permission-editor"> <select name="" id="permissions" class="form-select permission-editor">
<?php foreach (UserPermission::cases() as $userPermission):?> <?php foreach (UserPermission::cases() as $userPermission):?>
<option value="<?= $userPermission->value ?>"><?= $userPermission->getHumanReadableName() ?></option> <option value="<?= $userPermission->value ?>"><?= $userPermission->getHumanReadableName() ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <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> <button type="button" class="btn btn-primary js--save">Save changes</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<?php $this->end() ?> <?php $this->end() ?>

View file

@ -1,11 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
$this->layout('layout/main', [ 'resourceEntry' => 'index' ]); $this->layout('layout/main', [ 'resourceEntry' => 'index' ]);
/** /**
* @var int $errorCode * @var int $errorCode
*/ */
?> ?>
<img src="https://http.dog/<?= $errorCode ?>.jpg" alt="Error <?= $errorCode ?>" class="position-absolute top-50 start-50 translate-middle w-100" style="max-width: 800px" /> <img src="https://http.dog/<?= $errorCode ?>.jpg" alt="Error <?= $errorCode ?>" class="position-absolute top-50 start-50 translate-middle w-100" style="max-width: 800px" />

View file

@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
$this->layout('layout/main', [ 'resourceEntry' => 'index' ]); $this->layout('layout/main', [ 'resourceEntry' => 'index' ]);
?> ?>
<h1> <h1>
Hello Hello
</h1> </h1>

View file

@ -1,143 +1,153 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use GamesShop\Entities\Games\KeyAttribute; use GamesShop\Entities\Games\KeyAttribute;
use GamesShop\Entities\GamesList; use GamesShop\Entities\GamesList;
use League\Plates\Template\Template; use League\Plates\Template\Template;
assert($this instanceof Template); assert($this instanceof Template);
/** @var GamesList[] $usersLists */ /** @var GamesList[] $usersLists */
$this->layout('layout/main', [ 'resourceEntry' => 'keys' ]); $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
?> ?>
<meta name="key-attributes" content="<?= htmlspecialchars(json_encode(KeyAttribute::casesAsAssociative())) ?>" /> <meta name="key-attributes" content="<?= htmlspecialchars(json_encode(KeyAttribute::casesAsAssociative())) ?>" />
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<h1>My Keys</h1> <h1>My Keys</h1>
</div> </div>
<div class="col-sm-6 align-self-center"> <div class="col-sm-6 align-self-center">
<?php if (!empty($usersLists)): ?> <?php if (!empty($usersLists)): ?>
<select name="lists" id="list-select" class="form-select w-100"> <select name="lists" id="list-select" class="form-select w-100">
<?php foreach ($usersLists as $list): ?> <?php foreach ($usersLists as $list): ?>
<option value="<?= $list->getId() ?>"><?= $list->getName() ?></option> <option value="<?= $list->getId() ?>"><?= $list->getName() ?></option>
<?php endforeach; ?> <?php endforeach; ?>
<option value="_create">+ Create New</option> <option value="_create">+ Create New</option>
</select> </select>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
<?php if (empty($usersLists)): ?> <?php if (empty($usersLists)): ?>
<div class="text-center"> <div class="text-center">
<p class="fs-4 mb-4">You don't have a key list. Create one here.</p> <p class="fs-4 mb-4">You don't have a key list. Create one here.</p>
<button class="btn btn-primary btn-lg js--create-list-button"> <button class="btn btn-primary btn-lg js--create-list-button">
Create Create
</button> </button>
</div> </div>
<?php else: ?> <?php else: ?>
<ul id="key-tab" class="nav nav-tabs"> <ul id="key-tab" class="nav nav-tabs">
<li class="nav-item"> <li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keys-tab-pane" role="tab"> <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#keys-tab-pane" role="tab">
Keys Keys
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#import-tab-pane" role="tab"> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#import-tab-pane" role="tab">
Import Import
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#share-tab-pane" role="tab"> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#share-tab-pane" role="tab">
Share Share
</button> </button>
</li> </li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade show active" id="keys-tab-pane" role="tabpanel"> <div class="tab-pane fade show active" id="keys-tab-pane" role="tabpanel">
<table class="table table-striped key-table"> <table class="table table-striped key-table">
<thead> <thead>
<tr> <tr>
<td></td> <th></th>
<td>Game Name</td> <th>Game Name</th>
<td>Amount Keys</td> <th>Amount Keys</th>
<td>IGDB State</td> <th>IGDB State</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="import-tab-pane" role="tabpanel"> <div class="tab-pane fade" id="import-tab-pane" role="tabpanel">
<h2>Importer</h2> <h2>Importer</h2>
<label for="formFile" class="form-label mt-3">Insert import file:</label> <label for="formFile" class="form-label mt-3">Insert import file:</label>
<div class="mb-3 input-group"> <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"> <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> <button class="btn btn-primary js--send-import">Send</button>
</div> </div>
<div class="d-none" id="import-info-container"> <div class="d-none" id="import-info-container">
<h3>Import Details:</h3> <h3>Import Details:</h3>
<table class="table table-striped w-100" id="import-attribute-table"> <table class="table table-striped w-100" id="import-attribute-table">
<thead> <thead>
<tr> <tr>
<td>Column</td> <th>Column</th>
<td>Header</td> <th>Header</th>
<td>Attribute</td> <th>Attribute</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<button class="btn btn-primary js--do-import"> <button class="btn btn-primary js--do-import">
Import Import
</button> </button>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="share-tab-pane" role="tabpanel"> <div class="tab-pane fade" id="share-tab-pane" role="tabpanel">
<h2> <h2>
Share your list Share your list
</h2> </h2>
<label for="share-user-search">Search for a user...</label> <label for="share-user-search">Search for a user...</label>
<div class="input-group"> <div class="input-group mb-3">
<input type="text" class="form-control" id="share-user-search" placeholder=""> <input type="text" class="form-control" id="share-user-search" placeholder="">
<button class="btn btn-primary js--search-shared-user">Search</button> <button class="btn btn-primary js--search-shared-user">Search</button>
</div> </div>
<h3>Users shared to</h3>
</div> <table id="shared-users-table" class="table table-striped w-100">
</div> <thead>
<tr>
<?php endif; ?> <th width="2.4rem"></th>
<th>Name</th>
<?php $this->start('modal') ?> </tr>
</thead>
<div class="modal" id="create-list-modal"> <tbody></tbody>
<div class="modal-dialog"> </table>
<div class="modal-content">
<div class="modal-header"> </div>
<h1 class="modal-title h3"> </div>
Create list
</h1> <?php endif; ?>
</div>
<div class="modal-body"> <?php $this->start('modal') ?>
<div class="form-floating">
<input class="form-control" type="text" id="createListName" placeholder=""> <div class="modal" id="create-list-modal">
<label for="createListName">Name</label> <div class="modal-dialog">
</div> <div class="modal-content">
</div> <div class="modal-header">
<div class="modal-footer"> <h1 class="modal-title h3">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> Create list
<button type="button" class="btn btn-primary js--create-list">Create</button> </h1>
</div> </div>
</div> <div class="modal-body">
</div> <div class="form-floating">
</div> <input class="form-control" type="text" id="createListName" placeholder="">
<label for="createListName">Name</label>
<?php $this->end() ?> </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--create-list">Create</button>
</div>
</div>
</div>
</div>
<?php $this->end() ?>

View file

@ -1,15 +1,15 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
$this->layout('layout/main', [ 'resourceEntry' => 'index' ]); $this->layout('layout/main', [ 'resourceEntry' => 'index' ]);
/** @var string $discordUrl */ /** @var string $discordUrl */
?> ?>
<a href="<?= $discordUrl ?>"> <a href="<?= $discordUrl ?>">
<button class="btn btn-primary position-absolute top-50 start-50 translate-middle"> <button class="btn btn-primary position-absolute top-50 start-50 translate-middle">
<i class="fa-brands fa-discord"></i> <i class="fa-brands fa-discord"></i>
Login with Discord Login with Discord
</button> </button>
</a> </a>

View file

@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es2016",
"module": "commonjs", "module": "commonjs",
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"skipLibCheck": true "skipLibCheck": true
} }
} }

View file

@ -1,102 +1,102 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin'), 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 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'),
JS_FOLDER = Path.resolve(SOURCE_FOLDER, 'js'), JS_FOLDER = Path.resolve(SOURCE_FOLDER, 'js'),
CSS_FOLDER = Path.resolve(SOURCE_FOLDER, 'css'), CSS_FOLDER = Path.resolve(SOURCE_FOLDER, 'css'),
PHP_FOLDER = Path.resolve(SOURCE_FOLDER, 'php'); PHP_FOLDER = Path.resolve(SOURCE_FOLDER, 'php');
const PROD = false; const PROD = false;
const INDEX_PATH = Path.resolve(PHP_FOLDER, PROD ? 'index.prod.php' : 'index.dev.php'); const INDEX_PATH = Path.resolve(PHP_FOLDER, PROD ? 'index.prod.php' : 'index.dev.php');
module.exports = { module.exports = {
plugins: [ plugins: [
new AssetsPlugin({ new AssetsPlugin({
filename: 'file-index.json', filename: 'file-index.json',
path: SOURCE_FOLDER, path: SOURCE_FOLDER,
includeAllFileTypes: false, includeAllFileTypes: false,
entrypoints: true, entrypoints: true,
removeFullPathAutoPrefix: true removeFullPathAutoPrefix: true
}), }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: 'css/[name].css' filename: 'css/[name].css'
}), }),
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
{ {
from: INDEX_PATH, from: INDEX_PATH,
to: PUBLIC_FOLDER + '/index.php' to: PUBLIC_FOLDER + '/index.php'
}, },
] ]
}) })
], ],
mode: PROD ? 'production' : 'development', mode: PROD ? 'production' : 'development',
devtool: 'source-map', devtool: 'source-map',
optimization: { optimization: {
runtimeChunk: "single", runtimeChunk: "single",
minimize: true, minimize: true,
minimizer: [ minimizer: [
new CssMinimizerPlugin() new CssMinimizerPlugin()
], ],
splitChunks: { splitChunks: {
cacheGroups: { cacheGroups: {
vendor: { vendor: {
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
name: 'vendors', name: 'vendors',
chunks: 'all', chunks: 'all',
}, },
common: { common: {
test: /[\\/]common[\\/]/, test: /[\\/]common[\\/]/,
name: 'common', name: 'common',
chunks: 'all' chunks: 'all'
} }
}, },
}, },
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.css$/i, test: /\.css$/i,
use: [ use: [
MiniCssExtractPlugin.loader, MiniCssExtractPlugin.loader,
'css-loader' 'css-loader'
], ],
}, },
{ {
test: /\.scss$/i, test: /\.scss$/i,
use: [ use: [
MiniCssExtractPlugin.loader, MiniCssExtractPlugin.loader,
'css-loader', 'css-loader',
'resolve-url-loader', 'resolve-url-loader',
"sass-loader" "sass-loader"
] ]
}, },
{ {
test: /\.ts$/i, test: /\.ts$/i,
use: 'ts-loader', use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
}, },
{ {
test: /\.(woff|woff2|eot|ttf|otf|gif|svg)$/i, test: /\.(woff|woff2|eot|ttf|otf|gif|svg)$/i,
type: 'asset/resource', type: 'asset/resource',
} }
], ],
}, },
resolve: { resolve: {
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", 'admin/accounts': JS_FOLDER + "/pages/admin/accounts",
keys: JS_FOLDER + "/pages/keys/index", keys: JS_FOLDER + "/pages/keys/index",
}, },
output: { output: {
path: PUBLIC_FOLDER, path: PUBLIC_FOLDER,
filename: 'js/[name].js' filename: 'js/[name].js'
} }
} }