From ae7a65ccbc56d325258f19e509501d4b9ad54c28 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Tue, 2 Sep 2025 18:49:51 +0200
Subject: [PATCH] Add minimal UI for game requests Fix some minor bugs
---
app/src/main/frontend/App.tsx | 2 +
.../general/modals/MatchGameModal.tsx | 4 +-
.../general/modals/RequestGameModal.tsx | 162 ++++++++++++++++++
.../main/frontend/state/GameRequestState.ts | 50 ++++++
app/src/main/frontend/views/MainLayout.tsx | 33 +++-
.../main/kotlin/org/gameyfin/app/core/Role.kt | 12 +-
.../org/gameyfin/app/core/events/Events.kt | 5 +-
.../app/core/security/SecurityUtils.kt | 4 +-
.../org/gameyfin/app/games/GameService.kt | 4 +-
.../app/games/entities/GameEntityListener.kt | 25 +--
.../org/gameyfin/app/media/ImageEndpoint.kt | 4 +-
.../gameyfin/app/messages/MessageService.kt | 2 +-
.../app/requests/GameRequestEndpoint.kt | 1 +
.../app/requests/GameRequestRepository.kt | 14 +-
.../app/requests/GameRequestService.kt | 37 ++--
.../requests/dto/GameRequestCreationDto.kt | 3 +-
.../app/requests/dto/GameRequestDto.kt | 4 +-
.../app/requests/entities/GameRequest.kt | 14 +-
.../extensions/GameRequestExtensions.kt | 3 +-
.../org/gameyfin/app/users/SessionService.kt | 2 +-
.../org/gameyfin/app/users/UserEndpoint.kt | 8 +-
.../org/gameyfin/app/users/UserService.kt | 6 +-
.../EmailConfirmationEndpoint.kt | 2 +-
.../preferences/UserPreferencesService.kt | 2 +-
.../users/registration/InvitationService.kt | 2 +-
25 files changed, 318 insertions(+), 87 deletions(-)
create mode 100644 app/src/main/frontend/components/general/modals/RequestGameModal.tsx
create mode 100644 app/src/main/frontend/state/GameRequestState.ts
diff --git a/app/src/main/frontend/App.tsx b/app/src/main/frontend/App.tsx
index 01f759e..1aadb07 100644
--- a/app/src/main/frontend/App.tsx
+++ b/app/src/main/frontend/App.tsx
@@ -15,6 +15,7 @@ import {initializePluginState} from "Frontend/state/PluginState";
import {isAdmin} from "Frontend/util/utils";
import {useRouteMetadata} from "Frontend/util/routing";
import {useEffect} from "react";
+import {initializeGameRequestState} from "Frontend/state/GameRequestState";
export default function App() {
client.middlewares = [ErrorHandlingMiddleware];
@@ -45,6 +46,7 @@ function ViewWithAuth() {
initializeLibraryState();
initializeGameState();
+ initializeGameRequestState();
if (isAdmin(auth)) {
initializeScanState();
diff --git a/app/src/main/frontend/components/general/modals/MatchGameModal.tsx b/app/src/main/frontend/components/general/modals/MatchGameModal.tsx
index f484648..6343ece 100644
--- a/app/src/main/frontend/components/general/modals/MatchGameModal.tsx
+++ b/app/src/main/frontend/components/general/modals/MatchGameModal.tsx
@@ -21,7 +21,7 @@ import {useSnapshot} from "valtio/react";
import {pluginState} from "Frontend/state/PluginState";
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
-interface EditGameMetadataModalProps {
+interface MatchGameModalProps {
path: string;
libraryId: number;
replaceGameId?: number;
@@ -37,7 +37,7 @@ export default function MatchGameModal({
initialSearchTerm,
isOpen,
onOpenChange
- }: EditGameMetadataModalProps) {
+ }: MatchGameModalProps) {
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
diff --git a/app/src/main/frontend/components/general/modals/RequestGameModal.tsx b/app/src/main/frontend/components/general/modals/RequestGameModal.tsx
new file mode 100644
index 0000000..453e66b
--- /dev/null
+++ b/app/src/main/frontend/components/general/modals/RequestGameModal.tsx
@@ -0,0 +1,162 @@
+import {
+ addToast,
+ Button,
+ Input,
+ Modal,
+ ModalBody,
+ ModalContent,
+ Table,
+ TableBody,
+ TableCell,
+ TableColumn,
+ TableHeader,
+ TableRow,
+ Tooltip
+} from "@heroui/react";
+import React, {useEffect, useState} from "react";
+import {ArrowRight, MagnifyingGlass} from "@phosphor-icons/react";
+import {GameEndpoint, GameRequestEndpoint} from "Frontend/generated/endpoints";
+import GameSearchResultDto from "Frontend/generated/org/gameyfin/app/games/dto/GameSearchResultDto";
+import PluginIcon from "../plugin/PluginIcon";
+import {useSnapshot} from "valtio/react";
+import {pluginState} from "Frontend/state/PluginState";
+import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
+import GameRequestCreationDto from "Frontend/generated/org/gameyfin/app/requests/dto/GameRequestCreationDto";
+
+interface RequestGameModalProps {
+ isOpen: boolean;
+ onOpenChange: () => void;
+}
+
+export default function RequestGameModal({
+ isOpen,
+ onOpenChange
+ }: RequestGameModalProps) {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const [isSearching, setIsSearching] = useState(false);
+ const [isRequesting, setIsRequesting] = useState(null);
+
+ const plugins = useSnapshot(pluginState).state;
+
+ useEffect(() => {
+ setSearchTerm("");
+ setSearchResults([]);
+ }, [isOpen]);
+
+ async function requestGame(game: GameSearchResultDto) {
+ const request: GameRequestCreationDto = {
+ 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"
+ })
+ }
+
+ async function search() {
+ setIsSearching(true);
+ const results = await GameEndpoint.getPotentialMatches(searchTerm);
+ setSearchResults(results);
+ setIsSearching(false);
+ }
+
+ return (
+
+
+ {(onClose) => (
+
+
+
Request a game
+
+
+ {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ await search();
+ }
+ }}
+ />
+
+
+
+
+
+
+
+
+ Title & Release
+ Developer(s)
+ Publisher(s)
+ {/* width={1} keeps the column as far to the right as possible*/}
+ Sources
+
+
+
+ {(item) => (
+
+
+ {item.title} ({item.release ? new Date(item.release).getFullYear() : "unknown"})
+
+
+
+ {item.developers ? item.developers.map(
+ developer =>
{developer}
+ ) : "unknown"}
+
+
+
+
+ {item.publishers ? item.publishers.map(
+ publisher =>
{publisher}
+ ) : "unknown"}
+
+
+
+
+ {Object.values(item.originalIds).map(
+ originalId =>
+ )}
+
+
+
+
+ {
+ setIsRequesting(item.id);
+ await requestGame(item);
+ setIsRequesting(null);
+ onClose();
+ }}>
+
+
+
+
+
+ )}
+
+
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/src/main/frontend/state/GameRequestState.ts b/app/src/main/frontend/state/GameRequestState.ts
new file mode 100644
index 0000000..00d47a4
--- /dev/null
+++ b/app/src/main/frontend/state/GameRequestState.ts
@@ -0,0 +1,50 @@
+import {Subscription} from "@vaadin/hilla-frontend";
+import {proxy} from "valtio/index";
+import {GameRequestEndpoint} from "Frontend/generated/endpoints";
+import GameRequestEvent from "Frontend/generated/org/gameyfin/app/requests/dto/GameRequestEvent";
+import GameRequestDto from "Frontend/generated/org/gameyfin/app/requests/dto/GameRequestDto";
+
+type GameRequestState = {
+ subscription?: Subscription;
+ isLoaded: boolean;
+ state: Record;
+ gameRequests: GameRequestDto[];
+};
+
+export const gameRequestState = proxy({
+ get isLoaded() {
+ return this.subscription != null;
+ },
+ state: {},
+ get gameRequests() {
+ return Object.values(this.state);
+ }
+});
+
+/** Subscribe to and process state updates from backend **/
+export async function initializeGameRequestState() {
+ if (gameRequestState.isLoaded) return;
+
+ // Fetch initial game request list
+ const initialEntries = await GameRequestEndpoint.getAll();
+ initialEntries.forEach((gameRequest: GameRequestDto) => {
+ gameRequestState.state[gameRequest.id] = gameRequest;
+ });
+
+ // Subscribe to real-time updates
+ gameRequestState.subscription = GameRequestEndpoint.subscribe().onNext((gameRequestEvents: GameRequestEvent[]) => {
+ gameRequestEvents.forEach((gameRequestEvent: GameRequestEvent) => {
+ switch (gameRequestEvent.type) {
+ case "created":
+ case "updated":
+ //@ts-ignore
+ gameRequestState.state[gameRequestEvent.id] = gameRequestEvent;
+ break;
+ case "deleted":
+ //@ts-ignore
+ delete gameRequestState.state[gameRequestEvent.id];
+ break;
+ }
+ })
+ });
+}
\ No newline at end of file
diff --git a/app/src/main/frontend/views/MainLayout.tsx b/app/src/main/frontend/views/MainLayout.tsx
index a0d632e..b7c1acc 100644
--- a/app/src/main/frontend/views/MainLayout.tsx
+++ b/app/src/main/frontend/views/MainLayout.tsx
@@ -1,11 +1,21 @@
import {useEffect, useState} from 'react';
import ProfileMenu from "Frontend/components/ProfileMenu";
-import {Button, Divider, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, Tooltip} from "@heroui/react";
+import {
+ Button,
+ Divider,
+ Link,
+ Navbar,
+ NavbarBrand,
+ NavbarContent,
+ NavbarItem,
+ Tooltip,
+ useDisclosure
+} from "@heroui/react";
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
import * as PackageJson from "../../../../package.json";
import {Outlet, useLocation, useNavigate} from "react-router";
import {useAuth} from "Frontend/util/auth";
-import {ArrowLeft, DiceSix, Heart, House, ListMagnifyingGlass, SignIn} from "@phosphor-icons/react";
+import {ArrowLeft, DiceSix, Heart, House, ListMagnifyingGlass, PlusCircle, SignIn} from "@phosphor-icons/react";
import Confetti, {ConfettiProps} from "react-confetti-boom";
import {useTheme} from "next-themes";
import {useUserPreferenceService} from "Frontend/util/user-preference-service";
@@ -14,6 +24,7 @@ import {useSnapshot} from "valtio/react";
import {gameState} from "Frontend/state/GameState";
import ScanProgressPopover from "Frontend/components/general/ScanProgressPopover";
import {isAdmin} from "Frontend/util/utils";
+import RequestGameModal from "Frontend/components/general/modals/RequestGameModal";
export default function MainLayout() {
const navigate = useNavigate();
@@ -26,6 +37,8 @@ export default function MainLayout() {
const [isExploding, setIsExploding] = useState(false);
const games = useSnapshot(gameState).games;
+ const requestGameModal = useDisclosure();
+
useEffect(() => {
userPreferenceService.sync()
.then(() => loadUserTheme().catch(console.error))
@@ -93,7 +106,17 @@ export default function MainLayout() {
}
-
+
+ {auth.state.user &&
+
+
+
+
+
+
+
+ }
{isAdmin(auth) &&
@@ -142,6 +165,10 @@ export default function MainLayout() {
+
+
+
);
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/gameyfin/app/core/Role.kt b/app/src/main/kotlin/org/gameyfin/app/core/Role.kt
index 0401300..76b33a4 100644
--- a/app/src/main/kotlin/org/gameyfin/app/core/Role.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/core/Role.kt
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import org.gameyfin.app.users.RoleService
import java.lang.Enum
+import kotlin.Int
+import kotlin.String
enum class Role(val roleName: String, val powerLevel: Int) {
@@ -21,12 +23,12 @@ enum class Role(val roleName: String, val powerLevel: Int) {
@JsonCreator
@JvmStatic
fun fromValue(value: String): Role? {
- val enumString = value.removePrefix(RoleService.Companion.INTERNAL_ROLE_PREFIX)
+ val enumString = value.removePrefix(RoleService.INTERNAL_ROLE_PREFIX)
return entries.find { it.roleName == enumString }
}
fun safeValueOf(type: String): Role? {
- val enumString = type.removePrefix(RoleService.Companion.INTERNAL_ROLE_PREFIX)
+ val enumString = type.removePrefix(RoleService.INTERNAL_ROLE_PREFIX)
return Enum.valueOf(Role::class.java, enumString)
}
}
@@ -34,9 +36,9 @@ enum class Role(val roleName: String, val powerLevel: Int) {
// necessary for the ability to use the Roles class in the @RolesAllowed annotation
class Names {
companion object {
- const val SUPERADMIN = "${RoleService.Companion.INTERNAL_ROLE_PREFIX}SUPERADMIN"
- const val ADMIN = "${RoleService.Companion.INTERNAL_ROLE_PREFIX}ADMIN"
- const val USER = "${RoleService.Companion.INTERNAL_ROLE_PREFIX}USER"
+ const val SUPERADMIN = "${RoleService.INTERNAL_ROLE_PREFIX}SUPERADMIN"
+ const val ADMIN = "${RoleService.INTERNAL_ROLE_PREFIX}ADMIN"
+ const val USER = "${RoleService.INTERNAL_ROLE_PREFIX}USER"
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/gameyfin/app/core/events/Events.kt b/app/src/main/kotlin/org/gameyfin/app/core/events/Events.kt
index 1e30da0..f201ef6 100644
--- a/app/src/main/kotlin/org/gameyfin/app/core/events/Events.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/core/events/Events.kt
@@ -1,5 +1,6 @@
package org.gameyfin.app.core.events
+import org.gameyfin.app.games.entities.Game
import org.gameyfin.app.shared.token.Token
import org.gameyfin.app.shared.token.TokenType
import org.gameyfin.app.users.entities.User
@@ -23,4 +24,6 @@ class PasswordResetRequestEvent(source: Any, val token: Token userService.getByUsernameNonNull(userDetails.username)
is OidcUser -> userService.getByUsernameNonNull(userDetails.preferredUsername)
- else -> throw IllegalStateException("Unkown user type: ${userDetails::class.java.name}")
+ else -> throw IllegalStateException("Unkown user type: ${userDetails?.javaClass?.name}")
}
// Update only non-null fields
diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt
index f1f8e53..b400493 100644
--- a/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt
@@ -3,38 +3,21 @@ package org.gameyfin.app.games.entities
import jakarta.persistence.PostPersist
import jakarta.persistence.PostRemove
import jakarta.persistence.PostUpdate
+import org.gameyfin.app.core.events.GameCreatedEvent
import org.gameyfin.app.games.GameService
import org.gameyfin.app.games.dto.GameAdminEvent
import org.gameyfin.app.games.dto.GameUserEvent
import org.gameyfin.app.games.extensions.toAdminDto
import org.gameyfin.app.games.extensions.toUserDto
-import org.gameyfin.app.requests.GameRequestService
-import org.springframework.context.ApplicationContext
-import org.springframework.context.ApplicationContextAware
-import org.springframework.stereotype.Component
+import org.gameyfin.app.util.EventPublisherHolder
-@Component
-class GameEntityListener : ApplicationContextAware {
-
- companion object {
- private lateinit var applicationContext: ApplicationContext
- }
-
- override fun setApplicationContext(context: ApplicationContext) {
- applicationContext = context
- }
-
- private fun getGameRequestService(): GameRequestService {
- return applicationContext.getBean(GameRequestService::class.java)
- }
+class GameEntityListener {
@PostPersist
fun created(game: Game) {
GameService.emitUser(GameUserEvent.Created(game.toUserDto()))
GameService.emitAdmin(GameAdminEvent.Created(game.toAdminDto()))
-
- // After a game is created, mark any matching game requests as FULFILLED
- getGameRequestService().completeMatchingRequests(game)
+ EventPublisherHolder.publish(GameCreatedEvent(this, game))
}
@PostUpdate
diff --git a/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt
index 725df8a..df451b2 100644
--- a/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt
@@ -60,7 +60,7 @@ class ImageEndpoint(
@PermitAll
@PostMapping("/avatar/upload")
fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val image: Image = if (!userService.hasAvatar(auth.name)) {
imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!)
@@ -75,7 +75,7 @@ class ImageEndpoint(
@PermitAll
@PostMapping("/avatar/delete")
fun deleteAvatar() {
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
userService.deleteAvatar(auth.name)
}
diff --git a/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt b/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt
index a0e48b8..530c879 100644
--- a/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt
@@ -60,7 +60,7 @@ class MessageService(
}
try {
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
val template = templateService.getMessageTemplate(templateKey)
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestEndpoint.kt
index 4cc235a..533cfdb 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestEndpoint.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestEndpoint.kt
@@ -26,6 +26,7 @@ class GameRequestEndpoint(
fun getAll() = gameRequestService.getAll()
+ @PermitAll
fun create(gameRequest: GameRequestCreationDto) {
gameRequestService.createRequest(gameRequest)
}
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt
index a8d2a95..12e9f43 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt
@@ -1,14 +1,20 @@
package org.gameyfin.app.requests
import org.gameyfin.app.requests.entities.GameRequest
+import org.gameyfin.app.requests.status.GameRequestStatus
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 GameRequestRepository : JpaRepository {
- fun findByTitleAndRelease(title: String, release: Instant): List
-
- @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): List
+ @Query("SELECT g FROM GameRequest g WHERE g.title = :title AND YEAR(g.release) = YEAR(:release) AND g.status NOT IN (:excludedStatuses)")
+ fun findOpenRequestsByTitleAndReleaseYear(
+ @Param("title") title: String,
+ @Param("release") release: Instant?,
+ @Param("excludedStatuses") excludedStatuses: List = listOf(
+ GameRequestStatus.FULFILLED,
+ GameRequestStatus.REJECTED
+ )
+ ): List
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt
index 9860d84..0551c1d 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt
@@ -1,8 +1,8 @@
package org.gameyfin.app.requests
import io.github.oshai.kotlinlogging.KotlinLogging
+import org.gameyfin.app.core.events.GameCreatedEvent
import org.gameyfin.app.core.security.getCurrentAuth
-import org.gameyfin.app.games.entities.Game
import org.gameyfin.app.requests.dto.GameRequestCreationDto
import org.gameyfin.app.requests.dto.GameRequestDto
import org.gameyfin.app.requests.dto.GameRequestEvent
@@ -11,6 +11,8 @@ import org.gameyfin.app.requests.extensions.toDto
import org.gameyfin.app.requests.extensions.toDtos
import org.gameyfin.app.requests.status.GameRequestStatus
import org.gameyfin.app.users.UserService
+import org.springframework.context.event.EventListener
+import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
@@ -52,13 +54,14 @@ class GameRequestService(
}
fun createRequest(gameRequest: GameRequestCreationDto) {
- val currentUser = userService.getByUsername(getCurrentAuth().name)
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
+ val currentUser =
+ userService.getByUsername(auth.name) ?: throw IllegalStateException("Current user not found")
val gameRequest = GameRequest(
title = gameRequest.title,
release = gameRequest.release,
status = GameRequestStatus.PENDING,
- externalProviderIds = gameRequest.externalProviderIds,
requester = currentUser
)
@@ -79,12 +82,13 @@ class GameRequestService(
}
fun toggleRequestVote(id: Long) {
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val currentUser =
- userService.getByUsername(getCurrentAuth().name) ?: throw IllegalStateException("Current user not found")
+ userService.getByUsername(auth.name) ?: throw IllegalStateException("Current user not found")
val gameRequest = gameRequestRepository.findById(id)
.orElseThrow { NoSuchElementException("No game request found with id $id") }
- if (gameRequest.requester?.id == currentUser.id) {
+ if (gameRequest.requester.id == currentUser.id) {
throw IllegalStateException("You cannot vote for your own request")
}
@@ -97,25 +101,24 @@ class GameRequestService(
gameRequestRepository.save(gameRequest)
}
- fun completeMatchingRequests(game: Game) {
+ @Async
+ @EventListener(GameCreatedEvent::class)
+ fun completeMatchingRequests(gameCreatedEvent: GameCreatedEvent) {
+ val game = gameCreatedEvent.game
val gameTitle = game.title
val gameRelease = game.release
- if (gameTitle == null || gameRelease == null) {
- log.debug { "Game '${game.id}' is missing title and/or release date, cannot complete matching requests" }
+ if (gameTitle == null) {
+ log.debug { "Game '${game.id}' is missing title, cannot complete matching requests" }
return
}
- // First match by exact title and release date, if not result could be found then by title and release year only
- val matchingRequestsByExactRelease = gameRequestRepository.findByTitleAndRelease(gameTitle, gameRelease)
- val matchingRequestsByReleaseYear = matchingRequestsByExactRelease.ifEmpty {
- gameRequestRepository.findByTitleAndReleaseYear(
- gameTitle,
- gameRelease
- )
- }
+ val matchingRequests = gameRequestRepository.findOpenRequestsByTitleAndReleaseYear(
+ gameTitle,
+ gameRelease
+ )
- matchingRequestsByReleaseYear.forEach { request ->
+ matchingRequests.forEach { request ->
request.status = GameRequestStatus.FULFILLED
request.linkedGameId = game.id
val persistedRequest = gameRequestRepository.save(request)
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt
index a31f8ed..8c2e22e 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt
@@ -5,6 +5,5 @@ import java.time.Instant
class GameRequestCreationDto(
val title: String,
- val release: Instant,
- val externalProviderIds: Map
+ val release: Instant?
)
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt
index 58f6743..7932c90 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt
@@ -1,6 +1,5 @@
package org.gameyfin.app.requests.dto
-import org.gameyfin.app.requests.entities.ExternalProviderIds
import org.gameyfin.app.requests.status.GameRequestStatus
import org.gameyfin.app.users.dto.UserInfoDto
import java.time.Instant
@@ -8,8 +7,7 @@ import java.time.Instant
class GameRequestDto(
val id: Long,
val title: String,
- val release: Instant,
- val externalProviderIds: ExternalProviderIds,
+ val release: Instant?,
val status: GameRequestStatus,
val requester: UserInfoDto?,
val voters: List,
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/entities/GameRequest.kt b/app/src/main/kotlin/org/gameyfin/app/requests/entities/GameRequest.kt
index 832322c..0fbd7e9 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/entities/GameRequest.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/entities/GameRequest.kt
@@ -8,8 +8,6 @@ import org.hibernate.annotations.UpdateTimestamp
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.Instant
-typealias ExternalProviderIds = Map
-
@Entity
@EntityListeners(GameRequestEntityListener::class, AuditingEntityListener::class)
class GameRequest(
@@ -21,18 +19,16 @@ class GameRequest(
val title: String,
@Column(nullable = false)
- val release: Instant,
-
- @ElementCollection
- val externalProviderIds: ExternalProviderIds,
+ val release: Instant?,
@Column(nullable = false)
+ @Enumerated(EnumType.STRING)
var status: GameRequestStatus,
- @ManyToOne(fetch = FetchType.EAGER)
- var requester: User? = null,
+ @ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
+ var requester: User,
- @OneToMany
+ @OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL], orphanRemoval = true)
var voters: MutableList = mutableListOf(),
var linkedGameId: Long? = null,
diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt b/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt
index 7301814..d798387 100644
--- a/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt
@@ -9,9 +9,8 @@ fun GameRequest.toDto(): GameRequestDto {
id = this.id!!,
title = this.title,
release = this.release,
- externalProviderIds = this.externalProviderIds,
status = this.status,
- requester = this.requester?.toUserInfoDto(),
+ requester = this.requester.toUserInfoDto(),
voters = this.voters.map { it.toUserInfoDto() },
createdAt = this.createdAt,
updatedAt = this.updatedAt
diff --git a/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt b/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt
index 3aa2d6c..4a8fc95 100644
--- a/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt
@@ -12,7 +12,7 @@ class SessionService(private val sessionRegistry: SessionRegistry) {
fun logoutAllSessions() {
val auth = getCurrentAuth()
- val sessions: List = sessionRegistry.getAllSessions(auth.principal, false)
+ val sessions: List = sessionRegistry.getAllSessions(auth?.principal, false)
for (sessionInfo in sessions) {
sessionInfo.expireNow()
}
diff --git a/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt
index d7c5739..b925817 100644
--- a/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt
@@ -20,13 +20,13 @@ class UserEndpoint(
@AnonymousAllowed
fun getUserInfo(): ExtendedUserInfoDto? {
val auth = getCurrentAuth()
- if (!auth.isAuthenticated || auth.principal == "anonymousUser") return null
+ if (auth?.isAuthenticated == false || auth?.principal == "anonymousUser") return null
return userService.getUserInfo()
}
@PermitAll
fun updateUser(updates: UserUpdateDto) {
- val auth: Authentication = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
userService.updateUser(auth.name, updates)
}
@@ -52,7 +52,7 @@ class UserEndpoint(
@PermitAll
fun deleteUser() {
- val auth: Authentication = getCurrentAuth()
+ val auth: Authentication = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
userService.deleteUser(auth.name)
}
@@ -68,7 +68,7 @@ class UserEndpoint(
@RolesAllowed(Role.Names.ADMIN)
fun getRolesBelow(): List {
- val auth: Authentication = getCurrentAuth()
+ val auth: Authentication = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
return roleService.getRolesBelowAuth(auth).map { it.roleName }
}
diff --git a/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt b/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt
index dad6137..b98b41b 100644
--- a/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt
@@ -94,7 +94,7 @@ class UserService(
}
fun getUserInfo(): ExtendedUserInfoDto {
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val principal = auth.principal
if (principal is OidcUser) {
@@ -238,7 +238,7 @@ class UserService(
return RoleAssignmentResult.NO_ROLES_PROVIDED
}
- val currentUser = getCurrentAuth()
+ val currentUser = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val targetUser = getByUsernameNonNull(username)
if (!canManage(targetUser)) {
@@ -266,7 +266,7 @@ class UserService(
}
fun canManage(targetUser: org.gameyfin.app.users.entities.User): Boolean {
- val currentUser = getCurrentAuth()
+ val currentUser = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel
val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel
return currentUserLevel > targetUserLevel
diff --git a/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt
index 4a84f83..0b913a3 100644
--- a/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt
@@ -19,7 +19,7 @@ class EmailConfirmationEndpoint(
@PermitAll
fun resendEmailConfirmation() {
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
userService.getByUsername(auth.name)?.let {
emailConfirmationService.resendEmailConfirmation(it)
}
diff --git a/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt b/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt
index 617b5c8..2d88f89 100644
--- a/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt
@@ -135,7 +135,7 @@ class UserPreferencesService(
}
private fun id(key: String): UserPreferenceKey {
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val user = userService.getByUsernameNonNull(auth.name)
return UserPreferenceKey(key, user.id!!)
}
diff --git a/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt b/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt
index 81d1974..14db409 100644
--- a/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt
+++ b/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt
@@ -29,7 +29,7 @@ class InvitationService(
if (userService.existsByEmail(email))
throw IllegalStateException("User with email ${Utils.Companion.maskEmail(email)} is already registered")
- val auth = getCurrentAuth()
+ val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
val payload = mapOf(EMAIL_KEY to email)
val token = super.generateWithPayload(user, payload)