install.sh + frontend: use external URL dsbmueller.spdns.org:8443, configurable via RMM_BACKEND env

This commit is contained in:
cynfo3000
2026-03-03 23:17:58 +01:00
parent 5f37e28046
commit eb52507c08
12 changed files with 593 additions and 108 deletions
+158 -3
View File
@@ -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>
)
}