mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 16:20:03 +00:00
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:
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>?
|
||||||
)
|
)
|
||||||
+23
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user