Feature: System settings in DB - configurable backend URLs and API key

This commit is contained in:
cynfo3000
2026-03-04 23:57:14 +01:00
parent a64ee3b944
commit 5b503e6dba
11 changed files with 274 additions and 24 deletions
+9
View File
@@ -236,6 +236,15 @@ class ApiClient {
return this.delete(`/api/v1/apikeys/${id}`)
}
// System Settings
getSettings() {
return this.get('/api/v1/settings')
}
updateSettings(data) {
return this.put('/api/v1/settings', data)
}
// Audit Log
getAuditLogs(params = {}) {
const q = new URLSearchParams()
+2 -2
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import api from '../api/client'
import StatusBadge from './StatusBadge'
import { copyToClipboard } from '../utils/clipboard'
import { BACKEND_HOST } from '../config'
import { useSettingsStore } from '../stores/settings'
import {
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone,
@@ -572,7 +572,7 @@ function TunnelTab({ agentId, agent }) {
const [error, setError] = useState('')
const [sshCopied, setSshCopied] = useState(null)
const backendHost = BACKEND_HOST
const backendHost = useSettingsStore.getState().getBackendHost()
const loadTunnels = () => {
api.getTunnels(agentId)
+4 -1
View File
@@ -1,11 +1,14 @@
import { useState } from 'react'
import { Copy, Check, Terminal, Monitor } from 'lucide-react'
import { copyToClipboard } from '../utils/clipboard'
import { BACKEND_URL, API_KEY } from '../config'
import { useSettingsStore } from '../stores/settings'
export default function InstallGuide({ agentName, platform: initialPlatform }) {
const [platform, setPlatform] = useState(initialPlatform || 'freebsd')
const [copied, setCopied] = useState(null)
const { settings } = useSettingsStore()
const BACKEND_URL = settings.backend_url_external || settings.backend_url_internal || ''
const API_KEY = settings.api_key || ''
const copyText = (text, id) => {
copyToClipboard(text)
+2 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import api from '../api/client'
import { BACKEND_HOST } from '../config'
import { useSettingsStore } from '../stores/settings'
import StatusBadge from './StatusBadge'
import {
X, Cpu, MemoryStick, HardDrive, Clock, Network, Server, Monitor,
@@ -674,7 +674,7 @@ function TunnelTab({ agentId, agent, webguiPort = 8006 }) {
const [customPort, setCustomPort] = useState('')
const [error, setError] = useState('')
const backendHost = BACKEND_HOST
const backendHost = useSettingsStore.getState().getBackendHost()
const loadTunnels = () => {
api.getTunnels(agentId)
+2 -1
View File
@@ -9,9 +9,10 @@ const PLATFORMS = [
{ id: 'windows', label: 'Windows', icon: Cpu },
]
import { BACKEND_URL } from '../config'
import { useSettingsStore } from '../stores/settings'
export default function Firmware() {
const BACKEND_URL = useSettingsStore((s) => s.settings.backend_url_external || s.settings.backend_url_internal || '')
const [firmwareData, setFirmwareData] = useState(null)
const [installerData, setInstallerData] = useState(null)
const [agents, setAgents] = useState([])
+112 -18
View File
@@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'
import api from '../api/client'
import { useAuthStore } from '../stores/auth'
import { copyToClipboard } from '../utils/clipboard'
import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key } from 'lucide-react'
import { BACKEND_HOST } from '../config'
import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key, Save, Settings } from 'lucide-react'
import { useSettingsStore } from '../stores/settings'
export default function SettingsPage() {
const [users, setUsers] = useState([])
@@ -204,22 +204,8 @@ export default function SettingsPage() {
</div>
</div>
{/* Info */}
<div className="bg-gray-900 rounded-lg border border-gray-800 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800">
<span className="text-sm font-medium text-gray-300">System</span>
</div>
<div className="px-4 py-3 space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Backend</span>
<span className="text-gray-400">{BACKEND_HOST + ':8443'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Authentifizierung</span>
<span className="text-gray-400">JWT (8h Token) + API-Keys</span>
</div>
</div>
</div>
{/* System Settings */}
<SystemSettingsSection />
</div>
)
}
@@ -375,3 +361,111 @@ function APIKeysSection() {
</div>
)
}
function SystemSettingsSection() {
const { settings, fetchSettings } = useSettingsStore()
const [form, setForm] = useState({})
const [saving, setSaving] = useState(false)
const [msg, setMsg] = useState('')
const [copied, setCopied] = useState(null)
useEffect(() => {
fetchSettings()
}, [])
useEffect(() => {
setForm({
backend_url_internal: settings.backend_url_internal || '',
backend_url_external: settings.backend_url_external || '',
api_key: settings.api_key || '',
})
}, [settings])
const handleSave = async () => {
setSaving(true)
setMsg('')
try {
await api.updateSettings(form)
await fetchSettings()
setMsg('Gespeichert')
setTimeout(() => setMsg(''), 3000)
} catch (e) {
setMsg('Fehler: ' + e.message)
} finally {
setSaving(false)
}
}
const copyText = (text, id) => {
copyToClipboard(text)
setCopied(id)
setTimeout(() => setCopied(null), 2000)
}
return (
<div className="bg-gray-900 rounded-lg border border-gray-800 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800 flex items-center gap-2">
<Settings className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-300">System-Einstellungen</span>
</div>
<div className="px-4 py-4 space-y-3">
<div>
<label className="text-xs text-gray-500">Backend URL (intern)</label>
<input
type="text"
value={form.backend_url_internal || ''}
onChange={(e) => setForm({ ...form, backend_url_internal: e.target.value })}
placeholder="https://192.168.85.13:8443"
className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-orange-500 mt-1"
/>
<div className="text-[10px] text-gray-600 mt-0.5">Fuer Tunnel-Zugriff und lokale Verbindungen</div>
</div>
<div>
<label className="text-xs text-gray-500">Backend URL (extern)</label>
<input
type="text"
value={form.backend_url_external || ''}
onChange={(e) => setForm({ ...form, backend_url_external: e.target.value })}
placeholder="https://example.dyndns.org:8443"
className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-orange-500 mt-1"
/>
<div className="text-[10px] text-gray-600 mt-0.5">Fuer Remote-Firewalls in der Installationsanleitung</div>
</div>
<div>
<label className="text-xs text-gray-500">API-Key</label>
<div className="flex gap-2 mt-1">
<input
type="text"
value={form.api_key || ''}
onChange={(e) => setForm({ ...form, api_key: e.target.value })}
className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-orange-500"
/>
<button
onClick={() => copyText(form.api_key, 'sys-apikey')}
className="px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded text-gray-400 transition-colors"
title="Kopieren"
>
{copied === 'sys-apikey' ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
</button>
</div>
<div className="text-[10px] text-gray-600 mt-0.5">Wird in Installationsanleitungen angezeigt</div>
</div>
<div className="flex items-center gap-3 pt-2">
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-700 disabled:text-gray-500 rounded text-sm text-white transition-colors"
>
<Save className="w-4 h-4" />
{saving ? 'Speichere...' : 'Speichern'}
</button>
{msg && (
<span className={`text-sm ${msg.startsWith('Fehler') ? 'text-red-400' : 'text-green-400'}`}>
{msg}
</span>
)}
</div>
</div>
</div>
)
}
+4
View File
@@ -1,5 +1,6 @@
import { create } from 'zustand'
import api from '../api/client'
import { useSettingsStore } from './settings'
export const useAuthStore = create((set) => ({
user: null,
@@ -9,6 +10,8 @@ export const useAuthStore = create((set) => ({
login: async (username, password) => {
const data = await api.login(username, password)
set({ user: data.user, isAuthenticated: true })
// Load system settings after login
useSettingsStore.getState().fetchSettings()
return data
},
@@ -25,6 +28,7 @@ export const useAuthStore = create((set) => ({
try {
const data = await api.me()
set({ user: data.user || data, isAuthenticated: true, loading: false })
useSettingsStore.getState().fetchSettings()
} catch {
api.logout()
set({ user: null, isAuthenticated: false, loading: false })
+30
View File
@@ -0,0 +1,30 @@
import { create } from 'zustand'
import api from '../api/client'
export const useSettingsStore = create((set, get) => ({
settings: {},
loaded: false,
fetchSettings: async () => {
try {
const resp = await api.getSettings()
set({ settings: resp.data || {}, loaded: true })
} catch {
// ignore - settings not available before login
}
},
getSetting: (key, fallback = '') => {
return get().settings[key] || fallback
},
getBackendHost: () => {
const url = get().settings.backend_url_internal || ''
try {
return new URL(url).hostname
} catch {
// fallback: strip protocol and port manually
return url.replace(/^https?:\/\//, '').replace(/:\d+$/, '')
}
},
}))