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
+65
View File
@@ -0,0 +1,65 @@
package api
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"strconv"
)
// GET /api/v1/apikeys
func (h *Handler) listAPIKeys(w http.ResponseWriter, r *http.Request) {
keys, err := h.db.ListAPIKeys()
if err != nil {
log.Printf("API-Keys laden fehlgeschlagen: %v", err)
writeError(w, http.StatusInternalServerError, "Laden fehlgeschlagen")
return
}
writeJSON(w, http.StatusOK, keys)
}
// POST /api/v1/apikeys
func (h *Handler) createAPIKey(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
writeError(w, http.StatusBadRequest, "Parameter 'name' erforderlich")
return
}
// Zufaelligen 32-Byte Key generieren
b := make([]byte, 16)
rand.Read(b)
key := hex.EncodeToString(b)
apiKey, err := h.db.CreateAPIKey(req.Name, key)
if err != nil {
log.Printf("API-Key erstellen fehlgeschlagen: %v", err)
writeError(w, http.StatusInternalServerError, "Erstellen fehlgeschlagen")
return
}
log.Printf("Neuer API-Key erstellt: %s (%s...)", apiKey.Name, key[:8])
writeJSON(w, http.StatusCreated, apiKey)
}
// DELETE /api/v1/apikeys/{id}
func (h *Handler) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige ID")
return
}
if err := h.db.DeleteAPIKey(id); err != nil {
writeError(w, http.StatusNotFound, "API-Key nicht gefunden")
return
}
log.Printf("API-Key %d geloescht", id)
writeJSON(w, http.StatusOK, map[string]string{"message": "API-Key geloescht"})
}
+5
View File
@@ -54,6 +54,11 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
mux.HandleFunc("PUT /api/v1/users/{id}/password", h.changePassword)
mux.HandleFunc("DELETE /api/v1/users/{id}", h.deleteUser)
// API-Key Routes
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)
// Firmware/Agent-Update Routes
mux.HandleFunc("GET /api/v1/firmware", h.getFirmwareInfo)
mux.HandleFunc("POST /api/v1/firmware/upload", h.uploadFirmware)
+76 -11
View File
@@ -5,34 +5,101 @@ import (
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/cynfo/rmm-backend/db"
)
type contextKey string
const UserContextKey contextKey = "user"
// APIKeyAuth - Middleware fuer API-Key Authentifizierung
func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
keySet := make(map[string]bool, len(validKeys))
for _, k := range validKeys {
keySet[k] = true
// APIKeyCache cached DB-basierte API-Keys mit periodischem Refresh
type APIKeyCache struct {
db *db.Database
mu sync.RWMutex
keys map[string]bool
lastLoad time.Time
ttl time.Duration
}
func NewAPIKeyCache(database *db.Database) *APIKeyCache {
c := &APIKeyCache{
db: database,
keys: make(map[string]bool),
ttl: 30 * time.Second,
}
c.refresh()
return c
}
func (c *APIKeyCache) refresh() {
dbKeys, err := c.db.GetAllAPIKeys()
if err != nil {
log.Printf("API-Key Cache Refresh fehlgeschlagen: %v", err)
return
}
newKeys := make(map[string]bool, len(dbKeys))
for _, k := range dbKeys {
newKeys[k] = true
}
c.mu.Lock()
c.keys = newKeys
c.lastLoad = time.Now()
c.mu.Unlock()
}
func (c *APIKeyCache) IsValid(key string) bool {
c.mu.RLock()
expired := time.Since(c.lastLoad) > c.ttl
valid := c.keys[key]
c.mu.RUnlock()
if expired {
c.refresh()
c.mu.RLock()
valid = c.keys[key]
c.mu.RUnlock()
}
if valid {
go c.db.UpdateAPIKeyLastUsed(key)
}
return valid
}
// CombinedAuthWithCache - API-Key (aus DB-Cache) ODER JWT Token
func CombinedAuthWithCache(cache *APIKeyCache, auth *AuthHandler, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try API key first
key := r.Header.Get("X-API-Key")
if key == "" {
key = r.URL.Query().Get("api_key")
}
if key == "" || !keySet[key] {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
if key != "" && cache.IsValid(key) {
next.ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r)
// Try JWT
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := auth.ValidateToken(token)
if err == nil {
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
}
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
})
}
// CombinedAuth - API-Key ODER JWT Token
// CombinedAuth - Abwaertskompatibel mit statischen Keys (Fallback)
func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http.Handler {
keySet := make(map[string]bool, len(validKeys))
for _, k := range validKeys {
@@ -40,7 +107,6 @@ func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try API key first
key := r.Header.Get("X-API-Key")
if key == "" {
key = r.URL.Query().Get("api_key")
@@ -50,7 +116,6 @@ func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http
return
}
// Try JWT
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")