diff --git a/backend/api/apikeys.go b/backend/api/apikeys.go index a5bf688..0ae6362 100644 --- a/backend/api/apikeys.go +++ b/backend/api/apikeys.go @@ -58,6 +58,21 @@ func (h *Handler) createAPIKey(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, apiKey) } +// GET /api/v1/customers/{id}/apikeys +func (h *Handler) listCustomerAPIKeys(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + writeError(w, http.StatusBadRequest, "Ungueltige ID") + return + } + keys, err := h.db.ListCustomerAPIKeys(id) + if err != nil { + writeError(w, http.StatusInternalServerError, "Laden fehlgeschlagen") + return + } + writeJSON(w, http.StatusOK, keys) +} + // DELETE /api/v1/apikeys/{id} func (h *Handler) deleteAPIKey(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") diff --git a/backend/api/handlers.go b/backend/api/handlers.go index feb1545..84a385c 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -59,6 +59,7 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) { mux.HandleFunc("GET /api/v1/apikeys", h.listAPIKeys) mux.HandleFunc("POST /api/v1/apikeys", h.createAPIKey) mux.HandleFunc("DELETE /api/v1/apikeys/{id}", h.deleteAPIKey) + mux.HandleFunc("GET /api/v1/customers/{id}/apikeys", h.listCustomerAPIKeys) // System Settings mux.HandleFunc("GET /api/v1/settings", h.getSettings) diff --git a/backend/db/postgres.go b/backend/db/postgres.go index 7fdebed..e2fb816 100644 --- a/backend/db/postgres.go +++ b/backend/db/postgres.go @@ -1259,6 +1259,38 @@ func (d *Database) UpdateAPIKeyLastUsed(key string) { d.db.Exec("UPDATE api_keys SET last_used = NOW() WHERE key = $1", key) } +func (d *Database) ListCustomerAPIKeys(customerID int) ([]APIKey, error) { + rows, err := d.db.Query( + "SELECT id, name, key, permissions, customer_id, created_at, last_used FROM api_keys WHERE customer_id = $1 ORDER BY permissions, created_at", + customerID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var keys []APIKey + for rows.Next() { + var k APIKey + var lastUsed sql.NullTime + var custID sql.NullInt64 + if err := rows.Scan(&k.ID, &k.Name, &k.Key, &k.Permissions, &custID, &k.CreatedAt, &lastUsed); err != nil { + return nil, err + } + if lastUsed.Valid { + k.LastUsed = &lastUsed.Time + } + if custID.Valid { + id := int(custID.Int64) + k.CustomerID = &id + } + keys = append(keys, k) + } + if keys == nil { + keys = []APIKey{} + } + return keys, rows.Err() +} + // MigrateConfigAPIKeys migriert API-Keys aus der Config in die DB (einmalig) // --- Audit Log --- diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 2400973..d06bbc6 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -250,6 +250,10 @@ class ApiClient { return this.get('/api/v1/apikeys') } + getCustomerAPIKeys(customerId) { + return this.get(`/api/v1/customers/${customerId}/apikeys`) + } + createAPIKey(name, permissions = 'read', customerId = null) { return this.post('/api/v1/apikeys', { name, permissions, customer_id: customerId }) } diff --git a/frontend/src/pages/Customers.jsx b/frontend/src/pages/Customers.jsx index 556503a..2f7f89c 100644 --- a/frontend/src/pages/Customers.jsx +++ b/frontend/src/pages/Customers.jsx @@ -1,6 +1,13 @@ import { useEffect, useState } from 'react' import api from '../api/client' -import { Plus, Trash2, Edit2, X, Check } from 'lucide-react' +import { Plus, Trash2, Edit2, X, Check, ChevronDown, ChevronRight, Copy, Key } from 'lucide-react' + +const PERM_LABELS = { + agent: { label: 'Agent', color: 'text-blue-400 bg-blue-900/30 border-blue-700/50' }, + read: { label: 'Read-only', color: 'text-green-400 bg-green-900/30 border-green-700/50' }, + write: { label: 'Write', color: 'text-yellow-400 bg-yellow-900/30 border-yellow-700/50' }, + admin: { label: 'Admin', color: 'text-orange-400 bg-orange-900/30 border-orange-700/50' }, +} export default function Customers() { const [customers, setCustomers] = useState([]) @@ -8,6 +15,7 @@ export default function Customers() { const [showAdd, setShowAdd] = useState(false) const [editId, setEditId] = useState(null) const [form, setForm] = useState({ number: '', name: '' }) + const [expandedId, setExpandedId] = useState(null) const reload = () => { api.getCustomers().then((c) => setCustomers(c || [])).finally(() => setLoading(false)) @@ -35,6 +43,10 @@ export default function Customers() { reload() } + const toggleExpand = (id) => { + setExpandedId(prev => prev === id ? null : id) + } + return (
@@ -52,6 +64,7 @@ export default function Customers() { + @@ -61,6 +74,7 @@ export default function Customers() { {showAdd && ( + @@ -94,9 +109,10 @@ export default function Customers() { )} {customers.map((c) => ( - + <> {editId === c.id ? ( - <> + + - + ) : ( - <> - - - toggleExpand(c.id)} + > + + + + - - + )} - + {expandedId === c.id && ( + + + + )} + ))}
Nummer Name Erstellt
setForm({ ...form, name: e.target.value })} 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" + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} />
{c.number}{c.name} +
+ {expandedId === c.id + ? + : + } + {c.number}{c.name} {new Date(c.created_at).toLocaleDateString('de-DE')} + e.stopPropagation()}>
-
+ +
@@ -158,3 +194,68 @@ export default function Customers() {
) } + +function CustomerKeyPanel({ customerId }) { + const [keys, setKeys] = useState(null) + const [copied, setCopied] = useState(null) + + useEffect(() => { + api.getCustomerAPIKeys(customerId).then(setKeys) + }, [customerId]) + + const copyKey = (key, id) => { + navigator.clipboard.writeText(key).catch(() => { + const el = document.createElement('textarea') + el.value = key; document.body.appendChild(el); el.select() + document.execCommand('copy'); document.body.removeChild(el) + }) + setCopied(id) + setTimeout(() => setCopied(null), 2000) + } + + if (keys === null) { + return
Laden...
+ } + + if (keys.length === 0) { + return ( +
+ + Keine API-Keys fuer diesen Kunden +
+ ) + } + + return ( +
+
+ + API-Keys +
+ {keys.map((k) => { + const perm = PERM_LABELS[k.permissions] || PERM_LABELS.read + return ( +
+ + {perm.label} + + {k.name} + + {k.key.substring(0, 8)}...{k.key.substring(k.key.length - 4)} + + +
+ ) + })} +
+ ) +}