mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Add minimal UI for game requests
Fix some minor bugs
This commit is contained in:
@@ -15,6 +15,7 @@ import {initializePluginState} from "Frontend/state/PluginState";
|
|||||||
import {isAdmin} from "Frontend/util/utils";
|
import {isAdmin} from "Frontend/util/utils";
|
||||||
import {useRouteMetadata} from "Frontend/util/routing";
|
import {useRouteMetadata} from "Frontend/util/routing";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
|
import {initializeGameRequestState} from "Frontend/state/GameRequestState";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
client.middlewares = [ErrorHandlingMiddleware];
|
client.middlewares = [ErrorHandlingMiddleware];
|
||||||
@@ -45,6 +46,7 @@ function ViewWithAuth() {
|
|||||||
|
|
||||||
initializeLibraryState();
|
initializeLibraryState();
|
||||||
initializeGameState();
|
initializeGameState();
|
||||||
|
initializeGameRequestState();
|
||||||
|
|
||||||
if (isAdmin(auth)) {
|
if (isAdmin(auth)) {
|
||||||
initializeScanState();
|
initializeScanState();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {useSnapshot} from "valtio/react";
|
|||||||
import {pluginState} from "Frontend/state/PluginState";
|
import {pluginState} from "Frontend/state/PluginState";
|
||||||
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
||||||
|
|
||||||
interface EditGameMetadataModalProps {
|
interface MatchGameModalProps {
|
||||||
path: string;
|
path: string;
|
||||||
libraryId: number;
|
libraryId: number;
|
||||||
replaceGameId?: number;
|
replaceGameId?: number;
|
||||||
@@ -37,7 +37,7 @@ export default function MatchGameModal({
|
|||||||
initialSearchTerm,
|
initialSearchTerm,
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange
|
onOpenChange
|
||||||
}: EditGameMetadataModalProps) {
|
}: MatchGameModalProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState<GameSearchResultDto[]>([]);
|
const [searchResults, setSearchResults] = useState<GameSearchResultDto[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|||||||
@@ -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<GameSearchResultDto[]>([]);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const [isRequesting, setIsRequesting] = useState<string | null>(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 (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}
|
||||||
|
hideCloseButton
|
||||||
|
isDismissable={!isSearching && !isRequesting}
|
||||||
|
isKeyboardDismissDisabled={!isSearching && !isRequesting}
|
||||||
|
backdrop="opaque" size="5xl">
|
||||||
|
<ModalContent>
|
||||||
|
{(onClose) => (
|
||||||
|
<ModalBody className="my-4">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<h2 className="text-xl font-semibold">Request a game</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 mb-4">
|
||||||
|
<Input value={searchTerm}
|
||||||
|
onValueChange={setSearchTerm}
|
||||||
|
onKeyDown={async (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
await search();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button isIconOnly onPress={search} color="primary" isLoading={isSearching}>
|
||||||
|
<MagnifyingGlass/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Table removeWrapper isStriped isHeaderSticky
|
||||||
|
classNames={{
|
||||||
|
base: "h-80 overflow-y-auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>Title & Release</TableColumn>
|
||||||
|
<TableColumn>Developer(s)</TableColumn>
|
||||||
|
<TableColumn>Publisher(s)</TableColumn>
|
||||||
|
{/* width={1} keeps the column as far to the right as possible*/}
|
||||||
|
<TableColumn>Sources</TableColumn>
|
||||||
|
<TableColumn width={1}> </TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="Your search did not match any games." items={searchResults}>
|
||||||
|
{(item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>
|
||||||
|
{item.title} ({item.release ? new Date(item.release).getFullYear() : "unknown"})
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{item.developers ? item.developers.map(
|
||||||
|
developer => <p>{developer}</p>
|
||||||
|
) : "unknown"}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{item.publishers ? item.publishers.map(
|
||||||
|
publisher => <p>{publisher}</p>
|
||||||
|
) : "unknown"}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
{Object.values(item.originalIds).map(
|
||||||
|
originalId => <PluginIcon
|
||||||
|
plugin={plugins[originalId.pluginId] as PluginDto}/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip content="Pick this result">
|
||||||
|
<Button isIconOnly size="sm"
|
||||||
|
isDisabled={isRequesting !== null}
|
||||||
|
isLoading={isRequesting === item.id}
|
||||||
|
onPress={async () => {
|
||||||
|
setIsRequesting(item.id);
|
||||||
|
await requestGame(item);
|
||||||
|
setIsRequesting(null);
|
||||||
|
onClose();
|
||||||
|
}}>
|
||||||
|
<ArrowRight/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<GameRequestEvent[]>;
|
||||||
|
isLoaded: boolean;
|
||||||
|
state: Record<number, GameRequestDto>;
|
||||||
|
gameRequests: GameRequestDto[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gameRequestState = proxy<GameRequestState>({
|
||||||
|
get isLoaded() {
|
||||||
|
return this.subscription != null;
|
||||||
|
},
|
||||||
|
state: {},
|
||||||
|
get gameRequests() {
|
||||||
|
return Object.values<GameRequestDto>(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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import ProfileMenu from "Frontend/components/ProfileMenu";
|
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 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, 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 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";
|
||||||
@@ -14,6 +24,7 @@ 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();
|
||||||
@@ -26,6 +37,8 @@ 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))
|
||||||
@@ -93,7 +106,17 @@ export default function MainLayout() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</NavbarContent>}
|
</NavbarContent>}
|
||||||
<NavbarContent justify="end">
|
<NavbarContent justify="end" className="items-center">
|
||||||
|
{auth.state.user &&
|
||||||
|
<NavbarItem>
|
||||||
|
<Tooltip content="Request a game" placement="bottom">
|
||||||
|
<Button isIconOnly color="primary" variant="light"
|
||||||
|
onPress={requestGameModal.onOpen}>
|
||||||
|
<PlusCircle size={26} weight="fill"/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</NavbarItem>
|
||||||
|
}
|
||||||
{isAdmin(auth) &&
|
{isAdmin(auth) &&
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<ScanProgressPopover/>
|
<ScanProgressPopover/>
|
||||||
@@ -142,6 +165,10 @@ export default function MainLayout() {
|
|||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<RequestGameModal isOpen={requestGameModal.isOpen}
|
||||||
|
onOpenChange={requestGameModal.onOpenChange}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonCreator
|
|||||||
import com.fasterxml.jackson.annotation.JsonValue
|
import com.fasterxml.jackson.annotation.JsonValue
|
||||||
import org.gameyfin.app.users.RoleService
|
import org.gameyfin.app.users.RoleService
|
||||||
import java.lang.Enum
|
import java.lang.Enum
|
||||||
|
import kotlin.Int
|
||||||
|
import kotlin.String
|
||||||
|
|
||||||
enum class Role(val roleName: String, val powerLevel: Int) {
|
enum class Role(val roleName: String, val powerLevel: Int) {
|
||||||
|
|
||||||
@@ -21,12 +23,12 @@ enum class Role(val roleName: String, val powerLevel: Int) {
|
|||||||
@JsonCreator
|
@JsonCreator
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromValue(value: String): Role? {
|
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 }
|
return entries.find { it.roleName == enumString }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun safeValueOf(type: String): Role? {
|
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)
|
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
|
// necessary for the ability to use the Roles class in the @RolesAllowed annotation
|
||||||
class Names {
|
class Names {
|
||||||
companion object {
|
companion object {
|
||||||
const val SUPERADMIN = "${RoleService.Companion.INTERNAL_ROLE_PREFIX}SUPERADMIN"
|
const val SUPERADMIN = "${RoleService.INTERNAL_ROLE_PREFIX}SUPERADMIN"
|
||||||
const val ADMIN = "${RoleService.Companion.INTERNAL_ROLE_PREFIX}ADMIN"
|
const val ADMIN = "${RoleService.INTERNAL_ROLE_PREFIX}ADMIN"
|
||||||
const val USER = "${RoleService.Companion.INTERNAL_ROLE_PREFIX}USER"
|
const val USER = "${RoleService.INTERNAL_ROLE_PREFIX}USER"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.gameyfin.app.core.events
|
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.Token
|
||||||
import org.gameyfin.app.shared.token.TokenType
|
import org.gameyfin.app.shared.token.TokenType
|
||||||
import org.gameyfin.app.users.entities.User
|
import org.gameyfin.app.users.entities.User
|
||||||
@@ -24,3 +25,5 @@ class PasswordResetRequestEvent(source: Any, val token: Token<TokenType.Password
|
|||||||
class AccountDeletedEvent(source: Any, val user: User, val baseUrl: String) : ApplicationEvent(source)
|
class AccountDeletedEvent(source: Any, val user: User, val baseUrl: String) : ApplicationEvent(source)
|
||||||
|
|
||||||
class LibraryScanScheduleUpdatedEvent(source: Any) : ApplicationEvent(source)
|
class LibraryScanScheduleUpdatedEvent(source: Any) : ApplicationEvent(source)
|
||||||
|
|
||||||
|
class GameCreatedEvent(source: Any, val game: Game) : ApplicationEvent(source)
|
||||||
@@ -4,11 +4,11 @@ import org.gameyfin.app.core.Role
|
|||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
|
||||||
fun getCurrentAuth(): Authentication {
|
fun getCurrentAuth(): Authentication? {
|
||||||
return SecurityContextHolder.getContext().authentication
|
return SecurityContextHolder.getContext().authentication
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCurrentUserAdmin(): Boolean {
|
fun isCurrentUserAdmin(): Boolean {
|
||||||
return getCurrentAuth().authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN }
|
return getCurrentAuth()?.authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN }
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
@@ -150,10 +150,10 @@ class GameService(
|
|||||||
val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id)
|
val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id)
|
||||||
?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found")
|
?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found")
|
||||||
|
|
||||||
val user = when (val userDetails = getCurrentAuth().principal) {
|
val user = when (val userDetails = getCurrentAuth()?.principal) {
|
||||||
is UserDetails -> userService.getByUsernameNonNull(userDetails.username)
|
is UserDetails -> userService.getByUsernameNonNull(userDetails.username)
|
||||||
is OidcUser -> userService.getByUsernameNonNull(userDetails.preferredUsername)
|
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
|
// Update only non-null fields
|
||||||
|
|||||||
@@ -3,38 +3,21 @@ package org.gameyfin.app.games.entities
|
|||||||
import jakarta.persistence.PostPersist
|
import jakarta.persistence.PostPersist
|
||||||
import jakarta.persistence.PostRemove
|
import jakarta.persistence.PostRemove
|
||||||
import jakarta.persistence.PostUpdate
|
import jakarta.persistence.PostUpdate
|
||||||
|
import org.gameyfin.app.core.events.GameCreatedEvent
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.games.GameService
|
||||||
import org.gameyfin.app.games.dto.GameAdminEvent
|
import org.gameyfin.app.games.dto.GameAdminEvent
|
||||||
import org.gameyfin.app.games.dto.GameUserEvent
|
import org.gameyfin.app.games.dto.GameUserEvent
|
||||||
import org.gameyfin.app.games.extensions.toAdminDto
|
import org.gameyfin.app.games.extensions.toAdminDto
|
||||||
import org.gameyfin.app.games.extensions.toUserDto
|
import org.gameyfin.app.games.extensions.toUserDto
|
||||||
import org.gameyfin.app.requests.GameRequestService
|
import org.gameyfin.app.util.EventPublisherHolder
|
||||||
import org.springframework.context.ApplicationContext
|
|
||||||
import org.springframework.context.ApplicationContextAware
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
@Component
|
class GameEntityListener {
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostPersist
|
@PostPersist
|
||||||
fun created(game: Game) {
|
fun created(game: Game) {
|
||||||
GameService.emitUser(GameUserEvent.Created(game.toUserDto()))
|
GameService.emitUser(GameUserEvent.Created(game.toUserDto()))
|
||||||
GameService.emitAdmin(GameAdminEvent.Created(game.toAdminDto()))
|
GameService.emitAdmin(GameAdminEvent.Created(game.toAdminDto()))
|
||||||
|
EventPublisherHolder.publish(GameCreatedEvent(this, game))
|
||||||
// After a game is created, mark any matching game requests as FULFILLED
|
|
||||||
getGameRequestService().completeMatchingRequests(game)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostUpdate
|
@PostUpdate
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class ImageEndpoint(
|
|||||||
@PermitAll
|
@PermitAll
|
||||||
@PostMapping("/avatar/upload")
|
@PostMapping("/avatar/upload")
|
||||||
fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
|
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)) {
|
val image: Image = if (!userService.hasAvatar(auth.name)) {
|
||||||
imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!)
|
imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!)
|
||||||
@@ -75,7 +75,7 @@ class ImageEndpoint(
|
|||||||
@PermitAll
|
@PermitAll
|
||||||
@PostMapping("/avatar/delete")
|
@PostMapping("/avatar/delete")
|
||||||
fun deleteAvatar() {
|
fun deleteAvatar() {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
userService.deleteAvatar(auth.name)
|
userService.deleteAvatar(auth.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class MessageService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
|
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
|
||||||
val template = templateService.getMessageTemplate(templateKey)
|
val template = templateService.getMessageTemplate(templateKey)
|
||||||
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
|
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package org.gameyfin.app.requests
|
package org.gameyfin.app.requests
|
||||||
|
|
||||||
import org.gameyfin.app.requests.entities.GameRequest
|
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.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.Query
|
import org.springframework.data.jpa.repository.Query
|
||||||
import org.springframework.data.repository.query.Param
|
import org.springframework.data.repository.query.Param
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
interface GameRequestRepository : JpaRepository<GameRequest, Long> {
|
interface GameRequestRepository : JpaRepository<GameRequest, Long> {
|
||||||
fun findByTitleAndRelease(title: String, release: Instant): List<GameRequest>
|
@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)")
|
@Param("title") title: String,
|
||||||
fun findByTitleAndReleaseYear(@Param("title") title: String, @Param("release") release: Instant): List<GameRequest>
|
@Param("release") release: Instant?,
|
||||||
|
@Param("excludedStatuses") excludedStatuses: List<GameRequestStatus> = listOf(
|
||||||
|
GameRequestStatus.FULFILLED,
|
||||||
|
GameRequestStatus.REJECTED
|
||||||
|
)
|
||||||
|
): List<GameRequest>
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
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.core.events.GameCreatedEvent
|
||||||
import org.gameyfin.app.core.security.getCurrentAuth
|
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.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,6 +11,8 @@ 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.springframework.context.event.EventListener
|
||||||
|
import org.springframework.scheduling.annotation.Async
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
@@ -52,13 +54,14 @@ class GameRequestService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createRequest(gameRequest: GameRequestCreationDto) {
|
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(
|
val gameRequest = GameRequest(
|
||||||
title = gameRequest.title,
|
title = gameRequest.title,
|
||||||
release = gameRequest.release,
|
release = gameRequest.release,
|
||||||
status = GameRequestStatus.PENDING,
|
status = GameRequestStatus.PENDING,
|
||||||
externalProviderIds = gameRequest.externalProviderIds,
|
|
||||||
requester = currentUser
|
requester = currentUser
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,12 +82,13 @@ class GameRequestService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toggleRequestVote(id: Long) {
|
fun toggleRequestVote(id: Long) {
|
||||||
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val currentUser =
|
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)
|
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) {
|
if (gameRequest.requester.id == currentUser.id) {
|
||||||
throw IllegalStateException("You cannot vote for your own request")
|
throw IllegalStateException("You cannot vote for your own request")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,25 +101,24 @@ class GameRequestService(
|
|||||||
gameRequestRepository.save(gameRequest)
|
gameRequestRepository.save(gameRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun completeMatchingRequests(game: Game) {
|
@Async
|
||||||
|
@EventListener(GameCreatedEvent::class)
|
||||||
|
fun completeMatchingRequests(gameCreatedEvent: GameCreatedEvent) {
|
||||||
|
val game = gameCreatedEvent.game
|
||||||
val gameTitle = game.title
|
val gameTitle = game.title
|
||||||
val gameRelease = game.release
|
val gameRelease = game.release
|
||||||
|
|
||||||
if (gameTitle == null || gameRelease == null) {
|
if (gameTitle == null) {
|
||||||
log.debug { "Game '${game.id}' is missing title and/or release date, cannot complete matching requests" }
|
log.debug { "Game '${game.id}' is missing title, cannot complete matching requests" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First match by exact title and release date, if not result could be found then by title and release year only
|
val matchingRequests = gameRequestRepository.findOpenRequestsByTitleAndReleaseYear(
|
||||||
val matchingRequestsByExactRelease = gameRequestRepository.findByTitleAndRelease(gameTitle, gameRelease)
|
gameTitle,
|
||||||
val matchingRequestsByReleaseYear = matchingRequestsByExactRelease.ifEmpty {
|
gameRelease
|
||||||
gameRequestRepository.findByTitleAndReleaseYear(
|
)
|
||||||
gameTitle,
|
|
||||||
gameRelease
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
matchingRequestsByReleaseYear.forEach { request ->
|
matchingRequests.forEach { request ->
|
||||||
request.status = GameRequestStatus.FULFILLED
|
request.status = GameRequestStatus.FULFILLED
|
||||||
request.linkedGameId = game.id
|
request.linkedGameId = game.id
|
||||||
val persistedRequest = gameRequestRepository.save(request)
|
val persistedRequest = gameRequestRepository.save(request)
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ import java.time.Instant
|
|||||||
|
|
||||||
class GameRequestCreationDto(
|
class GameRequestCreationDto(
|
||||||
val title: String,
|
val title: String,
|
||||||
val release: Instant,
|
val release: Instant?
|
||||||
val externalProviderIds: Map<String, String>
|
|
||||||
)
|
)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.gameyfin.app.requests.dto
|
package org.gameyfin.app.requests.dto
|
||||||
|
|
||||||
import org.gameyfin.app.requests.entities.ExternalProviderIds
|
|
||||||
import org.gameyfin.app.requests.status.GameRequestStatus
|
import org.gameyfin.app.requests.status.GameRequestStatus
|
||||||
import org.gameyfin.app.users.dto.UserInfoDto
|
import org.gameyfin.app.users.dto.UserInfoDto
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -8,8 +7,7 @@ import java.time.Instant
|
|||||||
class GameRequestDto(
|
class GameRequestDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val release: Instant,
|
val release: Instant?,
|
||||||
val externalProviderIds: ExternalProviderIds,
|
|
||||||
val status: GameRequestStatus,
|
val status: GameRequestStatus,
|
||||||
val requester: UserInfoDto?,
|
val requester: UserInfoDto?,
|
||||||
val voters: List<UserInfoDto>,
|
val voters: List<UserInfoDto>,
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import org.hibernate.annotations.UpdateTimestamp
|
|||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
typealias ExternalProviderIds = Map<String, String>
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@EntityListeners(GameRequestEntityListener::class, AuditingEntityListener::class)
|
@EntityListeners(GameRequestEntityListener::class, AuditingEntityListener::class)
|
||||||
class GameRequest(
|
class GameRequest(
|
||||||
@@ -21,18 +19,16 @@ class GameRequest(
|
|||||||
val title: String,
|
val title: String,
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
val release: Instant,
|
val release: Instant?,
|
||||||
|
|
||||||
@ElementCollection
|
|
||||||
val externalProviderIds: ExternalProviderIds,
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
var status: GameRequestStatus,
|
var status: GameRequestStatus,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.EAGER)
|
@ManyToOne(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
|
||||||
var requester: User? = null,
|
var requester: User,
|
||||||
|
|
||||||
@OneToMany
|
@OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||||
var voters: MutableList<User> = mutableListOf(),
|
var voters: MutableList<User> = mutableListOf(),
|
||||||
|
|
||||||
var linkedGameId: Long? = null,
|
var linkedGameId: Long? = null,
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ fun GameRequest.toDto(): GameRequestDto {
|
|||||||
id = this.id!!,
|
id = this.id!!,
|
||||||
title = this.title,
|
title = this.title,
|
||||||
release = this.release,
|
release = this.release,
|
||||||
externalProviderIds = this.externalProviderIds,
|
|
||||||
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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class SessionService(private val sessionRegistry: SessionRegistry) {
|
|||||||
|
|
||||||
fun logoutAllSessions() {
|
fun logoutAllSessions() {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth()
|
||||||
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(auth.principal, false)
|
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(auth?.principal, false)
|
||||||
for (sessionInfo in sessions) {
|
for (sessionInfo in sessions) {
|
||||||
sessionInfo.expireNow()
|
sessionInfo.expireNow()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ class UserEndpoint(
|
|||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
fun getUserInfo(): ExtendedUserInfoDto? {
|
fun getUserInfo(): ExtendedUserInfoDto? {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth()
|
||||||
if (!auth.isAuthenticated || auth.principal == "anonymousUser") return null
|
if (auth?.isAuthenticated == false || auth?.principal == "anonymousUser") return null
|
||||||
return userService.getUserInfo()
|
return userService.getUserInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun updateUser(updates: UserUpdateDto) {
|
fun updateUser(updates: UserUpdateDto) {
|
||||||
val auth: Authentication = getCurrentAuth()
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
userService.updateUser(auth.name, updates)
|
userService.updateUser(auth.name, updates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class UserEndpoint(
|
|||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun deleteUser() {
|
fun deleteUser() {
|
||||||
val auth: Authentication = getCurrentAuth()
|
val auth: Authentication = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
userService.deleteUser(auth.name)
|
userService.deleteUser(auth.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class UserEndpoint(
|
|||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun getRolesBelow(): List<String> {
|
fun getRolesBelow(): List<String> {
|
||||||
val auth: Authentication = getCurrentAuth()
|
val auth: Authentication = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
return roleService.getRolesBelowAuth(auth).map { it.roleName }
|
return roleService.getRolesBelowAuth(auth).map { it.roleName }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class UserService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getUserInfo(): ExtendedUserInfoDto {
|
fun getUserInfo(): ExtendedUserInfoDto {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val principal = auth.principal
|
val principal = auth.principal
|
||||||
|
|
||||||
if (principal is OidcUser) {
|
if (principal is OidcUser) {
|
||||||
@@ -238,7 +238,7 @@ class UserService(
|
|||||||
return RoleAssignmentResult.NO_ROLES_PROVIDED
|
return RoleAssignmentResult.NO_ROLES_PROVIDED
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentUser = getCurrentAuth()
|
val currentUser = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val targetUser = getByUsernameNonNull(username)
|
val targetUser = getByUsernameNonNull(username)
|
||||||
|
|
||||||
if (!canManage(targetUser)) {
|
if (!canManage(targetUser)) {
|
||||||
@@ -266,7 +266,7 @@ class UserService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canManage(targetUser: org.gameyfin.app.users.entities.User): Boolean {
|
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 currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel
|
||||||
val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel
|
val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel
|
||||||
return currentUserLevel > targetUserLevel
|
return currentUserLevel > targetUserLevel
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ class EmailConfirmationEndpoint(
|
|||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun resendEmailConfirmation() {
|
fun resendEmailConfirmation() {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
userService.getByUsername(auth.name)?.let {
|
userService.getByUsername(auth.name)?.let {
|
||||||
emailConfirmationService.resendEmailConfirmation(it)
|
emailConfirmationService.resendEmailConfirmation(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class UserPreferencesService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun id(key: String): UserPreferenceKey {
|
private fun id(key: String): UserPreferenceKey {
|
||||||
val auth = getCurrentAuth()
|
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val user = userService.getByUsernameNonNull(auth.name)
|
val user = userService.getByUsernameNonNull(auth.name)
|
||||||
return UserPreferenceKey(key, user.id!!)
|
return UserPreferenceKey(key, user.id!!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class InvitationService(
|
|||||||
if (userService.existsByEmail(email))
|
if (userService.existsByEmail(email))
|
||||||
throw IllegalStateException("User with email ${Utils.Companion.maskEmail(email)} is already registered")
|
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 user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
|
||||||
val payload = mapOf(EMAIL_KEY to email)
|
val payload = mapOf(EMAIL_KEY to email)
|
||||||
val token = super.generateWithPayload(user, payload)
|
val token = super.generateWithPayload(user, payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user