Add permission checks

Add UI for config
This commit is contained in:
grimsi
2025-09-03 10:26:17 +02:00
parent c50c3e43bb
commit 6e86584fd8
12 changed files with 1072 additions and 1280 deletions
@@ -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() {
+6
View File
@@ -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}/>
</>)
+5 -1
View File
@@ -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 &&