diff --git a/backend/main.go b/backend/main.go index 41b4c48..ab497b4 100644 --- a/backend/main.go +++ b/backend/main.go @@ -261,6 +261,8 @@ type User struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` + IsAdmin bool `json:"isAdmin"` + Enabled bool `json:"enabled"` CreatedAt string `json:"createdAt"` GroupIDs []string `json:"groupIds,omitempty"` } @@ -306,6 +308,8 @@ type UpdateUserRequest struct { Email string `json:"email,omitempty"` Password string `json:"password,omitempty"` OldPassword string `json:"oldPassword,omitempty"` + IsAdmin *bool `json:"isAdmin,omitempty"` + Enabled *bool `json:"enabled,omitempty"` GroupIDs []string `json:"groupIds,omitempty"` } @@ -314,6 +318,7 @@ type CreateUserRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` + IsAdmin bool `json:"isAdmin,omitempty"` GroupIDs []string `json:"groupIds,omitempty"` } @@ -535,6 +540,8 @@ func initDB() { username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, + is_admin INTEGER DEFAULT 0, + enabled INTEGER DEFAULT 1, created_at DATETIME NOT NULL );` @@ -550,6 +557,22 @@ func initDB() { log.Println("Datenbank erfolgreich initialisiert") + // Füge is_admin Spalte hinzu falls nicht vorhanden + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0") + cancel() + if err != nil && !strings.Contains(err.Error(), "duplicate column") { + log.Printf("Hinweis: is_admin-Spalte könnte bereits existieren: %v", err) + } + + // Füge enabled Spalte hinzu falls nicht vorhanden + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN enabled INTEGER DEFAULT 1") + cancel() + if err != nil && !strings.Contains(err.Error(), "duplicate column") { + log.Printf("Hinweis: enabled-Spalte könnte bereits existieren: %v", err) + } + // Erstelle Audit-Log-Tabelle log.Println("Erstelle audit_logs-Tabelle...") createAuditLogsTableSQL := ` @@ -679,19 +702,26 @@ func createDefaultAdmin() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - // Prüfe ob bereits ein Admin-User existiert + // Prüfe ob bereits ein Admin-User mit UID "admin" existiert var count int - err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count) + err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = 'admin'").Scan(&count) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Users: %v", err) return } if count > 0 { - log.Println("Admin-User existiert bereits") + log.Println("Admin-User mit UID 'admin' existiert bereits") + // Stelle sicher, dass der Admin-User als Admin markiert ist + _, err = db.ExecContext(ctx, "UPDATE users SET is_admin = 1 WHERE id = 'admin'") + if err != nil { + log.Printf("Warnung: Konnte Admin-Status nicht setzen: %v", err) + } else { + log.Println("Admin-User ist als Administrator markiert") + } // Prüfe ob das Passwort noch "admin" ist (für Debugging) var storedHash string - err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = 'admin'").Scan(&storedHash) + err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = 'admin'").Scan(&storedHash) if err == nil { // Teste ob das Passwort "admin" ist testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin")) @@ -704,7 +734,45 @@ func createDefaultAdmin() { return } - // Erstelle Default Admin-User + // Migration: Falls ein Admin-User mit username='admin' aber anderer UID existiert, migriere ihn + var existingAdminID string + err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = 'admin' AND id != 'admin' LIMIT 1").Scan(&existingAdminID) + if err == nil { + log.Printf("Migriere bestehenden Admin-User von UID '%s' zu UID 'admin'", existingAdminID) + // Hole alle Daten des alten Admin-Users + var oldUsername, oldEmail, oldPasswordHash string + var oldIsAdmin, oldEnabled int + var oldCreatedAt string + err = db.QueryRowContext(ctx, "SELECT username, email, password_hash, is_admin, enabled, created_at FROM users WHERE id = ?", existingAdminID). + Scan(&oldUsername, &oldEmail, &oldPasswordHash, &oldIsAdmin, &oldEnabled, &oldCreatedAt) + if err == nil { + // Erstelle neuen Admin-User mit UID 'admin' + _, err = db.ExecContext(ctx, + "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + "admin", oldUsername, oldEmail, oldPasswordHash, oldIsAdmin, oldEnabled, oldCreatedAt) + if err == nil { + // Migriere user_groups Zuweisungen + _, err = db.ExecContext(ctx, "UPDATE user_groups SET user_id = 'admin' WHERE user_id = ?", existingAdminID) + if err != nil { + log.Printf("Warnung: Konnte user_groups nicht migrieren: %v", err) + } + // Lösche den alten User (CASCADE sollte user_groups automatisch löschen) + _, err = db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", existingAdminID) + if err == nil { + log.Printf("✓ Admin-User erfolgreich zu UID 'admin' migriert") + return + } else { + log.Printf("Warnung: Konnte alten Admin-User nicht löschen: %v", err) + } + } else { + log.Printf("Warnung: Konnte neuen Admin-User nicht erstellen: %v", err) + } + } else { + log.Printf("Warnung: Konnte Daten des alten Admin-Users nicht lesen: %v", err) + } + } + + // Erstelle Default Admin-User mit fester UID "admin" adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) if err != nil { @@ -712,18 +780,18 @@ func createDefaultAdmin() { return } - adminID := uuid.New().String() + adminID := "admin" // Feste UID statt UUID createdAt := time.Now().Format(time.RFC3339) _, err = db.ExecContext(ctx, - "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)", - adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt) + "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + adminID, "admin", "admin@certigo.local", string(hashedPassword), 1, 1, createdAt) if err != nil { log.Printf("Fehler beim Erstellen des Admin-Users: %v", err) return } - log.Println("✓ Default Admin-User erstellt: username='admin', password='admin'") + log.Println("✓ Default Admin-User erstellt: UID='admin', username='admin', password='admin'") log.Printf(" User ID: %s", adminID) log.Printf(" Email: admin@certigo.local") } @@ -2765,6 +2833,13 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) { return } + // Prüfe ob User Admin ist + isAdmin, err := isUserAdmin(userID) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) + isAdmin = false + } + // Hole Berechtigungen permissions, err := getUserPermissions(userID) if err != nil { @@ -2781,11 +2856,12 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "userId": userID, - "hasFullAccess": permissions.HasFullAccess, + "isAdmin": isAdmin, + "hasFullAccess": permissions.HasFullAccess || isAdmin, "accessibleSpaces": []string{}, "permissions": map[string]interface{}{ - "canCreateSpace": false, - "canDeleteSpace": false, + "canCreateSpace": permissions.HasFullAccess || isAdmin, + "canDeleteSpace": permissions.HasFullAccess || isAdmin, "canCreateFqdn": canCreateFqdn, "canDeleteFqdn": canDeleteFqdn, "canUploadCSR": canUploadCSR, @@ -2838,17 +2914,46 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) { } // Prüfe globale Berechtigungen (Space erstellen/löschen) - hasFullAccessGlobal := false - for _, group := range permissions.Groups { - if group.Permission == PermissionFullAccess { - hasFullAccessGlobal = true - break + // Admins haben immer Vollzugriff + if isAdmin { + perms := response["permissions"].(map[string]interface{}) + perms["canCreateSpace"] = true + perms["canDeleteSpace"] = true + // Alle Spaces sind zugänglich für Admins + spaceRows, err := db.QueryContext(ctx, "SELECT id FROM spaces") + if err == nil { + defer spaceRows.Close() + var allSpaceIDs []string + for spaceRows.Next() { + var spaceID string + if err := spaceRows.Scan(&spaceID); err == nil { + allSpaceIDs = append(allSpaceIDs, spaceID) + canCreateFqdn[spaceID] = true + canDeleteFqdn[spaceID] = true + canUploadCSR[spaceID] = true + canSignCSR[spaceID] = true + } + } + spaceRows.Close() + response["accessibleSpaces"] = allSpaceIDs + perms["canCreateFqdn"] = canCreateFqdn + perms["canDeleteFqdn"] = canDeleteFqdn + perms["canUploadCSR"] = canUploadCSR + perms["canSignCSR"] = canSignCSR + } + } else { + hasFullAccessGlobal := false + for _, group := range permissions.Groups { + if group.Permission == PermissionFullAccess { + hasFullAccessGlobal = true + break + } } - } - perms := response["permissions"].(map[string]interface{}) - perms["canCreateSpace"] = hasFullAccessGlobal - perms["canDeleteSpace"] = hasFullAccessGlobal + perms := response["permissions"].(map[string]interface{}) + perms["canCreateSpace"] = hasFullAccessGlobal + perms["canDeleteSpace"] = hasFullAccessGlobal + } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) @@ -2869,7 +2974,7 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { defer cancel() // Lade alle Benutzer - rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC") + rows, err := db.QueryContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users ORDER BY created_at DESC") if err != nil { http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Benutzer: %v", err) @@ -2881,12 +2986,15 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { var userIDs []string for rows.Next() { var user User - err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) + var isAdmin, enabled int + err := rows.Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt) if err != nil { http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err) return } + user.IsAdmin = isAdmin == 1 + user.Enabled = enabled == 1 user.GroupIDs = []string{} // Initialisiere als leeres Array users = append(users, user) userIDs = append(userIDs, user.ID) @@ -2946,8 +3054,9 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) { defer cancel() var user User - err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). - Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) + var isAdmin, enabled int + err := db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID). + Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) @@ -2957,6 +3066,8 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Fehler beim Abrufen des Benutzers: %v", err) return } + user.IsAdmin = isAdmin == 1 + user.Enabled = enabled == 1 // Lade Gruppen-IDs für diesen Benutzer groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) @@ -3022,9 +3133,18 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + isAdmin := 0 + if req.IsAdmin { + isAdmin = 1 + } + // Admin muss immer enabled sein + enabledValue := 1 + if req.IsAdmin { + enabledValue = 1 // Admin immer enabled + } _, err = db.ExecContext(ctx, - "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)", - userID, req.Username, req.Email, string(hashedPassword), createdAt) + "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + userID, req.Username, req.Email, string(hashedPassword), isAdmin, enabledValue, createdAt) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "username") { @@ -3058,6 +3178,8 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { ID: userID, Username: req.Username, Email: req.Email, + IsAdmin: req.IsAdmin, + Enabled: true, CreatedAt: createdAt, GroupIDs: req.GroupIDs, } @@ -3068,12 +3190,17 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { // Audit-Log: User erstellt requestUserID, requestUsername := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) - auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{ + auditDetails := 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) + } + if req.IsAdmin { + auditDetails["isAdmin"] = true + auditDetails["message"] = fmt.Sprintf("Administrator erstellt: %s (%s)", req.Username, req.Email) + } + auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, auditDetails, ipAddress, userAgent) } func updateUserHandler(w http.ResponseWriter, r *http.Request) { @@ -3100,25 +3227,105 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { defer cancel() // Prüfe ob Benutzer existiert - var exists bool - err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists) - if err != nil || !exists { - http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) + var isAdmin int + var currentUsername, currentEmail string + err := db.QueryRowContext(ctx, "SELECT is_admin, username, email FROM users WHERE id = ?", userID). + Scan(&isAdmin, ¤tUsername, ¤tEmail) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) + return + } + http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen des Benutzers: %v", err) return } + // Nur der spezielle Admin-User mit UID "admin": Username und Email sind unveränderbar + // Andere Admin-User können ihre Daten ändern + if userID == "admin" { + if req.Username != "" && req.Username != currentUsername { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden"}) + return + } + if req.Email != "" && req.Email != currentEmail { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden"}) + return + } + } + // Update Felder updates := []string{} args := []interface{}{} - if req.Username != "" { + // Nur Username/Email updaten wenn nicht der spezielle Admin-User mit UID "admin" + // Andere Admin-User können ihre Daten ändern + if req.Username != "" && (userID != "admin" || req.Username == currentUsername) { updates = append(updates, "username = ?") args = append(args, req.Username) } - if req.Email != "" { + if req.Email != "" && (userID != "admin" || req.Email == currentEmail) { updates = append(updates, "email = ?") args = append(args, req.Email) } + + // isAdmin aktualisieren, falls angegeben + // UID 'admin' kann seinen Admin-Status nicht ändern + if req.IsAdmin != nil { + // UID 'admin' ist immer Admin und kann nicht geändert werden + if userID == "admin" { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "Der Admin-Status des Users mit UID 'admin' kann nicht geändert werden"}) + return + } + + adminValue := 0 + if *req.IsAdmin { + adminValue = 1 + } + updates = append(updates, "is_admin = ?") + args = append(args, adminValue) + + // Wenn Admin aktiviert wird, entferne alle Gruppen-Zuweisungen + if *req.IsAdmin { + _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID) + if err != nil { + log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen für Admin: %v", err) + } + } + } + + // enabled aktualisieren, falls angegeben + // Nur der spezielle Admin-User mit UID "admin" kann enabled geändert werden + if req.Enabled != nil { + // Nur UID "admin" kann enabled geändert werden + if userID != "admin" { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "Nur der Admin-User mit UID 'admin' kann aktiviert/deaktiviert werden"}) + return + } + + // Prüfe ob der anfragende User ein Admin ist (für Deaktivierung) + if !*req.Enabled { + requestUserID, _ := getUserFromRequest(r) + isRequestingAdmin, err := isUserAdmin(requestUserID) + if err != nil || !isRequestingAdmin { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren können den Admin-User mit UID 'admin' deaktivieren"}) + return + } + } + + enabledValue := 0 + if *req.Enabled { + enabledValue = 1 + } + updates = append(updates, "enabled = ?") + args = append(args, enabledValue) + } + if req.Password != "" { // Altes Passwort ist erforderlich, wenn Passwort geändert wird if req.OldPassword == "" { @@ -3185,22 +3392,36 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { } // Aktualisiere Gruppen-Zuweisungen, falls angegeben + // Nur wenn User nicht Admin ist oder Admin deaktiviert wird 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) + // Prüfe ob User nach Update Admin ist + var willBeAdmin int + if req.IsAdmin != nil { + if *req.IsAdmin { + willBeAdmin = 1 + } + } else { + willBeAdmin = isAdmin } - // 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) + // Nur Gruppen zuweisen wenn User nicht Admin ist + if willBeAdmin == 0 { + // 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) + } } } } @@ -3208,16 +3429,21 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { // Lade aktualisierten Benutzer var user User - err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). - Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) + var isAdminUpdated, enabledUpdated int + err = db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID). + Scan(&user.ID, &user.Username, &user.Email, &isAdminUpdated, &enabledUpdated, &user.CreatedAt) if err != nil { http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err) return } + user.IsAdmin = isAdminUpdated == 1 + user.Enabled = enabledUpdated == 1 - // Lade Gruppen-IDs - if req.GroupIDs != nil { + // Lade Gruppen-IDs (nur wenn nicht Admin) + if user.IsAdmin { + user.GroupIDs = []string{} // Admins haben keine Gruppen + } else if req.GroupIDs != nil { user.GroupIDs = req.GroupIDs } else { groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) @@ -3231,6 +3457,8 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { } groupRows.Close() user.GroupIDs = groupIDs + } else { + user.GroupIDs = []string{} } } @@ -3249,6 +3477,22 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { if req.Password != "" { details["passwordChanged"] = true } + if req.IsAdmin != nil { + details["isAdmin"] = *req.IsAdmin + if *req.IsAdmin { + details["message"] = "Benutzer wurde zum Administrator ernannt" + } else { + details["message"] = "Administrator-Rechte wurden entfernt" + } + } + if req.Enabled != nil { + details["enabled"] = *req.Enabled + if *req.Enabled { + details["message"] = "Benutzer wurde aktiviert" + } else { + details["message"] = "Benutzer wurde deaktiviert" + } + } if req.GroupIDs != nil { details["groupIds"] = req.GroupIDs } @@ -3272,6 +3516,29 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + // Prüfe ob der zu löschende User der spezielle Admin-User mit UID "admin" ist + // Nur dieser User kann nicht gelöscht werden, andere Admin-User können gelöscht werden + if userID == "admin" { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Der Administrator-Benutzer mit UID 'admin' kann nicht gelöscht werden. Verwenden Sie stattdessen die Deaktivierungsfunktion.", + }) + return + } + + // Prüfe ob User existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists) + if err != nil { + http.Error(w, "Fehler beim Prüfen des Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Prüfen des Benutzers: %v", err) + return + } + if !exists { + http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) + return + } + result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID) if err != nil { http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError) @@ -3294,9 +3561,9 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) // Audit-Log: User gelöscht - userID, username := getUserFromRequest(r) + requestUserID, username := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) - auditService.Track(r.Context(), "DELETE", "user", vars["id"], userID, username, map[string]interface{}{ + auditService.Track(r.Context(), "DELETE", "user", vars["id"], requestUserID, username, map[string]interface{}{ "message": fmt.Sprintf("User gelöscht: %s", vars["id"]), }, ipAddress, userAgent) } @@ -3952,7 +4219,8 @@ func getUserFromRequest(r *http.Request) (userID, username string) { defer cancel() var id string - err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id) + var enabled int + err = db.QueryRowContext(ctx, "SELECT id, enabled FROM users WHERE username = ?", username).Scan(&id, &enabled) if err != nil { // Logge Fehler nur wenn es nicht "no rows" ist if err != sql.ErrNoRows { @@ -3961,6 +4229,12 @@ func getUserFromRequest(r *http.Request) (userID, username string) { return "", username } + // Prüfe ob User aktiviert ist + if enabled == 0 { + log.Printf("API-Zugriff für deaktivierten Benutzer: %s", username) + return "", username + } + return id, username } @@ -3978,6 +4252,27 @@ type PermissionGroupInfo struct { SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces } +// isUserAdmin prüft, ob ein Benutzer Admin ist +func isUserAdmin(userID string) (bool, error) { + if userID == "" { + return false, nil + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + var isAdmin int + err := db.QueryRowContext(ctx, "SELECT is_admin FROM users WHERE id = ?", userID).Scan(&isAdmin) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, err + } + + return isAdmin == 1, nil +} + // getUserPermissions ruft die Berechtigungen eines Benutzers ab func getUserPermissions(userID string) (*UserPermissionInfo, error) { if userID == "" { @@ -3987,6 +4282,19 @@ func getUserPermissions(userID string) (*UserPermissionInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() + // Prüfe ob User Admin ist - Admins haben immer Vollzugriff + isAdmin, err := isUserAdmin(userID) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) + } + if isAdmin { + return &UserPermissionInfo{ + UserID: userID, + Groups: []PermissionGroupInfo{}, + HasFullAccess: true, + }, nil + } + // Hole alle Gruppen des Benutzers mit ihren Berechtigungen query := ` SELECT pg.id, pg.permission @@ -4071,6 +4379,12 @@ func hasSpaceAccess(userID, spaceID string) (bool, error) { return false, nil } + // Admins haben immer Zugriff + isAdmin, err := isUserAdmin(userID) + if err == nil && isAdmin { + return true, nil + } + permissions, err := getUserPermissions(userID) if err != nil { return false, err @@ -4105,6 +4419,12 @@ func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) ( return false, nil } + // Admins haben immer alle Berechtigungen + isAdmin, err := isUserAdmin(userID) + if err == nil && isAdmin { + return true, nil + } + permissions, err := getUserPermissions(userID) if err != nil { return false, err @@ -4605,7 +4925,8 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { defer cancel() var storedHash string - err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(&storedHash) + var enabled int + err = db.QueryRowContext(ctx, "SELECT password_hash, enabled FROM users WHERE username = ?", username).Scan(&storedHash, &enabled) if err != nil { if err == sql.ErrNoRows { if !isAjaxRequest { @@ -4623,6 +4944,17 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { return } + // Prüfe ob User aktiviert ist + if enabled == 0 { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"}) + return + } + // Prüfe Passwort err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) if err != nil { @@ -4640,6 +4972,37 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { } } +// adminOnlyMiddleware prüft, ob der Benutzer ein Admin ist +func adminOnlyMiddleware(next http.HandlerFunc) http.HandlerFunc { + return basicAuthMiddleware(func(w http.ResponseWriter, r *http.Request) { + userID, _ := getUserFromRequest(r) + if userID == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Nicht authentifiziert"}) + return + } + + isAdmin, err := isUserAdmin(userID) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Fehler beim Prüfen der Berechtigung"}) + return + } + + if !isAdmin { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren haben Zugriff auf diese Funktion"}) + return + } + + next(w, r) + }) +} + // Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück) func loginHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -4697,8 +5060,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { var user User var storedHash string - err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username). - Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt) + var enabled int + err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, enabled, created_at FROM users WHERE username = ?", username). + Scan(&user.ID, &user.Username, &user.Email, &storedHash, &enabled, &user.CreatedAt) if err != nil { if err == sql.ErrNoRows { log.Printf("Benutzer nicht gefunden: %s", username) @@ -4712,6 +5076,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { return } + // Prüfe ob User aktiviert ist + if enabled == 0 { + log.Printf("Login-Versuch für deaktivierten Benutzer: %s", username) + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"}) + return + } + // Prüfe Passwort err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) if err != nil { @@ -4780,22 +5152,22 @@ func main() { api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS") - // User Routes - api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS") - api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS") - api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS") - api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS") - api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "OPTIONS") + // User Routes (Admin only) + api.HandleFunc("/users", adminOnlyMiddleware(getUsersHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/users", adminOnlyMiddleware(createUserHandler)).Methods("POST", "OPTIONS") + api.HandleFunc("/users/{id}", adminOnlyMiddleware(getUserHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/users/{id}", adminOnlyMiddleware(updateUserHandler)).Methods("PUT", "OPTIONS") + api.HandleFunc("/users/{id}", adminOnlyMiddleware(deleteUserHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "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") + // Permission Groups Routes (Admin only) + api.HandleFunc("/permission-groups", adminOnlyMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/permission-groups", adminOnlyMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS") // Provider Routes (Protected) api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS") diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm index 816ec62..99be2a6 100644 Binary files a/backend/spaces.db-shm and b/backend/spaces.db-shm differ diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal index 5c13d9c..06c95e9 100644 Binary files a/backend/spaces.db-wal and b/backend/spaces.db-wal differ diff --git a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png index 803e549..d601cd8 100644 Binary files a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png and b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8f216b4..8ef5d4c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' import { AuthProvider, useAuth } from './contexts/AuthContext' +import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext' import Sidebar from './components/Sidebar' import Footer from './components/Footer' import Home from './pages/Home' @@ -34,6 +35,43 @@ const ProtectedRoute = ({ children }) => { return isAuthenticated ? children : } +// Admin Only Route Component +const AdminRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth() + const { isAdmin, loading: permissionsLoading } = usePermissions() + + if (loading || permissionsLoading) { + return ( +
+
+ + + + +

