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 {Button, Divider, Tooltip, useDisclosure} from "@heroui/react";
import InviteUserModal from "Frontend/components/general/modals/InviteUserModal";
import {useSnapshot} from "valtio/react";
import {configState} from "Frontend/state/ConfigState";
function UserManagementLayout({getConfig, formik}: any) {
const inviteUserModal = useDisclosure();
const [users, setUsers] = useState<UserInfoDto[]>([]);
const config = useSnapshot(configState);
useEffect(() => {
UserEndpoint.getAllUsers().then(
@@ -35,7 +32,7 @@ function UserManagementLayout({getConfig, formik}: any) {
<div className="flex flex-row items-baseline justify-between">
<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}
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 {ConfigEndpoint} from "Frontend/generated/endpoints";
import ConfigUpdateDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigUpdateDto";
import {Subscription} from "@vaadin/hilla-frontend";
type ConfigState = {
subscription?: Subscription<ConfigUpdateDto>;
isLoaded: boolean;
configEntries: Record<string, ConfigEntryDto>;
configNested: NestedConfig;
};
export const configState = proxy<ConfigState>({
isLoaded: false,
get isLoaded() {
return this.subscription != null;
},
configEntries: {},
get configNested() {
return toNestedConfig(Object.values(this.configEntries));
@@ -26,10 +30,9 @@ export async function initializeConfig() {
initialEntries.forEach((entry) => {
configState.configEntries[entry.key] = entry;
});
configState.isLoaded = true;
// Subscribe to real-time updates
ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
configState.subscription = ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
Object.entries(updateDto.updates).forEach(([key, value]) => {
if (configState.configEntries[key]) {
configState.configEntries[key].value = value;
@@ -44,63 +47,24 @@ export type NestedConfig = {
[field: string]: any;
}
function toNestedConfig(configArray: ConfigEntryDto[]): NestedConfig {
const nestedConfig: NestedConfig = {};
function toNestedConfig(entries: ConfigEntryDto[]): Record<string, any> {
const result: Record<string, any> = {};
configArray.forEach(item => {
const keys = item.key!.split('.');
let currentLevel = nestedConfig;
for (const entry of entries) {
const keys = entry.key.split('.');
let current = result;
// 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 = 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;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (i === keys.length - 1) {
current[key] = entry.value;
} else {
if (!currentLevel[key]) {
currentLevel[key] = {};
}
currentLevel = currentLevel[key];
current[key] = current[key] || {};
current = current[key];
}
});
});
return nestedConfig;
}
}
return result;
}
@@ -97,9 +97,9 @@ class ConfigService(
var configEntry = appConfigRepository.findByIdOrNull(key)
val parsedValue =
if (value.javaClass.isArray)
if (value.javaClass.isArray) {
(value as Array<Serializable>).joinToString(",")
else
} else
value.toString()
if (configEntry == null) {
@@ -173,8 +173,11 @@ class ConfigService(
configProperty.type.java.isArray -> {
val componentType = configProperty.type.java.componentType
// Remove the brackets and split the string by commas
val elements = value.removeSurrounding("[", "]").split(",")
if (elements.isEmpty()) return emptyArray<Serializable>() as T
val elements = value
.removeSurrounding("[", "]")
.split(",")
.filter { it.isNotBlank() }
when (componentType) {
String::class.java -> elements.toTypedArray() as T
Boolean::class.java -> elements.map { it.toBoolean() }.toTypedArray() as T
@@ -32,6 +32,7 @@ class SecurityConfig(
) : VaadinWebSecurity() {
private val ssoProviderKey: String = "oidc"
private val allowedOrigins: List<String>? = config.get(ConfigProperties.System.Cors.AllowedOrigins)?.toList()
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
@@ -55,7 +56,7 @@ class SecurityConfig(
http.cors { cors ->
cors.configurationSource { request ->
val configuration = CorsConfiguration()
configuration.allowedOrigins = config.get(ConfigProperties.System.Cors.AllowedOrigins)?.toList()
configuration.allowedOrigins = allowedOrigins
configuration
}
}