From 5caa4d3e5d56a33af1a44c2f38ecd76cb4a76496 Mon Sep 17 00:00:00 2001 From: Nick Adam Date: Thu, 20 Nov 2025 18:15:54 +0100 Subject: [PATCH 1/7] added users section in stats --- backend/main.go | 18 +++++++++++++++++- frontend/src/pages/Home.jsx | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/main.go b/backend/main.go index 28c51ca..c089a11 100644 --- a/backend/main.go +++ b/backend/main.go @@ -200,6 +200,7 @@ type StatsResponse struct { FQDNs int `json:"fqdns"` CSRs int `json:"csrs"` Certificates int `json:"certificates"` + Users int `json:"users"` } type Space struct { @@ -655,7 +656,7 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) { return } - var spacesCount, fqdnsCount, csrsCount, certificatesCount int + var spacesCount, fqdnsCount, csrsCount, certificatesCount, usersCount int // Zähle Spaces err := db.QueryRow("SELECT COUNT(*) FROM spaces").Scan(&spacesCount) @@ -689,11 +690,20 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) { return } + // Zähle Benutzer + err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&usersCount) + if err != nil { + http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) + log.Printf("Fehler beim Zählen der Benutzer: %v", err) + return + } + response := StatsResponse{ Spaces: spacesCount, FQDNs: fqdnsCount, CSRs: csrsCount, Certificates: certificatesCount, + Users: usersCount, } json.NewEncoder(w).Encode(response) @@ -2211,6 +2221,12 @@ components: csrs: type: integer example: 7 + certificates: + type: integer + example: 8 + users: + type: integer + example: 3 Space: type: object properties: diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index dd79380..2cef111 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -267,6 +267,20 @@ const Home = () => { + +
+
+
+

Benutzer

+

{stats.users || 0}

