mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Finish implementing game requests
This commit is contained in:
@@ -47,10 +47,10 @@ function ViewWithAuth() {
|
|||||||
initializeLibraryState();
|
initializeLibraryState();
|
||||||
initializeGameState();
|
initializeGameState();
|
||||||
initializeGameRequestState();
|
initializeGameRequestState();
|
||||||
|
initializePluginState();
|
||||||
|
|
||||||
if (isAdmin(auth)) {
|
if (isAdmin(auth)) {
|
||||||
initializeScanState();
|
initializeScanState();
|
||||||
initializePluginState();
|
|
||||||
}
|
}
|
||||||
}, [auth]);
|
}, [auth]);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import RecentlyAddedView from "Frontend/views/RecentlyAddedView";
|
|||||||
import LibraryView from "Frontend/views/LibraryView";
|
import LibraryView from "Frontend/views/LibraryView";
|
||||||
import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js";
|
import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js";
|
||||||
import ErrorView from "Frontend/views/ErrorView";
|
import ErrorView from "Frontend/views/ErrorView";
|
||||||
|
import GameRequestView from "Frontend/views/GameRequestView";
|
||||||
|
|
||||||
export const {router, routes} = new RouterConfigurationBuilder()
|
export const {router, routes} = new RouterConfigurationBuilder()
|
||||||
.withReactRoutes([
|
.withReactRoutes([
|
||||||
@@ -47,6 +48,11 @@ export const {router, routes} = new RouterConfigurationBuilder()
|
|||||||
element: <RecentlyAddedView/>,
|
element: <RecentlyAddedView/>,
|
||||||
handle: {title: 'Recently Added'}
|
handle: {title: 'Recently Added'}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/requests',
|
||||||
|
element: <GameRequestView/>,
|
||||||
|
handle: {title: 'Game requests'}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'library/:libraryId',
|
path: 'library/:libraryId',
|
||||||
element: <LibraryView/>
|
element: <LibraryView/>
|
||||||
|
|||||||
@@ -38,13 +38,19 @@ export async function initializeGameRequestState() {
|
|||||||
case "created":
|
case "created":
|
||||||
case "updated":
|
case "updated":
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
gameRequestState.state[gameRequestEvent.id] = gameRequestEvent;
|
gameRequestState.state[gameRequestEvent.gameRequest.id] = gameRequestEvent.gameRequest;
|
||||||
break;
|
break;
|
||||||
case "deleted":
|
case "deleted":
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
delete gameRequestState.state[gameRequestEvent.id];
|
delete gameRequestState.state[gameRequestEvent.gameRequestId];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
SortDescriptor,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableColumn,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
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 {useAuth} from "Frontend/util/auth";
|
||||||
|
import {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";
|
||||||
|
|
||||||
|
export default function GameRequestView() {
|
||||||
|
const rowsPerPage = 25;
|
||||||
|
|
||||||
|
const auth = useAuth();
|
||||||
|
const requestGameModal = useDisclosure();
|
||||||
|
const gameRequests = useSnapshot(gameRequestState).gameRequests
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [filters, setFilters] = useState<"all" | GameRequestStatus[]>([GameRequestStatus.PENDING, GameRequestStatus.APPROVED, GameRequestStatus.REJECTED]);
|
||||||
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({column: "votes", direction: "descending"});
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pages = useMemo(() => {
|
||||||
|
return Math.ceil(getFilteredRequests().length / rowsPerPage);
|
||||||
|
}, [gameRequests, filters]);
|
||||||
|
|
||||||
|
const filteredItems = useMemo(() => {
|
||||||
|
return getFilteredRequests();
|
||||||
|
}, [gameRequests, filters, searchTerm]);
|
||||||
|
|
||||||
|
const sortedItems = useMemo(() => {
|
||||||
|
return (filteredItems as GameRequestDto[]).slice().sort((a, b) => {
|
||||||
|
let cmp: number;
|
||||||
|
|
||||||
|
switch (sortDescriptor.column) {
|
||||||
|
case "title":
|
||||||
|
cmp = a.title.localeCompare(b.title);
|
||||||
|
break;
|
||||||
|
case "votes":
|
||||||
|
cmp = a.voters.length - b.voters.length;
|
||||||
|
if (cmp === 0) {
|
||||||
|
// If votes are equal, sort by creation date (newest first)
|
||||||
|
cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
const statusOrder = {
|
||||||
|
[GameRequestStatus.PENDING]: 1,
|
||||||
|
[GameRequestStatus.APPROVED]: 2,
|
||||||
|
[GameRequestStatus.REJECTED]: 3,
|
||||||
|
[GameRequestStatus.FULFILLED]: 4
|
||||||
|
};
|
||||||
|
cmp = (statusOrder[a.status] || 99) - (statusOrder[b.status] || 99);
|
||||||
|
break;
|
||||||
|
case "createdAt":
|
||||||
|
cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
|
break;
|
||||||
|
case "updatedAt":
|
||||||
|
cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0; // No sorting if the column is not recognized
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDescriptor.direction === "descending") {
|
||||||
|
cmp *= -1; // Reverse the comparison if sorting in descending order
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmp;
|
||||||
|
});
|
||||||
|
}, [filteredItems, sortDescriptor]);
|
||||||
|
|
||||||
|
const pagedItems = useMemo(() => {
|
||||||
|
const start = (page - 1) * rowsPerPage;
|
||||||
|
const end = start + rowsPerPage;
|
||||||
|
return sortedItems.slice(start, end);
|
||||||
|
}, [page, sortedItems]);
|
||||||
|
|
||||||
|
|
||||||
|
function getFilteredRequests() {
|
||||||
|
let filteredRequests = (gameRequests as GameRequestDto[]).filter((gameRequest) => {
|
||||||
|
return gameRequest.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(gameRequest.requester && gameRequest.requester.username.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredRequests = filteredRequests.filter((gameRequest) => {
|
||||||
|
return filters.includes(gameRequest.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleVote(gameRequestId: number) {
|
||||||
|
await GameRequestEndpoint.toggleVote(gameRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleApprove(gameRequest: GameRequestDto) {
|
||||||
|
if (gameRequest.status == GameRequestStatus.FULFILLED) return;
|
||||||
|
const newStatus = gameRequest.status === GameRequestStatus.APPROVED ? GameRequestStatus.PENDING : GameRequestStatus.APPROVED;
|
||||||
|
await GameRequestEndpoint.changeStatus(gameRequest.id, newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleReject(gameRequest: GameRequestDto) {
|
||||||
|
if (gameRequest.status == GameRequestStatus.FULFILLED) return;
|
||||||
|
const newStatus = gameRequest.status === GameRequestStatus.REJECTED ? GameRequestStatus.PENDING : GameRequestStatus.REJECTED;
|
||||||
|
await GameRequestEndpoint.changeStatus(gameRequest.id, newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasUserVotedForRequest(gameRequest: GameRequestDto): boolean {
|
||||||
|
if (!auth.state.user) return false;
|
||||||
|
return gameRequest.voters.map(v => v.id).includes(auth.state.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusToBadge(status: GameRequestStatus) {
|
||||||
|
switch (status) {
|
||||||
|
case GameRequestStatus.APPROVED:
|
||||||
|
return <Chip size="sm" radius="sm"
|
||||||
|
className="text-xs bg-success-300 text-success-foreground">Approved</Chip>;
|
||||||
|
case GameRequestStatus.FULFILLED:
|
||||||
|
return <Chip size="sm" radius="sm" className="text-xs bg-success">Fulfilled</Chip>;
|
||||||
|
case GameRequestStatus.REJECTED:
|
||||||
|
return <Chip size="sm" radius="sm"
|
||||||
|
className="text-xs bg-danger-300 text-danger-foreground">Rejected</Chip>;
|
||||||
|
case GameRequestStatus.PENDING:
|
||||||
|
default:
|
||||||
|
return <Chip size="sm" radius="sm" className="text-xs">Pending</Chip>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<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">
|
||||||
|
<Button className="w-fit"
|
||||||
|
color="primary"
|
||||||
|
startContent={<PlusCircle weight="fill"/>}
|
||||||
|
onPress={requestGameModal.onOpen}>
|
||||||
|
Request a Game
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2 justify-between mb-4">
|
||||||
|
<Input
|
||||||
|
className="w-96"
|
||||||
|
isClearable
|
||||||
|
placeholder="Search"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
onClear={() => setSearchTerm("")}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
selectedKeys={filters}
|
||||||
|
onSelectionChange={keys => setFilters(Array.from(keys) as any)}
|
||||||
|
selectionMode="multiple"
|
||||||
|
className="w-64"
|
||||||
|
>
|
||||||
|
<SelectItem key={GameRequestStatus.PENDING}>Pending</SelectItem>
|
||||||
|
<SelectItem key={GameRequestStatus.APPROVED}>Approved</SelectItem>
|
||||||
|
<SelectItem key={GameRequestStatus.FULFILLED}>Fulfilled</SelectItem>
|
||||||
|
<SelectItem key={GameRequestStatus.REJECTED}>Rejected</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table removeWrapper isStriped
|
||||||
|
sortDescriptor={sortDescriptor}
|
||||||
|
onSortChange={setSortDescriptor}
|
||||||
|
bottomContent={
|
||||||
|
<div className="flex w-full justify-center sticky">
|
||||||
|
{pagedItems.length > 0 &&
|
||||||
|
<Pagination
|
||||||
|
isCompact
|
||||||
|
showControls
|
||||||
|
showShadow
|
||||||
|
color="primary"
|
||||||
|
page={page}
|
||||||
|
total={pages}
|
||||||
|
onChange={(page) => setPage(page)}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn key="title" allowsSorting>Title & Release</TableColumn>
|
||||||
|
<TableColumn>Submitted by</TableColumn>
|
||||||
|
<TableColumn key="createdAt" allowsSorting>Submitted</TableColumn>
|
||||||
|
<TableColumn key="updatedAt" allowsSorting>Updated</TableColumn>
|
||||||
|
<TableColumn key="status" allowsSorting>Status</TableColumn>
|
||||||
|
{/* width={1} keeps the column as far to the right as possible*/}
|
||||||
|
<TableColumn key="votes" allowsSorting width={1}>Votes</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="Your search did not match any requests." items={pagedItems}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>
|
||||||
|
{item.title} ({item.release ? new Date(item.release).getFullYear() : "unknown"})
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<p className="text-foreground/70">
|
||||||
|
{item.requester ?
|
||||||
|
item.requester.username :
|
||||||
|
"Guest"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{new Date(item.createdAt).toLocaleDateString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{new Date(item.updatedAt).toLocaleDateString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="min-w-24">
|
||||||
|
{statusToBadge(item.status)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Tooltip content="Vote for this request">
|
||||||
|
<Button size="sm"
|
||||||
|
variant={hasUserVotedForRequest(item as GameRequestDto) ? "solid" : "bordered"}
|
||||||
|
color={hasUserVotedForRequest(item as GameRequestDto) ? "primary" : "default"}
|
||||||
|
isDisabled={!auth.state.user || item.status === GameRequestStatus.FULFILLED}
|
||||||
|
startContent={<ArrowUp/>}
|
||||||
|
onPress={async () => await toggleVote(item.id)}>
|
||||||
|
{item.voters.length}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{isAdmin(auth) && <div className="flex flex-row gap-2">
|
||||||
|
<Tooltip content="Approve this request">
|
||||||
|
<Button size="sm" isIconOnly
|
||||||
|
variant={item.status === GameRequestStatus.APPROVED ? "solid" : "bordered"}
|
||||||
|
color={item.status === GameRequestStatus.APPROVED ? "primary" : "default"}
|
||||||
|
isDisabled={item.status === GameRequestStatus.FULFILLED}
|
||||||
|
onPress={async () => await toggleApprove(item as GameRequestDto)}>
|
||||||
|
<Check/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Reject this request">
|
||||||
|
<Button size="sm" isIconOnly
|
||||||
|
variant={item.status === GameRequestStatus.REJECTED ? "solid" : "bordered"}
|
||||||
|
color={item.status === GameRequestStatus.REJECTED ? "primary" : "default"}
|
||||||
|
isDisabled={item.status === GameRequestStatus.FULFILLED}
|
||||||
|
onPress={async () => await toggleReject(item as GameRequestDto)}>
|
||||||
|
<X/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
|
||||||
|
<RequestGameModal isOpen={requestGameModal.isOpen}
|
||||||
|
onOpenChange={requestGameModal.onOpenChange}/>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -1,21 +1,11 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import ProfileMenu from "Frontend/components/ProfileMenu";
|
import ProfileMenu from "Frontend/components/ProfileMenu";
|
||||||
import {
|
import {Button, Divider, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, Tooltip} from "@heroui/react";
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Link,
|
|
||||||
Navbar,
|
|
||||||
NavbarBrand,
|
|
||||||
NavbarContent,
|
|
||||||
NavbarItem,
|
|
||||||
Tooltip,
|
|
||||||
useDisclosure
|
|
||||||
} from "@heroui/react";
|
|
||||||
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
||||||
import * as PackageJson from "../../../../package.json";
|
import * as PackageJson from "../../../../package.json";
|
||||||
import {Outlet, useLocation, useNavigate} from "react-router";
|
import {Outlet, useLocation, useNavigate} from "react-router";
|
||||||
import {useAuth} from "Frontend/util/auth";
|
import {useAuth} from "Frontend/util/auth";
|
||||||
import {ArrowLeft, DiceSix, Heart, House, ListMagnifyingGlass, PlusCircle, SignIn} from "@phosphor-icons/react";
|
import {ArrowLeft, DiceSix, Disc, Heart, House, ListMagnifyingGlass, SignIn} from "@phosphor-icons/react";
|
||||||
import Confetti, {ConfettiProps} from "react-confetti-boom";
|
import Confetti, {ConfettiProps} from "react-confetti-boom";
|
||||||
import {useTheme} from "next-themes";
|
import {useTheme} from "next-themes";
|
||||||
import {useUserPreferenceService} from "Frontend/util/user-preference-service";
|
import {useUserPreferenceService} from "Frontend/util/user-preference-service";
|
||||||
@@ -24,7 +14,6 @@ import {useSnapshot} from "valtio/react";
|
|||||||
import {gameState} from "Frontend/state/GameState";
|
import {gameState} from "Frontend/state/GameState";
|
||||||
import ScanProgressPopover from "Frontend/components/general/ScanProgressPopover";
|
import ScanProgressPopover from "Frontend/components/general/ScanProgressPopover";
|
||||||
import {isAdmin} from "Frontend/util/utils";
|
import {isAdmin} from "Frontend/util/utils";
|
||||||
import RequestGameModal from "Frontend/components/general/modals/RequestGameModal";
|
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -37,8 +26,6 @@ export default function MainLayout() {
|
|||||||
const [isExploding, setIsExploding] = useState(false);
|
const [isExploding, setIsExploding] = useState(false);
|
||||||
const games = useSnapshot(gameState).games;
|
const games = useSnapshot(gameState).games;
|
||||||
|
|
||||||
const requestGameModal = useDisclosure();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userPreferenceService.sync()
|
userPreferenceService.sync()
|
||||||
.then(() => loadUserTheme().catch(console.error))
|
.then(() => loadUserTheme().catch(console.error))
|
||||||
@@ -110,9 +97,11 @@ export default function MainLayout() {
|
|||||||
{auth.state.user &&
|
{auth.state.user &&
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<Tooltip content="Request a game" placement="bottom">
|
<Tooltip content="Request a game" placement="bottom">
|
||||||
<Button isIconOnly color="primary" variant="light"
|
<Button color="primary"
|
||||||
onPress={requestGameModal.onOpen}>
|
isDisabled={window.location.pathname.startsWith("/requests")}
|
||||||
<PlusCircle size={26} weight="fill"/>
|
onPress={() => navigate("/requests")}
|
||||||
|
startContent={<Disc weight="fill"/>}>
|
||||||
|
Requests
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
@@ -165,10 +154,6 @@ export default function MainLayout() {
|
|||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<RequestGameModal isOpen={requestGameModal.isOpen}
|
|
||||||
onOpenChange={requestGameModal.onOpenChange}/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,4 +55,8 @@ class ConfigEndpoint(
|
|||||||
@DynamicPublicAccess
|
@DynamicPublicAccess
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
fun isPublicAccessEnabled(): Boolean = configService.get(ConfigProperties.Libraries.AllowPublicAccess) == true
|
fun isPublicAccessEnabled(): Boolean = configService.get(ConfigProperties.Libraries.AllowPublicAccess) == true
|
||||||
|
|
||||||
|
@DynamicPublicAccess
|
||||||
|
@AnonymousAllowed
|
||||||
|
fun areGameRequestsEnabled(): Boolean = configService.get(ConfigProperties.Requests.Games.Enabled) == true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,32 @@ sealed class ConfigProperties<T : Serializable>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Requests */
|
||||||
|
sealed class Requests {
|
||||||
|
sealed class Games {
|
||||||
|
data object Enabled : ConfigProperties<Boolean>(
|
||||||
|
Boolean::class,
|
||||||
|
"requests.games.enabled",
|
||||||
|
"Enable game requests",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
data object AllowGuestsToRequestGames : ConfigProperties<Boolean>(
|
||||||
|
Boolean::class,
|
||||||
|
"requests.games.allow-guests-to-request-games",
|
||||||
|
"Allow guests (not logged in) to create game requests",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
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.",
|
||||||
|
10
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** User management */
|
/** User management */
|
||||||
sealed class Users {
|
sealed class Users {
|
||||||
sealed class SignUps {
|
sealed class SignUps {
|
||||||
|
|||||||
@@ -1,41 +1,46 @@
|
|||||||
package org.gameyfin.app.core.plugins
|
package org.gameyfin.app.core.plugins
|
||||||
|
|
||||||
|
import com.vaadin.flow.server.auth.AnonymousAllowed
|
||||||
import com.vaadin.hilla.Endpoint
|
import com.vaadin.hilla.Endpoint
|
||||||
import jakarta.annotation.security.PermitAll
|
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
|
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
||||||
import org.gameyfin.app.core.plugins.dto.PluginUpdateDto
|
import org.gameyfin.app.core.plugins.dto.PluginUpdateDto
|
||||||
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
|
||||||
import org.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
import org.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@DynamicPublicAccess
|
||||||
|
@AnonymousAllowed
|
||||||
class PluginEndpoint(
|
class PluginEndpoint(
|
||||||
private val pluginService: PluginService,
|
private val pluginService: PluginService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PermitAll
|
|
||||||
fun subscribe(): Flux<List<PluginUpdateDto>> {
|
fun subscribe(): Flux<List<PluginUpdateDto>> {
|
||||||
return if (isCurrentUserAdmin()) PluginService.subscribe()
|
return PluginService.subscribe()
|
||||||
else Flux.empty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll() = pluginService.getAll().sortedByDescending { it.priority }
|
fun getAll() = pluginService.getAll().sortedByDescending { it.priority }
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun enablePlugin(pluginId: String) = pluginService.enablePlugin(pluginId)
|
fun enablePlugin(pluginId: String) = pluginService.enablePlugin(pluginId)
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun disablePlugin(pluginId: String) = pluginService.disablePlugin(pluginId)
|
fun disablePlugin(pluginId: String) = pluginService.disablePlugin(pluginId)
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun setPluginPriorities(pluginPriorities: Map<String, Int>) =
|
fun setPluginPriorities(pluginPriorities: Map<String, Int>) =
|
||||||
pluginService.setPluginPriorities(pluginPriorities)
|
pluginService.setPluginPriorities(pluginPriorities)
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult =
|
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult =
|
||||||
pluginService.validatePluginConfig(pluginId, true)
|
pluginService.validatePluginConfig(pluginId, true)
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun validateNewConfig(pluginId: String, config: Map<String, String>): PluginConfigValidationResult =
|
fun validateNewConfig(pluginId: String, config: Map<String, String>): PluginConfigValidationResult =
|
||||||
pluginService.validatePluginConfig(pluginId, config)
|
pluginService.validatePluginConfig(pluginId, config)
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) =
|
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) =
|
||||||
pluginService.updateConfig(pluginId, updatedConfig)
|
pluginService.updateConfig(pluginId, updatedConfig)
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,7 @@ class SecurityConfig(
|
|||||||
.requestMatchers("/game/**").access(DynamicPublicAccessAuthorizationManager(config))
|
.requestMatchers("/game/**").access(DynamicPublicAccessAuthorizationManager(config))
|
||||||
.requestMatchers("/library/**").access(DynamicPublicAccessAuthorizationManager(config))
|
.requestMatchers("/library/**").access(DynamicPublicAccessAuthorizationManager(config))
|
||||||
.requestMatchers("/search/**").access(DynamicPublicAccessAuthorizationManager(config))
|
.requestMatchers("/search/**").access(DynamicPublicAccessAuthorizationManager(config))
|
||||||
|
.requestMatchers("/requests/**").access(DynamicPublicAccessAuthorizationManager(config))
|
||||||
.requestMatchers("/download/**").access(DynamicPublicAccessAuthorizationManager(config))
|
.requestMatchers("/download/**").access(DynamicPublicAccessAuthorizationManager(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ fun getCurrentAuth(): Authentication? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isCurrentUserAdmin(): Boolean {
|
fun isCurrentUserAdmin(): Boolean {
|
||||||
return getCurrentAuth()?.authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN }
|
return getCurrentAuth()?.isAdmin() ?: false
|
||||||
?: false
|
}
|
||||||
|
|
||||||
|
fun Authentication.isAdmin(): Boolean {
|
||||||
|
return this.authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN } ?: false
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,10 @@ class GameEndpoint(
|
|||||||
|
|
||||||
fun getAll(): List<GameDto> = gameService.getAll()
|
fun getAll(): List<GameDto> = gameService.getAll()
|
||||||
|
|
||||||
|
fun getPotentialMatches(searchTerm: String): List<GameSearchResultDto> {
|
||||||
|
return gameService.getPotentialMatches(searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun updateGame(game: GameUpdateDto) = gameService.edit(game)
|
fun updateGame(game: GameUpdateDto) = gameService.edit(game)
|
||||||
|
|
||||||
@@ -43,11 +47,6 @@ class GameEndpoint(
|
|||||||
gameService.delete(gameId)
|
gameService.delete(gameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
|
||||||
fun getPotentialMatches(searchTerm: String): List<GameSearchResultDto> {
|
|
||||||
return gameService.getPotentialMatches(searchTerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun matchManually(
|
fun matchManually(
|
||||||
originalIds: Map<String, ExternalProviderIdDto>,
|
originalIds: Map<String, ExternalProviderIdDto>,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.vaadin.flow.server.auth.AnonymousAllowed
|
|||||||
import com.vaadin.hilla.Endpoint
|
import com.vaadin.hilla.Endpoint
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
import org.gameyfin.app.config.ConfigService
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
||||||
import org.gameyfin.app.requests.dto.GameRequestCreationDto
|
import org.gameyfin.app.requests.dto.GameRequestCreationDto
|
||||||
@@ -17,7 +18,8 @@ import reactor.core.publisher.Flux
|
|||||||
@DynamicPublicAccess
|
@DynamicPublicAccess
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
class GameRequestEndpoint(
|
class GameRequestEndpoint(
|
||||||
private val gameRequestService: GameRequestService
|
private val gameRequestService: GameRequestService,
|
||||||
|
private val config: ConfigService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flux<List<GameRequestEvent>> {
|
fun subscribe(): Flux<List<GameRequestEvent>> {
|
||||||
@@ -26,7 +28,6 @@ class GameRequestEndpoint(
|
|||||||
|
|
||||||
fun getAll() = gameRequestService.getAll()
|
fun getAll() = gameRequestService.getAll()
|
||||||
|
|
||||||
@PermitAll
|
|
||||||
fun create(gameRequest: GameRequestCreationDto) {
|
fun create(gameRequest: GameRequestCreationDto) {
|
||||||
gameRequestService.createRequest(gameRequest)
|
gameRequestService.createRequest(gameRequest)
|
||||||
}
|
}
|
||||||
@@ -36,13 +37,13 @@ class GameRequestEndpoint(
|
|||||||
gameRequestService.toggleRequestVote(gameRequestId)
|
gameRequestService.toggleRequestVote(gameRequestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PermitAll
|
||||||
|
fun delete(gameRequestId: Long) {
|
||||||
|
gameRequestService.deleteRequest(gameRequestId)
|
||||||
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun changeStatus(gameRequestId: Long, newStatus: GameRequestStatus) {
|
fun changeStatus(gameRequestId: Long, newStatus: GameRequestStatus) {
|
||||||
gameRequestService.changeRequestStatus(gameRequestId, newStatus)
|
gameRequestService.changeRequestStatus(gameRequestId, newStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
|
||||||
fun delete(gameRequestId: Long) {
|
|
||||||
gameRequestService.deleteRequest(gameRequestId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -17,4 +17,13 @@ interface GameRequestRepository : JpaRepository<GameRequest, Long> {
|
|||||||
GameRequestStatus.REJECTED
|
GameRequestStatus.REJECTED
|
||||||
)
|
)
|
||||||
): List<GameRequest>
|
): List<GameRequest>
|
||||||
|
|
||||||
|
@Query("SELECT g FROM GameRequest g WHERE g.requester.id = :requesterId AND g.status NOT IN (:excludedStatuses)")
|
||||||
|
fun findOpenRequestsByRequesterId(
|
||||||
|
@Param("requesterId") requesterId: Long?,
|
||||||
|
@Param("excludedStatuses") excludedStatuses: List<GameRequestStatus> = listOf(
|
||||||
|
GameRequestStatus.FULFILLED,
|
||||||
|
GameRequestStatus.REJECTED
|
||||||
|
)
|
||||||
|
): List<GameRequest>
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package org.gameyfin.app.requests
|
package org.gameyfin.app.requests
|
||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
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.events.GameCreatedEvent
|
||||||
import org.gameyfin.app.core.security.getCurrentAuth
|
import org.gameyfin.app.core.security.getCurrentAuth
|
||||||
|
import org.gameyfin.app.core.security.isAdmin
|
||||||
import org.gameyfin.app.requests.dto.GameRequestCreationDto
|
import org.gameyfin.app.requests.dto.GameRequestCreationDto
|
||||||
import org.gameyfin.app.requests.dto.GameRequestDto
|
import org.gameyfin.app.requests.dto.GameRequestDto
|
||||||
import org.gameyfin.app.requests.dto.GameRequestEvent
|
import org.gameyfin.app.requests.dto.GameRequestEvent
|
||||||
@@ -11,9 +14,11 @@ import org.gameyfin.app.requests.extensions.toDto
|
|||||||
import org.gameyfin.app.requests.extensions.toDtos
|
import org.gameyfin.app.requests.extensions.toDtos
|
||||||
import org.gameyfin.app.requests.status.GameRequestStatus
|
import org.gameyfin.app.requests.status.GameRequestStatus
|
||||||
import org.gameyfin.app.users.UserService
|
import org.gameyfin.app.users.UserService
|
||||||
|
import org.gameyfin.app.users.entities.User
|
||||||
import org.springframework.context.event.EventListener
|
import org.springframework.context.event.EventListener
|
||||||
import org.springframework.scheduling.annotation.Async
|
import org.springframework.scheduling.annotation.Async
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -22,7 +27,8 @@ import kotlin.time.toJavaDuration
|
|||||||
@Service
|
@Service
|
||||||
class GameRequestService(
|
class GameRequestService(
|
||||||
private val gameRequestRepository: GameRequestRepository,
|
private val gameRequestRepository: GameRequestRepository,
|
||||||
private val userService: UserService
|
private val userService: UserService,
|
||||||
|
private val config: ConfigService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -54,33 +60,82 @@ class GameRequestService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createRequest(gameRequest: GameRequestCreationDto) {
|
fun createRequest(gameRequest: GameRequestCreationDto) {
|
||||||
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
|
||||||
val currentUser =
|
|
||||||
userService.getByUsername(auth.name) ?: throw IllegalStateException("Current user not found")
|
|
||||||
|
|
||||||
val gameRequest = GameRequest(
|
// Check if requests are enabled
|
||||||
|
if (config.get(ConfigProperties.Requests.Games.Enabled) != true) {
|
||||||
|
throw IllegalStateException("Game requests are disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a request with the same title and release year already exists
|
||||||
|
val existingRequests = gameRequestRepository.findOpenRequestsByTitleAndReleaseYear(
|
||||||
|
gameRequest.title,
|
||||||
|
gameRequest.release,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
if (existingRequests.isNotEmpty()) {
|
||||||
|
throw IllegalStateException("A request for this game already exists (ID: ${existingRequests[0].id})")
|
||||||
|
}
|
||||||
|
|
||||||
|
val auth = getCurrentAuth()
|
||||||
|
val currentUser = auth?.let { userService.getByUsername(it.name) }
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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})")
|
||||||
|
}
|
||||||
|
|
||||||
|
val newGameRequest = GameRequest(
|
||||||
title = gameRequest.title,
|
title = gameRequest.title,
|
||||||
release = gameRequest.release,
|
release = gameRequest.release,
|
||||||
status = GameRequestStatus.PENDING,
|
status = GameRequestStatus.PENDING,
|
||||||
requester = currentUser
|
requester = currentUser,
|
||||||
|
voters = mutableSetOf<User>().apply {
|
||||||
|
currentUser?.let { add(it) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
gameRequestRepository.save(gameRequest)
|
gameRequestRepository.save(newGameRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRequest(id: Long) {
|
fun deleteRequest(id: Long) {
|
||||||
val gameRequest = gameRequestRepository.findById(id)
|
val gameRequest = gameRequestRepository.findById(id)
|
||||||
.orElseThrow { NoSuchElementException("No game request found with id $id") }
|
.orElseThrow { NoSuchElementException("No game request found with id $id") }
|
||||||
|
|
||||||
|
val auth = getCurrentAuth()
|
||||||
|
val currentUser = auth?.let { userService.getByUsername(it.name) }
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
gameRequestRepository.delete(gameRequest)
|
gameRequestRepository.delete(gameRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeRequestStatus(id: Long, status: GameRequestStatus) {
|
fun changeRequestStatus(id: Long, status: GameRequestStatus) {
|
||||||
val gameRequest = gameRequestRepository.findById(id)
|
val gameRequest = gameRequestRepository.findById(id)
|
||||||
.orElseThrow { NoSuchElementException("No game request found with id $id") }
|
.orElseThrow { NoSuchElementException("No game request found with id $id") }
|
||||||
|
|
||||||
|
if (gameRequest.status == GameRequestStatus.FULFILLED) {
|
||||||
|
log.debug { "Status of requests with status ${GameRequestStatus.FULFILLED} can't be changed" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
gameRequest.status = status
|
gameRequest.status = status
|
||||||
gameRequestRepository.save(gameRequest)
|
gameRequestRepository.save(gameRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
fun toggleRequestVote(id: Long) {
|
fun toggleRequestVote(id: Long) {
|
||||||
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val currentUser =
|
val currentUser =
|
||||||
@@ -88,15 +143,17 @@ class GameRequestService(
|
|||||||
val gameRequest = gameRequestRepository.findById(id)
|
val gameRequest = gameRequestRepository.findById(id)
|
||||||
.orElseThrow { NoSuchElementException("No game request found with id $id") }
|
.orElseThrow { NoSuchElementException("No game request found with id $id") }
|
||||||
|
|
||||||
if (gameRequest.requester.id == currentUser.id) {
|
// Replace the voters collection to ensure Hibernate detects the change
|
||||||
throw IllegalStateException("You cannot vote for your own request")
|
val updatedVoters = gameRequest.voters.toMutableSet()
|
||||||
}
|
if (updatedVoters.contains(currentUser)) {
|
||||||
|
updatedVoters.remove(currentUser)
|
||||||
if (gameRequest.voters.contains(currentUser)) {
|
|
||||||
gameRequest.voters.remove(currentUser)
|
|
||||||
} else {
|
} else {
|
||||||
gameRequest.voters.add(currentUser)
|
updatedVoters.add(currentUser)
|
||||||
}
|
}
|
||||||
|
gameRequest.voters = updatedVoters
|
||||||
|
|
||||||
|
// Ensure the entity is marked as dirty
|
||||||
|
gameRequest.status = gameRequest.status
|
||||||
|
|
||||||
gameRequestRepository.save(gameRequest)
|
gameRequestRepository.save(gameRequest)
|
||||||
}
|
}
|
||||||
@@ -109,7 +166,7 @@ class GameRequestService(
|
|||||||
val gameRelease = game.release
|
val gameRelease = game.release
|
||||||
|
|
||||||
if (gameTitle == null) {
|
if (gameTitle == null) {
|
||||||
log.debug { "Game '${game.id}' is missing title, cannot complete matching requests" }
|
log.warn { "Game '${game.id}' is missing title, cannot complete matching requests" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ class GameRequestDto(
|
|||||||
val status: GameRequestStatus,
|
val status: GameRequestStatus,
|
||||||
val requester: UserInfoDto?,
|
val requester: UserInfoDto?,
|
||||||
val voters: List<UserInfoDto>,
|
val voters: List<UserInfoDto>,
|
||||||
val createdAt: Instant?,
|
val createdAt: Instant,
|
||||||
val updatedAt: Instant?
|
val updatedAt: Instant
|
||||||
)
|
)
|
||||||
@@ -4,12 +4,13 @@ import jakarta.persistence.*
|
|||||||
import org.gameyfin.app.requests.status.GameRequestStatus
|
import org.gameyfin.app.requests.status.GameRequestStatus
|
||||||
import org.gameyfin.app.users.entities.User
|
import org.gameyfin.app.users.entities.User
|
||||||
import org.hibernate.annotations.CreationTimestamp
|
import org.hibernate.annotations.CreationTimestamp
|
||||||
|
import org.hibernate.annotations.OnDelete
|
||||||
|
import org.hibernate.annotations.OnDeleteAction
|
||||||
import org.hibernate.annotations.UpdateTimestamp
|
import org.hibernate.annotations.UpdateTimestamp
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@EntityListeners(GameRequestEntityListener::class, AuditingEntityListener::class)
|
@EntityListeners(GameRequestEntityListener::class)
|
||||||
class GameRequest(
|
class GameRequest(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@@ -25,11 +26,13 @@ class GameRequest(
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
var status: GameRequestStatus,
|
var status: GameRequestStatus,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
var requester: User,
|
@OnDelete(action = OnDeleteAction.SET_NULL)
|
||||||
|
var requester: User? = null,
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL], orphanRemoval = true)
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
var voters: MutableList<User> = mutableListOf(),
|
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||||
|
var voters: MutableSet<User> = mutableSetOf(),
|
||||||
|
|
||||||
var linkedGameId: Long? = null,
|
var linkedGameId: Long? = null,
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ fun GameRequest.toDto(): GameRequestDto {
|
|||||||
title = this.title,
|
title = this.title,
|
||||||
release = this.release,
|
release = this.release,
|
||||||
status = this.status,
|
status = this.status,
|
||||||
requester = this.requester.toUserInfoDto(),
|
requester = this.requester?.toUserInfoDto(),
|
||||||
voters = this.voters.map { it.toUserInfoDto() },
|
voters = this.voters.map { it.toUserInfoDto() },
|
||||||
createdAt = this.createdAt,
|
createdAt = this.createdAt!!,
|
||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package org.gameyfin.app.requests.status
|
|||||||
enum class GameRequestStatus {
|
enum class GameRequestStatus {
|
||||||
PENDING,
|
PENDING,
|
||||||
APPROVED,
|
APPROVED,
|
||||||
FULFILLED,
|
REJECTED,
|
||||||
REJECTED
|
FULFILLED
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.gameyfin.app.users.dto
|
|||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
|
|
||||||
data class ExtendedUserInfoDto(
|
data class ExtendedUserInfoDto(
|
||||||
|
val id: Long,
|
||||||
val username: String,
|
val username: String,
|
||||||
val managedBySso: Boolean,
|
val managedBySso: Boolean,
|
||||||
val email: String,
|
val email: String,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.gameyfin.app.users.dto
|
package org.gameyfin.app.users.dto
|
||||||
|
|
||||||
data class UserInfoDto(
|
data class UserInfoDto(
|
||||||
|
val id: Long,
|
||||||
val username: String,
|
val username: String,
|
||||||
val hasAvatar: Boolean,
|
val hasAvatar: Boolean,
|
||||||
val avatarId: Long? = null,
|
val avatarId: Long? = null,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority
|
|||||||
|
|
||||||
fun User.toUserInfoDto(): UserInfoDto {
|
fun User.toUserInfoDto(): UserInfoDto {
|
||||||
return UserInfoDto(
|
return UserInfoDto(
|
||||||
|
id = this.id!!,
|
||||||
username = this.username,
|
username = this.username,
|
||||||
hasAvatar = this.avatar != null,
|
hasAvatar = this.avatar != null,
|
||||||
avatarId = this.avatar?.id
|
avatarId = this.avatar?.id
|
||||||
@@ -17,6 +18,7 @@ fun User.toUserInfoDto(): UserInfoDto {
|
|||||||
|
|
||||||
fun User.toExtendedUserInfoDto(): ExtendedUserInfoDto {
|
fun User.toExtendedUserInfoDto(): ExtendedUserInfoDto {
|
||||||
return ExtendedUserInfoDto(
|
return ExtendedUserInfoDto(
|
||||||
|
id = this.id!!,
|
||||||
username = this.username,
|
username = this.username,
|
||||||
email = this.email,
|
email = this.email,
|
||||||
emailConfirmed = this.emailConfirmed,
|
emailConfirmed = this.emailConfirmed,
|
||||||
|
|||||||
Reference in New Issue
Block a user