package api import ( "context" "log" "net/http" "strings" "sync" "time" "github.com/cynfo/rmm-backend/db" ) type contextKey string const UserContextKey contextKey = "user" // 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 (Header oder Query-Parameter) key := r.Header.Get("X-API-Key") if key == "" { key = r.URL.Query().Get("api_key") } if key != "" && cache.IsValid(key) { next.ServeHTTP(w, r) return } // Try JWT — Authorization Header oder ?token= Query-Parameter (fuer WebSocket) var jwtToken string authHeader := r.Header.Get("Authorization") if strings.HasPrefix(authHeader, "Bearer ") { jwtToken = strings.TrimPrefix(authHeader, "Bearer ") } else if t := r.URL.Query().Get("token"); t != "" { jwtToken = t } if jwtToken != "" { claims, err := auth.ValidateToken(jwtToken) 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 - 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 { keySet[k] = true } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := r.Header.Get("X-API-Key") if key == "" { key = r.URL.Query().Get("api_key") } if key != "" && keySet[key] { next.ServeHTTP(w, r) return } 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) }) } // JWTOnlyAuth - Nur JWT akzeptieren, kein API-Key (z.B. fuer Terminal-Zugriff) // Erfordert echten Login mit User/Pass (+2FA), kein Maschinentoken reicht. func JWTOnlyAuth(auth *AuthHandler, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var jwtToken string // Authorization Header authHeader := r.Header.Get("Authorization") if strings.HasPrefix(authHeader, "Bearer ") { jwtToken = strings.TrimPrefix(authHeader, "Bearer ") } // ?token= Query-Parameter (fuer WebSocket, Browser kann keinen Header setzen) if jwtToken == "" { jwtToken = r.URL.Query().Get("token") } if jwtToken == "" { http.Error(w, `{"error":"unauthorized: JWT erforderlich"}`, http.StatusUnauthorized) return } claims, err := auth.ValidateToken(jwtToken) if err != nil { http.Error(w, `{"error":"unauthorized: ungültiger Token"}`, http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), UserContextKey, claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } // CORS Middleware func CORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } // Logging - Request-Logging Middleware func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) }) }