mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Add permission checks
Add UI for config
This commit is contained in:
Generated
+798
-1124
File diff suppressed because it is too large
Load Diff
+109
-109
@@ -9,22 +9,22 @@
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@react-stately/data": "^3.12.2",
|
||||
"@react-types/shared": "^3.28.0",
|
||||
"@vaadin/bundles": "24.8.3",
|
||||
"@vaadin/bundles": "24.8.6",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/hilla-file-router": "24.8.2",
|
||||
"@vaadin/hilla-frontend": "24.8.2",
|
||||
"@vaadin/hilla-lit-form": "24.8.2",
|
||||
"@vaadin/hilla-react-auth": "24.8.2",
|
||||
"@vaadin/hilla-react-crud": "24.8.2",
|
||||
"@vaadin/hilla-react-form": "24.8.2",
|
||||
"@vaadin/hilla-react-i18n": "24.8.2",
|
||||
"@vaadin/hilla-react-signals": "24.8.2",
|
||||
"@vaadin/polymer-legacy-adapter": "24.8.3",
|
||||
"@vaadin/react-components": "24.8.3",
|
||||
"@vaadin/hilla-file-router": "24.8.7",
|
||||
"@vaadin/hilla-frontend": "24.8.7",
|
||||
"@vaadin/hilla-lit-form": "24.8.7",
|
||||
"@vaadin/hilla-react-auth": "24.8.7",
|
||||
"@vaadin/hilla-react-crud": "24.8.7",
|
||||
"@vaadin/hilla-react-form": "24.8.7",
|
||||
"@vaadin/hilla-react-i18n": "24.8.7",
|
||||
"@vaadin/hilla-react-signals": "24.8.7",
|
||||
"@vaadin/polymer-legacy-adapter": "24.8.6",
|
||||
"@vaadin/react-components": "24.8.6",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.8.3",
|
||||
"@vaadin/vaadin-material-styles": "24.8.3",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.3",
|
||||
"@vaadin/vaadin-lumo-styles": "24.8.6",
|
||||
"@vaadin/vaadin-material-styles": "24.8.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.6",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"classnames": "^2.5.1",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
@@ -61,17 +61,17 @@
|
||||
"@types/node": "^22.4.0",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@vaadin/hilla-generator-cli": "24.8.2",
|
||||
"@vaadin/hilla-generator-core": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-client": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-model": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-push": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-signals": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.2",
|
||||
"@vaadin/hilla-generator-utils": "24.8.2",
|
||||
"@vaadin/hilla-generator-cli": "24.8.7",
|
||||
"@vaadin/hilla-generator-core": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-client": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-model": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-push": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-signals": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.7",
|
||||
"@vaadin/hilla-generator-utils": "24.8.7",
|
||||
"@vitejs/plugin-react": "4.5.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"async": "3.2.6",
|
||||
@@ -142,85 +142,85 @@
|
||||
"valtio": "$valtio",
|
||||
"valtio-reactive": "$valtio-reactive",
|
||||
"fzf": "$fzf",
|
||||
"@vaadin/a11y-base": "24.8.3",
|
||||
"@vaadin/accordion": "24.8.3",
|
||||
"@vaadin/app-layout": "24.8.3",
|
||||
"@vaadin/avatar": "24.8.3",
|
||||
"@vaadin/avatar-group": "24.8.3",
|
||||
"@vaadin/button": "24.8.3",
|
||||
"@vaadin/card": "24.8.3",
|
||||
"@vaadin/checkbox": "24.8.3",
|
||||
"@vaadin/checkbox-group": "24.8.3",
|
||||
"@vaadin/combo-box": "24.8.3",
|
||||
"@vaadin/component-base": "24.8.3",
|
||||
"@vaadin/confirm-dialog": "24.8.3",
|
||||
"@vaadin/context-menu": "24.8.3",
|
||||
"@vaadin/custom-field": "24.8.3",
|
||||
"@vaadin/date-picker": "24.8.3",
|
||||
"@vaadin/date-time-picker": "24.8.3",
|
||||
"@vaadin/details": "24.8.3",
|
||||
"@vaadin/dialog": "24.8.3",
|
||||
"@vaadin/email-field": "24.8.3",
|
||||
"@vaadin/field-base": "24.8.3",
|
||||
"@vaadin/field-highlighter": "24.8.3",
|
||||
"@vaadin/form-layout": "24.8.3",
|
||||
"@vaadin/grid": "24.8.3",
|
||||
"@vaadin/horizontal-layout": "24.8.3",
|
||||
"@vaadin/icon": "24.8.3",
|
||||
"@vaadin/icons": "24.8.3",
|
||||
"@vaadin/input-container": "24.8.3",
|
||||
"@vaadin/integer-field": "24.8.3",
|
||||
"@vaadin/item": "24.8.3",
|
||||
"@vaadin/list-box": "24.8.3",
|
||||
"@vaadin/lit-renderer": "24.8.3",
|
||||
"@vaadin/login": "24.8.3",
|
||||
"@vaadin/markdown": "24.8.3",
|
||||
"@vaadin/master-detail-layout": "24.8.3",
|
||||
"@vaadin/menu-bar": "24.8.3",
|
||||
"@vaadin/message-input": "24.8.3",
|
||||
"@vaadin/message-list": "24.8.3",
|
||||
"@vaadin/multi-select-combo-box": "24.8.3",
|
||||
"@vaadin/notification": "24.8.3",
|
||||
"@vaadin/number-field": "24.8.3",
|
||||
"@vaadin/overlay": "24.8.3",
|
||||
"@vaadin/password-field": "24.8.3",
|
||||
"@vaadin/popover": "24.8.3",
|
||||
"@vaadin/progress-bar": "24.8.3",
|
||||
"@vaadin/radio-group": "24.8.3",
|
||||
"@vaadin/scroller": "24.8.3",
|
||||
"@vaadin/select": "24.8.3",
|
||||
"@vaadin/side-nav": "24.8.3",
|
||||
"@vaadin/split-layout": "24.8.3",
|
||||
"@vaadin/tabs": "24.8.3",
|
||||
"@vaadin/tabsheet": "24.8.3",
|
||||
"@vaadin/text-area": "24.8.3",
|
||||
"@vaadin/text-field": "24.8.3",
|
||||
"@vaadin/time-picker": "24.8.3",
|
||||
"@vaadin/tooltip": "24.8.3",
|
||||
"@vaadin/upload": "24.8.3",
|
||||
"@vaadin/a11y-base": "24.8.6",
|
||||
"@vaadin/accordion": "24.8.6",
|
||||
"@vaadin/app-layout": "24.8.6",
|
||||
"@vaadin/avatar": "24.8.6",
|
||||
"@vaadin/avatar-group": "24.8.6",
|
||||
"@vaadin/button": "24.8.6",
|
||||
"@vaadin/card": "24.8.6",
|
||||
"@vaadin/checkbox": "24.8.6",
|
||||
"@vaadin/checkbox-group": "24.8.6",
|
||||
"@vaadin/combo-box": "24.8.6",
|
||||
"@vaadin/component-base": "24.8.6",
|
||||
"@vaadin/confirm-dialog": "24.8.6",
|
||||
"@vaadin/context-menu": "24.8.6",
|
||||
"@vaadin/custom-field": "24.8.6",
|
||||
"@vaadin/date-picker": "24.8.6",
|
||||
"@vaadin/date-time-picker": "24.8.6",
|
||||
"@vaadin/details": "24.8.6",
|
||||
"@vaadin/dialog": "24.8.6",
|
||||
"@vaadin/email-field": "24.8.6",
|
||||
"@vaadin/field-base": "24.8.6",
|
||||
"@vaadin/field-highlighter": "24.8.6",
|
||||
"@vaadin/form-layout": "24.8.6",
|
||||
"@vaadin/grid": "24.8.6",
|
||||
"@vaadin/horizontal-layout": "24.8.6",
|
||||
"@vaadin/icon": "24.8.6",
|
||||
"@vaadin/icons": "24.8.6",
|
||||
"@vaadin/input-container": "24.8.6",
|
||||
"@vaadin/integer-field": "24.8.6",
|
||||
"@vaadin/item": "24.8.6",
|
||||
"@vaadin/list-box": "24.8.6",
|
||||
"@vaadin/lit-renderer": "24.8.6",
|
||||
"@vaadin/login": "24.8.6",
|
||||
"@vaadin/markdown": "24.8.6",
|
||||
"@vaadin/master-detail-layout": "24.8.6",
|
||||
"@vaadin/menu-bar": "24.8.6",
|
||||
"@vaadin/message-input": "24.8.6",
|
||||
"@vaadin/message-list": "24.8.6",
|
||||
"@vaadin/multi-select-combo-box": "24.8.6",
|
||||
"@vaadin/notification": "24.8.6",
|
||||
"@vaadin/number-field": "24.8.6",
|
||||
"@vaadin/overlay": "24.8.6",
|
||||
"@vaadin/password-field": "24.8.6",
|
||||
"@vaadin/popover": "24.8.6",
|
||||
"@vaadin/progress-bar": "24.8.6",
|
||||
"@vaadin/radio-group": "24.8.6",
|
||||
"@vaadin/scroller": "24.8.6",
|
||||
"@vaadin/select": "24.8.6",
|
||||
"@vaadin/side-nav": "24.8.6",
|
||||
"@vaadin/split-layout": "24.8.6",
|
||||
"@vaadin/tabs": "24.8.6",
|
||||
"@vaadin/tabsheet": "24.8.6",
|
||||
"@vaadin/text-area": "24.8.6",
|
||||
"@vaadin/text-field": "24.8.6",
|
||||
"@vaadin/time-picker": "24.8.6",
|
||||
"@vaadin/tooltip": "24.8.6",
|
||||
"@vaadin/upload": "24.8.6",
|
||||
"@vaadin/router": "2.0.0",
|
||||
"@vaadin/vertical-layout": "24.8.3",
|
||||
"@vaadin/virtual-list": "24.8.3"
|
||||
"@vaadin/vertical-layout": "24.8.6",
|
||||
"@vaadin/virtual-list": "24.8.6"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@vaadin/bundles": "24.8.3",
|
||||
"@vaadin/bundles": "24.8.6",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/hilla-file-router": "24.8.2",
|
||||
"@vaadin/hilla-frontend": "24.8.2",
|
||||
"@vaadin/hilla-lit-form": "24.8.2",
|
||||
"@vaadin/hilla-react-auth": "24.8.2",
|
||||
"@vaadin/hilla-react-crud": "24.8.2",
|
||||
"@vaadin/hilla-react-form": "24.8.2",
|
||||
"@vaadin/hilla-react-i18n": "24.8.2",
|
||||
"@vaadin/hilla-react-signals": "24.8.2",
|
||||
"@vaadin/polymer-legacy-adapter": "24.8.3",
|
||||
"@vaadin/react-components": "24.8.3",
|
||||
"@vaadin/hilla-file-router": "24.8.7",
|
||||
"@vaadin/hilla-frontend": "24.8.7",
|
||||
"@vaadin/hilla-lit-form": "24.8.7",
|
||||
"@vaadin/hilla-react-auth": "24.8.7",
|
||||
"@vaadin/hilla-react-crud": "24.8.7",
|
||||
"@vaadin/hilla-react-form": "24.8.7",
|
||||
"@vaadin/hilla-react-i18n": "24.8.7",
|
||||
"@vaadin/hilla-react-signals": "24.8.7",
|
||||
"@vaadin/polymer-legacy-adapter": "24.8.6",
|
||||
"@vaadin/react-components": "24.8.6",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.8.3",
|
||||
"@vaadin/vaadin-material-styles": "24.8.3",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.3",
|
||||
"@vaadin/vaadin-lumo-styles": "24.8.6",
|
||||
"@vaadin/vaadin-material-styles": "24.8.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.6",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
@@ -236,17 +236,17 @@
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@vaadin/hilla-generator-cli": "24.8.2",
|
||||
"@vaadin/hilla-generator-core": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-client": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-model": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-push": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-signals": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "24.8.2",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.2",
|
||||
"@vaadin/hilla-generator-utils": "24.8.2",
|
||||
"@vaadin/hilla-generator-cli": "24.8.7",
|
||||
"@vaadin/hilla-generator-core": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-client": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-model": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-push": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-signals": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "24.8.7",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.8.7",
|
||||
"@vaadin/hilla-generator-utils": "24.8.7",
|
||||
"@vitejs/plugin-react": "4.5.0",
|
||||
"async": "3.2.6",
|
||||
"glob": "11.0.2",
|
||||
@@ -263,6 +263,6 @@
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"disableUsageStatistics": true,
|
||||
"hash": "962eccc3fa0735d5234901be4f9e384096113c45bec22564a53688096d62aef4"
|
||||
"hash": "e499c8893c397649c698f302e100ee1e48833c88e57bd0829fbf86dc5a14cfd8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import * as Yup from 'yup';
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import {Button} from "@heroui/react";
|
||||
import {useNavigate} from "react-router";
|
||||
|
||||
function GameRequestManagementLayout({getConfig, formik}: any) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col flex-1">
|
||||
<Section title="Game requests configuration"/>
|
||||
<ConfigFormField configElement={getConfig("requests.games.enabled")}/>
|
||||
|
||||
<Section title="Permissions"/>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<ConfigFormField
|
||||
configElement={getConfig("requests.games.allow-guests-to-request-games")}
|
||||
isDisabled={!formik.values.library["allow-public-access"]}/>
|
||||
<ConfigFormField configElement={getConfig("requests.games.max-open-requests-per-user")}/>
|
||||
</div>
|
||||
|
||||
<Button onPress={() => navigate("/requests")}>
|
||||
Manage game requests
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
requests: Yup.object({
|
||||
games: Yup.object({
|
||||
enabled: Yup.boolean().required("Required"),
|
||||
"allow-guests-to-request-games": Yup.boolean().required("Required"),
|
||||
"max-open-requests-per-user": Yup.number()
|
||||
.min(0, "Must be at least 0")
|
||||
.max(Number.MAX_SAFE_INTEGER, `Must be lower than ${Number.MAX_SAFE_INTEGER}`)
|
||||
.required("Required"),
|
||||
}).required("Required"),
|
||||
}).required("Required"),
|
||||
});
|
||||
|
||||
export const GameRequestManagement = withConfigPage(GameRequestManagementLayout, "Game Requests", validationSchema);
|
||||
@@ -49,13 +49,19 @@ export default function RequestGameModal({
|
||||
title: game.title,
|
||||
release: game.release
|
||||
}
|
||||
await GameRequestEndpoint.create(request);
|
||||
|
||||
addToast({
|
||||
title: "Request submitted",
|
||||
description: `Your request for "${game.title}" has been submitted.`,
|
||||
color: "success"
|
||||
})
|
||||
try {
|
||||
await GameRequestEndpoint.create(request);
|
||||
|
||||
addToast({
|
||||
title: "Request submitted",
|
||||
description: `Your request for "${game.title}" has been submitted.`,
|
||||
color: "success"
|
||||
});
|
||||
} catch (e) {
|
||||
setIsSearching(false);
|
||||
setIsRequesting(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function search() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import LibraryView from "Frontend/views/LibraryView";
|
||||
import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js";
|
||||
import ErrorView from "Frontend/views/ErrorView";
|
||||
import GameRequestView from "Frontend/views/GameRequestView";
|
||||
import {GameRequestManagement} from "Frontend/components/administration/GameRequestManagement";
|
||||
|
||||
export const {router, routes} = new RouterConfigurationBuilder()
|
||||
.withReactRoutes([
|
||||
@@ -93,6 +94,11 @@ export const {router, routes} = new RouterConfigurationBuilder()
|
||||
element: <LibraryManagementView/>,
|
||||
handle: {title: 'Administration - Library'}
|
||||
},
|
||||
{
|
||||
path: 'requests',
|
||||
element: <GameRequestManagement/>,
|
||||
handle: {title: 'Administration - Game Requests'}
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
element: <UserManagement/>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Envelope, GameController, LockKey, Log, Plug, Users, Wrench} from "@phosphor-icons/react";
|
||||
import {Disc, Envelope, GameController, LockKey, Log, Plug, Users, Wrench} from "@phosphor-icons/react";
|
||||
import withSideMenu, {MenuItem} from "Frontend/components/general/withSideMenu";
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
@@ -7,6 +7,11 @@ const menuItems: MenuItem[] = [
|
||||
url: "libraries",
|
||||
icon: <GameController/>
|
||||
},
|
||||
{
|
||||
title: "Game Requests",
|
||||
url: "requests",
|
||||
icon: <Disc/>
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
url: "users",
|
||||
|
||||
@@ -16,22 +16,29 @@ import {
|
||||
useDisclosure
|
||||
} from "@heroui/react";
|
||||
import RequestGameModal from "Frontend/components/general/modals/RequestGameModal";
|
||||
import {ArrowUp, Check, PlusCircle, X} from "@phosphor-icons/react";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import {ArrowUp, Check, Info, PlusCircle, Trash, X} from "@phosphor-icons/react";
|
||||
import React, {useEffect, useMemo, useState} from "react";
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {GameRequestEndpoint} from "Frontend/generated/endpoints";
|
||||
import {ConfigEndpoint, GameRequestEndpoint} from "Frontend/generated/endpoints";
|
||||
import {gameRequestState} from "Frontend/state/GameRequestState";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import GameRequestDto from "Frontend/generated/org/gameyfin/app/requests/dto/GameRequestDto";
|
||||
import GameRequestStatus from "Frontend/generated/org/gameyfin/app/requests/status/GameRequestStatus";
|
||||
import {isAdmin} from "Frontend/util/utils";
|
||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||
|
||||
export default function GameRequestView() {
|
||||
const rowsPerPage = 25;
|
||||
|
||||
const auth = useAuth();
|
||||
const requestGameModal = useDisclosure();
|
||||
const gameRequests = useSnapshot(gameRequestState).gameRequests
|
||||
const gameRequests = useSnapshot(gameRequestState).gameRequests;
|
||||
|
||||
const [areGameRequestsEnabled, setAreGameRequestsEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
ConfigEndpoint.areGameRequestsEnabled().then(setAreGameRequestsEnabled);
|
||||
}, []);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filters, setFilters] = useState<"all" | GameRequestStatus[]>([GameRequestStatus.PENDING, GameRequestStatus.APPROVED, GameRequestStatus.REJECTED]);
|
||||
@@ -94,7 +101,6 @@ export default function GameRequestView() {
|
||||
return sortedItems.slice(start, end);
|
||||
}, [page, sortedItems]);
|
||||
|
||||
|
||||
function getFilteredRequests() {
|
||||
let filteredRequests = (gameRequests as GameRequestDto[]).filter((gameRequest) => {
|
||||
return gameRequest.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
@@ -124,6 +130,10 @@ export default function GameRequestView() {
|
||||
await GameRequestEndpoint.changeStatus(gameRequest.id, newStatus);
|
||||
}
|
||||
|
||||
async function deleteRequest(gameRequestId: number) {
|
||||
await GameRequestEndpoint.delete(gameRequestId);
|
||||
}
|
||||
|
||||
function hasUserVotedForRequest(gameRequest: GameRequestDto): boolean {
|
||||
if (!auth.state.user) return false;
|
||||
return gameRequest.voters.map(v => v.id).includes(auth.state.user.id);
|
||||
@@ -149,10 +159,16 @@ export default function GameRequestView() {
|
||||
<div className="flex flex-row justify-between mb-8">
|
||||
<h1 className="text-2xl font-bold">Game Requests</h1>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{!areGameRequestsEnabled &&
|
||||
<SmallInfoField icon={Info}
|
||||
message="Request submission is disabled"
|
||||
className="text-foreground/70"/>
|
||||
}
|
||||
<Button className="w-fit"
|
||||
color="primary"
|
||||
startContent={<PlusCircle weight="fill"/>}
|
||||
onPress={requestGameModal.onOpen}>
|
||||
onPress={requestGameModal.onOpen}
|
||||
isDisabled={!areGameRequestsEnabled}>
|
||||
Request a Game
|
||||
</Button>
|
||||
</div>
|
||||
@@ -176,8 +192,8 @@ export default function GameRequestView() {
|
||||
>
|
||||
<SelectItem key={GameRequestStatus.PENDING}>Pending</SelectItem>
|
||||
<SelectItem key={GameRequestStatus.APPROVED}>Approved</SelectItem>
|
||||
<SelectItem key={GameRequestStatus.FULFILLED}>Fulfilled</SelectItem>
|
||||
<SelectItem key={GameRequestStatus.REJECTED}>Rejected</SelectItem>
|
||||
<SelectItem key={GameRequestStatus.FULFILLED}>Fulfilled</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -263,6 +279,15 @@ export default function GameRequestView() {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>}
|
||||
{(isAdmin(auth) || (auth.state.user && item.requester && auth.state.user.id === item.requester.id)) &&
|
||||
<Tooltip content="Delete this request">
|
||||
<Button size="sm" isIconOnly
|
||||
color="danger"
|
||||
onPress={async () => await deleteRequest(item.id)}>
|
||||
<Trash/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -270,7 +295,6 @@ export default function GameRequestView() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
|
||||
<RequestGameModal isOpen={requestGameModal.isOpen}
|
||||
onOpenChange={requestGameModal.onOpenChange}/>
|
||||
</>)
|
||||
|
||||
@@ -108,7 +108,11 @@ export default function MainLayout() {
|
||||
}
|
||||
{isAdmin(auth) &&
|
||||
<NavbarItem>
|
||||
<ScanProgressPopover/>
|
||||
<Tooltip content="View library scan results" placement="bottom">
|
||||
<div>
|
||||
<ScanProgressPopover/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</NavbarItem>
|
||||
}
|
||||
{auth.state.user &&
|
||||
|
||||
@@ -109,7 +109,7 @@ sealed class ConfigProperties<T : Serializable>(
|
||||
data object Enabled : ConfigProperties<Boolean>(
|
||||
Boolean::class,
|
||||
"requests.games.enabled",
|
||||
"Enable game requests",
|
||||
"Enable submission of game requests",
|
||||
true
|
||||
)
|
||||
|
||||
@@ -123,7 +123,7 @@ sealed class ConfigProperties<T : Serializable>(
|
||||
data object MaxOpenRequestsPerUser : ConfigProperties<Int>(
|
||||
Int::class,
|
||||
"requests.games.max-open-requests-per-user",
|
||||
"Maximum number of open (not yet fulfilled or rejected) requests per user. Set to 0 for unlimited.",
|
||||
"Maximum number of pending requests per user. Set to 0 for unlimited.",
|
||||
10
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,5 +2,14 @@ package org.gameyfin.app.games.repositories
|
||||
|
||||
import org.gameyfin.app.games.entities.Game
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import java.time.Instant
|
||||
|
||||
interface GameRepository : JpaRepository<Game, Long>
|
||||
interface GameRepository : JpaRepository<Game, Long> {
|
||||
@Query("SELECT g FROM Game g WHERE g.title = :title AND YEAR(g.release) = YEAR(:release)")
|
||||
fun findByTitleAndReleaseYear(
|
||||
@Param("title") title: String,
|
||||
@Param("release") release: Instant?
|
||||
): List<Game>
|
||||
}
|
||||
@@ -8,22 +8,22 @@ import org.springframework.data.repository.query.Param
|
||||
import java.time.Instant
|
||||
|
||||
interface GameRequestRepository : JpaRepository<GameRequest, Long> {
|
||||
@Query("SELECT g FROM GameRequest g WHERE g.title = :title AND YEAR(g.release) = YEAR(:release) AND g.status NOT IN (:excludedStatuses)")
|
||||
fun findOpenRequestsByTitleAndReleaseYear(
|
||||
@Query("SELECT g FROM GameRequest g WHERE g.title = :title AND YEAR(g.release) = YEAR(:release)")
|
||||
fun findByTitleAndReleaseYear(
|
||||
@Param("title") title: String,
|
||||
@Param("release") release: Instant?,
|
||||
@Param("excludedStatuses") excludedStatuses: List<GameRequestStatus> = listOf(
|
||||
GameRequestStatus.FULFILLED,
|
||||
GameRequestStatus.REJECTED
|
||||
)
|
||||
@Param("release") release: Instant?
|
||||
): List<GameRequest>
|
||||
|
||||
@Query("SELECT g FROM GameRequest g WHERE g.requester.id = :requesterId AND g.status NOT IN (:excludedStatuses)")
|
||||
fun findOpenRequestsByRequesterId(
|
||||
@Query("SELECT g FROM GameRequest g WHERE g.title = :title AND YEAR(g.release) = YEAR(:release) AND g.status NOT IN (:excludedStatuses)")
|
||||
fun findRequestsByTitleAndReleaseYearAndStatusNotIn(
|
||||
@Param("title") title: String,
|
||||
@Param("release") release: Instant?,
|
||||
@Param("excludedStatuses") excludedStatuses: List<GameRequestStatus>
|
||||
): List<GameRequest>
|
||||
|
||||
@Query("SELECT g FROM GameRequest g WHERE g.requester.id = :requesterId AND g.status IN (:statuses)")
|
||||
fun findRequestsByRequesterIdAndStatusIn(
|
||||
@Param("requesterId") requesterId: Long?,
|
||||
@Param("excludedStatuses") excludedStatuses: List<GameRequestStatus> = listOf(
|
||||
GameRequestStatus.FULFILLED,
|
||||
GameRequestStatus.REJECTED
|
||||
)
|
||||
@Param("statuses") statuses: List<GameRequestStatus>
|
||||
): List<GameRequest>
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.gameyfin.app.requests
|
||||
|
||||
import com.vaadin.hilla.exception.EndpointException
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gameyfin.app.config.ConfigProperties
|
||||
import org.gameyfin.app.config.ConfigService
|
||||
import org.gameyfin.app.core.events.GameCreatedEvent
|
||||
import org.gameyfin.app.core.security.getCurrentAuth
|
||||
import org.gameyfin.app.core.security.isAdmin
|
||||
import org.gameyfin.app.games.repositories.GameRepository
|
||||
import org.gameyfin.app.requests.dto.GameRequestCreationDto
|
||||
import org.gameyfin.app.requests.dto.GameRequestDto
|
||||
import org.gameyfin.app.requests.dto.GameRequestEvent
|
||||
@@ -26,9 +28,10 @@ import kotlin.time.toJavaDuration
|
||||
|
||||
@Service
|
||||
class GameRequestService(
|
||||
private val gameRequestRepository: GameRequestRepository,
|
||||
private val config: ConfigService,
|
||||
private val userService: UserService,
|
||||
private val config: ConfigService
|
||||
private val gameRequestRepository: GameRequestRepository,
|
||||
private val gameRepository: GameRepository
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -63,17 +66,24 @@ class GameRequestService(
|
||||
|
||||
// Check if requests are enabled
|
||||
if (config.get(ConfigProperties.Requests.Games.Enabled) != true) {
|
||||
throw IllegalStateException("Game requests are disabled")
|
||||
throw EndpointException("Game requests are disabled")
|
||||
}
|
||||
|
||||
// Check if game is already available
|
||||
val existingGames = gameRepository.findByTitleAndReleaseYear(gameRequest.title, gameRequest.release)
|
||||
if (existingGames.isNotEmpty()) {
|
||||
throw EndpointException(
|
||||
"This game is already available (ID: ${existingGames[0].id})"
|
||||
)
|
||||
}
|
||||
|
||||
// Check if a request with the same title and release year already exists
|
||||
val existingRequests = gameRequestRepository.findOpenRequestsByTitleAndReleaseYear(
|
||||
val existingRequests = gameRequestRepository.findByTitleAndReleaseYear(
|
||||
gameRequest.title,
|
||||
gameRequest.release,
|
||||
emptyList()
|
||||
gameRequest.release
|
||||
)
|
||||
if (existingRequests.isNotEmpty()) {
|
||||
throw IllegalStateException("A request for this game already exists (ID: ${existingRequests[0].id})")
|
||||
throw EndpointException("A request for this game already exists (ID: ${existingRequests[0].id})")
|
||||
}
|
||||
|
||||
val auth = getCurrentAuth()
|
||||
@@ -81,16 +91,19 @@ class GameRequestService(
|
||||
|
||||
// Check if guests are allowed to create requests
|
||||
if (config.get(ConfigProperties.Requests.Games.AllowGuestsToRequestGames) != true && currentUser == null) {
|
||||
throw IllegalStateException("Only registered users can create game requests")
|
||||
throw EndpointException("Only registered users can create game requests")
|
||||
}
|
||||
|
||||
// Check if user has too many open requests (0 means no limit per user)
|
||||
// Note: All guests are treated as a single user with null ID and thus share their request limit
|
||||
// Note: Admins are exempt from this limit
|
||||
val openRequestsForUser = gameRequestRepository.findOpenRequestsByRequesterId(currentUser?.id)
|
||||
val pendingRequestsForUser = gameRequestRepository.findRequestsByRequesterIdAndStatusIn(
|
||||
currentUser?.id,
|
||||
listOf(GameRequestStatus.PENDING)
|
||||
)
|
||||
val maxRequestsPerUser = config.get(ConfigProperties.Requests.Games.MaxOpenRequestsPerUser) ?: 0
|
||||
if (maxRequestsPerUser == 0 || (auth?.isAdmin() != true && openRequestsForUser.size >= maxRequestsPerUser)) {
|
||||
throw IllegalStateException("You have reached the maximum number of open requests (${maxRequestsPerUser})")
|
||||
if (maxRequestsPerUser == 0 || (auth?.isAdmin() != true && pendingRequestsForUser.size >= maxRequestsPerUser)) {
|
||||
throw EndpointException("You have reached the maximum number of pending requests (${maxRequestsPerUser})")
|
||||
}
|
||||
|
||||
val newGameRequest = GameRequest(
|
||||
@@ -115,8 +128,9 @@ class GameRequestService(
|
||||
val requester = gameRequest.requester
|
||||
|
||||
// Check if the current user is the requester or an admin
|
||||
if (auth?.isAdmin() != true || requester == null || requester.id != currentUser?.id) {
|
||||
throw IllegalStateException("Only the requester or an admin can delete a game request")
|
||||
// Note: Requests submitted by guests (request is null) can only be deleted by an admin
|
||||
if (auth?.isAdmin() != true && (requester == null || requester.id != currentUser?.id)) {
|
||||
throw EndpointException("Only the requester or an admin can delete a game request")
|
||||
}
|
||||
|
||||
gameRequestRepository.delete(gameRequest)
|
||||
@@ -170,9 +184,10 @@ class GameRequestService(
|
||||
return
|
||||
}
|
||||
|
||||
val matchingRequests = gameRequestRepository.findOpenRequestsByTitleAndReleaseYear(
|
||||
val matchingRequests = gameRequestRepository.findRequestsByTitleAndReleaseYearAndStatusNotIn(
|
||||
gameTitle,
|
||||
gameRelease
|
||||
gameRelease,
|
||||
listOf(GameRequestStatus.FULFILLED)
|
||||
)
|
||||
|
||||
matchingRequests.forEach { request ->
|
||||
|
||||
Reference in New Issue
Block a user