Backend: Customers, Auth (JWT), Agent-Kundenzuordnung, CORS
This commit is contained in:
parent
5daf81b9de
commit
c6c8257cf0
144
backend/api/auth.go
Normal file
144
backend/api/auth.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cynfo/rmm-backend/db"
|
||||||
|
"github.com/cynfo/rmm-backend/models"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
db *db.Database
|
||||||
|
secret []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTClaims struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(database *db.Database, jwtSecret string) *AuthHandler {
|
||||||
|
secret := jwtSecret
|
||||||
|
if secret == "" {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
rand.Read(b)
|
||||||
|
secret = hex.EncodeToString(b)
|
||||||
|
log.Println("JWT Secret automatisch generiert (nicht persistent)")
|
||||||
|
}
|
||||||
|
return &AuthHandler{db: database, secret: []byte(secret)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthHandler) EnsureDefaultAdmin() {
|
||||||
|
count, err := a.db.UserCount()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("User-Count Fehler: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
_, err := a.db.CreateUser("admin", "admin", "Administrator", "admin")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Default-Admin anlegen fehlgeschlagen: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Default-Admin angelegt (admin/admin) - bitte Passwort aendern!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthHandler) GenerateToken(user *models.User) (string, time.Time, error) {
|
||||||
|
expiresAt := time.Now().Add(8 * time.Hour)
|
||||||
|
claims := JWTClaims{
|
||||||
|
UserID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Role: user.Role,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
signed, err := token.SignedString(a.secret)
|
||||||
|
return signed, expiresAt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthHandler) ValidateToken(tokenStr string) (*JWTClaims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return a.secret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
claims, ok := token.Claims.(*JWTClaims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/auth/login
|
||||||
|
func (a *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req models.LoginRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.db.GetUserByUsername(req.Username)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Ungueltige Anmeldedaten")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Ungueltige Anmeldedaten")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, expiresAt, err := a.GenerateToken(user)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Token-Generierung fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.db.UpdateUserLastLogin(user.ID)
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, models.LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
ExpiresAt: expiresAt.Format(time.RFC3339),
|
||||||
|
User: *user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/auth/me
|
||||||
|
func (a *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, ok := r.Context().Value(UserContextKey).(*JWTClaims)
|
||||||
|
if !ok {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Nicht authentifiziert")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.db.GetUserByUsername(claims.Username)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
writeError(w, http.StatusNotFound, "User nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupAuthRoutes registriert Auth-Routes (ohne Auth-Middleware)
|
||||||
|
func (a *AuthHandler) SetupAuthRoutes(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("POST /api/v1/auth/login", a.Login)
|
||||||
|
}
|
||||||
137
backend/api/customers.go
Normal file
137
backend/api/customers.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/v1/customers
|
||||||
|
func (h *Handler) listCustomers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
customers, err := h.db.GetCustomers()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, customers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/customers
|
||||||
|
func (h *Handler) createCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Number string `json:"number"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Number == "" || req.Name == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "number und name sind erforderlich")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err := h.db.CreateCustomer(req.Number, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Kunde anlegen fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/customers/{id}
|
||||||
|
func (h *Handler) getCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err := h.db.GetCustomer(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c == nil {
|
||||||
|
writeError(w, http.StatusNotFound, "Kunde nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/v1/customers/{id}
|
||||||
|
func (h *Handler) updateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req struct {
|
||||||
|
Number string `json:"number"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Number == "" || req.Name == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "number und name sind erforderlich")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.db.UpdateCustomer(id, req.Number, req.Name); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Aktualisierung fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Kunde aktualisiert"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/v1/customers/{id}
|
||||||
|
func (h *Handler) deleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deleted, err := h.db.DeleteCustomer(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !deleted {
|
||||||
|
writeError(w, http.StatusNotFound, "Kunde nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Kunde geloescht"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/customers/{id}/agents
|
||||||
|
func (h *Handler) getCustomerAgents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agents, err := h.db.GetAgentsByCustomer(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, agents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/v1/agents/{id}/customer
|
||||||
|
func (h *Handler) assignAgentCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
var req struct {
|
||||||
|
CustomerID int `json:"customer_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.CustomerID == 0 {
|
||||||
|
writeError(w, http.StatusBadRequest, "customer_id erforderlich")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.db.AssignAgentCustomer(agentID, req.CustomerID); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Zuordnung fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Agent zugeordnet"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/v1/agents/{id}/customer
|
||||||
|
func (h *Handler) unassignAgentCustomer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID := r.PathValue("id")
|
||||||
|
if err := h.db.UnassignAgentCustomer(agentID); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Zuordnung entfernen fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Zuordnung entfernt"})
|
||||||
|
}
|
||||||
@ -35,6 +35,23 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
|||||||
mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData)
|
mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData)
|
||||||
mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent)
|
mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent)
|
||||||
|
|
||||||
|
// Customer-Routes
|
||||||
|
mux.HandleFunc("GET /api/v1/customers", h.listCustomers)
|
||||||
|
mux.HandleFunc("POST /api/v1/customers", h.createCustomer)
|
||||||
|
mux.HandleFunc("GET /api/v1/customers/{id}", h.getCustomer)
|
||||||
|
mux.HandleFunc("PUT /api/v1/customers/{id}", h.updateCustomer)
|
||||||
|
mux.HandleFunc("DELETE /api/v1/customers/{id}", h.deleteCustomer)
|
||||||
|
mux.HandleFunc("GET /api/v1/customers/{id}/agents", h.getCustomerAgents)
|
||||||
|
|
||||||
|
// Agent-Customer Zuordnung
|
||||||
|
mux.HandleFunc("PUT /api/v1/agents/{id}/customer", h.assignAgentCustomer)
|
||||||
|
mux.HandleFunc("DELETE /api/v1/agents/{id}/customer", h.unassignAgentCustomer)
|
||||||
|
|
||||||
|
// User-Routes
|
||||||
|
mux.HandleFunc("GET /api/v1/users", h.listUsers)
|
||||||
|
mux.HandleFunc("POST /api/v1/users", h.createUser)
|
||||||
|
mux.HandleFunc("DELETE /api/v1/users/{id}", h.deleteUser)
|
||||||
|
|
||||||
// WebSocket und Tunnel-Routes
|
// WebSocket und Tunnel-Routes
|
||||||
h.setupTunnelRoutes(mux, hub)
|
h.setupTunnelRoutes(mux, hub)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const UserContextKey contextKey = "user"
|
||||||
|
|
||||||
// APIKeyAuth - Middleware fuer API-Key Authentifizierung
|
// APIKeyAuth - Middleware fuer API-Key Authentifizierung
|
||||||
func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
|
func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
|
||||||
keySet := make(map[string]bool, len(validKeys))
|
keySet := make(map[string]bool, len(validKeys))
|
||||||
@ -15,7 +21,6 @@ func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
|
|||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
key := r.Header.Get("X-API-Key")
|
key := r.Header.Get("X-API-Key")
|
||||||
// Fallback: Query-Parameter (fuer WebSocket-Verbindungen)
|
|
||||||
if key == "" {
|
if key == "" {
|
||||||
key = r.URL.Query().Get("api_key")
|
key = r.URL.Query().Get("api_key")
|
||||||
}
|
}
|
||||||
@ -27,6 +32,54 @@ func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CombinedAuth - API-Key ODER JWT Token
|
||||||
|
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) {
|
||||||
|
// Try API key first
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Logging - Request-Logging Middleware
|
||||||
func Logging(next http.Handler) http.Handler {
|
func Logging(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
59
backend/api/users.go
Normal file
59
backend/api/users.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/v1/users
|
||||||
|
func (h *Handler) listUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
users, err := h.db.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, users)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/users
|
||||||
|
func (h *Handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Username == "" || req.Password == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "username und password sind erforderlich")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Role == "" {
|
||||||
|
req.Role = "admin"
|
||||||
|
}
|
||||||
|
u, err := h.db.CreateUser(req.Username, req.Password, req.DisplayName, req.Role)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "User anlegen fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusCreated, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/v1/users/{id}
|
||||||
|
func (h *Handler) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.Atoi(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungueltige ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deleted, err := h.db.DeleteUser(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !deleted {
|
||||||
|
writeError(w, http.StatusNotFound, "User nicht gefunden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"message": "User geloescht"})
|
||||||
|
}
|
||||||
@ -9,10 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ListenAddr string `yaml:"listen_addr"`
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
TLSCert string `yaml:"tls_cert"`
|
TLSCert string `yaml:"tls_cert"`
|
||||||
TLSKey string `yaml:"tls_key"`
|
TLSKey string `yaml:"tls_key"`
|
||||||
APIKeys []string `yaml:"api_keys"`
|
APIKeys []string `yaml:"api_keys"`
|
||||||
|
JWTSecret string `yaml:"jwt_secret"`
|
||||||
Database db.DBConfig `yaml:"database"`
|
Database db.DBConfig `yaml:"database"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
backend/db/customers.go
Normal file
67
backend/db/customers.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/cynfo/rmm-backend/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Database) CreateCustomer(number, name string) (*models.Customer, error) {
|
||||||
|
var c models.Customer
|
||||||
|
err := d.db.QueryRow(
|
||||||
|
"INSERT INTO customers (number, name) VALUES ($1, $2) RETURNING id, number, name, created_at",
|
||||||
|
number, name,
|
||||||
|
).Scan(&c.ID, &c.Number, &c.Name, &c.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetCustomers() ([]models.Customer, error) {
|
||||||
|
rows, err := d.db.Query("SELECT id, number, name, created_at FROM customers ORDER BY name")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var customers []models.Customer
|
||||||
|
for rows.Next() {
|
||||||
|
var c models.Customer
|
||||||
|
if err := rows.Scan(&c.ID, &c.Number, &c.Name, &c.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
customers = append(customers, c)
|
||||||
|
}
|
||||||
|
if customers == nil {
|
||||||
|
customers = []models.Customer{}
|
||||||
|
}
|
||||||
|
return customers, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetCustomer(id int) (*models.Customer, error) {
|
||||||
|
var c models.Customer
|
||||||
|
err := d.db.QueryRow("SELECT id, number, name, created_at FROM customers WHERE id = $1", id).
|
||||||
|
Scan(&c.ID, &c.Number, &c.Name, &c.CreatedAt)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdateCustomer(id int, number, name string) error {
|
||||||
|
_, err := d.db.Exec("UPDATE customers SET number = $1, name = $2 WHERE id = $3", number, name, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) DeleteCustomer(id int) (bool, error) {
|
||||||
|
res, err := d.db.Exec("DELETE FROM customers WHERE id = $1", id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
return rows > 0, nil
|
||||||
|
}
|
||||||
@ -97,12 +97,33 @@ func (d *Database) migrate() error {
|
|||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_agent_events_agent ON agent_events(agent_id, created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_agent_events_agent ON agent_events(agent_id, created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS customers (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
number TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL DEFAULT '',
|
||||||
|
role TEXT NOT NULL DEFAULT 'admin',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_login TIMESTAMPTZ
|
||||||
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
if _, err := d.db.Exec(schema); err != nil {
|
if _, err := d.db.Exec(schema); err != nil {
|
||||||
return fmt.Errorf("Schema anlegen: %w", err)
|
return fmt.Errorf("Schema anlegen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agents um customer_id erweitern
|
||||||
|
d.db.Exec("ALTER TABLE agents ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id)")
|
||||||
|
d.db.Exec("CREATE INDEX IF NOT EXISTS idx_agents_customer ON agents(customer_id)")
|
||||||
|
|
||||||
// Metrics Hypertable
|
// Metrics Hypertable
|
||||||
metricsSchema := `
|
metricsSchema := `
|
||||||
CREATE TABLE IF NOT EXISTS metrics (
|
CREATE TABLE IF NOT EXISTS metrics (
|
||||||
@ -321,7 +342,7 @@ func (d *Database) writeMetrics(tx *sql.Tx, agentID string, ts time.Time, data *
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Database) GetAgents() ([]models.Agent, error) {
|
func (d *Database) GetAgents() ([]models.Agent, error) {
|
||||||
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents ORDER BY name")
|
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id FROM agents ORDER BY name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -331,13 +352,18 @@ func (d *Database) GetAgents() ([]models.Agent, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var a models.Agent
|
var a models.Agent
|
||||||
var lastHB sql.NullTime
|
var lastHB sql.NullTime
|
||||||
|
var custID sql.NullInt64
|
||||||
|
|
||||||
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB); err != nil {
|
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if lastHB.Valid {
|
if lastHB.Valid {
|
||||||
a.LastHeartbeat = &lastHB.Time
|
a.LastHeartbeat = &lastHB.Time
|
||||||
}
|
}
|
||||||
|
if custID.Valid {
|
||||||
|
id := int(custID.Int64)
|
||||||
|
a.CustomerID = &id
|
||||||
|
}
|
||||||
agents = append(agents, a)
|
agents = append(agents, a)
|
||||||
}
|
}
|
||||||
if agents == nil {
|
if agents == nil {
|
||||||
@ -370,11 +396,12 @@ func (d *Database) GetAgentByHostname(hostname string) (*models.Agent, error) {
|
|||||||
func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error) {
|
func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error) {
|
||||||
var a models.Agent
|
var a models.Agent
|
||||||
var lastHB sql.NullTime
|
var lastHB sql.NullTime
|
||||||
|
var custID sql.NullInt64
|
||||||
|
|
||||||
err := d.db.QueryRow(
|
err := d.db.QueryRow(
|
||||||
"SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE id = $1",
|
"SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id FROM agents WHERE id = $1",
|
||||||
id,
|
id,
|
||||||
).Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB)
|
).Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
@ -385,6 +412,10 @@ func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error
|
|||||||
if lastHB.Valid {
|
if lastHB.Valid {
|
||||||
a.LastHeartbeat = &lastHB.Time
|
a.LastHeartbeat = &lastHB.Time
|
||||||
}
|
}
|
||||||
|
if custID.Valid {
|
||||||
|
cid := int(custID.Int64)
|
||||||
|
a.CustomerID = &cid
|
||||||
|
}
|
||||||
|
|
||||||
// Systemdaten laden
|
// Systemdaten laden
|
||||||
var dataJSON sql.NullString
|
var dataJSON sql.NullString
|
||||||
@ -729,6 +760,48 @@ func (d *Database) GetAllAgentEvents(eventType string, limit int) ([]models.Agen
|
|||||||
return events, rows.Err()
|
return events, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Agent-Customer Zuordnung ---
|
||||||
|
|
||||||
|
func (d *Database) AssignAgentCustomer(agentID string, customerID int) error {
|
||||||
|
_, err := d.db.Exec("UPDATE agents SET customer_id = $1 WHERE id = $2", customerID, agentID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UnassignAgentCustomer(agentID string) error {
|
||||||
|
_, err := d.db.Exec("UPDATE agents SET customer_id = NULL WHERE id = $1", agentID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetAgentsByCustomer(customerID int) ([]models.Agent, error) {
|
||||||
|
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat, customer_id FROM agents WHERE customer_id = $1 ORDER BY name", customerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var agents []models.Agent
|
||||||
|
for rows.Next() {
|
||||||
|
var a models.Agent
|
||||||
|
var lastHB sql.NullTime
|
||||||
|
var custID sql.NullInt64
|
||||||
|
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &a.RegisteredAt, &lastHB, &custID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lastHB.Valid {
|
||||||
|
a.LastHeartbeat = &lastHB.Time
|
||||||
|
}
|
||||||
|
if custID.Valid {
|
||||||
|
cid := int(custID.Int64)
|
||||||
|
a.CustomerID = &cid
|
||||||
|
}
|
||||||
|
agents = append(agents, a)
|
||||||
|
}
|
||||||
|
if agents == nil {
|
||||||
|
agents = []models.Agent{}
|
||||||
|
}
|
||||||
|
return agents, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// GetAgentStatus berechnet den Status eines Agents aus last_heartbeat
|
// GetAgentStatus berechnet den Status eines Agents aus last_heartbeat
|
||||||
func (d *Database) GetAgentStatus(agentID string) string {
|
func (d *Database) GetAgentStatus(agentID string) string {
|
||||||
var lastHB sql.NullTime
|
var lastHB sql.NullTime
|
||||||
|
|||||||
89
backend/db/users.go
Normal file
89
backend/db/users.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/cynfo/rmm-backend/models"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Database) CreateUser(username, password, displayName, role string) (*models.User, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var u models.User
|
||||||
|
err = d.db.QueryRow(
|
||||||
|
"INSERT INTO users (username, password_hash, display_name, role) VALUES ($1, $2, $3, $4) RETURNING id, username, display_name, role, created_at",
|
||||||
|
username, string(hash), displayName, role,
|
||||||
|
).Scan(&u.ID, &u.Username, &u.DisplayName, &u.Role, &u.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetUserByUsername(username string) (*models.User, error) {
|
||||||
|
var u models.User
|
||||||
|
var lastLogin sql.NullTime
|
||||||
|
err := d.db.QueryRow(
|
||||||
|
"SELECT id, username, password_hash, display_name, role, created_at, last_login FROM users WHERE username = $1",
|
||||||
|
username,
|
||||||
|
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.DisplayName, &u.Role, &u.CreatedAt, &lastLogin)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lastLogin.Valid {
|
||||||
|
u.LastLogin = &lastLogin.Time
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetUsers() ([]models.User, error) {
|
||||||
|
rows, err := d.db.Query("SELECT id, username, display_name, role, created_at, last_login FROM users ORDER BY username")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var users []models.User
|
||||||
|
for rows.Next() {
|
||||||
|
var u models.User
|
||||||
|
var lastLogin sql.NullTime
|
||||||
|
if err := rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Role, &u.CreatedAt, &lastLogin); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lastLogin.Valid {
|
||||||
|
u.LastLogin = &lastLogin.Time
|
||||||
|
}
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
if users == nil {
|
||||||
|
users = []models.User{}
|
||||||
|
}
|
||||||
|
return users, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdateUserLastLogin(id int) error {
|
||||||
|
_, err := d.db.Exec("UPDATE users SET last_login = NOW() WHERE id = $1", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) DeleteUser(id int) (bool, error) {
|
||||||
|
res, err := d.db.Exec("DELETE FROM users WHERE id = $1", id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
return rows > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UserCount() (int, error) {
|
||||||
|
var count int
|
||||||
|
err := d.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
@ -9,11 +9,13 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@ -25,10 +27,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@ -57,15 +57,30 @@ func main() {
|
|||||||
mon := monitor.New(database)
|
mon := monitor.New(database)
|
||||||
go mon.Run()
|
go mon.Run()
|
||||||
|
|
||||||
// Router aufbauen
|
// Auth-Handler initialisieren
|
||||||
mux := http.NewServeMux()
|
authHandler := api.NewAuthHandler(database, cfg.JWTSecret)
|
||||||
handler := api.NewHandler(database)
|
authHandler.EnsureDefaultAdmin()
|
||||||
handler.SetupRoutes(mux, hub)
|
|
||||||
|
|
||||||
// Middleware-Chain: Logging -> Auth -> Handler
|
// Router aufbauen - Auth-Routes ohne Auth-Middleware
|
||||||
|
authMux := http.NewServeMux()
|
||||||
|
authHandler.SetupAuthRoutes(authMux)
|
||||||
|
|
||||||
|
// Geschuetzte Routes
|
||||||
|
protectedMux := http.NewServeMux()
|
||||||
|
handler := api.NewHandler(database)
|
||||||
|
handler.SetupRoutes(protectedMux, hub)
|
||||||
|
// GET /api/v1/auth/me braucht JWT
|
||||||
|
protectedMux.HandleFunc("GET /api/v1/auth/me", authHandler.Me)
|
||||||
|
|
||||||
|
// Combined router: auth routes ungeschuetzt, rest mit CombinedAuth
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("POST /api/v1/auth/login", authMux)
|
||||||
|
mux.Handle("/", api.CombinedAuth(cfg.APIKeys, authHandler, protectedMux))
|
||||||
|
|
||||||
|
// Middleware-Chain: CORS -> Logging -> Handler
|
||||||
var chain http.Handler = mux
|
var chain http.Handler = mux
|
||||||
chain = api.APIKeyAuth(cfg.APIKeys, chain)
|
|
||||||
chain = api.Logging(chain)
|
chain = api.Logging(chain)
|
||||||
|
chain = api.CORS(chain)
|
||||||
|
|
||||||
log.Printf("RMM Backend startet auf %s (TLS)", cfg.ListenAddr)
|
log.Printf("RMM Backend startet auf %s (TLS)", cfg.ListenAddr)
|
||||||
log.Printf("API-Keys konfiguriert: %d", len(cfg.APIKeys))
|
log.Printf("API-Keys konfiguriert: %d", len(cfg.APIKeys))
|
||||||
|
|||||||
@ -11,6 +11,7 @@ type Agent struct {
|
|||||||
OPNsenseVersion string `json:"opnsense_version"`
|
OPNsenseVersion string `json:"opnsense_version"`
|
||||||
RegisteredAt time.Time `json:"registered_at"`
|
RegisteredAt time.Time `json:"registered_at"`
|
||||||
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"`
|
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"`
|
||||||
|
CustomerID *int `json:"customer_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgentResponse erweitert Agent um den berechneten Status
|
// AgentResponse erweitert Agent um den berechneten Status
|
||||||
|
|||||||
10
backend/models/customer.go
Normal file
10
backend/models/customer.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Customer struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Number string `json:"number"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
24
backend/models/user.go
Normal file
24
backend/models/user.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt string `json:"expires_at"`
|
||||||
|
User User `json:"user"`
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user