v2.0.0.beta5 (#626)

* Fix wrong version property used in release.yml

* Implement "Allow access to Gameyfin without login"

* Implement filter by keyword (closes #613)

* Fix bug where secret fields would be displayed as normal text

* Optimize Gradle build performance

* Fix ant path matchers

* Fix NPE in role authority mapper (fixes #614)
This commit is contained in:
Simon
2025-07-16 22:39:09 +02:00
committed by GitHub
parent 49ff9474fb
commit edf7a569df
25 changed files with 168 additions and 66 deletions
+4 -4
View File
@@ -116,12 +116,12 @@ jobs:
if: ${{ github.event.inputs.update_version }} if: ${{ github.event.inputs.update_version }}
uses: stefanzweifel/git-auto-commit-action@v6 uses: stefanzweifel/git-auto-commit-action@v6
with: with:
commit_message: 'chore: release v${{ needs.setup.outputs.release_version }}' commit_message: 'chore: release v${{ github.event.inputs.version }}'
tagging_message: v${{ needs.setup.outputs.release_version }} tagging_message: v${{ github.event.inputs.version }}
- name: Detect prerelease - name: Detect prerelease
id: detect_prerelease id: detect_prerelease
run: | run: |
if [[ "${{ needs.setup.outputs.release_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then if [[ "${{ github.event.inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "IS_PRERELEASE=false" >> $GITHUB_ENV echo "IS_PRERELEASE=false" >> $GITHUB_ENV
echo "MAKE_LATEST=true" >> $GITHUB_ENV echo "MAKE_LATEST=true" >> $GITHUB_ENV
else else
@@ -132,6 +132,6 @@ jobs:
if: ${{ github.event.inputs.update_version }} if: ${{ github.event.inputs.update_version }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{ needs.setup.outputs.release_version }} tag_name: v${{ github.event.inputs.version }}
prerelease: ${{ env.IS_PRERELEASE }} prerelease: ${{ env.IS_PRERELEASE }}
make_latest: ${{ env.MAKE_LATEST }} make_latest: ${{ env.MAKE_LATEST }}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "gameyfin", "name": "gameyfin",
"version": "2.0.0.beta4", "version": "2.0.0.beta5",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@heroui/react": "2.7.9", "@heroui/react": "2.7.9",
@@ -13,7 +13,7 @@ export default function ProfileMenu() {
async function logout() { async function logout() {
if (auth.state.user?.managedBySso) { if (auth.state.user?.managedBySso) {
window.location.href = (await ConfigEndpoint.getLogoutUrl()) || "/"; window.location.href = (await ConfigEndpoint.getSsoLogoutUrl()) || "/";
} else { } else {
await auth.logout(); await auth.logout();
} }
@@ -38,7 +38,7 @@ function LibraryManagementLayout({getConfig, formik}: any) {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<Section title="Permissions"/> <Section title="Permissions"/>
<ConfigFormField configElement={getConfig("library.allow-public-access")} isDisabled/> <ConfigFormField configElement={getConfig("library.allow-public-access")}/>
<Section title="Scanning"/> <Section title="Scanning"/>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -87,6 +87,8 @@ export default function EditGameMetadataModal({game, isOpen, onOpenChange}: Edit
<ArrayInput key="features" name="features" label="Features"/> <ArrayInput key="features" name="features" label="Features"/>
<ArrayInput key="perspectives" name="perspectives" <ArrayInput key="perspectives" name="perspectives"
label="Perspectives"/> label="Perspectives"/>
<ArrayInput key="keywords" name="keywords"
label="Keywords"/>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</ModalBody> </ModalBody>
+3 -1
View File
@@ -23,6 +23,7 @@ import SearchView from "Frontend/views/SearchView";
import RecentlyAddedView from "Frontend/views/RecentlyAddedView"; 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 {ConfigEndpoint} from "Frontend/generated/endpoints";
export const {router, routes} = new RouterConfigurationBuilder() export const {router, routes} = new RouterConfigurationBuilder()
.withReactRoutes([ .withReactRoutes([
@@ -32,7 +33,7 @@ export const {router, routes} = new RouterConfigurationBuilder()
children: [ children: [
{ {
element: <MainLayout/>, element: <MainLayout/>,
handle: {requiresLogin: true}, handle: {requiresLogin: !ConfigEndpoint.isPublicAccessEnabled()},
children: [ children: [
{ {
index: true, element: <HomeView/> index: true, element: <HomeView/>
@@ -64,6 +65,7 @@ export const {router, routes} = new RouterConfigurationBuilder()
{ {
path: 'administration', path: 'administration',
element: <AdministrationView/>, element: <AdministrationView/>,
handle: {requiresLogin: true},
children: [ children: [
{ {
path: 'libraries', path: 'libraries',
+16 -1
View File
@@ -6,7 +6,7 @@ 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} from "@phosphor-icons/react"; import {ArrowLeft, DiceSix, 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 {UserPreferenceService} from "Frontend/util/user-preference-service"; import {UserPreferenceService} from "Frontend/util/user-preference-service";
@@ -103,9 +103,24 @@ export default function MainLayout() {
<ScanProgressPopover/> <ScanProgressPopover/>
</NavbarItem> </NavbarItem>
} }
{auth.state.user &&
<NavbarItem> <NavbarItem>
<ProfileMenu/> <ProfileMenu/>
</NavbarItem> </NavbarItem>
}
{!auth.state.user &&
<NavbarItem>
<Tooltip content="Sign in to your account" placement="bottom">
<Button color="primary"
radius="full"
isIconOnly
className="gradient-primary"
onPress={() => navigate("/login")}>
<SignIn fill="text-background/80"/>
</Button>
</Tooltip>
</NavbarItem>
}
</NavbarContent> </NavbarContent>
</Navbar> </Navbar>
+41 -9
View File
@@ -18,6 +18,7 @@ export default function SearchView() {
const knownThemes = useSnapshot(gameState).knownThemes; const knownThemes = useSnapshot(gameState).knownThemes;
const knownFeatures = useSnapshot(gameState).knownFeatures; const knownFeatures = useSnapshot(gameState).knownFeatures;
const knownPerspectives = useSnapshot(gameState).knownPerspectives; const knownPerspectives = useSnapshot(gameState).knownPerspectives;
const knownKeywords = useSnapshot(gameState).knownKeywords;
const libraries = useSnapshot(libraryState).libraries as LibraryDto[]; const libraries = useSnapshot(libraryState).libraries as LibraryDto[];
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -31,6 +32,7 @@ export default function SearchView() {
const [selectedThemes, setSelectedThemes] = useState<Set<string>>(new Set()); const [selectedThemes, setSelectedThemes] = useState<Set<string>>(new Set());
const [selectedFeatures, setSelectedFeatures] = useState<Set<string>>(new Set()); const [selectedFeatures, setSelectedFeatures] = useState<Set<string>>(new Set());
const [selectedPerspectives, setSelectedPerspectives] = useState<Set<string>>(new Set()); const [selectedPerspectives, setSelectedPerspectives] = useState<Set<string>>(new Set());
const [selectedKeywords, setSelectedKeywords] = useState<Set<string>>(new Set());
// Load initial filter values from URL parameters on component mount // Load initial filter values from URL parameters on component mount
useEffect(() => { useEffect(() => {
@@ -42,6 +44,7 @@ export default function SearchView() {
const themes = searchParams.getAll("theme"); const themes = searchParams.getAll("theme");
const features = searchParams.getAll("feature"); const features = searchParams.getAll("feature");
const perspectives = searchParams.getAll("perspective"); const perspectives = searchParams.getAll("perspective");
const keywords = searchParams.getAll("keyword");
setSearchTerm(term); setSearchTerm(term);
setSelectedLibraries(new Set(libs)); setSelectedLibraries(new Set(libs));
@@ -50,6 +53,7 @@ export default function SearchView() {
setSelectedThemes(new Set(themes)); setSelectedThemes(new Set(themes));
setSelectedFeatures(new Set(features)); setSelectedFeatures(new Set(features));
setSelectedPerspectives(new Set(perspectives)); setSelectedPerspectives(new Set(perspectives));
setSelectedKeywords(new Set(keywords));
setInitialLoadComplete(true); setInitialLoadComplete(true);
}, []); }, []);
@@ -102,15 +106,21 @@ export default function SearchView() {
}); });
} }
if (selectedKeywords.size > 0) {
selectedKeywords.forEach(keyword => {
newParams.append("keyword", keyword);
});
}
setSearchParams(newParams, {replace: true}); setSearchParams(newParams, {replace: true});
}, [searchTerm, selectedLibraries, selectedDevelopers, selectedGenres, }, [searchTerm, selectedLibraries, selectedDevelopers, selectedGenres,
selectedThemes, selectedFeatures, selectedPerspectives]); selectedThemes, selectedFeatures, selectedPerspectives, selectedKeywords]);
const filteredGames = useMemo(() => filterGames(), [ const filteredGames = useMemo(() => filterGames(), [
games, searchTerm, games, searchTerm,
selectedLibraries, selectedDevelopers, selectedLibraries, selectedDevelopers,
selectedGenres, selectedThemes, selectedGenres, selectedThemes,
selectedFeatures, selectedPerspectives selectedFeatures, selectedPerspectives, selectedKeywords
]); ]);
function filterGames(): GameDto[] { function filterGames(): GameDto[] {
@@ -164,6 +174,13 @@ export default function SearchView() {
); );
} }
// Apply keyword filter
if (selectedKeywords.size > 0) {
filtered = filtered.filter(game =>
game.keywords?.some(keyword => selectedKeywords.has(keyword))
);
}
return filtered; return filtered;
} }
@@ -183,10 +200,17 @@ export default function SearchView() {
onChange={(event) => setSearchTerm(event.target.value)} onChange={(event) => setSearchTerm(event.target.value)}
onClear={() => setSearchTerm("")} onClear={() => setSearchTerm("")}
/> />
<div className="flex flex-row flex-wrap gap-2 justify-center"> <div
className="w-full justify-center"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
gap: "0.5rem",
margin: "0 auto"
}}
>
<Select <Select
size="sm" size="sm"
className="max-w-xs"
selectionMode="multiple" selectionMode="multiple"
label="Libraries" label="Libraries"
placeholder="Filter by library" placeholder="Filter by library"
@@ -200,7 +224,6 @@ export default function SearchView() {
</Select> </Select>
<Select <Select
size="sm" size="sm"
className="max-w-xs"
selectionMode="multiple" selectionMode="multiple"
label="Developers" label="Developers"
placeholder="Filter by developer" placeholder="Filter by developer"
@@ -214,7 +237,6 @@ export default function SearchView() {
</Select> </Select>
<Select <Select
size="sm" size="sm"
className="max-w-xs"
selectionMode="multiple" selectionMode="multiple"
label="Genres" label="Genres"
placeholder="Filter by genre" placeholder="Filter by genre"
@@ -228,7 +250,6 @@ export default function SearchView() {
</Select> </Select>
<Select <Select
size="sm" size="sm"
className="max-w-xs"
selectionMode="multiple" selectionMode="multiple"
label="Themes" label="Themes"
placeholder="Filter by theme" placeholder="Filter by theme"
@@ -242,7 +263,6 @@ export default function SearchView() {
</Select> </Select>
<Select <Select
size="sm" size="sm"
className="max-w-xs"
selectionMode="multiple" selectionMode="multiple"
label="Features" label="Features"
placeholder="Filter by feature" placeholder="Filter by feature"
@@ -256,7 +276,6 @@ export default function SearchView() {
</Select> </Select>
<Select <Select
size="sm" size="sm"
className="max-w-xs"
selectionMode="multiple" selectionMode="multiple"
label="Perspectives" label="Perspectives"
placeholder="Filter by perspective" placeholder="Filter by perspective"
@@ -268,6 +287,19 @@ export default function SearchView() {
<SelectItem key={perspective}>{toTitleCase(perspective)}</SelectItem> <SelectItem key={perspective}>{toTitleCase(perspective)}</SelectItem>
))} ))}
</Select> </Select>
<Select
size="sm"
selectionMode="multiple"
label="Keywords"
placeholder="Filter by keyword"
selectedKeys={selectedKeywords}
//@ts-ignore
onSelectionChange={setSelectedKeywords}
>
{Array.from(knownKeywords).map((keyword) => (
<SelectItem key={keyword}>{keyword}</SelectItem>
))}
</Select>
</div> </div>
<div className="mt-4 w-full px-4 select-none"> <div className="mt-4 w-full px-4 select-none">
<CoverGrid games={filteredGames}/> <CoverGrid games={filteredGames}/>
@@ -1,5 +1,6 @@
package org.gameyfin.app.config package org.gameyfin.app.config
import com.vaadin.flow.server.auth.AnonymousAllowed
import com.vaadin.hilla.Endpoint import com.vaadin.hilla.Endpoint
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.annotation.security.PermitAll import jakarta.annotation.security.PermitAll
@@ -7,6 +8,7 @@ import jakarta.annotation.security.RolesAllowed
import org.gameyfin.app.config.dto.ConfigEntryDto import org.gameyfin.app.config.dto.ConfigEntryDto
import org.gameyfin.app.config.dto.ConfigUpdateDto import org.gameyfin.app.config.dto.ConfigUpdateDto
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.core.annotations.DynamicPublicAccess
import org.gameyfin.app.users.UserService import org.gameyfin.app.users.UserService
import org.gameyfin.app.users.util.isAdmin import org.gameyfin.app.users.util.isAdmin
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -36,9 +38,16 @@ class ConfigEndpoint(
/** Specific read-only endpoint for all users **/ /** Specific read-only endpoint for all users **/
@PermitAll @DynamicPublicAccess
fun isSsoEnabled(): Boolean? = configService.get(ConfigProperties.SSO.OIDC.Enabled) @AnonymousAllowed
fun isSsoEnabled(): Boolean = configService.get(ConfigProperties.SSO.OIDC.Enabled) == true
@DynamicPublicAccess
@AnonymousAllowed
fun getSsoLogoutUrl(): String? = configService.get(ConfigProperties.SSO.OIDC.LogoutUrl)
@DynamicPublicAccess
@AnonymousAllowed
fun isPublicAccessEnabled(): Boolean = configService.get(ConfigProperties.Libraries.AllowPublicAccess) == true
@PermitAll
fun getLogoutUrl(): String? = configService.get(ConfigProperties.SSO.OIDC.LogoutUrl)
} }
@@ -17,7 +17,7 @@ sealed class ConfigProperties<T : Serializable>(
data object AllowPublicAccess : ConfigProperties<Boolean>( data object AllowPublicAccess : ConfigProperties<Boolean>(
Boolean::class, Boolean::class,
"library.allow-public-access", "library.allow-public-access",
"Allow access to game libraries without login (coming soon™)", "Allow access to Gameyfin without login",
false false
) )
@@ -1,16 +1,16 @@
package org.gameyfin.app.core.annotations package org.gameyfin.app.core.annotations
import org.gameyfin.app.config.ConfigService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.gameyfin.app.config.ConfigProperties import org.gameyfin.app.config.ConfigProperties
import org.gameyfin.app.config.ConfigService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
@Component @Component
class DynamicAccessInterceptor( class DynamicAccessInterceptor(
private val configService: ConfigService private val config: ConfigService
) : HandlerInterceptor { ) : HandlerInterceptor {
override fun preHandle( override fun preHandle(
@@ -20,15 +20,16 @@ class DynamicAccessInterceptor(
): Boolean { ): Boolean {
val handlerMethod = (handler as? HandlerMethod) ?: return true val handlerMethod = (handler as? HandlerMethod) ?: return true
val method = handlerMethod.method val method = handlerMethod.method
val clazz = handlerMethod.beanType
// Check if method is annotated with @DynamicPublicAccess val hasDynamicPublicAccess =
if (method.isAnnotationPresent(DynamicPublicAccess::class.java)) { method.isAnnotationPresent(DynamicPublicAccess::class.java) ||
// Check if user is authenticated or public access is enabled clazz.isAnnotationPresent(DynamicPublicAccess::class.java)
if (request.userPrincipal != null || configService.get(ConfigProperties.Libraries.AllowPublicAccess) == true) {
if (hasDynamicPublicAccess) {
if (request.userPrincipal != null || config.get(ConfigProperties.Libraries.AllowPublicAccess) == true) {
return true return true
} }
// Deny access if user is not logged in and public access is disabled
response.status = HttpServletResponse.SC_UNAUTHORIZED response.status = HttpServletResponse.SC_UNAUTHORIZED
return false return false
} }
@@ -1,5 +1,6 @@
package org.gameyfin.app.core.download package org.gameyfin.app.core.download
import com.vaadin.flow.server.auth.AnonymousAllowed
import org.gameyfin.app.core.annotations.DynamicPublicAccess import org.gameyfin.app.core.annotations.DynamicPublicAccess
import org.gameyfin.app.games.GameService import org.gameyfin.app.games.GameService
import org.gameyfin.pluginapi.download.FileDownload import org.gameyfin.pluginapi.download.FileDownload
@@ -11,6 +12,7 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
@RestController @RestController
@RequestMapping("/download") @RequestMapping("/download")
@DynamicPublicAccess @DynamicPublicAccess
@AnonymousAllowed
class DownloadEndpoint( class DownloadEndpoint(
private val downloadService: DownloadService, private val downloadService: DownloadService,
private val gameService: GameService private val gameService: GameService
@@ -1,10 +1,12 @@
package org.gameyfin.app.core.download package org.gameyfin.app.core.download
import com.vaadin.flow.server.auth.AnonymousAllowed
import com.vaadin.hilla.Endpoint import com.vaadin.hilla.Endpoint
import jakarta.annotation.security.PermitAll import org.gameyfin.app.core.annotations.DynamicPublicAccess
@Endpoint @Endpoint
@PermitAll @DynamicPublicAccess
@AnonymousAllowed
class DownloadProviderEndpoint( class DownloadProviderEndpoint(
private val downloadService: DownloadService private val downloadService: DownloadService
) { ) {
@@ -109,8 +109,8 @@ class PluginService(
label = meta.label, label = meta.label,
description = meta.description, description = meta.description,
default = meta.default, default = meta.default,
isSecret = meta.isSecret, secret = meta.isSecret,
isRequired = meta.isRequired, required = meta.isRequired,
allowedValues = meta.allowedValues?.map { it.toString() } allowedValues = meta.allowedValues?.map { it.toString() }
) )
} }
@@ -10,7 +10,7 @@ class PluginConfigMetadataDto(
val label: String, val label: String,
val description: String, val description: String,
val default: Serializable?, val default: Serializable?,
val isSecret: Boolean, val secret: Boolean,
val isRequired: Boolean, val required: Boolean,
val allowedValues: List<String>? val allowedValues: List<String>?
) )
@@ -0,0 +1,23 @@
package org.gameyfin.app.core.security
import org.gameyfin.app.config.ConfigProperties
import org.gameyfin.app.config.ConfigService
import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.authorization.AuthorizationManager
import org.springframework.security.core.Authentication
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
import java.util.function.Supplier
class DynamicPublicAccessAuthorizationManager(
private val config: ConfigService
) : AuthorizationManager<RequestAuthorizationContext> {
@Deprecated("Deprecated in superclass")
override fun check(
authentication: Supplier<Authentication?>?,
`object`: RequestAuthorizationContext?
): AuthorizationDecision? {
val allow = config.get(ConfigProperties.Libraries.AllowPublicAccess) == true
return AuthorizationDecision(allow)
}
}
@@ -39,11 +39,18 @@ class SecurityConfig(
// Configure your static resources with public access before calling super.configure(HttpSecurity) as it adds final anyRequest matcher // Configure your static resources with public access before calling super.configure(HttpSecurity) as it adds final anyRequest matcher
http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry -> http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry ->
auth.requestMatchers("/setup").permitAll() auth.requestMatchers("/login").permitAll()
.requestMatchers("/setup").permitAll()
.requestMatchers("/reset-password").permitAll() .requestMatchers("/reset-password").permitAll()
.requestMatchers("/accept-invitation").permitAll() .requestMatchers("/accept-invitation").permitAll()
.requestMatchers("/public/**").permitAll() .requestMatchers("/public/**").permitAll()
.requestMatchers("/images/**").permitAll() .requestMatchers("/images/**").permitAll()
// Dynamic public access for certain endpoints
auth.requestMatchers("/game/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/library/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/search/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/download/**").access(DynamicPublicAccessAuthorizationManager(config))
} }
http.sessionManagement { sessionManagement -> http.sessionManagement { sessionManagement ->
@@ -1,20 +1,18 @@
package org.gameyfin.app.games package org.gameyfin.app.games
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.games.dto.GameDto import org.gameyfin.app.core.annotations.DynamicPublicAccess
import org.gameyfin.app.games.dto.GameEvent import org.gameyfin.app.games.dto.*
import org.gameyfin.app.games.dto.GameSearchResultDto
import org.gameyfin.app.games.dto.GameUpdateDto
import org.gameyfin.app.games.dto.OriginalIdDto
import org.gameyfin.app.libraries.LibraryService import org.gameyfin.app.libraries.LibraryService
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import java.nio.file.Path import java.nio.file.Path
@Endpoint @Endpoint
@PermitAll @DynamicPublicAccess
@AnonymousAllowed
class GameEndpoint( class GameEndpoint(
private val gameService: GameService, private val gameService: GameService,
private val libraryService: LibraryService private val libraryService: LibraryService
@@ -1,9 +1,10 @@
package org.gameyfin.app.libraries package org.gameyfin.app.libraries
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.libraries.dto.LibraryDto import org.gameyfin.app.libraries.dto.LibraryDto
import org.gameyfin.app.libraries.dto.LibraryEvent import org.gameyfin.app.libraries.dto.LibraryEvent
import org.gameyfin.app.libraries.dto.LibraryScanProgress import org.gameyfin.app.libraries.dto.LibraryScanProgress
@@ -14,7 +15,8 @@ import org.gameyfin.app.users.util.isAdmin
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@Endpoint @Endpoint
@PermitAll @DynamicPublicAccess
@AnonymousAllowed
class LibraryEndpoint( class LibraryEndpoint(
private val libraryService: LibraryService, private val libraryService: LibraryService,
private val userService: UserService, private val userService: UserService,
@@ -1,13 +1,15 @@
package org.gameyfin.app.media package org.gameyfin.app.media
import org.gameyfin.app.core.plugins.PluginService import com.vaadin.flow.server.auth.AnonymousAllowed
import org.gameyfin.app.games.entities.Image import jakarta.annotation.security.PermitAll
import org.gameyfin.app.games.entities.ImageType
import org.gameyfin.app.users.UserService
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.Utils import org.gameyfin.app.core.Utils
import org.gameyfin.app.core.annotations.DynamicPublicAccess import org.gameyfin.app.core.annotations.DynamicPublicAccess
import org.gameyfin.app.core.plugins.PluginService
import org.gameyfin.app.games.entities.Image
import org.gameyfin.app.games.entities.ImageType
import org.gameyfin.app.users.UserService
import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.InputStreamResource import org.springframework.core.io.InputStreamResource
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
@@ -18,9 +20,10 @@ import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
@DynamicPublicAccess
@RestController @RestController
@RequestMapping("/images") @RequestMapping("/images")
@DynamicPublicAccess
@AnonymousAllowed
class ImageEndpoint( class ImageEndpoint(
private val imageService: ImageService, private val imageService: ImageService,
private val userService: UserService, private val userService: UserService,
@@ -36,6 +39,7 @@ class ImageEndpoint(
fun getCover(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? { fun getCover(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
return getImageContent(id) return getImageContent(id)
} }
@GetMapping("/header/{id}") @GetMapping("/header/{id}")
fun getHeader(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? { fun getHeader(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
return getImageContent(id) return getImageContent(id)
@@ -54,6 +58,7 @@ class ImageEndpoint(
return getImageContent(avatar.id!!) return getImageContent(avatar.id!!)
} }
@PermitAll
@PostMapping("/avatar/upload") @PostMapping("/avatar/upload")
fun uploadAvatar(@RequestParam("file") file: MultipartFile) { fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth: Authentication = SecurityContextHolder.getContext().authentication
@@ -68,6 +73,7 @@ class ImageEndpoint(
userService.updateAvatar(auth.name, image) userService.updateAvatar(auth.name, image)
} }
@PermitAll
@PostMapping("/avatar/delete") @PostMapping("/avatar/delete")
fun deleteAvatar() { fun deleteAvatar() {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth: Authentication = SecurityContextHolder.getContext().authentication
@@ -5,11 +5,10 @@ import jakarta.annotation.security.RolesAllowed
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
@Endpoint @Endpoint
@RolesAllowed(Role.Names.ADMIN)
class SystemEndpoint( class SystemEndpoint(
private val systemService: SystemService private val systemService: SystemService
) { ) {
@RolesAllowed(Role.Names.ADMIN)
fun restart() { fun restart() {
systemService.restart() systemService.restart()
} }
@@ -1,8 +1,8 @@
package org.gameyfin.app.users package org.gameyfin.app.users
import org.gameyfin.app.users.persistence.UserRepository
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.users.entities.User import org.gameyfin.app.users.entities.User
import org.gameyfin.app.users.persistence.UserRepository
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
@@ -66,7 +66,7 @@ class RoleService(
.filterIsInstance<OidcUserAuthority>() .filterIsInstance<OidcUserAuthority>()
.flatMap { oidcUserAuthority -> .flatMap { oidcUserAuthority ->
val userInfo = oidcUserAuthority.userInfo val userInfo = oidcUserAuthority.userInfo
val roles = userInfo.getClaim<List<String>>("roles") val roles = userInfo.getClaim<List<String>>("roles") ?: return@flatMap emptySequence()
roles.asSequence().mapNotNull { roles.asSequence().mapNotNull {
if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority( if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority(
it.replace(SSO_ROLE_PREFIX, INTERNAL_ROLE_PREFIX) it.replace(SSO_ROLE_PREFIX, INTERNAL_ROLE_PREFIX)
@@ -16,11 +16,6 @@ class UserEndpoint(
private val userService: UserService, private val userService: UserService,
private val roleService: RoleService private val roleService: RoleService
) { ) {
@PermitAll
fun existsByMail(email: String): Boolean {
return userService.existsByEmail(email)
}
@PermitAll @PermitAll
fun getUserInfo(): UserInfoDto { fun getUserInfo(): UserInfoDto {
return userService.getUserInfo() return userService.getUserInfo()
@@ -32,6 +27,11 @@ class UserEndpoint(
userService.updateUser(auth.name, updates) userService.updateUser(auth.name, updates)
} }
@RolesAllowed(Role.Names.ADMIN)
fun existsByMail(email: String): Boolean {
return userService.existsByEmail(email)
}
@RolesAllowed(Role.Names.ADMIN) @RolesAllowed(Role.Names.ADMIN)
fun getAllUsers(): List<UserInfoDto> { fun getAllUsers(): List<UserInfoDto> {
return userService.getAllUsers() return userService.getAllUsers()
+1 -1
View File
@@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.nio.file.Files import java.nio.file.Files
group = "org.gameyfin" group = "org.gameyfin"
version = "2.0.0.beta4" version = "2.0.0.beta5"
allprojects { allprojects {
repositories { repositories {
+3 -1
View File
@@ -1,5 +1,7 @@
# Increase Gradle metaspace size # Gradle properties
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
org.gradle.parallel=true
org.gradle.caching=true
# Plugin versions # Plugin versions
kotlinVersion=2.2.0 kotlinVersion=2.2.0
kspVersion=2.2.0-2.0.2 kspVersion=2.2.0-2.0.2