/** * React hooks for the configuration and server settings data. */ import { useCallback, useEffect, useRef, useState } from "react"; import { addLogPath, fetchGlobalConfig, fetchJailConfig, fetchJailConfigs, fetchServerSettings, flushLogs, previewLog, reloadConfig, testRegex, updateGlobalConfig, updateJailConfig, updateServerSettings, } from "../api/config"; import type { AddLogPathRequest, GlobalConfig, GlobalConfigUpdate, JailConfig, JailConfigUpdate, LogPreviewRequest, LogPreviewResponse, RegexTestRequest, RegexTestResponse, ServerSettings, ServerSettingsUpdate, } from "../types/config"; // --------------------------------------------------------------------------- // useJailConfigs — list all jail configs // --------------------------------------------------------------------------- interface UseJailConfigsResult { jails: JailConfig[]; total: number; loading: boolean; error: string | null; refresh: () => void; updateJail: (name: string, update: JailConfigUpdate) => Promise; reloadAll: () => Promise; } export function useJailConfigs(): UseJailConfigsResult { const [jails, setJails] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchJailConfigs() .then((resp) => { setJails(resp.jails); setTotal(resp.total); }) .catch((err: unknown) => { if (err instanceof Error && err.name !== "AbortError") { setError(err.message); } }) .finally(() => { setLoading(false); }); }, []); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const updateJail = useCallback( async (name: string, update: JailConfigUpdate): Promise => { await updateJailConfig(name, update); load(); }, [load], ); const reloadAll = useCallback(async (): Promise => { await reloadConfig(); load(); }, [load]); return { jails, total, loading, error, refresh: load, updateJail, reloadAll }; } // --------------------------------------------------------------------------- // useJailConfigDetail — single jail config with mutation // --------------------------------------------------------------------------- interface UseJailConfigDetailResult { jail: JailConfig | null; loading: boolean; error: string | null; refresh: () => void; updateJail: (update: JailConfigUpdate) => Promise; addLog: (req: AddLogPathRequest) => Promise; } export function useJailConfigDetail(name: string): UseJailConfigDetailResult { const [jail, setJail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchJailConfig(name) .then((resp) => { setJail(resp.jail); }) .catch((err: unknown) => { if (err instanceof Error && err.name !== "AbortError") { setError(err.message); } }) .finally(() => { setLoading(false); }); }, [name]); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const updateJail = useCallback( async (update: JailConfigUpdate): Promise => { await updateJailConfig(name, update); load(); }, [name, load], ); const addLog = useCallback( async (req: AddLogPathRequest): Promise => { await addLogPath(name, req); load(); }, [name, load], ); return { jail, loading, error, refresh: load, updateJail, addLog }; } // --------------------------------------------------------------------------- // useGlobalConfig // --------------------------------------------------------------------------- interface UseGlobalConfigResult { config: GlobalConfig | null; loading: boolean; error: string | null; refresh: () => void; updateConfig: (update: GlobalConfigUpdate) => Promise; } export function useGlobalConfig(): UseGlobalConfigResult { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchGlobalConfig() .then(setConfig) .catch((err: unknown) => { if (err instanceof Error && err.name !== "AbortError") { setError(err.message); } }) .finally(() => { setLoading(false); }); }, []); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const updateConfig = useCallback( async (update: GlobalConfigUpdate): Promise => { await updateGlobalConfig(update); load(); }, [load], ); return { config, loading, error, refresh: load, updateConfig }; } // --------------------------------------------------------------------------- // useServerSettings // --------------------------------------------------------------------------- interface UseServerSettingsResult { settings: ServerSettings | null; loading: boolean; error: string | null; refresh: () => void; updateSettings: (update: ServerSettingsUpdate) => Promise; flush: () => Promise; reload: () => Promise; restart: () => Promise; } export function useServerSettings(): UseServerSettingsResult { const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchServerSettings() .then((resp) => { setSettings(resp.settings); }) .catch((err: unknown) => { if (err instanceof Error && err.name !== "AbortError") { setError(err.message); } }) .finally(() => { setLoading(false); }); }, []); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const updateSettings_ = useCallback( async (update: ServerSettingsUpdate): Promise => { await updateServerSettings(update); load(); }, [load], ); const reload = useCallback(async (): Promise => { await reloadConfig(); load(); }, [load]); const restart = useCallback(async (): Promise => { await restartFail2Ban(); load(); }, [load]); const flush = useCallback(async (): Promise => { return flushLogs(); }, []); return { settings, loading, error, refresh: load, updateSettings: updateSettings_, flush, reload, restart, }; } // --------------------------------------------------------------------------- // useRegexTester — lazy, triggered by test(req) // --------------------------------------------------------------------------- interface UseRegexTesterResult { result: RegexTestResponse | null; testing: boolean; test: (req: RegexTestRequest) => Promise; } export function useRegexTester(): UseRegexTesterResult { const [result, setResult] = useState(null); const [testing, setTesting] = useState(false); const test_ = useCallback(async (req: RegexTestRequest): Promise => { setTesting(true); try { const resp = await testRegex(req); setResult(resp); } catch (err: unknown) { if (err instanceof Error) { setResult({ matched: false, groups: [], error: err.message }); } } finally { setTesting(false); } }, []); return { result, testing, test: test_ }; } // --------------------------------------------------------------------------- // useLogPreview — lazy, triggered by preview(req) // --------------------------------------------------------------------------- interface UseLogPreviewResult { preview: LogPreviewResponse | null; loading: boolean; run: (req: LogPreviewRequest) => Promise; } export function useLogPreview(): UseLogPreviewResult { const [preview, setPreview] = useState(null); const [loading, setLoading] = useState(false); const run_ = useCallback(async (req: LogPreviewRequest): Promise => { setLoading(true); try { const resp = await previewLog(req); setPreview(resp); } catch (err: unknown) { if (err instanceof Error) { setPreview({ lines: [], total_lines: 0, matched_count: 0, regex_error: err.message, }); } } finally { setLoading(false); } }, []); return { preview, loading, run: run_ }; }