Lade...

+
+
+ ) + } + + if (!isAuthenticated) { + return + } + + if (!isAdmin) { + return ( +
+
+

Zugriff verweigert

+

Nur Administratoren haben Zugriff auf diese Seite.

+
+
+ ) + } + + return children +} + // Public Route Component (redirects to home if already logged in) const PublicRoute = ({ children }) => { const { isAuthenticated, loading } = useAuth() @@ -71,8 +109,8 @@ const AppContent = () => { } /> } /> } /> - } /> - } /> + } /> + } /> } /> @@ -87,7 +125,9 @@ function App() { return ( - + + + ) diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 2087766..52ccb4f 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,11 +1,13 @@ import { Link, useLocation, useNavigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' +import { usePermissions } from '../contexts/PermissionsContext' import { useState, useEffect } from 'react' const Sidebar = ({ isOpen, setIsOpen }) => { const location = useLocation() const navigate = useNavigate() const { user, logout } = useAuth() + const { isAdmin } = usePermissions() const [expandedMenus, setExpandedMenus] = useState({}) const menuItems = [ @@ -128,7 +130,8 @@ const Sidebar = ({ isOpen, setIsOpen }) => { ))} - {/* Settings Menu mit Unterpunkten */} + {/* Settings Menu mit Unterpunkten - nur für Admins */} + {isAdmin && (
  • + )} {/* Profil-Eintrag und Logout am unteren Ende */}
    diff --git a/frontend/src/contexts/PermissionsContext.jsx b/frontend/src/contexts/PermissionsContext.jsx new file mode 100644 index 0000000..fb1e399 --- /dev/null +++ b/frontend/src/contexts/PermissionsContext.jsx @@ -0,0 +1,102 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react' +import { useAuth } from './AuthContext' + +const PermissionsContext = createContext(null) + +export const PermissionsProvider = ({ children }) => { + const { authFetch, isAuthenticated } = useAuth() + const [permissions, setPermissions] = useState({ + isAdmin: false, + hasFullAccess: false, + accessibleSpaces: [], + canCreateSpace: false, + canDeleteSpace: false, + canCreateFqdn: {}, + canDeleteFqdn: {}, + canUploadCSR: {}, + canSignCSR: {}, + }) + const [loading, setLoading] = useState(true) + + const fetchPermissions = useCallback(async () => { + if (!isAuthenticated) { + setLoading(false) + return + } + + try { + setLoading(true) + const response = await authFetch('/api/user/permissions') + if (response.ok) { + const data = await response.json() + setPermissions({ + isAdmin: data.isAdmin || false, + hasFullAccess: data.hasFullAccess || false, + accessibleSpaces: data.accessibleSpaces || [], + canCreateSpace: data.permissions?.canCreateSpace || false, + canDeleteSpace: data.permissions?.canDeleteSpace || false, + canCreateFqdn: data.permissions?.canCreateFqdn || {}, + canDeleteFqdn: data.permissions?.canDeleteFqdn || {}, + canUploadCSR: data.permissions?.canUploadCSR || {}, + canSignCSR: data.permissions?.canSignCSR || {}, + }) + } + } catch (err) { + console.error('Error fetching permissions:', err) + } finally { + setLoading(false) + } + }, [isAuthenticated, authFetch]) + + useEffect(() => { + if (isAuthenticated) { + fetchPermissions() + } else { + setPermissions({ + isAdmin: false, + hasFullAccess: false, + accessibleSpaces: [], + canCreateSpace: false, + canDeleteSpace: false, + canCreateFqdn: {}, + canDeleteFqdn: {}, + canUploadCSR: {}, + canSignCSR: {}, + }) + setLoading(false) + } + }, [isAuthenticated, fetchPermissions]) + + const canCreateSpace = () => permissions.canCreateSpace + const canDeleteSpace = (spaceId) => permissions.canDeleteSpace + const canCreateFqdn = (spaceId) => permissions.canCreateFqdn[spaceId] === true + const canDeleteFqdn = (spaceId) => permissions.canDeleteFqdn[spaceId] === true + const canUploadCSR = (spaceId) => permissions.canUploadCSR[spaceId] === true + const canSignCSR = (spaceId) => permissions.canSignCSR[spaceId] === true + const hasAccessToSpace = (spaceId) => permissions.accessibleSpaces.includes(spaceId) + + const value = { + permissions, + loading, + refreshPermissions: fetchPermissions, + isAdmin: permissions.isAdmin, + canCreateSpace, + canDeleteSpace, + canCreateFqdn, + canDeleteFqdn, + canUploadCSR, + canSignCSR, + hasAccessToSpace, + } + + return {children} +} + +export const usePermissions = () => { + const context = useContext(PermissionsContext) + if (!context) { + throw new Error('usePermissions muss innerhalb eines PermissionsProvider verwendet werden') + } + return context +} + diff --git a/frontend/src/hooks/usePermissions.js b/frontend/src/hooks/usePermissions.js index 51a8719..24ea552 100644 --- a/frontend/src/hooks/usePermissions.js +++ b/frontend/src/hooks/usePermissions.js @@ -1,86 +1,3 @@ -import { useState, useEffect, useCallback } from 'react' -import { useAuth } from '../contexts/AuthContext' - -export const usePermissions = () => { - const { authFetch, isAuthenticated } = useAuth() - const [permissions, setPermissions] = useState({ - hasFullAccess: false, - accessibleSpaces: [], - canCreateSpace: false, - canDeleteSpace: false, - canCreateFqdn: {}, - canDeleteFqdn: {}, - canUploadCSR: {}, - canSignCSR: {}, - }) - const [loading, setLoading] = useState(true) - - const fetchPermissions = useCallback(async () => { - if (!isAuthenticated) { - setLoading(false) - return - } - - try { - setLoading(true) - const response = await authFetch('/api/user/permissions') - if (response.ok) { - const data = await response.json() - setPermissions({ - hasFullAccess: data.hasFullAccess || false, - accessibleSpaces: data.accessibleSpaces || [], - canCreateSpace: data.permissions?.canCreateSpace || false, - canDeleteSpace: data.permissions?.canDeleteSpace || false, - canCreateFqdn: data.permissions?.canCreateFqdn || {}, - canDeleteFqdn: data.permissions?.canDeleteFqdn || {}, - canUploadCSR: data.permissions?.canUploadCSR || {}, - canSignCSR: data.permissions?.canSignCSR || {}, - }) - } - } catch (err) { - console.error('Error fetching permissions:', err) - } finally { - setLoading(false) - } - }, [isAuthenticated, authFetch]) - - useEffect(() => { - if (isAuthenticated) { - fetchPermissions() - } else { - setPermissions({ - hasFullAccess: false, - accessibleSpaces: [], - canCreateSpace: false, - canDeleteSpace: false, - canCreateFqdn: {}, - canDeleteFqdn: {}, - canUploadCSR: {}, - canSignCSR: {}, - }) - setLoading(false) - } - }, [isAuthenticated, fetchPermissions]) - - const canCreateSpace = () => permissions.canCreateSpace - const canDeleteSpace = (spaceId) => permissions.canDeleteSpace - const canCreateFqdn = (spaceId) => permissions.canCreateFqdn[spaceId] === true - const canDeleteFqdn = (spaceId) => permissions.canDeleteFqdn[spaceId] === true - const canUploadCSR = (spaceId) => permissions.canUploadCSR[spaceId] === true - const canSignCSR = (spaceId) => permissions.canSignCSR[spaceId] === true - const hasAccessToSpace = (spaceId) => permissions.accessibleSpaces.includes(spaceId) - - return { - permissions, - loading, - refreshPermissions: fetchPermissions, - canCreateSpace, - canDeleteSpace, - canCreateFqdn, - canDeleteFqdn, - canUploadCSR, - canSignCSR, - hasAccessToSpace, - } -} +// Re-export from PermissionsContext for backward compatibility +export { usePermissions } from '../contexts/PermissionsContext' diff --git a/frontend/src/pages/Permissions.jsx b/frontend/src/pages/Permissions.jsx index 265247b..b7bec68 100644 --- a/frontend/src/pages/Permissions.jsx +++ b/frontend/src/pages/Permissions.jsx @@ -1,8 +1,10 @@ import { useState, useEffect } from 'react' import { useAuth } from '../contexts/AuthContext' +import { usePermissions } from '../hooks/usePermissions' const Permissions = () => { const { authFetch } = useAuth() + const { refreshPermissions } = usePermissions() const [groups, setGroups] = useState([]) const [spaces, setSpaces] = useState([]) const [loading, setLoading] = useState(false) @@ -112,6 +114,8 @@ const Permissions = () => { setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] }) setShowForm(false) setEditingGroup(null) + // Aktualisiere Berechtigungen nach Änderung an Berechtigungsgruppen + refreshPermissions() } else { const errorData = await response.json() setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe') @@ -156,6 +160,8 @@ const Permissions = () => { setShowDeleteModal(false) setGroupToDelete(null) setConfirmChecked(false) + // Aktualisiere Berechtigungen nach Löschen einer Berechtigungsgruppe + refreshPermissions() } else { const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' })) alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe') diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index bd82104..ade551d 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -1,8 +1,10 @@ import { useState, useEffect } from 'react' import { useAuth } from '../contexts/AuthContext' +import { usePermissions } from '../contexts/PermissionsContext' const Profile = () => { const { authFetch, user } = useAuth() + const { isAdmin } = usePermissions() const [loading, setLoading] = useState(false) const [showSuccessAnimation, setShowSuccessAnimation] = useState(false) const [error, setError] = useState('') @@ -286,8 +288,10 @@ const Profile = () => { try { const body = { - ...(formData.username && { username: formData.username }), - ...(formData.email && { email: formData.email }), + // Nur der spezielle Admin-User mit UID 'admin': Username und Email nicht ändern + // Andere Admin-User können ihre Daten ändern + ...(user?.id !== 'admin' && formData.username && { username: formData.username }), + ...(user?.id !== 'admin' && formData.email && { email: formData.email }), ...(formData.password && { password: formData.password, oldPassword: formData.oldPassword @@ -414,9 +418,15 @@ const Profile = () => { value={formData.username} onChange={handleChange} required - className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={user?.id === 'admin'} + className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ + user?.id === 'admin' ? 'opacity-50 cursor-not-allowed' : '' + }`} placeholder="Geben Sie Ihren Benutzernamen ein" /> + {user?.id === 'admin' && ( +

    Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden

    + )}
    @@ -430,9 +440,15 @@ const Profile = () => { value={formData.email} onChange={handleChange} required - className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={user?.id === 'admin'} + className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ + user?.id === 'admin' ? 'opacity-50 cursor-not-allowed' : '' + }`} placeholder="Geben Sie Ihre E-Mail-Adresse ein" /> + {user?.id === 'admin' && ( +

    Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden

    + )}
    diff --git a/frontend/src/pages/Users.jsx b/frontend/src/pages/Users.jsx index 7bd51eb..dd5d106 100644 --- a/frontend/src/pages/Users.jsx +++ b/frontend/src/pages/Users.jsx @@ -1,8 +1,10 @@ import { useState, useEffect } from 'react' import { useAuth } from '../contexts/AuthContext' +import { usePermissions } from '../hooks/usePermissions' const Users = () => { const { authFetch } = useAuth() + const { refreshPermissions } = usePermissions() const [users, setUsers] = useState([]) const [groups, setGroups] = useState([]) const [loading, setLoading] = useState(false) @@ -12,14 +14,20 @@ const Users = () => { const [showDeleteModal, setShowDeleteModal] = useState(false) const [userToDelete, setUserToDelete] = useState(null) const [confirmChecked, setConfirmChecked] = useState(false) + const [showToggleModal, setShowToggleModal] = useState(false) + const [userToToggle, setUserToToggle] = useState(null) + const [confirmToggleChecked, setConfirmToggleChecked] = useState(false) const [formData, setFormData] = useState({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', + isAdmin: false, + enabled: true, groupIds: [] }) + const [showAdminWarning, setShowAdminWarning] = useState(false) useEffect(() => { fetchUsers() @@ -81,18 +89,24 @@ const Users = () => { const body = editingUser ? { - ...(formData.username && { username: formData.username }), - ...(formData.email && { email: formData.email }), + // Username/Email nur setzen wenn nicht der spezielle Admin-User mit UID 'admin' + ...(formData.username && editingUser.id !== 'admin' && { username: formData.username }), + ...(formData.email && editingUser.id !== 'admin' && { email: formData.email }), ...(formData.password && { password: formData.password, oldPassword: formData.oldPassword }), + // isAdmin nur setzen wenn nicht UID 'admin' (UID 'admin' ist immer Admin) + ...(formData.isAdmin !== undefined && editingUser.id !== 'admin' && { isAdmin: formData.isAdmin }), + // enabled wird nicht über das Bearbeitungsformular geändert, nur über den Button in der Liste ...(formData.groupIds !== undefined && { groupIds: formData.groupIds }) } : { username: formData.username, email: formData.email, password: formData.password, + isAdmin: formData.isAdmin || false, + enabled: true, // Neue User sind immer aktiviert, enabled kann nur für UID 'admin' geändert werden groupIds: formData.groupIds || [] } @@ -106,9 +120,12 @@ const Users = () => { if (response.ok) { await fetchUsers() - setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] }) + setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] }) setShowForm(false) setEditingUser(null) + setShowAdminWarning(false) + // Aktualisiere Berechtigungen nach Änderung an Benutzern (Gruppen-Zuweisungen könnten sich geändert haben) + refreshPermissions() } else { const errorData = await response.json() setError(errorData.error || 'Fehler beim Speichern des Benutzers') @@ -129,6 +146,8 @@ const Users = () => { oldPassword: '', password: '', confirmPassword: '', + isAdmin: user.isAdmin || false, + enabled: user.enabled !== undefined ? user.enabled : true, // Wird nicht im Formular angezeigt, nur für internen Zustand groupIds: user.groupIds || [] }) setShowForm(true) @@ -140,6 +159,67 @@ const Users = () => { setConfirmChecked(false) } + const handleToggleEnabled = (user) => { + if (user.id !== 'admin') { + return + } + setUserToToggle(user) + setShowToggleModal(true) + setConfirmToggleChecked(false) + } + + const confirmToggle = async () => { + if (!confirmToggleChecked || !userToToggle) { + return + } + + const newEnabled = !userToToggle.enabled + const action = newEnabled ? 'aktivieren' : 'deaktivieren' + + try { + setLoading(true) + const response = await authFetch(`/api/users/${userToToggle.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + enabled: newEnabled + }), + }) + + if (response.ok) { + await fetchUsers() + setShowToggleModal(false) + setUserToToggle(null) + setConfirmToggleChecked(false) + // Aktualisiere Berechtigungen nach Änderung + refreshPermissions() + } else { + const errorData = await response.json().catch(() => ({ error: `Fehler beim ${action}` })) + const errorMessage = errorData.error || `Fehler beim ${action} des Admin-Users` + setError(errorMessage) + // Schließe Modal bei Fehler + if (response.status === 403) { + setShowToggleModal(false) + setUserToToggle(null) + setConfirmToggleChecked(false) + } + } + } catch (err) { + console.error(`Error toggling enabled for admin user:`, err) + setError(`Fehler beim ${action} des Admin-Users`) + } finally { + setLoading(false) + } + } + + const cancelToggle = () => { + setShowToggleModal(false) + setUserToToggle(null) + setConfirmToggleChecked(false) + } + const confirmDelete = async () => { if (!confirmChecked || !userToDelete) { return @@ -155,13 +235,23 @@ const Users = () => { setShowDeleteModal(false) setUserToDelete(null) setConfirmChecked(false) + // Aktualisiere Berechtigungen nach Löschen eines Benutzers + refreshPermissions() } else { const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' })) - alert(errorData.error || 'Fehler beim Löschen des Benutzers') + const errorMessage = errorData.error || 'Fehler beim Löschen des Benutzers' + // Zeige Fehlermeldung + setError(errorMessage) + // Wenn Admin-Löschung verhindert wurde, schließe Modal + if (response.status === 403) { + setShowDeleteModal(false) + setUserToDelete(null) + setConfirmChecked(false) + } } } catch (err) { console.error('Error deleting user:', err) - alert('Fehler beim Löschen des Benutzers') + setError('Fehler beim Löschen des Benutzers') } } @@ -189,6 +279,20 @@ const Users = () => { }) } + const handleAdminToggle = (e) => { + const isAdmin = e.target.checked + if (isAdmin && !showAdminWarning) { + setShowAdminWarning(true) + } + setFormData(prev => ({ + ...prev, + isAdmin, + // Wenn Admin aktiviert wird, entferne alle Gruppen und stelle sicher dass enabled=true + groupIds: isAdmin ? [] : prev.groupIds, + enabled: isAdmin ? true : (prev.enabled !== undefined ? prev.enabled : true) // Admin muss immer enabled sein + })) + } + const getPermissionLabel = (permission) => { switch (permission) { case 'READ': @@ -216,7 +320,8 @@ const Users = () => { onClick={() => { setShowForm(!showForm) setEditingUser(null) - setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] }) + setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] }) + setShowAdminWarning(false) }} className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200" > @@ -242,9 +347,15 @@ const Users = () => { value={formData.username} onChange={handleChange} required={!editingUser} - className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={editingUser && editingUser.id === 'admin'} + className={`w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ + editingUser && editingUser.id === 'admin' ? 'opacity-50 cursor-not-allowed' : '' + }`} placeholder="Geben Sie einen Benutzernamen ein" /> + {editingUser && editingUser.id === 'admin' && ( +

    Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden

    + )}
    {editingUser && (
    @@ -344,21 +461,56 @@ const Users = () => {

    ✓ Passwörter stimmen überein

    )}
    + + {/* Admin Checkbox - nicht für UID 'admin' */} + {(!editingUser || editingUser.id !== 'admin') && ( +
    + +
    + )} + + + {/* Berechtigungsgruppen - ausgegraut wenn Admin oder UID 'admin' */}
    -
    +
    {groups.length === 0 ? (

    Keine Berechtigungsgruppen vorhanden

    ) : (
    {groups.map(group => ( -
    )