From 0fc4a7d474ccee732a44009bba4f5159f848d39a Mon Sep 17 00:00:00 2001 From: andreutu <91362974+andreutu@users.noreply.github.com> Date: Sun, 5 Jul 2026 16:07:36 +0300 Subject: [PATCH 1/5] feat(settings): add startup arguments setting --- apps/core/src/modules/process/manager.ts | 4 + .../src/pages/settings/tabs/fxserver.tsx | 79 +++++++++++++++++-- packages/shared/src/constants/settings.ts | 3 +- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/apps/core/src/modules/process/manager.ts b/apps/core/src/modules/process/manager.ts index 88c4f3cb..999e0b91 100644 --- a/apps/core/src/modules/process/manager.ts +++ b/apps/core/src/modules/process/manager.ts @@ -15,6 +15,7 @@ import { disconnectManager } from '../disconnect/manager'; import { sessionManager } from '../session/manager'; import { gameManager } from '../game/manager'; import { txAdminCompat } from '../txadmin/compat'; +import { repo } from '@fxmanager/database'; const STARTUP_STALL_MS = 90_000; const KILL_GRACE_MS = 5_000; @@ -75,6 +76,9 @@ export class ProcessManager { '+add_convar_permission', 'fxManager', 'read', 'api-port', ]; + const configuredArgs = repo.settings.get("fxserver.startupArguments")?.split(' ') ?? []; + args.push(...configuredArgs); + console.log(`[core] Starting fxServer`); this.injectConsoleLine({ diff --git a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx index bb04bcd8..e0406b0e 100644 --- a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx +++ b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx @@ -10,6 +10,65 @@ import { SETTINGS_DEFAULTS } from '@fxmanager/shared/constants'; import type { SettingsTabProps } from '@/types/settings'; import { Separator } from '@fxmanager/ui/components/separator'; import { Input } from '@fxmanager/ui/components/input'; +import { Field, FieldDescription } from '@fxmanager/ui/components/field'; +import { useState } from 'react'; + +const RESTRICTED_ARGS = [ + '+set onesync', + '+set resource-api-token', + '+set api-port', + '+ensure fxManager', + '+add_convar_permission fxManager read resource-api-token', + '+add_convar_permission fxManager read api-port', +]; + +function checkStartupArguments(args: string) { + if (!args) return ''; + + const restricted = RESTRICTED_ARGS.find((arg) => args.includes(arg)); + if (!restricted) return ''; + + return `"${restricted}" is not allowed here.`; +} + +function blur(event: React.KeyboardEvent) { + if (event.key !== 'Enter') return; + event.currentTarget.blur(); +} + +function StartupArgumentsField({ + value: defaultValue, + disabled, + onChange, +}: { + value: string; + disabled: boolean; + onChange: (value: string) => void; +}) { + const [error, setError] = useState(''); + + return ( + + { + const value = event.currentTarget.value.trim(); + const validationError = checkStartupArguments(value); + + setError(validationError); + + if (validationError) return; + + onChange(value); + }} + onKeyDown={blur} + /> + {error} + + ); +} export default function FXServerTab({ data, @@ -18,6 +77,7 @@ export default function FXServerTab({ }: SettingsTabProps<'fxserver'>) { const onesync = data['fxserver.onesync'] ?? SETTINGS_DEFAULTS['fxserver.onesync']; + const startupArguments = data['fxserver.startupArguments'] ?? ''; const serverDataPath = data['fxserver.serverDataPath'] ?? SETTINGS_DEFAULTS['fxserver.serverDataPath']; @@ -28,11 +88,6 @@ export default function FXServerTab({ data['fxserver.serverConfigPath'] ?? SETTINGS_DEFAULTS['fxserver.serverConfigPath']; - function blur(event: React.KeyboardEvent) { - if (event.key !== 'Enter') return; - event.currentTarget.blur(); - } - return (
@@ -52,6 +107,20 @@ export default function FXServerTab({ + + { + if (value === startupArguments) return; + + onChange('fxserver.startupArguments', value); + }} + /> + + diff --git a/packages/shared/src/constants/settings.ts b/packages/shared/src/constants/settings.ts index 12fc44ce..fa35d173 100644 --- a/packages/shared/src/constants/settings.ts +++ b/packages/shared/src/constants/settings.ts @@ -2,7 +2,7 @@ import type { SettingsKey, SettingsKeysByScope } from '../types'; export const SETTINGS_SCOPES = { general: [], - fxserver: ['onesync', 'executablePath', 'serverDataPath', 'serverConfigPath'], + fxserver: ['onesync', 'startupArguments', 'executablePath', 'serverDataPath', 'serverConfigPath'], whitelist: ['mode', 'discordBotToken', 'discordGuildId', 'discordRoleIds'], restarts: ['enabled', 'times'], } as const; @@ -29,6 +29,7 @@ export const SETTINGS_SENSITIVE_KEYS: SettingsKey[] = [ ]; export const SETTINGS_MASTER_ONLY_KEYS: SettingsKey[] = [ + 'fxserver.startupArguments', 'fxserver.executablePath', 'fxserver.serverDataPath', 'fxserver.serverConfigPath', From 2add5795fdb65e32d420eda973f6c20d3fcbcf54 Mon Sep 17 00:00:00 2001 From: andreutu <91362974+andreutu@users.noreply.github.com> Date: Sun, 5 Jul 2026 16:12:58 +0300 Subject: [PATCH 2/5] fix(core/process): add mock module for settings repo in tests --- apps/core/src/modules/process/manager.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/core/src/modules/process/manager.test.ts b/apps/core/src/modules/process/manager.test.ts index 4b306123..23712ac6 100644 --- a/apps/core/src/modules/process/manager.test.ts +++ b/apps/core/src/modules/process/manager.test.ts @@ -22,6 +22,7 @@ import { txAdminCompat } from '../txadmin/compat'; const mockGetHistory = mock(() => []); const mockBufferPush = mock(() => {}); +const mockSettingsGet = mock(() => undefined); const mockRegenerateApiToken = mock(() => {}); const mockGetSystemValues = mock(() => ({ @@ -36,6 +37,12 @@ const mockGetFxServerValues = mock(() => ({ serverConfigFile: 'server.cfg', })); +mock.module('@fxmanager/database', () => ({ + repo: { + settings: { get: mockSettingsGet }, + }, +})); + // Use spyOn to safely intercept instances without corrupting Bun's global module cache const getInstanceSpy = spyOn(ConfigManager, 'getInstance').mockReturnValue({ regenerateApiToken: mockRegenerateApiToken, From b86daa6ce715f360d855942baf52477e268fc956 Mon Sep 17 00:00:00 2001 From: andreutu <91362974+andreutu@users.noreply.github.com> Date: Sun, 5 Jul 2026 16:15:33 +0300 Subject: [PATCH 3/5] chore: biome format and cleanup --- apps/core/src/modules/process/manager.ts | 3 ++- apps/webpanel/src/pages/settings/tabs/fxserver.tsx | 4 +--- packages/shared/src/constants/settings.ts | 8 +++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/core/src/modules/process/manager.ts b/apps/core/src/modules/process/manager.ts index 999e0b91..d72c021a 100644 --- a/apps/core/src/modules/process/manager.ts +++ b/apps/core/src/modules/process/manager.ts @@ -76,7 +76,8 @@ export class ProcessManager { '+add_convar_permission', 'fxManager', 'read', 'api-port', ]; - const configuredArgs = repo.settings.get("fxserver.startupArguments")?.split(' ') ?? []; + const configuredArgs = + repo.settings.get('fxserver.startupArguments')?.split(' ') ?? []; args.push(...configuredArgs); console.log(`[core] Starting fxServer`); diff --git a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx index e0406b0e..ac43fad4 100644 --- a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx +++ b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx @@ -107,9 +107,7 @@ export default function FXServerTab({ - + Date: Sun, 5 Jul 2026 18:58:17 +0300 Subject: [PATCH 4/5] refactor(webpanel/settings): user regex for validating arguments --- .../src/pages/settings/tabs/fxserver.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx index ac43fad4..cafdd439 100644 --- a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx +++ b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx @@ -13,19 +13,22 @@ import { Input } from '@fxmanager/ui/components/input'; import { Field, FieldDescription } from '@fxmanager/ui/components/field'; import { useState } from 'react'; -const RESTRICTED_ARGS = [ - '+set onesync', - '+set resource-api-token', - '+set api-port', - '+ensure fxManager', - '+add_convar_permission fxManager read resource-api-token', - '+add_convar_permission fxManager read api-port', +const RESTRICTED_PATTERNS = [ + /^\+set\s+onesync\b/i, + /^\+set\s+resource-api-token\b/i, + /^\+set\s+api-port\b/i, + /^\+(ensure|start|stop|restart)\s+fxManager\b/i, + + /^\+add_convar_permission\s+\S+\s+\S+\s+resource-api-token\b/i, + /^\+add_convar_permission\s+\S+\s+\S+\s+api-port\b/i, ]; -function checkStartupArguments(args: string) { - if (!args) return ''; +function checkStartupArguments(value: string) { + if (!value) return ''; + + const args = value.match(/(?:\+|--)\S+(?:\s+(?!(?:\+|--))\S+)*/g) ?? []; + const restricted = args.find((arg) => RESTRICTED_PATTERNS.some((patt) => patt.test(arg))); - const restricted = RESTRICTED_ARGS.find((arg) => args.includes(arg)); if (!restricted) return ''; return `"${restricted}" is not allowed here.`; From e0e3e79d3a237b3de2b02c3cde3a1209fa612d8f Mon Sep 17 00:00:00 2001 From: andreutu <91362974+andreutu@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:41:07 +0300 Subject: [PATCH 5/5] feat(settings): validate invalid arguments server-side --- apps/core/src/routes/api/settings/index.ts | 22 ++++++++++++- .../src/pages/settings/tabs/fxserver.tsx | 32 ++++--------------- packages/shared/src/constants/settings.ts | 13 ++++++++ packages/shared/src/utils/index.ts | 1 + .../src/utils/startup-arguments.test.ts | 28 ++++++++++++++++ .../shared/src/utils/startup-arguments.ts | 23 +++++++++++++ 6 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 packages/shared/src/utils/startup-arguments.test.ts create mode 100644 packages/shared/src/utils/startup-arguments.ts diff --git a/apps/core/src/routes/api/settings/index.ts b/apps/core/src/routes/api/settings/index.ts index 1c1ffa38..75a1b686 100644 --- a/apps/core/src/routes/api/settings/index.ts +++ b/apps/core/src/routes/api/settings/index.ts @@ -8,7 +8,11 @@ import type { SettingsKey, SettingsScope, } from '@fxmanager/shared/types'; -import { PermissionManager, canWriteSetting } from '@fxmanager/shared/utils'; +import { + PermissionManager, + canWriteSetting, + validateStartupArguments, +} from '@fxmanager/shared/utils'; import { SETTINGS_KEYS, SETTINGS_SCOPES, @@ -18,6 +22,17 @@ import { import { repo } from '@fxmanager/database'; import { restartScheduler } from '../../../modules/schedule/manager'; +interface HookResult { + valid: boolean; + [key: string]: unknown; +} + +const SETTINGS_HOOKS: Partial< + Record HookResult> +> = { + 'fxserver.startupArguments': validateStartupArguments, +}; + const SettingsEndpoints: RouteModule['handler'] = async ( fastify, { pm, gm }, @@ -80,6 +95,11 @@ const SettingsEndpoints: RouteModule['handler'] = async ( throw new Error('Unauthorized'); } + const hook = SETTINGS_HOOKS[key as SettingsKey]; + if (hook && !hook(value).valid) { + throw new Error('Invalid value'); + } + repo.settings.set(key, value); const logValue = SETTINGS_SENSITIVE_KEYS.includes(key as SettingsKey) diff --git a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx index cafdd439..fa7905f4 100644 --- a/apps/webpanel/src/pages/settings/tabs/fxserver.tsx +++ b/apps/webpanel/src/pages/settings/tabs/fxserver.tsx @@ -11,29 +11,9 @@ import type { SettingsTabProps } from '@/types/settings'; import { Separator } from '@fxmanager/ui/components/separator'; import { Input } from '@fxmanager/ui/components/input'; import { Field, FieldDescription } from '@fxmanager/ui/components/field'; +import { validateStartupArguments } from '@fxmanager/shared/utils'; import { useState } from 'react'; -const RESTRICTED_PATTERNS = [ - /^\+set\s+onesync\b/i, - /^\+set\s+resource-api-token\b/i, - /^\+set\s+api-port\b/i, - /^\+(ensure|start|stop|restart)\s+fxManager\b/i, - - /^\+add_convar_permission\s+\S+\s+\S+\s+resource-api-token\b/i, - /^\+add_convar_permission\s+\S+\s+\S+\s+api-port\b/i, -]; - -function checkStartupArguments(value: string) { - if (!value) return ''; - - const args = value.match(/(?:\+|--)\S+(?:\s+(?!(?:\+|--))\S+)*/g) ?? []; - const restricted = args.find((arg) => RESTRICTED_PATTERNS.some((patt) => patt.test(arg))); - - if (!restricted) return ''; - - return `"${restricted}" is not allowed here.`; -} - function blur(event: React.KeyboardEvent) { if (event.key !== 'Enter') return; event.currentTarget.blur(); @@ -58,12 +38,14 @@ function StartupArgumentsField({ placeholder={'+exec server.cfg'} onBlur={(event) => { const value = event.currentTarget.value.trim(); - const validationError = checkStartupArguments(value); - - setError(validationError); + const validation = validateStartupArguments(value); - if (validationError) return; + if (validation.valid !== true) { + setError(`"${validation.argument}" is not allowed here.`); + return; + } + setError(''); onChange(value); }} onKeyDown={blur} diff --git a/packages/shared/src/constants/settings.ts b/packages/shared/src/constants/settings.ts index f7349601..7ad8bcdc 100644 --- a/packages/shared/src/constants/settings.ts +++ b/packages/shared/src/constants/settings.ts @@ -40,3 +40,16 @@ export const SETTINGS_MASTER_ONLY_KEYS: SettingsKey[] = [ 'fxserver.serverDataPath', 'fxserver.serverConfigPath', ]; + +export const RESTRICTED_STARTUP_ARGUMENTS: RegExp[] = [ + /^\+set\s+onesync\b/i, + /^\+set\s+resource-api-token\b/i, + /^\+set\s+api-port\b/i, + /^\+set\s+citizen-dir\b/i, + /^\+(ensure|start|stop|restart)\s+fxManager\b/i, + + /^\+add_convar_permission\s+\S+\s+\S+\s+resource-api-token\b/i, + /^\+add_convar_permission\s+\S+\s+\S+\s+api-port\b/i, + + /^--library-path\s+\S+/i, +]; diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index fa607cae..31d3aaf9 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './group-slug'; export * from './permissions'; export * from './resource-name'; export * from './settings-access'; +export * from './startup-arguments'; diff --git a/packages/shared/src/utils/startup-arguments.test.ts b/packages/shared/src/utils/startup-arguments.test.ts new file mode 100644 index 00000000..b2ce7e9d --- /dev/null +++ b/packages/shared/src/utils/startup-arguments.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'bun:test'; +import { validateStartupArguments } from './startup-arguments'; + +describe('validateStartupArguments()', () => { + it('should return true for valid arguments', () => { + expect(validateStartupArguments('+svgui --trace-warnings')).toEqual({ + valid: true, + }); + expect(validateStartupArguments('+exec somecfg.cfg')).toEqual({ + valid: true, + }); + }); + + it('should return the first restricted value for invalid arguments', () => { + expect( + validateStartupArguments('+exec somecfg.cfg +set onesync on'), + ).toEqual({ valid: false, argument: '+set onesync on' }); + expect( + validateStartupArguments('+stop fxManager +set api-port 40120'), + ).toEqual({ valid: false, argument: '+stop fxManager' }); + expect( + validateStartupArguments('--library-path /home/user/somefolder'), + ).toEqual({ + valid: false, + argument: '--library-path /home/user/somefolder', + }); + }); +}); diff --git a/packages/shared/src/utils/startup-arguments.ts b/packages/shared/src/utils/startup-arguments.ts new file mode 100644 index 00000000..be69fe8e --- /dev/null +++ b/packages/shared/src/utils/startup-arguments.ts @@ -0,0 +1,23 @@ +import { RESTRICTED_STARTUP_ARGUMENTS } from '../constants'; + +export type ValidationResult = + | { valid: true } + | { valid: false; argument: string }; + +/** + * Validates the given string for restricted startup arguments and, if invalid, returns the first invalid argument. + * @param value String of arguments to validate. + * @returns `true` if valid, otherwise the first invalid argument. + */ +export function validateStartupArguments(value: string): ValidationResult { + if (!value) return { valid: true }; + + const args = value.match(/(?:\+|--)\S+(?:\s+(?!(?:\+|--))\S+)*/g) ?? []; + const restricted = args.find((arg) => + RESTRICTED_STARTUP_ARGUMENTS.some((patt) => patt.test(arg)), + ); + + if (!restricted) return { valid: true }; + + return { valid: false, argument: restricted }; +}