Fix excessive DB access in SecurityConfig

Fix array parsing bug in ConfigService
Potential fix for push subscription being cancelled in frontend
Simplify ConfigState since we are already returning the correct types from the backend
This commit is contained in:
grimsi
2025-05-17 22:30:51 +02:00
parent 457c997ac7
commit 80230b3d7e
4 changed files with 32 additions and 67 deletions
@@ -9,13 +9,10 @@ import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
import {Info, UserPlus} from "@phosphor-icons/react"; import {Info, UserPlus} from "@phosphor-icons/react";
import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react"; import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react";
import InviteUserModal from "Frontend/components/general/modals/InviteUserModal"; import InviteUserModal from "Frontend/components/general/modals/InviteUserModal";
import {useSnapshot} from "valtio/react";
import {configState} from "Frontend/state/ConfigState";
function UserManagementLayout({getConfig, formik}: any) { function UserManagementLayout({getConfig, formik}: any) {
const inviteUserModal = useDisclosure(); const inviteUserModal = useDisclosure();
const [users, setUsers] = useState<UserInfoDto[]>([]); const [users, setUsers] = useState<UserInfoDto[]>([]);
const config = useSnapshot(configState);
useEffect(() => { useEffect(() => {
UserEndpoint.getAllUsers().then( UserEndpoint.getAllUsers().then(
@@ -35,7 +32,7 @@ function UserManagementLayout({getConfig, formik}: any) {
<div className="flex flex-row items-baseline justify-between"> <div className="flex flex-row items-baseline justify-between">
<h2 className="text-xl font-bold mt-8 mb-1">Users</h2> <h2 className="text-xl font-bold mt-8 mb-1">Users</h2>
{!config.configEntries["sso.oidc.auto-register-new-users"].value && {!getConfig("sso.oidc.auto-register-new-users").value &&
<SmallInfoField className="mb-4 text-warning" icon={Info} <SmallInfoField className="mb-4 text-warning" icon={Info}
message="Automatic user registration for SSO users is disabled"/> message="Automatic user registration for SSO users is disabled"/>
} }
+22 -58
View File
@@ -2,15 +2,19 @@ import {proxy} from 'valtio';
import ConfigEntryDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigEntryDto"; import ConfigEntryDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigEntryDto";
import {ConfigEndpoint} from "Frontend/generated/endpoints"; import {ConfigEndpoint} from "Frontend/generated/endpoints";
import ConfigUpdateDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigUpdateDto"; import ConfigUpdateDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigUpdateDto";
import {Subscription} from "@vaadin/hilla-frontend";
type ConfigState = { type ConfigState = {
subscription?: Subscription<ConfigUpdateDto>;
isLoaded: boolean; isLoaded: boolean;
configEntries: Record<string, ConfigEntryDto>; configEntries: Record<string, ConfigEntryDto>;
configNested: NestedConfig; configNested: NestedConfig;
}; };
export const configState = proxy<ConfigState>({ export const configState = proxy<ConfigState>({
isLoaded: false, get isLoaded() {
return this.subscription != null;
},
configEntries: {}, configEntries: {},
get configNested() { get configNested() {
return toNestedConfig(Object.values(this.configEntries)); return toNestedConfig(Object.values(this.configEntries));
@@ -26,10 +30,9 @@ export async function initializeConfig() {
initialEntries.forEach((entry) => { initialEntries.forEach((entry) => {
configState.configEntries[entry.key] = entry; configState.configEntries[entry.key] = entry;
}); });
configState.isLoaded = true;
// Subscribe to real-time updates // Subscribe to real-time updates
ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => { configState.subscription = ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
Object.entries(updateDto.updates).forEach(([key, value]) => { Object.entries(updateDto.updates).forEach(([key, value]) => {
if (configState.configEntries[key]) { if (configState.configEntries[key]) {
configState.configEntries[key].value = value; configState.configEntries[key].value = value;
@@ -44,63 +47,24 @@ export type NestedConfig = {
[field: string]: any; [field: string]: any;
} }
function toNestedConfig(configArray: ConfigEntryDto[]): NestedConfig { function toNestedConfig(entries: ConfigEntryDto[]): Record<string, any> {
const nestedConfig: NestedConfig = {}; const result: Record<string, any> = {};
configArray.forEach(item => { for (const entry of entries) {
const keys = item.key!.split('.'); const keys = entry.key.split('.');
let currentLevel = nestedConfig; let current = result;
// Traverse the nested structure and create objects as needed for (let i = 0; i < keys.length; i++) {
keys.forEach((key, index) => { const key = keys[i];
if (index === keys.length - 1) {
// Convert value to the appropriate type if (i === keys.length - 1) {
let value: any; current[key] = entry.value;
switch (item.type) {
case 'Boolean':
value = typeof item.value == 'boolean' ? item.value : item.value === 'true';
break;
case 'Int':
value = typeof item.value == 'number' ? item.value : 0;
break;
case 'Float':
value = typeof item.value == 'number' ? item.value : 0.0;
break;
case 'Array':
if (Array.isArray(item.value)) {
switch (item.elementType) {
case 'Boolean':
value = item.value.map(v => typeof v === 'boolean' ? v : v === 'true');
break;
case 'Int':
case 'Integer':
value = item.value.map(v => typeof v == 'number' ? v : 0);
break;
case 'Float':
value = item.value.map(v => typeof v == 'number' ? v : 0.0);
break;
case 'String':
default:
value = item.value.map(v => v.toString());
break;
}
} else {
value = [];
}
break;
case 'String':
default:
value = item.value;
break;
}
currentLevel[key] = value;
} else { } else {
if (!currentLevel[key]) { current[key] = current[key] || {};
currentLevel[key] = {}; current = current[key];
}
currentLevel = currentLevel[key];
} }
}); }
}); }
return nestedConfig;
return result;
} }
@@ -97,9 +97,9 @@ class ConfigService(
var configEntry = appConfigRepository.findByIdOrNull(key) var configEntry = appConfigRepository.findByIdOrNull(key)
val parsedValue = val parsedValue =
if (value.javaClass.isArray) if (value.javaClass.isArray) {
(value as Array<Serializable>).joinToString(",") (value as Array<Serializable>).joinToString(",")
else } else
value.toString() value.toString()
if (configEntry == null) { if (configEntry == null) {
@@ -173,8 +173,11 @@ class ConfigService(
configProperty.type.java.isArray -> { configProperty.type.java.isArray -> {
val componentType = configProperty.type.java.componentType val componentType = configProperty.type.java.componentType
// Remove the brackets and split the string by commas // Remove the brackets and split the string by commas
val elements = value.removeSurrounding("[", "]").split(",") val elements = value
if (elements.isEmpty()) return emptyArray<Serializable>() as T .removeSurrounding("[", "]")
.split(",")
.filter { it.isNotBlank() }
when (componentType) { when (componentType) {
String::class.java -> elements.toTypedArray() as T String::class.java -> elements.toTypedArray() as T
Boolean::class.java -> elements.map { it.toBoolean() }.toTypedArray() as T Boolean::class.java -> elements.map { it.toBoolean() }.toTypedArray() as T
@@ -32,6 +32,7 @@ class SecurityConfig(
) : VaadinWebSecurity() { ) : VaadinWebSecurity() {
private val ssoProviderKey: String = "oidc" private val ssoProviderKey: String = "oidc"
private val allowedOrigins: List<String>? = config.get(ConfigProperties.System.Cors.AllowedOrigins)?.toList()
@Throws(Exception::class) @Throws(Exception::class)
override fun configure(http: HttpSecurity) { override fun configure(http: HttpSecurity) {
@@ -55,7 +56,7 @@ class SecurityConfig(
http.cors { cors -> http.cors { cors ->
cors.configurationSource { request -> cors.configurationSource { request ->
val configuration = CorsConfiguration() val configuration = CorsConfiguration()
configuration.allowedOrigins = config.get(ConfigProperties.System.Cors.AllowedOrigins)?.toList() configuration.allowedOrigins = allowedOrigins
configuration configuration
} }
} }