mirror of
https://github.com/grimsi/gameyfin.git
synced 2026-02-06 11:27:07 +00:00
WIP: Implement config in Frontend
This commit is contained in:
parent
0a3245ddf9
commit
3b97b6bbfa
@ -4,7 +4,7 @@
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="--stacktrace" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080" useFirstLineBreakpoints="true">
|
||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080" useFirstLineBreakpoints="true">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
15631
package-lock.json
generated
15631
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
src/main/frontend/components/CheckboxInput.tsx
Normal file
20
src/main/frontend/components/CheckboxInput.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import {useField} from "formik";
|
||||
import {Checkbox} from "@nextui-org/react";
|
||||
|
||||
// @ts-ignore
|
||||
const CheckboxInput = ({label, ...props}) => {
|
||||
// @ts-ignore
|
||||
const [field] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-grow items-center gap-2 my-2">
|
||||
<Checkbox
|
||||
{...field}
|
||||
id={field.name}>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckboxInput;
|
||||
@ -8,12 +8,12 @@ const Input = ({label, ...props}) => {
|
||||
const [field, meta] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<div className="grid w-full max-w-sm items-center gap-2 my-2">
|
||||
<NextUiInput
|
||||
{...props}
|
||||
{...field}
|
||||
id={label}
|
||||
placeholder={label}
|
||||
label={label}
|
||||
isInvalid={meta.touched && !!meta.error}
|
||||
errorMessage={
|
||||
<small className="flex flex-row items-center gap-1 text-danger">
|
||||
|
||||
@ -11,12 +11,12 @@ export default function ProfileMenu() {
|
||||
{
|
||||
label: "My Profile",
|
||||
icon: <User/>,
|
||||
onClick: () => navigate('/profile')
|
||||
onClick: () => navigate('/profile/')
|
||||
},
|
||||
{
|
||||
label: "Administration",
|
||||
icon: <GearFine/>,
|
||||
onClick: () => alert("Administration"),
|
||||
onClick: () => navigate("/administration/libraries"),
|
||||
showIf: state.user?.roles?.some(a => a?.includes("ADMIN"))
|
||||
},
|
||||
{
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import ConfigEntryDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigEntryDto";
|
||||
import React from "react";
|
||||
import Input from "Frontend/components/Input";
|
||||
import CheckboxInput from "Frontend/components/CheckboxInput";
|
||||
|
||||
export default function ConfigFormField({configElement}: {
|
||||
configElement: ConfigEntryDto | undefined
|
||||
}) {
|
||||
function inputElement(configElement: ConfigEntryDto) {
|
||||
switch (configElement.type) {
|
||||
case "Boolean":
|
||||
return (
|
||||
<CheckboxInput label={configElement.description} name={configElement.key}/>
|
||||
);
|
||||
case "String":
|
||||
return (
|
||||
<Input label={configElement.description} name={configElement.key} type="text"/>
|
||||
);
|
||||
case "Int" || "Float":
|
||||
return (
|
||||
<Input label={configElement.description} name={configElement.key} type="number"/>
|
||||
);
|
||||
default:
|
||||
return <pre>Unsupported type: {configElement.type} for key {configElement.key}</pre>;
|
||||
}
|
||||
}
|
||||
|
||||
return (inputElement(configElement!));
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {ConfigController} from "Frontend/generated/endpoints";
|
||||
import ConfigEntryDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigEntryDto";
|
||||
import {Form, Formik} from "formik";
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import {Button, Divider, Skeleton} from "@nextui-org/react";
|
||||
import {toast} from "sonner";
|
||||
|
||||
type NestedConfig = {
|
||||
[field: string]: any;
|
||||
}
|
||||
|
||||
type ConfigValuePair = {
|
||||
key: string;
|
||||
value: string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
export function LibraryManagement() {
|
||||
const isInitialized = useRef(false);
|
||||
const [configDtos, setConfigDtos] = useState<ConfigEntryDto[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
ConfigController.getConfigs("library").then((response: any) => {
|
||||
setConfigDtos(response as ConfigEntryDto[]);
|
||||
isInitialized.current = true;
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(values: NestedConfig) {
|
||||
const configValues = toConfigValuePair(values);
|
||||
await Promise.all(configValues.map(async (c: ConfigValuePair) => {
|
||||
if (c.value === null || c.value === undefined) {
|
||||
await ConfigController.deleteConfig(c.key);
|
||||
return;
|
||||
}
|
||||
|
||||
await ConfigController.setConfig(c.key, c.value.toString());
|
||||
}));
|
||||
|
||||
toast.success("Configuration saved");
|
||||
}
|
||||
|
||||
function getConfig(key: string) {
|
||||
return configDtos.find((configDto: ConfigEntryDto) => configDto.key === key);
|
||||
}
|
||||
|
||||
function toNestedConfig(configArray: ConfigEntryDto[]): NestedConfig {
|
||||
const nestedConfig: NestedConfig = {};
|
||||
|
||||
configArray.forEach(item => {
|
||||
const keys = item.key!.split('.');
|
||||
let currentLevel = nestedConfig;
|
||||
|
||||
// Traverse the nested structure and create objects as needed
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
// Convert value to the appropriate type
|
||||
let value: any;
|
||||
switch (item.type) {
|
||||
case 'Boolean':
|
||||
value = item.value === 'true';
|
||||
break;
|
||||
case 'Int':
|
||||
value = parseInt(item.value!);
|
||||
break;
|
||||
case 'Float':
|
||||
value = parseFloat(item.value!);
|
||||
break;
|
||||
case 'String':
|
||||
default:
|
||||
value = item.value;
|
||||
break;
|
||||
}
|
||||
currentLevel[key] = value;
|
||||
} else {
|
||||
if (!currentLevel[key]) {
|
||||
currentLevel[key] = {};
|
||||
}
|
||||
currentLevel = currentLevel[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return nestedConfig;
|
||||
}
|
||||
|
||||
function toConfigValuePair(obj: NestedConfig, parentKey: string = ''): ConfigValuePair[] {
|
||||
let result: ConfigValuePair[] = [];
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
result = result.concat(toConfigValuePair(obj[key], newKey));
|
||||
} else {
|
||||
result.push({key: newKey, value: obj[key]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!isInitialized.current) {
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="h-3 w-3/5 rounded-md"/>
|
||||
<Skeleton className="h-3 w-4/5 rounded-md"/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={toNestedConfig(configDtos)}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{(formik: { values: any; isSubmitting: any; }) => (
|
||||
<Form>
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h1 className="text-2xl font-bold">Library Management</h1>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
isLoading={formik.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-8 flex flex-col flex-grow">
|
||||
|
||||
<ConfigFormField configElement={getConfig("library.allow-public-access")}></ConfigFormField>
|
||||
<ConfigFormField
|
||||
configElement={getConfig("library.scan.enable-filesystem-watcher")}></ConfigFormField>
|
||||
|
||||
<h2 className="text-xl font-bold mt-4">Metadata</h2>
|
||||
<Divider/>
|
||||
<div className="flex flex-row">
|
||||
<ConfigFormField
|
||||
configElement={getConfig("library.metadata.update.enabled")}></ConfigFormField>
|
||||
<ConfigFormField
|
||||
configElement={getConfig("library.metadata.update.schedule")}></ConfigFormField>
|
||||
</div>
|
||||
|
||||
<ConfigFormField
|
||||
configElement={getConfig("library.display.games-per-page")}></ConfigFormField>
|
||||
</div>
|
||||
<pre>{JSON.stringify(formik.values, null, 2)}</pre>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
@ -52,7 +52,6 @@ const Wizard = ({children, initialValues, onSubmit}: {
|
||||
{(formik: { values: any; isSubmitting: any; }) => (
|
||||
<Form className="flex flex-col h-full">
|
||||
<div className="w-full mb-8">
|
||||
{/*<p>Step {stepNumber + 1} of {steps.length}</p>*/}
|
||||
<Stepper activeStep={stepNumber} activeLineClassName="bg-primary"
|
||||
lineClassName="bg-foreground"
|
||||
placeholder={undefined}
|
||||
|
||||
@ -7,6 +7,8 @@ import SetupView from "Frontend/views/SetupView";
|
||||
import ProfileView from "Frontend/views/ProfileView";
|
||||
import {ThemeSelector} from "Frontend/components/theming/ThemeSelector";
|
||||
import App from "Frontend/App";
|
||||
import AdministrationView from "Frontend/views/AdministrationView";
|
||||
import {LibraryManagement} from "Frontend/components/administration/LibraryManagement";
|
||||
|
||||
export const routes = protectRoutes([
|
||||
{
|
||||
@ -26,6 +28,13 @@ export const routes = protectRoutes([
|
||||
children: [
|
||||
{path: 'appearance', element: <ThemeSelector/>}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'administration',
|
||||
element: <AdministrationView/>,
|
||||
children: [
|
||||
{path: 'libraries', element: <LibraryManagement/>},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
13
src/main/frontend/util/useUpdateEffect.tsx
Normal file
13
src/main/frontend/util/useUpdateEffect.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import {useEffect, useRef} from "react";
|
||||
|
||||
export default function useUpdateEffect(effect: Function, dependencies?: [any]) {
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
} else {
|
||||
return effect();
|
||||
}
|
||||
}, dependencies);
|
||||
}
|
||||
45
src/main/frontend/views/AdministrationView.tsx
Normal file
45
src/main/frontend/views/AdministrationView.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import {Outlet, useNavigate} from "react-router-dom";
|
||||
import {Envelope, GameController, Users} from "@phosphor-icons/react";
|
||||
import {Listbox, ListboxItem} from "@nextui-org/react";
|
||||
|
||||
export default function AdministrationView() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: "Libraries",
|
||||
key: "libraries",
|
||||
icon: <GameController/>,
|
||||
action: () => navigate('libraries')
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
key: "users",
|
||||
icon: <Users/>,
|
||||
action: () => navigate('users')
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
icon: <Envelope/>,
|
||||
key: "notifications",
|
||||
action: () => navigate('notifications')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col pr-8">
|
||||
<Listbox className="min-w-60">
|
||||
{menuItems.map((i) => (
|
||||
<ListboxItem key={i.key} onPress={i.action} startContent={i.icon}>
|
||||
{i.title}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -5,7 +5,6 @@ import {useState} from "react";
|
||||
|
||||
export default function ProfileView() {
|
||||
const navigate = useNavigate();
|
||||
const [selectedKeys, setSelectedKeys] = useState(new Set(["profile"]));
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
||||
import de.grimsi.gameyfin.meta.Roles
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@ -10,12 +11,16 @@ class ConfigController(
|
||||
private val appConfigService: ConfigService
|
||||
) {
|
||||
|
||||
fun getConfigs(prefix: String?): List<ConfigEntryDto> {
|
||||
return appConfigService.getAllConfigValues(prefix)
|
||||
}
|
||||
|
||||
fun getConfig(key: String): String {
|
||||
return appConfigService.getConfigValue(key)
|
||||
}
|
||||
|
||||
fun setConfig(config: Pair<String, String>) {
|
||||
appConfigService.setConfigValue(config.first, config.second)
|
||||
fun setConfig(key: String, value: String) {
|
||||
appConfigService.setConfigValue(key, value)
|
||||
}
|
||||
|
||||
fun resetConfig(key: String) {
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class ConfigProperties<T : Serializable>(
|
||||
val type: KClass<T>,
|
||||
val key: String,
|
||||
val description: String,
|
||||
val default: T? = null
|
||||
) {
|
||||
|
||||
/** Libraries */
|
||||
data object LibraryAllowPublicAccess :
|
||||
ConfigProperties<Boolean>(
|
||||
Boolean::class,
|
||||
"library.allow-public-access",
|
||||
"Allow access to game libraries without login",
|
||||
false
|
||||
)
|
||||
|
||||
data object LibraryEnableFilesystemWatcher :
|
||||
ConfigProperties<Boolean>(
|
||||
Boolean::class,
|
||||
"library.scan.enable-filesystem-watcher",
|
||||
"Enable automatic library scanning using file system watchers",
|
||||
true
|
||||
)
|
||||
|
||||
data object LibraryMetadataUpdateEnabled :
|
||||
ConfigProperties<Boolean>(
|
||||
Boolean::class,
|
||||
"library.metadata.update.enabled",
|
||||
"Enable periodic refresh of video game meta-data",
|
||||
true
|
||||
)
|
||||
|
||||
data object LibraryMetadataUpdateSchedule :
|
||||
ConfigProperties<String>(
|
||||
String::class,
|
||||
"library.metadata.update.schedule",
|
||||
"Schedule for periodic metadata refresh in cron format",
|
||||
"0 0 * * 0"
|
||||
)
|
||||
|
||||
data object LibraryGamesPerPage :
|
||||
ConfigProperties<Int>(
|
||||
Int::class,
|
||||
"library.display.games-per-page",
|
||||
"How many games should be displayed per page",
|
||||
25
|
||||
)
|
||||
|
||||
data object LibraryRatingCutoff :
|
||||
ConfigProperties<Float>(
|
||||
Float::class,
|
||||
"library.display.rating-cutoff",
|
||||
"Minimum rating for games to be displayed",
|
||||
4.5f
|
||||
)
|
||||
|
||||
/** User management */
|
||||
data object UsersAllowNewSignUps : ConfigProperties<Boolean>(
|
||||
Boolean::class,
|
||||
"users.sign-ups.allow",
|
||||
"Allow new users to sign up by themselves",
|
||||
false
|
||||
)
|
||||
|
||||
data object UsersConfirmNewSignUps :
|
||||
ConfigProperties<Boolean>(
|
||||
Boolean::class,
|
||||
"users.sign-ups.confirm",
|
||||
"Admins need to confirm new sign-ups before they are allowed to log in",
|
||||
false
|
||||
)
|
||||
|
||||
/** Notifications */
|
||||
data object NotificationsEmailHost :
|
||||
ConfigProperties<String>(String::class, "notifications.email.host", "URL of the email server")
|
||||
|
||||
data object NotificationsEmailPort :
|
||||
ConfigProperties<String>(String::class, "notifications.email.port", "Port of the email server")
|
||||
|
||||
data object NotificationsEmailUsername :
|
||||
ConfigProperties<String>(String::class, "notifications.email.username", "Username for the email account")
|
||||
|
||||
data object NotificationsEmailPassword :
|
||||
ConfigProperties<String>(String::class, "notifications.email.password", "Password for the email account")
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class ConfigProperty<T : Serializable>(val type: KClass<T>, val key: String, val default: T? = null) {
|
||||
|
||||
/** Libraries */
|
||||
// Allow access to game libraries without login
|
||||
data object LibraryAllowPublicAccess :
|
||||
ConfigProperty<Boolean>(Boolean::class, "library.allow-public-access", false)
|
||||
|
||||
// Enable automatic library scanning using file system watchers
|
||||
data object LibraryEnableFilesystemWatcher :
|
||||
ConfigProperty<Boolean>(Boolean::class, "library.scan.enable-filesystem-watcher", true)
|
||||
|
||||
// Enable periodic refresh of video game meta-data and set the schedule (default is once per week)
|
||||
data object LibraryMetadataUpdateEnabled :
|
||||
ConfigProperty<Boolean>(Boolean::class, "library.metadata.update.enabled", true)
|
||||
|
||||
data object LibraryMetadataUpdateSchedule :
|
||||
ConfigProperty<String>(String::class, "library.metadata.update.schedule", "0 0 * * 0")
|
||||
|
||||
/** User management */
|
||||
// Allow new users to sign up by themselves
|
||||
data object UsersAllowNewSignUps : ConfigProperty<Boolean>(Boolean::class, "users.sign-ups.allow", false)
|
||||
|
||||
// If an administrator needs to confirm new sign-ups before they are allowed to log in
|
||||
data object UsersConfirmNewSignUps :
|
||||
ConfigProperty<Boolean>(Boolean::class, "users.sign-ups.confirm", false)
|
||||
|
||||
/** Notifications */
|
||||
// Settings for the mail server used by Gameyfin to send notifications
|
||||
data object NotificationsEmailHost : ConfigProperty<String>(String::class, "notifications.email.host")
|
||||
data object NotificationsEmailPort : ConfigProperty<String>(String::class, "notifications.email.port")
|
||||
data object NotificationsEmailUsername : ConfigProperty<String>(String::class, "notifications.email.username")
|
||||
data object NotificationsEmailPassword : ConfigProperty<String>(String::class, "notifications.email.password")
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
||||
import de.grimsi.gameyfin.config.entities.ConfigEntry
|
||||
import de.grimsi.gameyfin.config.persistence.ConfigRepository
|
||||
import jakarta.transaction.Transactional
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.safeCast
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@ -13,6 +13,33 @@ class ConfigService(
|
||||
private val appConfigRepository: ConfigRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Get all known config values.
|
||||
*
|
||||
* @param prefix: Optional prefix to filter the config values
|
||||
* @return A map of all config values
|
||||
*/
|
||||
fun getAllConfigValues(prefix: String?): List<ConfigEntryDto> {
|
||||
var configProperties = ConfigProperties::class.sealedSubclasses.flatMap { subclass ->
|
||||
subclass.objectInstance?.let { listOf(it) } ?: listOf()
|
||||
}
|
||||
|
||||
if (prefix != null) {
|
||||
configProperties = configProperties.filter { it.key.startsWith(prefix) }
|
||||
}
|
||||
|
||||
return configProperties.map { configProperty ->
|
||||
val appConfig = appConfigRepository.findById(configProperty.key).orElse(null)
|
||||
ConfigEntryDto(
|
||||
key = configProperty.key,
|
||||
value = appConfig?.value ?: configProperty.default?.toString(),
|
||||
defaultValue = configProperty.default?.toString(),
|
||||
type = configProperty.type.simpleName ?: "Unknown",
|
||||
description = configProperty.description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of a config property in a type-safe way.
|
||||
* Used internally.
|
||||
@ -21,7 +48,7 @@ class ConfigService(
|
||||
* @return The current value if set or the default value
|
||||
* @throws IllegalArgumentException if no value is set and no default value exists
|
||||
*/
|
||||
fun <T : Serializable> getConfigValue(configProperty: ConfigProperty<T>): T {
|
||||
fun <T : Serializable> getConfigValue(configProperty: ConfigProperties<T>): T {
|
||||
val appConfig = appConfigRepository.findById(configProperty.key).orElse(null)
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, configProperty)
|
||||
@ -61,12 +88,18 @@ class ConfigService(
|
||||
fun <T : Serializable> setConfigValue(key: String, value: T) {
|
||||
val configKey = findConfigProperty(key)
|
||||
|
||||
if (configKey.type.safeCast(value) == null) {
|
||||
throw IllegalArgumentException("Type mismatch for key: ${configKey.key}")
|
||||
// Check if the value can be cast to the type defined for the config property
|
||||
val castedValue = getValue(value.toString(), configKey)
|
||||
|
||||
var configEntry = appConfigRepository.findById(key).orElse(null)
|
||||
|
||||
if (configEntry == null) {
|
||||
configEntry = ConfigEntry(configKey.key, castedValue.toString())
|
||||
} else {
|
||||
configEntry.value = castedValue.toString()
|
||||
}
|
||||
|
||||
val appConfig = ConfigEntry(configKey.key, value.toString())
|
||||
appConfigRepository.save(appConfig)
|
||||
appConfigRepository.save(configEntry)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,11 +137,11 @@ class ConfigService(
|
||||
* Get the value of the config property in a type-safe way.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : Serializable> getValue(value: String, configProperty: ConfigProperty<T>): T {
|
||||
private fun <T : Serializable> getValue(value: String, configProperty: ConfigProperties<T>): T {
|
||||
return when (configProperty.type) {
|
||||
String::class -> value as T
|
||||
Boolean::class -> value.toBoolean() as T
|
||||
Number::class -> value.toInt() as T
|
||||
Int::class -> value.toInt() as T
|
||||
Float::class -> value.toFloat() as T
|
||||
else -> {
|
||||
throw RuntimeException("Unknown config type ${configProperty.type}: '$value' for key ${configProperty.key}")
|
||||
@ -119,9 +152,9 @@ class ConfigService(
|
||||
/**
|
||||
* Returns a config property
|
||||
*/
|
||||
private fun findConfigProperty(key: String): ConfigProperty<*> {
|
||||
private fun findConfigProperty(key: String): ConfigProperties<*> {
|
||||
// Use reflection to get all objects defined within ConfigKey
|
||||
val configProperties = ConfigProperty::class.sealedSubclasses.flatMap { subclass ->
|
||||
val configProperties = ConfigProperties::class.sealedSubclasses.flatMap { subclass ->
|
||||
subclass.objectInstance?.let { listOf(it) } ?: listOf()
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package de.grimsi.gameyfin.config.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import jakarta.annotation.Nonnull
|
||||
|
||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||
data class ConfigEntryDto(
|
||||
@Nonnull val key: String,
|
||||
val value: String?,
|
||||
val defaultValue: String?,
|
||||
@Nonnull val type: String,
|
||||
@Nonnull val description: String
|
||||
)
|
||||
@ -11,9 +11,10 @@ import jakarta.validation.constraints.NotNull
|
||||
class ConfigEntry(
|
||||
@Id
|
||||
@NotNull
|
||||
@Column(unique = true)
|
||||
@Column(name = "`key`", unique = true)
|
||||
val key: String,
|
||||
|
||||
@NotNull
|
||||
@Column(name = "`value`")
|
||||
var value: String
|
||||
)
|
||||
@ -1,14 +1,22 @@
|
||||
package de.grimsi.gameyfin.meta
|
||||
|
||||
import de.grimsi.gameyfin.meta.annotations.DynamicAccessInterceptor
|
||||
import de.grimsi.gameyfin.meta.development.DelayInterceptor
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
@Configuration
|
||||
class WebConfig(val dynamicAccessInterceptor: DynamicAccessInterceptor) : WebMvcConfigurer {
|
||||
class WebConfig(
|
||||
private val dynamicAccessInterceptor: DynamicAccessInterceptor
|
||||
) : WebMvcConfigurer {
|
||||
|
||||
@Autowired(required = false)
|
||||
private var delayInterceptor: DelayInterceptor? = null
|
||||
|
||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||
registry.addInterceptor(dynamicAccessInterceptor)
|
||||
delayInterceptor?.let { registry.addInterceptor(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package de.grimsi.gameyfin.meta.annotations
|
||||
|
||||
import de.grimsi.gameyfin.config.ConfigProperty
|
||||
import de.grimsi.gameyfin.config.ConfigProperties
|
||||
import de.grimsi.gameyfin.config.ConfigService
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
@ -24,7 +24,7 @@ class DynamicAccessInterceptor(
|
||||
// Check if method is annotated with @DynamicPublicAccess
|
||||
if (method.isAnnotationPresent(DynamicPublicAccess::class.java)) {
|
||||
// Check if user is authenticated or public access is enabled
|
||||
if (request.userPrincipal != null || configService.getConfigValue(ConfigProperty.LibraryAllowPublicAccess)) {
|
||||
if (request.userPrincipal != null || configService.getConfigValue(ConfigProperties.LibraryAllowPublicAccess)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package de.grimsi.gameyfin.meta.development
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
|
||||
|
||||
@Component
|
||||
@Profile("development")
|
||||
class DelayInterceptor : HandlerInterceptor {
|
||||
|
||||
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
|
||||
Thread.sleep(2000)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@ -8,14 +8,14 @@ import jakarta.validation.constraints.NotNull
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
class User(
|
||||
@NotNull
|
||||
@Column(unique = true)
|
||||
var username: String,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
var id: Long? = null,
|
||||
|
||||
@NotNull
|
||||
@Column(unique = true)
|
||||
var username: String,
|
||||
|
||||
@NotNull
|
||||
var password: String,
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user