diff --git a/src/api/hydrusClient.ts b/src/api/hydrusClient.ts index 69bae06..56868a0 100644 --- a/src/api/hydrusClient.ts +++ b/src/api/hydrusClient.ts @@ -32,6 +32,17 @@ export type HydrusFileDetails = HydrusMediaInfo & { tags: string[] } +function sanitizeApiKey(value?: string) { + return (value || '').replace(/\s+/g, '') +} + +function sanitizePort(value?: string | number) { + if (value == null) return undefined + if (typeof value === 'number') return Number.isFinite(value) ? value : undefined + const trimmed = value.trim() + return trimmed ? trimmed : undefined +} + export function makeId() { return Date.now().toString() + '-' + Math.random().toString(36).slice(2, 9) } @@ -54,10 +65,10 @@ export class HydrusClient { constructor(cfg: Partial = {}) { this.cfg = { id: (cfg.id as string) || makeId(), - name: cfg.name || '', - host: cfg.host || '', - port: cfg.port, - apiKey: cfg.apiKey, + name: (cfg.name || '').trim(), + host: (cfg.host || '').trim(), + port: sanitizePort(cfg.port), + apiKey: sanitizeApiKey(cfg.apiKey), ssl: !!cfg.ssl, forceApiKeyInQuery: !!cfg.forceApiKeyInQuery } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 22ee3f0..97d28f4 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -36,6 +36,56 @@ import { syncLibraryCache } from '../librarySync' import { APP_THEME_PRESETS, type AppThemeId } from '../themes' const DEFAULT_SERVER_FORM = { name: '', host: '', port: undefined, apiKey: '', ssl: false, forceApiKeyInQuery: false } +function normalizeServerForm(form: Omit) { + return { + ...form, + name: (form.name || '').trim(), + host: (form.host || '').trim(), + port: typeof form.port === 'string' ? form.port.trim() || undefined : form.port, + apiKey: (form.apiKey || '').replace(/\s+/g, ''), + } +} + +function validateServerForm(form: Omit) { + const normalized = normalizeServerForm(form) + + if (!normalized.host) { + return { normalized, error: 'Host is required.' } + } + + try { + let candidate = normalized.host + if (!/^https?:\/\//i.test(candidate)) { + candidate = `${normalized.ssl ? 'https' : 'http'}://${candidate}` + } + + const parsed = new URL(candidate) + if (!parsed.hostname) { + return { normalized, error: 'Host is invalid. Use an IP, hostname, or full URL.' } + } + } catch { + return { normalized, error: 'Host is invalid. Use an IP, hostname, or full URL.' } + } + + if (normalized.port !== undefined) { + const portText = String(normalized.port) + if (!/^\d+$/.test(portText)) { + return { normalized, error: 'Port must contain digits only.' } + } + + const portNumber = Number(portText) + if (portNumber < 1 || portNumber > 65535) { + return { normalized, error: 'Port must be between 1 and 65535.' } + } + } + + if (normalized.apiKey && !/^[a-f0-9]{64}$/i.test(normalized.apiKey)) { + return { normalized, error: 'API key should be a 64-character hexadecimal key. Any pasted spaces or line breaks were removed automatically.' } + } + + return { normalized, error: null } +} + type SettingsPageProps = { onClose?: () => void devOverlayEnabled: boolean @@ -139,6 +189,7 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE const startAdd = () => { setEditing(null) setForm(DEFAULT_SERVER_FORM) + setLastTest(null) } const startEdit = (s: Server) => { @@ -148,10 +199,17 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE } const handleSave = () => { + const { normalized, error } = validateServerForm(form) + if (error) { + setLastTest(error) + return + } + + setForm(normalized) if (editing) { - updateServer(editing.id, form) + updateServer(editing.id, normalized) } else { - const srv = addServer(form) + const srv = addServer(normalized) setActiveServerId(srv.id) } onClose && onClose() @@ -176,9 +234,16 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE } const handleTestForm = async () => { + const { normalized, error } = validateServerForm(form) + if (error) { + setLastTest(error) + return + } + + setForm(normalized) setTesting(true) try { - const res = await testServerConfig(form) + const res = await testServerConfig(normalized) setLastTest(`${res.message}`) } catch (e: any) { setLastTest(`Error: ${e?.message ?? String(e)}`) @@ -453,9 +518,9 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE )} setForm({ ...form, name: e.target.value })} fullWidth /> - setForm({ ...form, host: e.target.value })} fullWidth /> - setForm({ ...form, port: e.target.value })} fullWidth /> - setForm({ ...form, apiKey: e.target.value })} fullWidth /> + setForm({ ...form, host: e.target.value })} fullWidth autoCapitalize="off" autoCorrect="off" spellCheck={false} /> + setForm({ ...form, port: e.target.value })} fullWidth inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }} /> + setForm({ ...form, apiKey: e.target.value })} fullWidth autoCapitalize="off" autoCorrect="off" spellCheck={false} helperText="Hex API key. Pasted spaces or line breaks are ignored." /> setForm({ ...form, ssl: e.target.checked })} />} label="Use HTTPS (SSL)" /> setForm({ ...form, forceApiKeyInQuery: e.target.checked })} />} label="Send API key in query parameter" />