From 15a9dcf09be25930949e83aca6a67dc47eb80bcd Mon Sep 17 00:00:00 2001 From: Michel Date: Thu, 31 Oct 2024 15:34:57 +0100 Subject: [PATCH 1/2] Adds share feature --- package-lock.json | 7 ++ package.json | 1 + src/js/pages/keys/share.ts | 110 +++++++++++++++++- src/php/Entities/GamesList.php | 11 +- src/php/Importer/GameImporter.php | 2 +- .../Api/DataTables/DataTablesAPIRoutes.php | 1 + .../Api/DataTables/SharedUsersEndpoint.php | 63 ++++++++++ src/php/Routing/Api/Web/AddUserToList.php | 61 ++++++++++ .../Routing/Api/Web/RemoveUserFromList.php | 60 ++++++++++ src/php/Routing/Api/Web/SearchForUsers.php | 59 ++++++++++ src/php/Routing/Api/Web/WebAPIRoutes.php | 4 + src/php/Routing/Router.php | 11 +- src/templates/pages/key-manager.php | 9 +- webpack.config.js | 3 + 14 files changed, 379 insertions(+), 23 deletions(-) create mode 100644 src/php/Routing/Api/DataTables/SharedUsersEndpoint.php create mode 100644 src/php/Routing/Api/Web/AddUserToList.php create mode 100644 src/php/Routing/Api/Web/RemoveUserFromList.php create mode 100644 src/php/Routing/Api/Web/SearchForUsers.php diff --git a/package-lock.json b/package-lock.json index f9e24b3..f315ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fortawesome/fontawesome-free": "^6.5.2", "@types/bootstrap": "^5.2.10", "bootstrap": "^5.3.3", + "bootstrap5-autocomplete": "^1.1.33", "datatables.net-bs5": "^2.0.8" }, "devDependencies": { @@ -699,6 +700,12 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bootstrap5-autocomplete": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.33.tgz", + "integrity": "sha512-VgHSx2hCNEBThFzb57HziDA2BNuc0wT5+V9XqIbXsV6oKYXcyRE2ytFIJcHjTCEIYqTsNCFiCQILIXc3YANGPQ==", + "license": "MIT" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", diff --git a/package.json b/package.json index f2e9f45..114a70b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@fortawesome/fontawesome-free": "^6.5.2", "@types/bootstrap": "^5.2.10", "bootstrap": "^5.3.3", + "bootstrap5-autocomplete": "^1.1.33", "datatables.net-bs5": "^2.0.8" }, "devDependencies": { diff --git a/src/js/pages/keys/share.ts b/src/js/pages/keys/share.ts index fad468a..d7a9ef9 100644 --- a/src/js/pages/keys/share.ts +++ b/src/js/pages/keys/share.ts @@ -1,24 +1,130 @@ -import DataTable from "datatables.net-bs5"; +import DataTable, {Api} from "datatables.net-bs5"; +// @ts-expect-error +import Autocomplete from "bootstrap5-autocomplete"; +import {getCurrentlySelectedList} from "./userlists"; + +const SEARCH_API_URL = '/api/web/share/search'; +const ADD_API_URL = '/api/web/share/add'; +const REMOVE_API_URL = '/api/web/share/remove'; + +const DT_API_URL = '/api/dt/list/users'; + +let dtApi: Api; + +function actionColumnRender( + id: number, type: string +) { + if (type !== 'display') { + return id; + } + + const icon = document.createElement('i'); + icon.classList.add('fa-solid', 'fa-xmark', 'text-danger', 'cursor-pointer', 'fa-xl'); + icon.addEventListener('click', async () => { + const formData = new FormData(); + formData.append('requestedUser', id.toString() ?? ''); + formData.append('listid', getCurrentlySelectedList()?.toString() ?? ''); + + const response = await fetch( + REMOVE_API_URL, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + dtApi.ajax.reload(); + }) + + return icon; +} export function initShare() { + const input = document.querySelector('#share-user-search'); + if (!input) { + throw new Error("Missing search element"); + } + let currentlySelected: number|null = null; + const autocomplete = new Autocomplete(input, { + server: SEARCH_API_URL, + liveServer: true, + fullWidth: true, + fixed: true, + + onSelectItem(item: Object, instance: Autocomplete) { + // @ts-expect-error + currentlySelected = item.value; + } + }) + + document.querySelector('.js--adds-user')?.addEventListener('click', async (e) => { + const formData = new FormData(); + formData.append('requested', currentlySelected?.toString() ?? ''); + formData.append('listid', getCurrentlySelectedList()?.toString() ?? ''); + + const response = await fetch( + ADD_API_URL, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + dtApi.ajax.reload() + }) + const table = document.querySelector('#shared-users-table'); if (!table) { return; } - new DataTable(table, { + dtApi = new DataTable(table, { columns: [ { name: '', + title: 'Icon', data: 'icon', + className: 'avatar', searchable: false, orderable: false, + width: 'auto', + render(data, type) { + if (type !== 'display') { + return data; + } + + return `Profile Picture`; + } }, { name: 'Name', data: 'name', + title: 'Name' + }, + { + name: 'actions', + data: 'id', + render: actionColumnRender, + searchable: false, + orderable: false, + type: 'html' } ], + ajax: { + url: DT_API_URL, + data(data) { + // @ts-expect-error + data.listid = getCurrentlySelectedList() + } + }, order: [ [1, "desc"] ] }); } \ No newline at end of file diff --git a/src/php/Entities/GamesList.php b/src/php/Entities/GamesList.php index 6872628..647d207 100644 --- a/src/php/Entities/GamesList.php +++ b/src/php/Entities/GamesList.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace GamesShop\Entities; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use GamesShop\Entities\Account\User; @@ -35,6 +36,7 @@ final class GamesList { $this->owner = $owner; $this->name = $name; + $this->claimer = new ArrayCollection([$owner]); } @@ -53,10 +55,13 @@ final class GamesList return $this->name; } - public function getClaimer(): array + public function getClaimer(): Collection { return $this->claimer; } - - + + public function addClaimer(User $claimer): void + { + $this->claimer[] = $claimer; + } } \ No newline at end of file diff --git a/src/php/Importer/GameImporter.php b/src/php/Importer/GameImporter.php index c579a21..b686d12 100644 --- a/src/php/Importer/GameImporter.php +++ b/src/php/Importer/GameImporter.php @@ -119,7 +119,7 @@ final class GameImporter } } - if ($values['key'] === null || $values['name'] === null || $values['store'] === null) { + if (empty($values['key']) || empty($values['name']) || empty($values['store'])) { continue; } diff --git a/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php b/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php index 3d6a860..ef2a964 100644 --- a/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php +++ b/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php @@ -12,5 +12,6 @@ final class DataTablesAPIRoutes AccountsEndpoint::applyRoutes($group); $group->get('/keys/provider', ProviderKeysEndpoint::class); + $group->get('/list/users', SharedUsersEndpoint::class); } } \ No newline at end of file diff --git a/src/php/Routing/Api/DataTables/SharedUsersEndpoint.php b/src/php/Routing/Api/DataTables/SharedUsersEndpoint.php new file mode 100644 index 0000000..6e2314b --- /dev/null +++ b/src/php/Routing/Api/DataTables/SharedUsersEndpoint.php @@ -0,0 +1,63 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $body = $request->getQueryParams(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + $claimer = $list->getClaimer(); + + return new JsonResponse( + [ 'data' => $claimer + ->filter(fn ($claimerUser) => $claimerUser !== $user) + ->map( + function (User $user) { + + return [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'icon' => $user->getProfilePictureUrl() + ]; + } + )->toArray() + ] + ); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/AddUserToList.php b/src/php/Routing/Api/Web/AddUserToList.php new file mode 100644 index 0000000..627fc62 --- /dev/null +++ b/src/php/Routing/Api/Web/AddUserToList.php @@ -0,0 +1,61 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $body = $request->getParsedBody(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } + + + $requestedUser = $this->entityManager->getRepository(User::class) + ->find($body['requested']); + + if ($list->getClaimer()->contains($requestedUser)) { + return new Response(); + } + $list->addClaimer($requestedUser); + + $this->entityManager->flush(); + + return new Response(); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/RemoveUserFromList.php b/src/php/Routing/Api/Web/RemoveUserFromList.php new file mode 100644 index 0000000..365524e --- /dev/null +++ b/src/php/Routing/Api/Web/RemoveUserFromList.php @@ -0,0 +1,60 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $body = $request->getParsedBody(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy([ 'owner' => $user, 'id' => $body['listid'] ]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } + + $requestedUser = $this->entityManager->getRepository(User::class) + ->find($body['requestedUser']); + + if ($requestedUser === $user) { + return new Response(); + } + + $list->getClaimer()->removeElement($requestedUser); + + $this->entityManager->flush(); + + return new Response(); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/SearchForUsers.php b/src/php/Routing/Api/Web/SearchForUsers.php new file mode 100644 index 0000000..dc39bd9 --- /dev/null +++ b/src/php/Routing/Api/Web/SearchForUsers.php @@ -0,0 +1,59 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $searchQuery = $request->getQueryParams()['query'] ?? ''; + + $repo = $this->entityManager->getRepository(User::class); + + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->contains('name', $searchQuery)); + $criteria->setMaxResults(10); + + $values = $repo->matching($criteria); + return new JsonResponse( + $values + ->filter(fn ($value) => $value !== $user) + ->map(function (User $user) { + return [ + 'value' => $user->getId(), + 'label' => $user->getName() + ]; + }) + ->toArray() + ); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/WebAPIRoutes.php b/src/php/Routing/Api/Web/WebAPIRoutes.php index 350f210..67b6253 100644 --- a/src/php/Routing/Api/Web/WebAPIRoutes.php +++ b/src/php/Routing/Api/Web/WebAPIRoutes.php @@ -14,5 +14,9 @@ final class WebAPIRoutes $group->post('/keys/import/perform', ImportKeysRoute::class); $group->post('/keys/list/create', CreateKeyListRoute::class); + + $group->get('/share/search', SearchForUsers::class); + $group->post('/share/add', AddUserToList::class); + $group->post('/share/remove', RemoveUserFromList::class); } } \ No newline at end of file diff --git a/src/php/Routing/Router.php b/src/php/Routing/Router.php index a0b4ee2..cc33369 100644 --- a/src/php/Routing/Router.php +++ b/src/php/Routing/Router.php @@ -20,21 +20,12 @@ use Psr\Http\Message\ServerRequestInterface; final class Router { - public function __construct( - private ResourceRoute $resourceRoute - ) - { - } - public function route(): ResponseInterface { $request = ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES ); - if ($this->resourceRoute->isValid($request->getUri())) { - return $this->resourceRoute->getResponse($request->getUri()); - } - + $router = new \League\Route\Router; $strategy = (new ApplicationStrategy)->setContainer(ContainerHandler::getInstance()); $router->setStrategy($strategy); diff --git a/src/templates/pages/key-manager.php b/src/templates/pages/key-manager.php index 0e444b3..4de5494 100644 --- a/src/templates/pages/key-manager.php +++ b/src/templates/pages/key-manager.php @@ -96,8 +96,7 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]); - +