+
+
+ + + +
+
+
) : (

Fehler beim Laden der Statistiken

-- 2.49.1 From 9c2d649adf7c92a21f894a49a37844e5bfbd1f8e Mon Sep 17 00:00:00 2001 From: Nick Adam Date: Fri, 21 Nov 2025 00:28:53 +0100 Subject: [PATCH 2/7] added main permission group feature --- backend/main.go | 733 +++++++++++++++++++++++++--- backend/spaces.db-shm | Bin 32768 -> 32768 bytes backend/spaces.db-wal | Bin 2303112 -> 2698632 bytes frontend/src/App.jsx | 2 + frontend/src/components/Sidebar.jsx | 1 + frontend/src/pages/Permissions.jsx | 550 +++++++++++++++++++++ frontend/src/pages/Users.jsx | 105 +++- 7 files changed, 1326 insertions(+), 65 deletions(-) create mode 100644 frontend/src/pages/Permissions.jsx diff --git a/backend/main.go b/backend/main.go index c089a11..a86dee6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,9 +5,9 @@ import ( "crypto/x509" "database/sql" "encoding/asn1" + "encoding/base64" "encoding/hex" "encoding/json" - "encoding/base64" "encoding/pem" "fmt" "io" @@ -258,25 +258,63 @@ type CSR struct { // User struct für Benutzer type User struct { - ID string `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - CreatedAt string `json:"createdAt"` + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + CreatedAt string `json:"createdAt"` + GroupIDs []string `json:"groupIds,omitempty"` } -// CreateUserRequest struct für Benutzer-Erstellung -type CreateUserRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` +// PermissionLevel definiert die Berechtigungsstufen +type PermissionLevel string + +const ( + PermissionRead PermissionLevel = "READ" + PermissionReadWrite PermissionLevel = "READ_WRITE" + PermissionFullAccess PermissionLevel = "FULL_ACCESS" +) + +// PermissionGroup struct für Berechtigungsgruppen +type PermissionGroup struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Permission PermissionLevel `json:"permission"` + SpaceIDs []string `json:"spaceIds"` + CreatedAt string `json:"createdAt"` +} + +// CreatePermissionGroupRequest struct für Gruppen-Erstellung +type CreatePermissionGroupRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Permission PermissionLevel `json:"permission"` + SpaceIDs []string `json:"spaceIds"` +} + +// UpdatePermissionGroupRequest struct für Gruppen-Update +type UpdatePermissionGroupRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Permission PermissionLevel `json:"permission"` + SpaceIDs []string `json:"spaceIds"` } // UpdateUserRequest struct für Benutzer-Update type UpdateUserRequest struct { - Username string `json:"username,omitempty"` - Email string `json:"email,omitempty"` - OldPassword string `json:"oldPassword,omitempty"` - Password string `json:"password,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` + OldPassword string `json:"oldPassword,omitempty"` + GroupIDs []string `json:"groupIds,omitempty"` +} + +// CreateUserRequest struct für Benutzer-Erstellung +type CreateUserRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + GroupIDs []string `json:"groupIds,omitempty"` } // MessageResponse struct für einfache Nachrichten @@ -318,7 +356,7 @@ func initDB() { log.Println("Teste Datenbank-Verbindung...") maxRetries := 5 for i := 0; i < maxRetries; i++ { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) err := db.PingContext(ctx) cancel() if err != nil { @@ -570,10 +608,75 @@ func initDB() { } else { log.Printf("Avatar-Ordner erstellt: %s", avatarDir) } + + // Erstelle Permission Groups-Tabelle + log.Println("Erstelle permission_groups-Tabelle...") + createPermissionGroupsTableSQL := ` + CREATE TABLE IF NOT EXISTS permission_groups ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + permission TEXT NOT NULL, + created_at DATETIME NOT NULL + );` + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, createPermissionGroupsTableSQL) + cancel() + if err != nil { + if strings.Contains(err.Error(), "database is locked") { + log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.") + } + log.Fatal("Fehler beim Erstellen der permission_groups-Tabelle:", err) + } + + // Erstelle group_spaces-Tabelle für Space-Zuweisungen + log.Println("Erstelle group_spaces-Tabelle...") + createGroupSpacesTableSQL := ` + CREATE TABLE IF NOT EXISTS group_spaces ( + group_id TEXT NOT NULL, + space_id TEXT NOT NULL, + PRIMARY KEY (group_id, space_id), + FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE + );` + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, createGroupSpacesTableSQL) + cancel() + if err != nil { + if strings.Contains(err.Error(), "database is locked") { + log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.") + } + log.Fatal("Fehler beim Erstellen der group_spaces-Tabelle:", err) + } + + // Erstelle user_groups-Tabelle für Benutzer-Gruppen-Zuweisungen + log.Println("Erstelle user_groups-Tabelle...") + createUserGroupsTableSQL := ` + CREATE TABLE IF NOT EXISTS user_groups ( + user_id TEXT NOT NULL, + group_id TEXT NOT NULL, + PRIMARY KEY (user_id, group_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE + );` + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, createUserGroupsTableSQL) + cancel() + if err != nil { + if strings.Contains(err.Error(), "database is locked") { + log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.") + } + log.Fatal("Fehler beim Erstellen der user_groups-Tabelle:", err) + } + + log.Println("Berechtigungssystem-Tabellen erfolgreich erstellt") } func createDefaultAdmin() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() // Prüfe ob bereits ein Admin-User existiert @@ -699,11 +802,11 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) { } response := StatsResponse{ - Spaces: spacesCount, - FQDNs: fqdnsCount, - CSRs: csrsCount, + Spaces: spacesCount, + FQDNs: fqdnsCount, + CSRs: csrsCount, Certificates: certificatesCount, - Users: usersCount, + Users: usersCount, } json.NewEncoder(w).Encode(response) @@ -2410,9 +2513,10 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { return } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + // Lade alle Benutzer rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC") if err != nil { http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError) @@ -2422,6 +2526,7 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { defer rows.Close() var users []User + var userIDs []string for rows.Next() { var user User err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) @@ -2430,7 +2535,42 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err) return } + user.GroupIDs = []string{} // Initialisiere als leeres Array users = append(users, user) + userIDs = append(userIDs, user.ID) + } + rows.Close() + + // Lade alle Gruppen-Zuweisungen in einem einzigen Query + if len(userIDs) > 0 { + // Erstelle Platzhalter für IN-Clause + placeholders := make([]string, len(userIDs)) + args := make([]interface{}, len(userIDs)) + for i, id := range userIDs { + placeholders[i] = "?" + args[i] = id + } + + query := fmt.Sprintf("SELECT user_id, group_id FROM user_groups WHERE user_id IN (%s)", strings.Join(placeholders, ",")) + groupRows, err := db.QueryContext(ctx, query, args...) + if err == nil { + // Erstelle eine Map von user_id zu group_ids + groupMap := make(map[string][]string) + for groupRows.Next() { + var userID, groupID string + if err := groupRows.Scan(&userID, &groupID); err == nil { + groupMap[userID] = append(groupMap[userID], groupID) + } + } + groupRows.Close() + + // Weise Gruppen-IDs den Benutzern zu + for i := range users { + if groupIDs, exists := groupMap[users[i].ID]; exists { + users[i].GroupIDs = groupIDs + } + } + } } json.NewEncoder(w).Encode(users) @@ -2450,7 +2590,7 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userID := vars["id"] - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var user User @@ -2466,6 +2606,22 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Lade Gruppen-IDs für diesen Benutzer + groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) + if err == nil { + var groupIDs []string + for groupRows.Next() { + var groupID string + if err := groupRows.Scan(&groupID); err == nil { + groupIDs = append(groupIDs, groupID) + } + } + groupRows.Close() + user.GroupIDs = groupIDs + } else { + user.GroupIDs = []string{} // Initialisiere als leeres Array bei Fehler + } + json.NewEncoder(w).Encode(user) } @@ -2511,7 +2667,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { userID := uuid.New().String() createdAt := time.Now().Format(time.RFC3339) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, err = db.ExecContext(ctx, @@ -2531,11 +2687,27 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Weise Gruppen zu, falls angegeben + if len(req.GroupIDs) > 0 { + for _, groupID := range req.GroupIDs { + // Prüfe ob Gruppe existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists) + if err == nil && exists { + _, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID) + if err != nil { + log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err) + } + } + } + } + user := User{ ID: userID, Username: req.Username, Email: req.Email, CreatedAt: createdAt, + GroupIDs: req.GroupIDs, } w.WriteHeader(http.StatusCreated) @@ -2547,6 +2719,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{ "username": req.Username, "email": req.Email, + "groupIds": req.GroupIDs, "message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email), }, ipAddress, userAgent) } @@ -2571,7 +2744,7 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { return } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() // Prüfe ob Benutzer existiert @@ -2659,6 +2832,28 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Aktualisiere Gruppen-Zuweisungen, falls angegeben + if req.GroupIDs != nil { + // Lösche alle bestehenden Gruppen-Zuweisungen + _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID) + if err != nil { + log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err) + } + + // Füge neue Gruppen-Zuweisungen hinzu + for _, groupID := range req.GroupIDs { + // Prüfe ob Gruppe existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists) + if err == nil && exists { + _, err = db.ExecContext(ctx, "INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID) + if err != nil { + log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err) + } + } + } + } + // Lade aktualisierten Benutzer var user User err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). @@ -2669,10 +2864,28 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Lade Gruppen-IDs + if req.GroupIDs != nil { + user.GroupIDs = req.GroupIDs + } else { + groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) + if err == nil { + var groupIDs []string + for groupRows.Next() { + var groupID string + if err := groupRows.Scan(&groupID); err == nil { + groupIDs = append(groupIDs, groupID) + } + } + groupRows.Close() + user.GroupIDs = groupIDs + } + } + json.NewEncoder(w).Encode(user) // Audit-Log: User aktualisiert - userID, username := getUserFromRequest(r) + requestUserID, requestUsername := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) details := map[string]interface{}{} if req.Username != "" { @@ -2684,7 +2897,10 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { if req.Password != "" { details["passwordChanged"] = true } - auditService.Track(r.Context(), "UPDATE", "user", vars["id"], userID, username, details, ipAddress, userAgent) + if req.GroupIDs != nil { + details["groupIds"] = req.GroupIDs + } + auditService.Track(r.Context(), "UPDATE", "user", vars["id"], requestUserID, requestUsername, details, ipAddress, userAgent) } func deleteUserHandler(w http.ResponseWriter, r *http.Request) { @@ -2701,7 +2917,7 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) userID := vars["id"] - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID) @@ -2921,10 +3137,400 @@ func getAvatarHandler(w http.ResponseWriter, r *http.Request) { io.Copy(w, file) } +// Permission Groups Handler Functions + +func getPermissionGroupsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // Lade alle Gruppen + rows, err := db.QueryContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups ORDER BY created_at DESC") + if err != nil { + http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppen", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungsgruppen: %v", err) + return + } + defer rows.Close() + + var groups []PermissionGroup + var groupIDs []string + for rows.Next() { + var group PermissionGroup + err := rows.Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt) + if err != nil { + http.Error(w, "Fehler beim Lesen der Gruppen-Daten", http.StatusInternalServerError) + log.Printf("Fehler beim Lesen der Gruppen-Daten: %v", err) + return + } + group.SpaceIDs = []string{} // Initialisiere als leeres Array + groups = append(groups, group) + groupIDs = append(groupIDs, group.ID) + } + rows.Close() + + // Lade alle Space-Zuweisungen in einem einzigen Query + if len(groupIDs) > 0 { + // Erstelle Platzhalter für IN-Clause + placeholders := make([]string, len(groupIDs)) + args := make([]interface{}, len(groupIDs)) + for i, id := range groupIDs { + placeholders[i] = "?" + args[i] = id + } + + query := fmt.Sprintf("SELECT group_id, space_id FROM group_spaces WHERE group_id IN (%s)", strings.Join(placeholders, ",")) + spaceRows, err := db.QueryContext(ctx, query, args...) + if err == nil { + // Erstelle eine Map von group_id zu space_ids + spaceMap := make(map[string][]string) + for spaceRows.Next() { + var groupID, spaceID string + if err := spaceRows.Scan(&groupID, &spaceID); err == nil { + spaceMap[groupID] = append(spaceMap[groupID], spaceID) + } + } + spaceRows.Close() + + // Weise Space-IDs den Gruppen zu + for i := range groups { + if spaceIDs, exists := spaceMap[groups[i].ID]; exists { + groups[i].SpaceIDs = spaceIDs + } + } + } + } + + json.NewEncoder(w).Encode(groups) +} + +func getPermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + var group PermissionGroup + err := db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID). + Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound) + return + } + http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungsgruppe: %v", err) + return + } + + // Lade Space-IDs für diese Gruppe + spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID) + if err == nil { + var spaceIDs []string + for spaceRows.Next() { + var spaceID string + if err := spaceRows.Scan(&spaceID); err == nil { + spaceIDs = append(spaceIDs, spaceID) + } + } + spaceRows.Close() + group.SpaceIDs = spaceIDs + } + + json.NewEncoder(w).Encode(group) +} + +func createPermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + var req CreatePermissionGroupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) + return + } + + // Validierung + if req.Name == "" { + http.Error(w, "Name ist erforderlich", http.StatusBadRequest) + return + } + + // Validiere Berechtigungsstufe + if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess { + http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest) + return + } + + // Erstelle Gruppe + groupID := uuid.New().String() + createdAt := time.Now().Format(time.RFC3339) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + _, err := db.ExecContext(ctx, + "INSERT INTO permission_groups (id, name, description, permission, created_at) VALUES (?, ?, ?, ?, ?)", + groupID, req.Name, req.Description, string(req.Permission), createdAt) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict) + return + } + http.Error(w, "Fehler beim Erstellen der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Erstellen der Berechtigungsgruppe: %v", err) + return + } + + // Weise Spaces zu, falls angegeben + if len(req.SpaceIDs) > 0 { + for _, spaceID := range req.SpaceIDs { + // Prüfe ob Space existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) + if err == nil && exists { + _, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID) + if err != nil { + log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err) + } + } + } + } + + group := PermissionGroup{ + ID: groupID, + Name: req.Name, + Description: req.Description, + Permission: req.Permission, + SpaceIDs: req.SpaceIDs, + CreatedAt: createdAt, + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(group) + + // Audit-Log + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "CREATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{ + "name": req.Name, + "permission": req.Permission, + "spaceIds": req.SpaceIDs, + "message": fmt.Sprintf("Berechtigungsgruppe erstellt: %s", req.Name), + }, ipAddress, userAgent) +} + +func updatePermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + + var req UpdatePermissionGroupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // Prüfe ob Gruppe existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists) + if err != nil || !exists { + http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound) + return + } + + // Validiere Berechtigungsstufe, falls angegeben + if req.Permission != "" { + if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess { + http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest) + return + } + } + + // Update Felder + updates := []string{} + args := []interface{}{} + + if req.Name != "" { + updates = append(updates, "name = ?") + args = append(args, req.Name) + } + if req.Description != "" { + updates = append(updates, "description = ?") + args = append(args, req.Description) + } + if req.Permission != "" { + updates = append(updates, "permission = ?") + args = append(args, string(req.Permission)) + } + + if len(updates) > 0 { + args = append(args, groupID) + query := fmt.Sprintf("UPDATE permission_groups SET %s WHERE id = ?", strings.Join(updates, ", ")) + _, err = db.ExecContext(ctx, query, args...) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict) + return + } + http.Error(w, "Fehler beim Aktualisieren der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Aktualisieren der Berechtigungsgruppe: %v", err) + return + } + } + + // Aktualisiere Space-Zuweisungen, falls angegeben + if req.SpaceIDs != nil { + // Lösche alle bestehenden Space-Zuweisungen + _, err = db.ExecContext(ctx, "DELETE FROM group_spaces WHERE group_id = ?", groupID) + if err != nil { + log.Printf("Fehler beim Löschen der Space-Zuweisungen: %v", err) + } + + // Füge neue Space-Zuweisungen hinzu + for _, spaceID := range req.SpaceIDs { + // Prüfe ob Space existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) + if err == nil && exists { + _, err = db.ExecContext(ctx, "INSERT INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID) + if err != nil { + log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err) + } + } + } + } + + // Lade aktualisierte Gruppe + var group PermissionGroup + err = db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID). + Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt) + if err != nil { + http.Error(w, "Fehler beim Abrufen der aktualisierten Gruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der aktualisierten Gruppe: %v", err) + return + } + + // Lade Space-IDs + if req.SpaceIDs != nil { + group.SpaceIDs = req.SpaceIDs + } else { + spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID) + if err == nil { + var spaceIDs []string + for spaceRows.Next() { + var spaceID string + if err := spaceRows.Scan(&spaceID); err == nil { + spaceIDs = append(spaceIDs, spaceID) + } + } + spaceRows.Close() + group.SpaceIDs = spaceIDs + } + } + + json.NewEncoder(w).Encode(group) + + // Audit-Log + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "UPDATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{ + "name": req.Name, + "permission": req.Permission, + "spaceIds": req.SpaceIDs, + "message": fmt.Sprintf("Berechtigungsgruppe aktualisiert: %s", groupID), + }, ipAddress, userAgent) +} + +func deletePermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + result, err := db.ExecContext(ctx, "DELETE FROM permission_groups WHERE id = ?", groupID) + if err != nil { + http.Error(w, "Fehler beim Löschen der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Löschen der Berechtigungsgruppe: %v", err) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) + return + } + + if rowsAffected == 0 { + http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound) + return + } + + response := MessageResponse{Message: "Berechtigungsgruppe erfolgreich gelöscht"} + json.NewEncoder(w).Encode(response) + + // Audit-Log + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "DELETE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{ + "message": fmt.Sprintf("Berechtigungsgruppe gelöscht: %s", groupID), + }, ipAddress, userAgent) +} + // Passwortvalidierung nach Richtlinien func validatePassword(password string) error { if len(password) < 8 { - return fmt.Errorf("Passwort muss mindestens 8 Zeichen lang sein") + return fmt.Errorf("passwort muss mindestens 8 Zeichen lang sein") } hasUpper := false @@ -2963,7 +3569,7 @@ func validatePassword(password string) error { } if len(missing) > 0 { - return fmt.Errorf("Passwort muss enthalten: %s", strings.Join(missing, ", ")) + return fmt.Errorf("passwort muss enthalten: %s", strings.Join(missing, ", ")) } return nil @@ -2990,12 +3596,16 @@ func getUserFromRequest(r *http.Request) (userID, username string) { username = parts[0] // Hole User-ID aus der Datenbank - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var id string err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id) if err != nil { + // Logge Fehler nur wenn es nicht "no rows" ist + if err != sql.ErrNoRows { + log.Printf("Fehler beim Abrufen der User-ID für %s: %v", username, err) + } return "", username } @@ -3014,13 +3624,13 @@ func getRequestInfo(r *http.Request) (ipAddress, userAgent string) { // Hole User-Agent userAgent = r.Header.Get("User-Agent") - + // Prüfe, ob es sich um einen API-Aufruf handelt // API-Aufrufe haben entweder keinen User-Agent oder einen speziellen Header if userAgent == "" || r.Header.Get("X-API-Request") == "true" || r.Header.Get("X-Request-Source") == "api" { userAgent = "API" } - + return ipAddress, userAgent } @@ -3099,7 +3709,7 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) { // Hole Logs - Verwende Prepared Statement für bessere Kompatibilität var querySQL string var queryArgs []interface{} - + if whereSQL != "" { querySQL = fmt.Sprintf(` SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent @@ -3123,7 +3733,7 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) { queryCtx, queryCancel := context.WithTimeout(context.Background(), time.Second*10) defer queryCancel() // Cancel wird erst aufgerufen, wenn die Funktion beendet ist - + rows, err := db.QueryContext(queryCtx, querySQL, queryArgs...) if err != nil { http.Error(w, "Fehler beim Abrufen der Logs", http.StatusInternalServerError) @@ -3201,11 +3811,11 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Zeilen gelesen: %d, Scan-Fehler: %d, Logs hinzugefügt: %d", rowCount, scanErrors, len(logs)) response := map[string]interface{}{ - "logs": logs, - "total": totalCount, - "limit": limit, - "offset": offset, - "hasMore": offset+limit < totalCount, + "logs": logs, + "total": totalCount, + "limit": limit, + "offset": offset, + "hasMore": offset+limit < totalCount, } log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset) @@ -3275,14 +3885,14 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) { } var req struct { - Action string `json:"action"` - Entity string `json:"entity"` - EntityID string `json:"entityID"` - UserID string `json:"userID"` - Username string `json:"username"` - Details map[string]interface{} `json:"details"` - IPAddress string `json:"ipAddress"` - UserAgent string `json:"userAgent"` + Action string `json:"action"` + Entity string `json:"entity"` + EntityID string `json:"entityID"` + UserID string `json:"userID"` + Username string `json:"username"` + Details map[string]interface{} `json:"details"` + IPAddress string `json:"ipAddress"` + UserAgent string `json:"userAgent"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -3303,7 +3913,7 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) { // Erstelle Context für die Anfrage ctx := r.Context() - + // Track das Event auditService.Track(ctx, req.Action, req.Entity, req.EntityID, req.UserID, req.Username, req.Details, req.IPAddress, req.UserAgent) @@ -3383,7 +3993,7 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { password := parts[1] // Validiere Benutzer - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var storedHash string @@ -3474,7 +4084,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Login-Versuch für Benutzer: %s", username) // Validiere Benutzer - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var user User @@ -3518,7 +4128,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { func main() { log.Println("Starte certigo-addon Backend...") - + // Initialisiere Datenbank log.Println("Initialisiere Datenbank...") initDB() @@ -3542,11 +4152,11 @@ func main() { // API Routes api := r.PathPrefix("/api").Subrouter() - + // Public Routes (keine Auth erforderlich) api.HandleFunc("/health", healthHandler).Methods("GET", "OPTIONS") api.HandleFunc("/login", loginHandler).Methods("POST", "OPTIONS") - + // Protected Routes (Basic Auth erforderlich) api.HandleFunc("/stats", basicAuthMiddleware(getStatsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", getSpacesHandler).Methods("GET", "OPTIONS") @@ -3571,6 +4181,13 @@ func main() { api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS") + // Permission Groups Routes (Protected) + api.HandleFunc("/permission-groups", basicAuthMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/permission-groups", basicAuthMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS") + // Provider Routes (Protected) api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/providers/{id}", basicAuthMiddleware(getProviderHandler)).Methods("GET", "OPTIONS") @@ -3965,12 +4582,12 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) { userID, username := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "SIGN", "csr", csrID, userID, username, map[string]interface{}{ - "providerId": req.ProviderID, - "fqdnId": fqdnID, - "spaceId": spaceID, + "providerId": req.ProviderID, + "fqdnId": fqdnID, + "spaceId": spaceID, "certificateId": result.OrderID, - "status": result.Status, - "message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID), + "status": result.Status, + "message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID), }, ipAddress, userAgent) } diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm index d9f70fb15dc71367a97437be6bb50bc5b4775df3..aa0ec1d58d92697db5e8f1eb5450d232a70a67d8 100644 GIT binary patch delta 745 zcmb7;IZPB$7{_OR%LJEZ7B`?KCV;F*W8#H)%i^y0v4XdNt0=OFGNK@g?hbgL6tA+e zv9O>rQ<@lK)XK`N#uzmlVvHda#@eXwP2M1e$}joyegErw&vkKKa??lV8>$}2HVq>( zgkj8ldsC{m277pt(@c>ml@Kbv^UnAH;Y%3WC!~-*)B%YvCsmJ zYW%}m!+KI0b%<}R<)MWfQ}|&jY)0$Yz)lX(#3>xEaE%+<+vI(1%bU?S8)@Jm&2({^ zt6b;ixZz7CZ(8fPz{TLcH_|Do1pQnJC{gHYu@=!rpJKdJxL}IU-3``aj&oj9W2X4n zQ)kv_2;xJZV=rYBjkM5CH(73J@r5bg_d9kio7u%7GMpjD?Z4u?tYa@@3%g0v!C74H HNOAZNQtrK& delta 319 zcmZo@U}|V!s+V}A%K!pRK+MR%AfV60z#s=?*L|sP6_k1$0A#W@38{Thk@@|M`-(dw zsp^6H!2o3LeWAhO%0rSllxEdHIUvVs$d?{wvB}f<~Old7{RQ`uM$*%iq3&}JdBfHB#Ui6#Pxz{^RqOlg3V7dVXQ|vFij8g w<2Ey;>|&gJf$Q1idqqJo&Zo(DN*%z)0cAc+zEx2GleseaMl}-WS{>L$0J&OxDF6Tf diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal index 647eeae22b113b3533f9721b637fab1457ff2bde..dc681d3f111e5d08d5bb88ca10cbc6448411808e 100644 GIT binary patch delta 20920 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 B!w3KX delta 81 zcmV~$TNZ&(006*q3FWO)$s^H1eay%GunbFR6V_leX5#M?;`be1f{Cfb%-ljMv$V3d cv9+^zkSmmqDz(N*>+Is{rgPVOjL$gy0Yo<$#Q*>R diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5764df8..8f216b4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,7 @@ import SpaceDetail from './pages/SpaceDetail' import Impressum from './pages/Impressum' import Profile from './pages/Profile' import Users from './pages/Users' +import Permissions from './pages/Permissions' import Login from './pages/Login' import AuditLogs from './pages/AuditLogs' @@ -71,6 +72,7 @@ const AppContent = () => { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index dfc92f2..2087766 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -22,6 +22,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => { path: '/settings', subItems: [ { path: '/settings/users', label: 'User', icon: '👥' }, + { path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' }, ] } diff --git a/frontend/src/pages/Permissions.jsx b/frontend/src/pages/Permissions.jsx new file mode 100644 index 0000000..9c556b2 --- /dev/null +++ b/frontend/src/pages/Permissions.jsx @@ -0,0 +1,550 @@ +import { useState, useEffect } from 'react' +import { useAuth } from '../contexts/AuthContext' + +const Permissions = () => { + const { authFetch } = useAuth() + const [groups, setGroups] = useState([]) + const [spaces, setSpaces] = useState([]) + const [loading, setLoading] = useState(false) + const [fetching, setFetching] = useState(true) + const [error, setError] = useState('') + const [showForm, setShowForm] = useState(false) + const [editingGroup, setEditingGroup] = useState(null) + const [formData, setFormData] = useState({ + name: '', + description: '', + permission: 'READ', + spaceIds: [] + }) + + useEffect(() => { + // Lade Daten parallel beim Mount und beim Zurückkehren zur Seite + let isMounted = true + + const loadData = async () => { + if (isMounted) { + setFetching(true) + } + await Promise.all([fetchGroups(), fetchSpaces()]) + } + + loadData() + + return () => { + isMounted = false + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const fetchGroups = async () => { + try { + setError('') + const response = await authFetch('/api/permission-groups') + if (response.ok) { + const data = await response.json() + setGroups(Array.isArray(data) ? data : []) + } else { + setError('Fehler beim Abrufen der Berechtigungsgruppen') + } + } catch (err) { + setError('Fehler beim Abrufen der Berechtigungsgruppen') + console.error('Error fetching permission groups:', err) + } finally { + setFetching(false) + } + } + + const fetchSpaces = async () => { + try { + const response = await authFetch('/api/spaces') + if (response.ok) { + const data = await response.json() + setSpaces(Array.isArray(data) ? data : []) + } + } catch (err) { + console.error('Error fetching spaces:', err) + } + } + + const handleSubmit = async (e) => { + e.preventDefault() + setError('') + setLoading(true) + + if (!formData.name.trim()) { + setError('Bitte geben Sie einen Namen ein') + setLoading(false) + return + } + + if (!formData.permission) { + setError('Bitte wählen Sie eine Berechtigungsstufe') + setLoading(false) + return + } + + try { + const url = editingGroup + ? `/api/permission-groups/${editingGroup.id}` + : '/api/permission-groups' + const method = editingGroup ? 'PUT' : 'POST' + + const body = { + name: formData.name, + description: formData.description, + permission: formData.permission, + spaceIds: formData.spaceIds + } + + const response = await authFetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (response.ok) { + await fetchGroups() + setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] }) + setShowForm(false) + setEditingGroup(null) + } else { + const errorData = await response.json() + setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe') + } + } catch (err) { + setError('Fehler beim Speichern der Berechtigungsgruppe') + console.error('Error saving permission group:', err) + } finally { + setLoading(false) + } + } + + const handleEdit = (group) => { + setEditingGroup(group) + setFormData({ + name: group.name, + description: group.description || '', + permission: group.permission, + spaceIds: group.spaceIds || [] + }) + setShowForm(true) + } + + const handleDelete = async (groupId) => { + if (!window.confirm('Möchten Sie diese Berechtigungsgruppe wirklich löschen?')) { + return + } + + try { + const response = await authFetch(`/api/permission-groups/${groupId}`, { + method: 'DELETE', + }) + + if (response.ok) { + await fetchGroups() + } else { + const errorData = await response.json() + alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe') + } + } catch (err) { + alert('Fehler beim Löschen der Berechtigungsgruppe') + console.error('Error deleting permission group:', err) + } + } + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + const handleSpaceToggle = (spaceId) => { + setFormData(prev => { + const spaceIds = prev.spaceIds || [] + if (spaceIds.includes(spaceId)) { + return { ...prev, spaceIds: spaceIds.filter(id => id !== spaceId) } + } else { + return { ...prev, spaceIds: [...spaceIds, spaceId] } + } + }) + } + + const getPermissionLabel = (permission) => { + switch (permission) { + case 'READ': + return 'Lesen' + case 'READ_WRITE': + return 'Lesen/Schreiben' + case 'FULL_ACCESS': + return 'Vollzugriff' + default: + return permission + } + } + + const getPermissionBadgeColor = (permission) => { + switch (permission) { + case 'READ': + return 'bg-green-600/20 text-green-300 border-green-500/30' + case 'READ_WRITE': + return 'bg-yellow-600/20 text-yellow-300 border-yellow-500/30' + case 'FULL_ACCESS': + return 'bg-purple-600/20 text-purple-300 border-purple-500/30' + default: + return 'bg-blue-600/20 text-blue-300 border-blue-500/30' + } + } + + const getPermissionIcon = (permission) => { + switch (permission) { + case 'READ': + return '👁️' + case 'READ_WRITE': + return '✏️' + case 'FULL_ACCESS': + return '🔓' + default: + return '🔐' + } + } + + const getPermissionDescription = (permission) => { + switch (permission) { + case 'READ': + return 'Nur CSRs und Zertifikate ansehen. Keine Requests, keine Lösch-/Erstellrechte.' + case 'READ_WRITE': + return 'FQDNs innerhalb eines Spaces erstellen (nicht löschen), CSRs requesten und ansehen. Keine Spaces löschen/erstellen.' + case 'FULL_ACCESS': + return 'Vollzugriff: Alles darf gemacht werden. Löschen, Erstellen, CSR requesten und ansehen.' + default: + return '' + } + } + + return ( +
+
+
+
+

+ 🔐 + Berechtigungsgruppen +

+

Verwalten Sie Berechtigungsgruppen und weisen Sie Spaces zu

+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {showForm && ( +
+
+

+ {editingGroup ? '✏️' : '➕'} + {editingGroup ? 'Berechtigungsgruppe bearbeiten' : 'Neue Berechtigungsgruppe'} +

+ +
+
+
+
+ + +
+ +
+ +