install.sh + frontend: use external URL dsbmueller.spdns.org:8443, configurable via RMM_BACKEND env
This commit is contained in:
@@ -223,6 +223,19 @@ class ApiClient {
|
||||
return this.put(`/api/v1/users/${userId}/password`, { password })
|
||||
}
|
||||
|
||||
// API-Keys
|
||||
getAPIKeys() {
|
||||
return this.get('/api/v1/apikeys')
|
||||
}
|
||||
|
||||
createAPIKey(name) {
|
||||
return this.post('/api/v1/apikeys', { name })
|
||||
}
|
||||
|
||||
deleteAPIKey(id) {
|
||||
return this.delete(`/api/v1/apikeys/${id}`)
|
||||
}
|
||||
|
||||
// Firmware
|
||||
getFirmwareInfo() {
|
||||
return this.get('/api/v1/firmware')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Copy, Check, Terminal, Monitor } from 'lucide-react'
|
||||
|
||||
const BACKEND_URL = 'https://192.168.85.13:8443'
|
||||
const BACKEND_URL = 'https://dsbmueller.spdns.org:8443'
|
||||
const API_KEY = '01532e5a7c9e70bf2757df77a2f5b9b9'
|
||||
const INSTALL_SH_URL = 'https://git.cynfo.net/christian/rmm/raw/branch/main/opnsense-plugin/install.sh'
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import api from '../api/client'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { Plus, Trash2, KeyRound, ShieldAlert } from 'lucide-react'
|
||||
import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key } from 'lucide-react'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [users, setUsers] = useState([])
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
const [form, setForm] = useState({ username: '', password: '', display_name: '' })
|
||||
const [pwChange, setPwChange] = useState(null) // userId being changed
|
||||
const [pwChange, setPwChange] = useState(null)
|
||||
const [pwForm, setPwForm] = useState({ password: '', confirm: '' })
|
||||
const [pwMsg, setPwMsg] = useState('')
|
||||
const { user: currentUser } = useAuthStore()
|
||||
@@ -185,6 +185,9 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Keys */}
|
||||
<APIKeysSection />
|
||||
|
||||
{/* 2FA */}
|
||||
<div className="bg-gray-900 rounded-lg border border-gray-800 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-800">
|
||||
@@ -211,10 +214,162 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Authentifizierung</span>
|
||||
<span className="text-gray-400">JWT (8h Token)</span>
|
||||
<span className="text-gray-400">JWT (8h Token) + API-Keys</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function APIKeysSection() {
|
||||
const [keys, setKeys] = useState([])
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
const [newName, setNewName] = useState('')
|
||||
const [newKey, setNewKey] = useState(null)
|
||||
const [copied, setCopied] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const reload = () => {
|
||||
setLoading(true)
|
||||
api.getAPIKeys()
|
||||
.then(k => setKeys(k || []))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
useEffect(() => { reload() }, [])
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newName.trim()) return
|
||||
try {
|
||||
const result = await api.createAPIKey(newName.trim())
|
||||
setNewKey(result)
|
||||
setNewName('')
|
||||
reload()
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id, name) => {
|
||||
if (!confirm(`API-Key "${name}" wirklich loeschen? Alle Agents mit diesem Key verlieren den Zugang.`)) return
|
||||
await api.deleteAPIKey(id)
|
||||
reload()
|
||||
}
|
||||
|
||||
const copyToClipboard = (text, id) => {
|
||||
navigator.clipboard.writeText(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 justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm font-medium text-gray-300">API-Keys</span>
|
||||
<span className="text-xs text-gray-600">({keys.length})</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setShowAdd(true); setNewKey(null); setNewName('') }}
|
||||
className="flex items-center gap-1 text-sm text-orange-400 hover:text-orange-300"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Neuer Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Neuer Key Dialog */}
|
||||
{showAdd && !newKey && (
|
||||
<div className="px-4 py-3 border-b border-gray-800 flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-gray-500">Bezeichnung</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="z.B. Agent-Key Kunde XY"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-white text-sm focus:outline-none focus:border-orange-500"
|
||||
autoFocus
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleCreate} className="px-3 py-1 bg-orange-600 hover:bg-orange-700 rounded text-sm text-white">
|
||||
Erstellen
|
||||
</button>
|
||||
<button onClick={() => setShowAdd(false)} className="px-3 py-1 bg-gray-800 rounded text-sm text-gray-400">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Neuer Key Anzeige */}
|
||||
{newKey && (
|
||||
<div className="px-4 py-3 border-b border-gray-800 bg-green-900/20">
|
||||
<div className="text-sm text-green-400 mb-2">API-Key erstellt — jetzt kopieren, er wird nur einmal angezeigt!</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-gray-950 rounded px-3 py-2 text-sm text-white font-mono select-all">
|
||||
{newKey.key}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(newKey.key, 'new')}
|
||||
className="px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded text-gray-300 transition-colors"
|
||||
>
|
||||
{copied === 'new' ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setNewKey(null); setShowAdd(false) }}
|
||||
className="mt-2 text-xs text-gray-500 hover:text-gray-400"
|
||||
>
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Key Liste */}
|
||||
{loading ? (
|
||||
<div className="px-4 py-3 text-gray-500 text-sm">Laden...</div>
|
||||
) : keys.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-gray-500 text-sm">Keine API-Keys vorhanden</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-800">
|
||||
{keys.map((k) => (
|
||||
<div key={k.id} className="px-4 py-2.5 flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-white">{k.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-0.5">
|
||||
<span className="text-xs text-gray-600 font-mono">{k.key.substring(0, 8)}...{k.key.substring(k.key.length - 4)}</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(k.key, k.id)}
|
||||
className="text-gray-600 hover:text-orange-400 transition-colors"
|
||||
title="Key kopieren"
|
||||
>
|
||||
{copied === k.id ? <Check className="w-3 h-3 text-green-400" /> : <Copy className="w-3 h-3" />}
|
||||
</button>
|
||||
<span className="text-xs text-gray-600">
|
||||
Erstellt: {new Date(k.created_at).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
{k.last_used && (
|
||||
<span className="text-xs text-gray-600">
|
||||
Letzter Zugriff: {new Date(k.last_used).toLocaleString('de-DE')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(k.id, k.name)}
|
||||
className="text-gray-500 hover:text-red-400 p-1 rounded hover:bg-gray-800 transition-colors ml-2"
|
||||
title="Key loeschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user