@@ -107,16 +106,12 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]);
- +

Users shared to

- - - -
Name
diff --git a/webpack.config.js b/webpack.config.js index 20ecb99..53d38dd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -89,6 +89,9 @@ module.exports = { }, resolve: { extensions: ['.js', '.ts'], + fallback: { + 'bootstrap5-autocomplete/types/autocomplete': 'bootstrap5-autocomplete' + } }, entry: { index: JS_FOLDER + "/pages/index", -- 2.45.2 From d6ebf8f4ffe3da6f16a6709aeafc87dc7f8a5510 Mon Sep 17 00:00:00 2001 From: Michel Date: Mon, 4 Nov 2024 16:31:21 +0100 Subject: [PATCH 2/2] Implermented front page and set public --- deploy/create_doctrine.sh | 9 + src/js/pages/index.ts | 193 +++++++++++++++++- src/js/pages/keys/share.ts | 32 +++ src/js/pages/types/gameindex.d.ts | 21 ++ src/php/Entities/Account/User.php | 2 +- src/php/Entities/Games/Key.php | 21 +- src/php/Entities/Games/Store.php | 29 +++ src/php/Entities/GamesList.php | 13 +- .../Api/DataTables/AvailableKeysEndpoint.php | 74 +++++++ .../Api/DataTables/DataTablesAPIRoutes.php | 1 + src/php/Routing/Api/Web/ClaimKey.php | 71 +++++++ src/php/Routing/Api/Web/GetGameData.php | 75 +++++++ src/php/Routing/Api/Web/SetListPublic.php | 53 +++++ src/php/Routing/Api/Web/WebAPIRoutes.php | 4 + src/php/UserManager.php | 41 ++++ src/templates/layout/main.php | 2 +- src/templates/pages/index.php | 10 +- src/templates/pages/key-manager.php | 30 +-- 18 files changed, 658 insertions(+), 23 deletions(-) create mode 100644 deploy/create_doctrine.sh create mode 100644 src/js/pages/types/gameindex.d.ts create mode 100644 src/php/Routing/Api/DataTables/AvailableKeysEndpoint.php create mode 100644 src/php/Routing/Api/Web/ClaimKey.php create mode 100644 src/php/Routing/Api/Web/GetGameData.php create mode 100644 src/php/Routing/Api/Web/SetListPublic.php create mode 100644 src/php/UserManager.php diff --git a/deploy/create_doctrine.sh b/deploy/create_doctrine.sh new file mode 100644 index 0000000..5e46fde --- /dev/null +++ b/deploy/create_doctrine.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +docker run \ + --volume .:/app \ + --rm \ + --interactive \ + --tty \ + php:8.3-cli \ + php /app/src/php/bin/doctrine.php orm:schema-tool:update --force \ No newline at end of file diff --git a/src/js/pages/index.ts b/src/js/pages/index.ts index 600609c..1b395c6 100644 --- a/src/js/pages/index.ts +++ b/src/js/pages/index.ts @@ -1,2 +1,193 @@ import '../../css/common/index.scss'; -import '../common/index'; \ No newline at end of file +import '../common/index'; + +import DataTable, {Api} from "datatables.net-bs5"; +import 'datatables.net-bs5/css/dataTables.bootstrap5.css'; + +const TABLE_AJAX_URL = '/api/dt/keys/available'; +const GAME_GET_URL = '/api/web/game'; +const CLAIM_URL = '/api/web/key/claim'; +function getDetailsHTML(gameData: Game): string +{ + let keyString = ''; + gameData.keys.forEach(key => { + keyString += ` +
+ + + + ${ key.store.name } + + + provided by ${ key.list.owner } + + + +
+ `; + }) + + return ` +
+
+
+
Image
+
+
+
+

+ ${ gameData.name } +

+ +
+ ${ keyString } +
+ +
+
+ + + + + + #####-#####-##### + + + Open Claim Page +
+
+ + provided by:
+ +
+ + key from:
+ +
+
+
+
+
+ `; +} + +async function renderDetails(childRow: HTMLDivElement, gameid: number) { + const loadingContainer = document.createElement('div'); + loadingContainer.classList.add('m-3', 'mx-auto'); + loadingContainer.style.width = '2rem'; + const spinner = document.createElement('div'); + spinner.classList.add('spinner-border'); + loadingContainer.appendChild(spinner); + childRow.appendChild(loadingContainer); + + const queryParams = new URLSearchParams(); + queryParams.set('gameid', gameid.toString()); + const response = await fetch( + GAME_GET_URL + '?' + queryParams.toString(), + ) + + if (!response.ok) { + throw new Error(response.statusText); + } + + const gameData: Game = await response.json(); + + childRow.removeChild(loadingContainer); + + const parser = new DOMParser(); + const gameDisplay: HTMLDivElement|null = parser.parseFromString(getDetailsHTML(gameData), 'text/html').body.firstChild; + + if (!gameDisplay) { + return; + } + + gameDisplay.querySelectorAll('.claim-btn').forEach(btn => { + btn.addEventListener('click', async () => { + const key = btn.closest('.key')?.dataset.keyId ?? 0; + + const formData = new FormData(); + formData.set('keyid', key); + + const response = await fetch(CLAIM_URL, { + method: 'POST', + body: formData + }) + + if (!response.ok) { + throw new Error(response.statusText); + } + + const data = await response.json(); + + gameDisplay.querySelector('.key-list')?.classList.add('d-none'); + const claimDisplay: HTMLDivElement|null = gameDisplay.querySelector('.key-claim-display') + + if (!claimDisplay) { + throw new Error("Can't find claim display..."); + } + + claimDisplay.classList.remove('d-none'); + + // @ts-expect-error + claimDisplay.querySelector('.store-display').textContent = data.store.name; + // @ts-expect-error + claimDisplay.querySelector('.key-display').textContent = data.key; + // @ts-expect-error + claimDisplay.querySelector('.providedBy').textContent = data.providedBy; + // @ts-expect-error + claimDisplay.querySelector('.from').textContent = data.from; + const claimBtn = claimDisplay.querySelector('.open-claim-page-btn'); + if (!claimBtn) { + return; + } + + if (data.store.claimLink === null) { + claimBtn.remove(); + return; + } + + claimBtn.href = data.store.claimLink; + }) + }) + + childRow.appendChild(gameDisplay); +} + +document.addEventListener('DOMContentLoaded', () => { + const keyTable = document.getElementById('keyTable'); + if (!keyTable) { + return; + } + + const table = new DataTable(keyTable, { + ajax: { + url: TABLE_AJAX_URL, + }, + columns: [ + { + data: 'name', + } + ], + createdRow(row: Node, data: any) { + const tableRow = row; + tableRow.classList.add('cursor-pointer'); + + tableRow.addEventListener('click', () => { + const rowAPI = table.row(row); + if (rowAPI.child.isShown()) { + rowAPI.child.hide(); + return; + } + + const childRow = document.createElement('tr'); + const cell = childRow.insertCell(); + cell.colSpan = row.childNodes.length; + const cellContainer = document.createElement('div'); + cell.appendChild(cellContainer); + + rowAPI.child(childRow).show(); + renderDetails(cellContainer, data.id); + }) + }, + }) +}) \ No newline at end of file diff --git a/src/js/pages/keys/share.ts b/src/js/pages/keys/share.ts index d7a9ef9..a1d2285 100644 --- a/src/js/pages/keys/share.ts +++ b/src/js/pages/keys/share.ts @@ -6,6 +6,7 @@ import {getCurrentlySelectedList} from "./userlists"; const SEARCH_API_URL = '/api/web/share/search'; const ADD_API_URL = '/api/web/share/add'; const REMOVE_API_URL = '/api/web/share/remove'; +const SET_PUBLIC_API_URL = '/api/web/share/setPublic'; const DT_API_URL = '/api/dt/list/users'; @@ -44,6 +45,37 @@ function actionColumnRender( } export function initShare() { + const shareContainer = document.querySelector('.share-content'); + if (!shareContainer) { + throw new Error("Missing share container"); + } + + const publicSwitch = document.querySelector('#public-switch'); + if (!publicSwitch) { + throw new Error("Missing public switch"); + } + + shareContainer.classList.toggle('d-none', publicSwitch.checked); + publicSwitch.addEventListener('click', async () => { + shareContainer.classList.toggle('d-none', publicSwitch.checked); + + const formData = new FormData(); + formData.append('listid', getCurrentlySelectedList()?.toString() ?? ''); + formData.append('publicState', publicSwitch.checked ? "1" : "0"); + + const response = await fetch( + SET_PUBLIC_API_URL, + { + method: 'POST', + body: formData + } + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + }) + const input = document.querySelector('#share-user-search'); if (!input) { throw new Error("Missing search element"); diff --git a/src/js/pages/types/gameindex.d.ts b/src/js/pages/types/gameindex.d.ts new file mode 100644 index 0000000..ffbb4f8 --- /dev/null +++ b/src/js/pages/types/gameindex.d.ts @@ -0,0 +1,21 @@ +type GameList = { + owner: string; + name: string; +} +type Store = { + name: string; + icon: string; +} + +type Key = { + id: number; + store: Store; + fromWhere: string; + list: GameList; +} + +type Game = { + name: string; + id: number; + keys: Key[]; +} \ No newline at end of file diff --git a/src/php/Entities/Account/User.php b/src/php/Entities/Account/User.php index daf3684..be94e5f 100644 --- a/src/php/Entities/Account/User.php +++ b/src/php/Entities/Account/User.php @@ -11,7 +11,7 @@ use Symfony\Component\Uid\UuidV4; #[ORM\Entity] #[ORM\Table(name: "users")] -final class User +class User { #[ORM\Id()] diff --git a/src/php/Entities/Games/Key.php b/src/php/Entities/Games/Key.php index cb1cc17..10ba10f 100644 --- a/src/php/Entities/Games/Key.php +++ b/src/php/Entities/Games/Key.php @@ -31,6 +31,9 @@ final class Key implements JsonSerializable #[ORM\Column(type: 'integer', enumType: KeyState::class)] private KeyState $state; + #[ORM\ManyToOne] + private User $claimedUser; + public function __construct(Game $game, GamesList $list, string $key, Store $store, ?string $storeLink, ?string $fromWhere) { $this->game = $game; @@ -52,11 +55,6 @@ final class Key implements JsonSerializable return $this->game; } - public function getContributedUser(): User - { - return $this->contributedUser; - } - public function getKey(): string { return $this->key; @@ -81,6 +79,19 @@ final class Key implements JsonSerializable { return $this->state; } + + public function getList(): GamesList + { + return $this->list; + } + + public function setState(KeyState $state): void { + $this->state = $state; + } + + public function setClaimedUser(User $claimedUser): void { + $this->claimedUser = $claimedUser; + } public function jsonSerialize(): mixed { diff --git a/src/php/Entities/Games/Store.php b/src/php/Entities/Games/Store.php index a8bf1b3..1ea838b 100644 --- a/src/php/Entities/Games/Store.php +++ b/src/php/Entities/Games/Store.php @@ -12,4 +12,33 @@ enum Store: string case UPLAY = 'uplay'; case BATTLENET = 'battlenet'; case EXTERNAL = 'external'; + + public function getName(): string + { + return match ($this) { + self::STEAM => 'Steam', + self::GOG => "GOG", + self::EPICGAMES => "Epic Games Store", + self::ORIGIN => "EA Play / Origin", + self::UPLAY => "UPlay", + self::BATTLENET => "Battlenet", + self::EXTERNAL => "Other", + }; + } + + public function getIcon(): string + { + return match ($this) { + self::STEAM => 'fa-solid fa-steam', + default => '', + }; + } + + public function getClaimURL(Key $key): ?string { + return match ($this) { + self::STEAM => 'https://store.steampowered.com/account/registerkey?key=' . $key->getKey(), + self::EXTERNAL => $key->getStoreLink(), + default => null, + }; + } } diff --git a/src/php/Entities/GamesList.php b/src/php/Entities/GamesList.php index 647d207..35bbdda 100644 --- a/src/php/Entities/GamesList.php +++ b/src/php/Entities/GamesList.php @@ -10,7 +10,7 @@ use GamesShop\Entities\Account\User; #[ORM\Entity] #[ORM\Table(name: 'games_lists')] -final class GamesList +class GamesList { #[ORM\Id] #[ORM\Column(type: 'integer', options: ['unsigned' => true])] @@ -27,6 +27,9 @@ final class GamesList #[ORM\JoinColumn(name: 'id', referencedColumnName: 'id')] #[ORM\ManyToMany(targetEntity: User::class)] private Collection $claimer; + + #[ORM\Column(type: 'integer', options: ['unsigned' => true, 'default' => 0])] + private bool $isPublic = false; /** * @param User $owner @@ -60,6 +63,14 @@ final class GamesList return $this->claimer; } + public function isPublic(): bool { + return $this->isPublic; + } + + public function setIsPublic(bool $isPublic): void { + $this->isPublic = $isPublic; + } + public function addClaimer(User $claimer): void { $this->claimer[] = $claimer; diff --git a/src/php/Routing/Api/DataTables/AvailableKeysEndpoint.php b/src/php/Routing/Api/DataTables/AvailableKeysEndpoint.php new file mode 100644 index 0000000..379f77d --- /dev/null +++ b/src/php/Routing/Api/DataTables/AvailableKeysEndpoint.php @@ -0,0 +1,74 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::VIEWER)) { + throw new ForbiddenException(); + } + + $applicableLists = ContainerHandler::get(UserManager::class) + ->getApplicableGameLists($user); + + $keyRepo = $this->entityManager->getRepository(Key::class); + $keys = $keyRepo->matching(Criteria::create() + ->where(Criteria::expr()->in('list', $applicableLists->toArray())) + ->andWhere(Criteria::expr()->eq('state', KeyState::AVAILABLE)) + ); + + $games = new ArrayCollection(); + foreach ($keys as $key) { + $game = $key->getGame(); + + if ($games->contains($game)) { + continue; + } + + $games->add($game); + } + + return new JsonResponse( + [ + 'data' => + $games + ->map(fn ($game) => [ + 'name' => $game->getName(), + 'id' => $game->getId(), + ]) + ->toArray() + ] + ); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php b/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php index ef2a964..ec6d1e9 100644 --- a/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php +++ b/src/php/Routing/Api/DataTables/DataTablesAPIRoutes.php @@ -12,6 +12,7 @@ final class DataTablesAPIRoutes AccountsEndpoint::applyRoutes($group); $group->get('/keys/provider', ProviderKeysEndpoint::class); + $group->get('/keys/available', AvailableKeysEndpoint::class); $group->get('/list/users', SharedUsersEndpoint::class); } } \ No newline at end of file diff --git a/src/php/Routing/Api/Web/ClaimKey.php b/src/php/Routing/Api/Web/ClaimKey.php new file mode 100644 index 0000000..f939b4d --- /dev/null +++ b/src/php/Routing/Api/Web/ClaimKey.php @@ -0,0 +1,71 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::VIEWER)) { + throw new ForbiddenException(); + } + + $body = $request->getParsedBody(); + if (!array_key_exists('keyid', $body)) { + throw new BadRequestException(); + } + + $key = $this->entityManager->getRepository(Key::class)->find($body['keyid']); + if (!$key instanceof Key) { + throw new BadRequestException(); + } + + if (!$key->getList()->isPublic() && !$key->getList()->getClaimer()->contains($user)) { + throw new BadRequestException(); + } + + if ($key->getState() !== KeyState::AVAILABLE) { + throw new BadRequestException(); + } + + $key->setState(KeyState::CLAIMED); + $key->setClaimedUser($user); + $this->entityManager->flush(); + + return new JsonResponse([ + 'key' => $key->getKey(), + 'providedBy' => $key->getList()->getOwner()->getName(), + 'from' => $key->getFromWhere() ?? 'unknown', + 'store' => [ + 'name' => $key->getStore()->getName(), + 'icon' => $key->getStore()->getIcon(), + 'claimLink' => $key->getStore()->getClaimURL($key), + ], + ]); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/GetGameData.php b/src/php/Routing/Api/Web/GetGameData.php new file mode 100644 index 0000000..3d828d1 --- /dev/null +++ b/src/php/Routing/Api/Web/GetGameData.php @@ -0,0 +1,75 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::VIEWER)) { + throw new ForbiddenException(); + } + + $game = $this->entityManager->getRepository(Game::class) + ->find($request->getQueryParams()['gameid']); + if (!$game) { + throw new NotFoundException(); + } + + $applicableLists = ContainerHandler::get(UserManager::class) + ->getApplicableGameLists($user); + + $keyRepo = $this->entityManager->getRepository(Key::class); + $keys = $keyRepo->matching(Criteria::create() + ->where(Criteria::expr()->in('list', $applicableLists->toArray())) + ->andWhere(Criteria::expr()->eq('game', $game)) + ->andWhere(Criteria::expr()->eq('state', KeyState::AVAILABLE)) + ); + + return new JsonResponse([ + 'name' => $game->getName(), + 'id' => $game->getId(), + 'keys' => $keys->map(fn (Key $key) => [ + 'id' => $key->getId(), + 'store' => [ + 'name' => $key->getStore()->getName(), + 'icon' => $key->getStore()->getIcon() + ], + 'fromWhere' => $key->getFromWhere(), + 'list' => [ + 'owner' => $key->getList()->getOwner()->getName(), + 'name' => $key->getList()->getName(), + ], + ])->toArray(), + ]); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/SetListPublic.php b/src/php/Routing/Api/Web/SetListPublic.php new file mode 100644 index 0000000..a55faea --- /dev/null +++ b/src/php/Routing/Api/Web/SetListPublic.php @@ -0,0 +1,53 @@ +loginHandler->isLoggedIn()) { + throw new UnauthorizedException(); + } + + $user = $this->loginHandler->getCurrentUser(); + if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) { + throw new ForbiddenException(); + } + + $body = $request->getParsedBody(); + if (!array_key_exists('listid', $body)) { + throw new BadRequestException(); + } + + $list = $this->entityManager->getRepository(GamesList::class)->findOneBy(['owner' => $user, 'id' => $body['listid']]); + if (!$list instanceof GamesList) { + throw new BadRequestException(); + } + + $list->setIsPublic($body['publicState'] === '1'); + $this->entityManager->flush(); + + return new Response(); + } +} \ No newline at end of file diff --git a/src/php/Routing/Api/Web/WebAPIRoutes.php b/src/php/Routing/Api/Web/WebAPIRoutes.php index 67b6253..02f2033 100644 --- a/src/php/Routing/Api/Web/WebAPIRoutes.php +++ b/src/php/Routing/Api/Web/WebAPIRoutes.php @@ -18,5 +18,9 @@ final class WebAPIRoutes $group->get('/share/search', SearchForUsers::class); $group->post('/share/add', AddUserToList::class); $group->post('/share/remove', RemoveUserFromList::class); + $group->post('/share/setPublic', SetListPublic::class); + + $group->get('/game', GetGameData::class); + $group->post('/key/claim', ClaimKey::class); } } \ No newline at end of file diff --git a/src/php/UserManager.php b/src/php/UserManager.php new file mode 100644 index 0000000..1da6e70 --- /dev/null +++ b/src/php/UserManager.php @@ -0,0 +1,41 @@ +entityManager->getRepository(GamesList::class); + + $allLists = $listRepo->findAll(); + $applicableLists = new ArrayCollection(); + foreach ($allLists as $list) { + if ($list->isPublic()) { + $applicableLists->add($list); + continue; + } + + if (!$list->getClaimer()->contains($user)) { + continue; + } + + $applicableLists->add($list); + } + + return $applicableLists; + } +} \ No newline at end of file diff --git a/src/templates/layout/main.php b/src/templates/layout/main.php index 6eb5f31..c1cb854 100644 --- a/src/templates/layout/main.php +++ b/src/templates/layout/main.php @@ -41,7 +41,7 @@ $resource = $resources->getResource($resourceEntry); section('modal') ?> -
+
PROTOTYPE / POC
diff --git a/src/templates/pages/index.php b/src/templates/pages/index.php index ddd0cca..8b8b007 100644 --- a/src/templates/pages/index.php +++ b/src/templates/pages/index.php @@ -5,5 +5,11 @@ $this->layout('layout/main', [ 'resourceEntry' => 'index' ]); ?>

- Hello -

\ No newline at end of file + Games + + + + + + +
Name
\ No newline at end of file diff --git a/src/templates/pages/key-manager.php b/src/templates/pages/key-manager.php index 4de5494..4182414 100644 --- a/src/templates/pages/key-manager.php +++ b/src/templates/pages/key-manager.php @@ -87,8 +87,7 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]); Column - Header - Attribute + Header Attribute @@ -103,18 +102,25 @@ $this->layout('layout/main', [ 'resourceEntry' => 'keys' ]); Share your list - -
- - +
+ isPublic() ? "checked" : "" ?>> +
-

Users shared to

- - - - -
+
-- 2.45.2