feature/permissionsAndRoles #2

Merged
nick.adam merged 8 commits from feature/permissionsAndRoles into main 2025-11-21 00:54:59 +00:00
15 changed files with 3230 additions and 183 deletions
Showing only changes of commit 24d97f6057 - Show all commits

View File

@@ -261,6 +261,8 @@ type User struct {
ID string `json:"id"` ID string `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
IsAdmin bool `json:"isAdmin"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"createdAt"` CreatedAt string `json:"createdAt"`
GroupIDs []string `json:"groupIds,omitempty"` GroupIDs []string `json:"groupIds,omitempty"`
} }
@@ -306,6 +308,8 @@ type UpdateUserRequest struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
OldPassword string `json:"oldPassword,omitempty"` OldPassword string `json:"oldPassword,omitempty"`
IsAdmin *bool `json:"isAdmin,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
GroupIDs []string `json:"groupIds,omitempty"` GroupIDs []string `json:"groupIds,omitempty"`
} }
@@ -314,6 +318,7 @@ type CreateUserRequest struct {
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
IsAdmin bool `json:"isAdmin,omitempty"`
GroupIDs []string `json:"groupIds,omitempty"` GroupIDs []string `json:"groupIds,omitempty"`
} }
@@ -535,6 +540,8 @@ func initDB() {
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
is_admin INTEGER DEFAULT 0,
enabled INTEGER DEFAULT 1,
created_at DATETIME NOT NULL created_at DATETIME NOT NULL
);` );`
@@ -550,6 +557,22 @@ func initDB() {
log.Println("Datenbank erfolgreich initialisiert") 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 // Erstelle Audit-Log-Tabelle
log.Println("Erstelle audit_logs-Tabelle...") log.Println("Erstelle audit_logs-Tabelle...")
createAuditLogsTableSQL := ` createAuditLogsTableSQL := `
@@ -679,19 +702,26 @@ func createDefaultAdmin() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() defer cancel()
// Prüfe ob bereits ein Admin-User existiert // Prüfe ob bereits ein Admin-User mit UID "admin" existiert
var count int 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 { if err != nil {
log.Printf("Fehler beim Prüfen des Admin-Users: %v", err) log.Printf("Fehler beim Prüfen des Admin-Users: %v", err)
return return
} }
if count > 0 { 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) // Prüfe ob das Passwort noch "admin" ist (für Debugging)
var storedHash string 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 { if err == nil {
// Teste ob das Passwort "admin" ist // Teste ob das Passwort "admin" ist
testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin")) testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin"))
@@ -704,7 +734,45 @@ func createDefaultAdmin() {
return 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 adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil { if err != nil {
@@ -712,18 +780,18 @@ func createDefaultAdmin() {
return return
} }
adminID := uuid.New().String() adminID := "admin" // Feste UID statt UUID
createdAt := time.Now().Format(time.RFC3339) createdAt := time.Now().Format(time.RFC3339)
_, err = db.ExecContext(ctx, _, err = db.ExecContext(ctx,
"INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt) adminID, "admin", "admin@certigo.local", string(hashedPassword), 1, 1, createdAt)
if err != nil { if err != nil {
log.Printf("Fehler beim Erstellen des Admin-Users: %v", err) log.Printf("Fehler beim Erstellen des Admin-Users: %v", err)
return 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(" User ID: %s", adminID)
log.Printf(" Email: admin@certigo.local") log.Printf(" Email: admin@certigo.local")
} }
@@ -2765,6 +2833,13 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
return 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 // Hole Berechtigungen
permissions, err := getUserPermissions(userID) permissions, err := getUserPermissions(userID)
if err != nil { if err != nil {
@@ -2781,11 +2856,12 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{ response := map[string]interface{}{
"userId": userID, "userId": userID,
"hasFullAccess": permissions.HasFullAccess, "isAdmin": isAdmin,
"hasFullAccess": permissions.HasFullAccess || isAdmin,
"accessibleSpaces": []string{}, "accessibleSpaces": []string{},
"permissions": map[string]interface{}{ "permissions": map[string]interface{}{
"canCreateSpace": false, "canCreateSpace": permissions.HasFullAccess || isAdmin,
"canDeleteSpace": false, "canDeleteSpace": permissions.HasFullAccess || isAdmin,
"canCreateFqdn": canCreateFqdn, "canCreateFqdn": canCreateFqdn,
"canDeleteFqdn": canDeleteFqdn, "canDeleteFqdn": canDeleteFqdn,
"canUploadCSR": canUploadCSR, "canUploadCSR": canUploadCSR,
@@ -2838,17 +2914,46 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
} }
// Prüfe globale Berechtigungen (Space erstellen/löschen) // Prüfe globale Berechtigungen (Space erstellen/löschen)
hasFullAccessGlobal := false // Admins haben immer Vollzugriff
for _, group := range permissions.Groups { if isAdmin {
if group.Permission == PermissionFullAccess { perms := response["permissions"].(map[string]interface{})
hasFullAccessGlobal = true perms["canCreateSpace"] = true
break 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 := response["permissions"].(map[string]interface{})
perms["canCreateSpace"] = hasFullAccessGlobal perms["canCreateSpace"] = hasFullAccessGlobal
perms["canDeleteSpace"] = hasFullAccessGlobal perms["canDeleteSpace"] = hasFullAccessGlobal
}
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
@@ -2869,7 +2974,7 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
defer cancel() defer cancel()
// Lade alle Benutzer // 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 { if err != nil {
http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError) http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der Benutzer: %v", err) log.Printf("Fehler beim Abrufen der Benutzer: %v", err)
@@ -2881,12 +2986,15 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
var userIDs []string var userIDs []string
for rows.Next() { for rows.Next() {
var user User 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 { if err != nil {
http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError) http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err) log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
return return
} }
user.IsAdmin = isAdmin == 1
user.Enabled = enabled == 1
user.GroupIDs = []string{} // Initialisiere als leeres Array user.GroupIDs = []string{} // Initialisiere als leeres Array
users = append(users, user) users = append(users, user)
userIDs = append(userIDs, user.ID) userIDs = append(userIDs, user.ID)
@@ -2946,8 +3054,9 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
defer cancel() defer cancel()
var user User var user User
err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). var isAdmin, enabled int
Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) 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 != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) 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) log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return return
} }
user.IsAdmin = isAdmin == 1
user.Enabled = enabled == 1
// Lade Gruppen-IDs für diesen Benutzer // Lade Gruppen-IDs für diesen Benutzer
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) 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) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() 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, _, err = db.ExecContext(ctx,
"INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
userID, req.Username, req.Email, string(hashedPassword), createdAt) userID, req.Username, req.Email, string(hashedPassword), isAdmin, enabledValue, createdAt)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "UNIQUE constraint failed") {
if strings.Contains(err.Error(), "username") { if strings.Contains(err.Error(), "username") {
@@ -3058,6 +3178,8 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
ID: userID, ID: userID,
Username: req.Username, Username: req.Username,
Email: req.Email, Email: req.Email,
IsAdmin: req.IsAdmin,
Enabled: true,
CreatedAt: createdAt, CreatedAt: createdAt,
GroupIDs: req.GroupIDs, GroupIDs: req.GroupIDs,
} }
@@ -3068,12 +3190,17 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
// Audit-Log: User erstellt // Audit-Log: User erstellt
requestUserID, requestUsername := getUserFromRequest(r) requestUserID, requestUsername := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r) ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{ auditDetails := map[string]interface{}{
"username": req.Username, "username": req.Username,
"email": req.Email, "email": req.Email,
"groupIds": req.GroupIDs, "groupIds": req.GroupIDs,
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email), "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) { func updateUserHandler(w http.ResponseWriter, r *http.Request) {
@@ -3100,25 +3227,105 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
defer cancel() defer cancel()
// Prüfe ob Benutzer existiert // Prüfe ob Benutzer existiert
var exists bool var isAdmin int
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists) var currentUsername, currentEmail string
if err != nil || !exists { err := db.QueryRowContext(ctx, "SELECT is_admin, username, email FROM users WHERE id = ?", userID).
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) Scan(&isAdmin, &currentUsername, &currentEmail)
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 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 // Update Felder
updates := []string{} updates := []string{}
args := []interface{}{} 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 = ?") updates = append(updates, "username = ?")
args = append(args, req.Username) args = append(args, req.Username)
} }
if req.Email != "" { if req.Email != "" && (userID != "admin" || req.Email == currentEmail) {
updates = append(updates, "email = ?") updates = append(updates, "email = ?")
args = append(args, req.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 != "" { if req.Password != "" {
// Altes Passwort ist erforderlich, wenn Passwort geändert wird // Altes Passwort ist erforderlich, wenn Passwort geändert wird
if req.OldPassword == "" { if req.OldPassword == "" {
@@ -3185,22 +3392,36 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
} }
// Aktualisiere Gruppen-Zuweisungen, falls angegeben // Aktualisiere Gruppen-Zuweisungen, falls angegeben
// Nur wenn User nicht Admin ist oder Admin deaktiviert wird
if req.GroupIDs != nil { if req.GroupIDs != nil {
// Lösche alle bestehenden Gruppen-Zuweisungen // Prüfe ob User nach Update Admin ist
_, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID) var willBeAdmin int
if err != nil { if req.IsAdmin != nil {
log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err) if *req.IsAdmin {
willBeAdmin = 1
}
} else {
willBeAdmin = isAdmin
} }
// Füge neue Gruppen-Zuweisungen hinzu // Nur Gruppen zuweisen wenn User nicht Admin ist
for _, groupID := range req.GroupIDs { if willBeAdmin == 0 {
// Prüfe ob Gruppe existiert // Lösche alle bestehenden Gruppen-Zuweisungen
var exists bool _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists) if err != nil {
if err == nil && exists { log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err)
_, 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) // 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 // Lade aktualisierten Benutzer
var user User var user User
err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). var isAdminUpdated, enabledUpdated int
Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) 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 { if err != nil {
http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError) http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err) log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err)
return return
} }
user.IsAdmin = isAdminUpdated == 1
user.Enabled = enabledUpdated == 1
// Lade Gruppen-IDs // Lade Gruppen-IDs (nur wenn nicht Admin)
if req.GroupIDs != nil { if user.IsAdmin {
user.GroupIDs = []string{} // Admins haben keine Gruppen
} else if req.GroupIDs != nil {
user.GroupIDs = req.GroupIDs user.GroupIDs = req.GroupIDs
} else { } else {
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) 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() groupRows.Close()
user.GroupIDs = groupIDs user.GroupIDs = groupIDs
} else {
user.GroupIDs = []string{}
} }
} }
@@ -3249,6 +3477,22 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
if req.Password != "" { if req.Password != "" {
details["passwordChanged"] = true 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 { if req.GroupIDs != nil {
details["groupIds"] = req.GroupIDs 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) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() 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) result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID)
if err != nil { if err != nil {
http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError) 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) json.NewEncoder(w).Encode(response)
// Audit-Log: User gelöscht // Audit-Log: User gelöscht
userID, username := getUserFromRequest(r) requestUserID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(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"]), "message": fmt.Sprintf("User gelöscht: %s", vars["id"]),
}, ipAddress, userAgent) }, ipAddress, userAgent)
} }
@@ -3952,7 +4219,8 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
defer cancel() defer cancel()
var id string 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 { if err != nil {
// Logge Fehler nur wenn es nicht "no rows" ist // Logge Fehler nur wenn es nicht "no rows" ist
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
@@ -3961,6 +4229,12 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
return "", username 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 return id, username
} }
@@ -3978,6 +4252,27 @@ type PermissionGroupInfo struct {
SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces 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 // getUserPermissions ruft die Berechtigungen eines Benutzers ab
func getUserPermissions(userID string) (*UserPermissionInfo, error) { func getUserPermissions(userID string) (*UserPermissionInfo, error) {
if userID == "" { if userID == "" {
@@ -3987,6 +4282,19 @@ func getUserPermissions(userID string) (*UserPermissionInfo, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() 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 // Hole alle Gruppen des Benutzers mit ihren Berechtigungen
query := ` query := `
SELECT pg.id, pg.permission SELECT pg.id, pg.permission
@@ -4071,6 +4379,12 @@ func hasSpaceAccess(userID, spaceID string) (bool, error) {
return false, nil return false, nil
} }
// Admins haben immer Zugriff
isAdmin, err := isUserAdmin(userID)
if err == nil && isAdmin {
return true, nil
}
permissions, err := getUserPermissions(userID) permissions, err := getUserPermissions(userID)
if err != nil { if err != nil {
return false, err return false, err
@@ -4105,6 +4419,12 @@ func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (
return false, nil return false, nil
} }
// Admins haben immer alle Berechtigungen
isAdmin, err := isUserAdmin(userID)
if err == nil && isAdmin {
return true, nil
}
permissions, err := getUserPermissions(userID) permissions, err := getUserPermissions(userID)
if err != nil { if err != nil {
return false, err return false, err
@@ -4605,7 +4925,8 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
defer cancel() defer cancel()
var storedHash string 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 != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
if !isAjaxRequest { if !isAjaxRequest {
@@ -4623,6 +4944,17 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return 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 // Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil { 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) // Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück)
func loginHandler(w http.ResponseWriter, r *http.Request) { func loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -4697,8 +5060,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
var user User var user User
var storedHash string var storedHash string
err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username). var enabled int
Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt) 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 != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
log.Printf("Benutzer nicht gefunden: %s", username) log.Printf("Benutzer nicht gefunden: %s", username)
@@ -4712,6 +5076,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
return 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 // Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil { if err != nil {
@@ -4780,22 +5152,22 @@ func main() {
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS")
// User Routes // User Routes (Admin only)
api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS") api.HandleFunc("/users", adminOnlyMiddleware(getUsersHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS") api.HandleFunc("/users", adminOnlyMiddleware(createUserHandler)).Methods("POST", "OPTIONS")
api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS") api.HandleFunc("/users/{id}", adminOnlyMiddleware(getUserHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS") api.HandleFunc("/users/{id}", adminOnlyMiddleware(updateUserHandler)).Methods("PUT", "OPTIONS")
api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "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(getAvatarHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS")
// Permission Groups Routes (Protected) // Permission Groups Routes (Admin only)
api.HandleFunc("/permission-groups", basicAuthMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/permission-groups", adminOnlyMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/permission-groups", basicAuthMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/permission-groups", adminOnlyMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS")
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS") api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS")
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS")
// Provider Routes (Protected) // Provider Routes (Protected)
api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './contexts/AuthContext' import { AuthProvider, useAuth } from './contexts/AuthContext'
import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext'
import Sidebar from './components/Sidebar' import Sidebar from './components/Sidebar'
import Footer from './components/Footer' import Footer from './components/Footer'
import Home from './pages/Home' import Home from './pages/Home'
@@ -34,6 +35,43 @@ const ProtectedRoute = ({ children }) => {
return isAuthenticated ? children : <Navigate to="/login" replace /> return isAuthenticated ? children : <Navigate to="/login" replace />
} }
// Admin Only Route Component
const AdminRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth()
const { isAdmin, loading: permissionsLoading } = usePermissions()
if (loading || permissionsLoading) {
return (
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
<div className="text-center">
<svg className="animate-spin h-12 w-12 text-blue-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="text-slate-300">Lade...</p>
</div>
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 flex items-center justify-center">
<div className="text-center">
<p className="text-red-400 text-xl font-semibold mb-2">Zugriff verweigert</p>
<p className="text-slate-300">Nur Administratoren haben Zugriff auf diese Seite.</p>
</div>
</div>
)
}
return children
}
// Public Route Component (redirects to home if already logged in) // Public Route Component (redirects to home if already logged in)
const PublicRoute = ({ children }) => { const PublicRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth() const { isAuthenticated, loading } = useAuth()
@@ -71,8 +109,8 @@ const AppContent = () => {
<Route path="/spaces/:id" element={<ProtectedRoute><SpaceDetail /></ProtectedRoute>} /> <Route path="/spaces/:id" element={<ProtectedRoute><SpaceDetail /></ProtectedRoute>} />
<Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} /> <Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} /> <Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/settings/users" element={<ProtectedRoute><Users /></ProtectedRoute>} /> <Route path="/settings/users" element={<AdminRoute><Users /></AdminRoute>} />
<Route path="/settings/permissions" element={<ProtectedRoute><Permissions /></ProtectedRoute>} /> <Route path="/settings/permissions" element={<AdminRoute><Permissions /></AdminRoute>} />
<Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} /> <Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} />
</Routes> </Routes>
</div> </div>
@@ -87,7 +125,9 @@ function App() {
return ( return (
<Router> <Router>
<AuthProvider> <AuthProvider>
<AppContent /> <PermissionsProvider>
<AppContent />
</PermissionsProvider>
</AuthProvider> </AuthProvider>
</Router> </Router>
) )

View File

@@ -1,11 +1,13 @@
import { Link, useLocation, useNavigate } from 'react-router-dom' import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
const Sidebar = ({ isOpen, setIsOpen }) => { const Sidebar = ({ isOpen, setIsOpen }) => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const { user, logout } = useAuth() const { user, logout } = useAuth()
const { isAdmin } = usePermissions()
const [expandedMenus, setExpandedMenus] = useState({}) const [expandedMenus, setExpandedMenus] = useState({})
const menuItems = [ const menuItems = [
@@ -128,7 +130,8 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
</li> </li>
))} ))}
{/* Settings Menu mit Unterpunkten */} {/* Settings Menu mit Unterpunkten - nur für Admins */}
{isAdmin && (
<li> <li>
<button <button
onClick={() => isOpen && toggleMenu(settingsMenu.path)} onClick={() => isOpen && toggleMenu(settingsMenu.path)}
@@ -184,6 +187,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
</ul> </ul>
)} )}
</li> </li>
)}
</ul> </ul>
{/* Profil-Eintrag und Logout am unteren Ende */} {/* Profil-Eintrag und Logout am unteren Ende */}
<div className="mt-auto pt-2 border-t border-slate-700/50 space-y-2"> <div className="mt-auto pt-2 border-t border-slate-700/50 space-y-2">

View File

@@ -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 <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>
}
export const usePermissions = () => {
const context = useContext(PermissionsContext)
if (!context) {
throw new Error('usePermissions muss innerhalb eines PermissionsProvider verwendet werden')
}
return context
}

View File

@@ -1,86 +1,3 @@
import { useState, useEffect, useCallback } from 'react' // Re-export from PermissionsContext for backward compatibility
import { useAuth } from '../contexts/AuthContext' export { usePermissions } from '../contexts/PermissionsContext'
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,
}
}

View File

@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
const Permissions = () => { const Permissions = () => {
const { authFetch } = useAuth() const { authFetch } = useAuth()
const { refreshPermissions } = usePermissions()
const [groups, setGroups] = useState([]) const [groups, setGroups] = useState([])
const [spaces, setSpaces] = useState([]) const [spaces, setSpaces] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -112,6 +114,8 @@ const Permissions = () => {
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] }) setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
setShowForm(false) setShowForm(false)
setEditingGroup(null) setEditingGroup(null)
// Aktualisiere Berechtigungen nach Änderung an Berechtigungsgruppen
refreshPermissions()
} else { } else {
const errorData = await response.json() const errorData = await response.json()
setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe') setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe')
@@ -156,6 +160,8 @@ const Permissions = () => {
setShowDeleteModal(false) setShowDeleteModal(false)
setGroupToDelete(null) setGroupToDelete(null)
setConfirmChecked(false) setConfirmChecked(false)
// Aktualisiere Berechtigungen nach Löschen einer Berechtigungsgruppe
refreshPermissions()
} else { } else {
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' })) const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' }))
alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe') alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe')

View File

@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../contexts/PermissionsContext'
const Profile = () => { const Profile = () => {
const { authFetch, user } = useAuth() const { authFetch, user } = useAuth()
const { isAdmin } = usePermissions()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showSuccessAnimation, setShowSuccessAnimation] = useState(false) const [showSuccessAnimation, setShowSuccessAnimation] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -286,8 +288,10 @@ const Profile = () => {
try { try {
const body = { const body = {
...(formData.username && { username: formData.username }), // Nur der spezielle Admin-User mit UID 'admin': Username und Email nicht ändern
...(formData.email && { email: formData.email }), // 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 && { ...(formData.password && {
password: formData.password, password: formData.password,
oldPassword: formData.oldPassword oldPassword: formData.oldPassword
@@ -414,9 +418,15 @@ const Profile = () => {
value={formData.username} value={formData.username}
onChange={handleChange} onChange={handleChange}
required 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" placeholder="Geben Sie Ihren Benutzernamen ein"
/> />
{user?.id === 'admin' && (
<p className="mt-1 text-xs text-slate-400">Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
)}
</div> </div>
<div> <div>
@@ -430,9 +440,15 @@ const Profile = () => {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
required 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" placeholder="Geben Sie Ihre E-Mail-Adresse ein"
/> />
{user?.id === 'admin' && (
<p className="mt-1 text-xs text-slate-400">Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
)}
</div> </div>
<div className="pt-4 border-t border-slate-700/50"> <div className="pt-4 border-t border-slate-700/50">

View File

@@ -1,8 +1,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { usePermissions } from '../hooks/usePermissions'
const Users = () => { const Users = () => {
const { authFetch } = useAuth() const { authFetch } = useAuth()
const { refreshPermissions } = usePermissions()
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
const [groups, setGroups] = useState([]) const [groups, setGroups] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -12,14 +14,20 @@ const Users = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false)
const [userToDelete, setUserToDelete] = useState(null) const [userToDelete, setUserToDelete] = useState(null)
const [confirmChecked, setConfirmChecked] = useState(false) const [confirmChecked, setConfirmChecked] = useState(false)
const [showToggleModal, setShowToggleModal] = useState(false)
const [userToToggle, setUserToToggle] = useState(null)
const [confirmToggleChecked, setConfirmToggleChecked] = useState(false)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
username: '', username: '',
email: '', email: '',
oldPassword: '', oldPassword: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
isAdmin: false,
enabled: true,
groupIds: [] groupIds: []
}) })
const [showAdminWarning, setShowAdminWarning] = useState(false)
useEffect(() => { useEffect(() => {
fetchUsers() fetchUsers()
@@ -81,18 +89,24 @@ const Users = () => {
const body = editingUser const body = editingUser
? { ? {
...(formData.username && { username: formData.username }), // Username/Email nur setzen wenn nicht der spezielle Admin-User mit UID 'admin'
...(formData.email && { email: formData.email }), ...(formData.username && editingUser.id !== 'admin' && { username: formData.username }),
...(formData.email && editingUser.id !== 'admin' && { email: formData.email }),
...(formData.password && { ...(formData.password && {
password: formData.password, password: formData.password,
oldPassword: formData.oldPassword 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 }) ...(formData.groupIds !== undefined && { groupIds: formData.groupIds })
} }
: { : {
username: formData.username, username: formData.username,
email: formData.email, email: formData.email,
password: formData.password, 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 || [] groupIds: formData.groupIds || []
} }
@@ -106,9 +120,12 @@ const Users = () => {
if (response.ok) { if (response.ok) {
await fetchUsers() await fetchUsers()
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] }) setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
setShowForm(false) setShowForm(false)
setEditingUser(null) setEditingUser(null)
setShowAdminWarning(false)
// Aktualisiere Berechtigungen nach Änderung an Benutzern (Gruppen-Zuweisungen könnten sich geändert haben)
refreshPermissions()
} else { } else {
const errorData = await response.json() const errorData = await response.json()
setError(errorData.error || 'Fehler beim Speichern des Benutzers') setError(errorData.error || 'Fehler beim Speichern des Benutzers')
@@ -129,6 +146,8 @@ const Users = () => {
oldPassword: '', oldPassword: '',
password: '', password: '',
confirmPassword: '', 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 || [] groupIds: user.groupIds || []
}) })
setShowForm(true) setShowForm(true)
@@ -140,6 +159,67 @@ const Users = () => {
setConfirmChecked(false) 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 () => { const confirmDelete = async () => {
if (!confirmChecked || !userToDelete) { if (!confirmChecked || !userToDelete) {
return return
@@ -155,13 +235,23 @@ const Users = () => {
setShowDeleteModal(false) setShowDeleteModal(false)
setUserToDelete(null) setUserToDelete(null)
setConfirmChecked(false) setConfirmChecked(false)
// Aktualisiere Berechtigungen nach Löschen eines Benutzers
refreshPermissions()
} else { } else {
const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' })) 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) { } catch (err) {
console.error('Error deleting user:', 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) => { const getPermissionLabel = (permission) => {
switch (permission) { switch (permission) {
case 'READ': case 'READ':
@@ -216,7 +320,8 @@ const Users = () => {
onClick={() => { onClick={() => {
setShowForm(!showForm) setShowForm(!showForm)
setEditingUser(null) 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" 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} value={formData.username}
onChange={handleChange} onChange={handleChange}
required={!editingUser} 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" placeholder="Geben Sie einen Benutzernamen ein"
/> />
{editingUser && editingUser.id === 'admin' && (
<p className="mt-1 text-xs text-slate-400">Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
)}
</div> </div>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-slate-200 mb-2">
@@ -257,9 +368,15 @@ const Users = () => {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
required={!editingUser} 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 eine E-Mail-Adresse ein" placeholder="Geben Sie eine E-Mail-Adresse ein"
/> />
{editingUser && editingUser.id === 'admin' && (
<p className="mt-1 text-xs text-slate-400">Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden</p>
)}
</div> </div>
{editingUser && ( {editingUser && (
<div> <div>
@@ -344,21 +461,56 @@ const Users = () => {
<p className="mt-1 text-xs text-green-400"> Passwörter stimmen überein</p> <p className="mt-1 text-xs text-green-400"> Passwörter stimmen überein</p>
)} )}
</div> </div>
{/* Admin Checkbox - nicht für UID 'admin' */}
{(!editingUser || editingUser.id !== 'admin') && (
<div className="bg-slate-700/30 border border-slate-600/50 rounded-lg p-4">
<label className="flex items-start cursor-pointer group">
<input
type="checkbox"
checked={formData.isAdmin || false}
onChange={handleAdminToggle}
disabled={editingUser && editingUser.username === 'admin'} // Admin user kann seinen Status nicht ändern
className="mt-1 w-5 h-5 text-blue-600 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer"
/>
<div className="ml-3 flex-1">
<div className="flex items-center gap-2">
<span className="text-slate-200 font-semibold">Administrator</span>
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium">
VOLLZUGRIFF
</span>
</div>
<p className="text-xs text-slate-400 mt-1">
Ein Administrator hat vollständigen Zugriff auf alle Funktionen und kann alle Einstellungen verwalten.
</p>
</div>
</label>
</div>
)}
{/* Berechtigungsgruppen - ausgegraut wenn Admin oder UID 'admin' */}
<div> <div>
<label className="block text-sm font-medium text-slate-200 mb-2"> <label className="block text-sm font-medium text-slate-200 mb-2">
Berechtigungsgruppen Berechtigungsgruppen
{(formData.isAdmin || (editingUser && editingUser.id === 'admin')) && <span className="text-xs text-slate-400 ml-2">(nicht verfügbar für Administratoren)</span>}
</label> </label>
<div className="bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto"> <div className={`bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto ${
formData.isAdmin || (editingUser && editingUser.id === 'admin') ? 'opacity-50 pointer-events-none' : ''
}`}>
{groups.length === 0 ? ( {groups.length === 0 ? (
<p className="text-slate-400 text-sm">Keine Berechtigungsgruppen vorhanden</p> <p className="text-slate-400 text-sm">Keine Berechtigungsgruppen vorhanden</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{groups.map(group => ( {groups.map(group => (
<label key={group.id} className="flex items-start cursor-pointer hover:bg-slate-600/50 p-2 rounded"> <label key={group.id} className={`flex items-start p-2 rounded ${
formData.isAdmin || (editingUser && editingUser.id === 'admin') ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-slate-600/50'
}`}>
<input <input
type="checkbox" type="checkbox"
checked={formData.groupIds?.includes(group.id) || false} checked={formData.groupIds?.includes(group.id) || false}
onChange={() => handleGroupToggle(group.id)} onChange={() => handleGroupToggle(group.id)}
disabled={formData.isAdmin || (editingUser && editingUser.id === 'admin')}
className="w-4 h-4 text-blue-600 bg-slate-600 border-slate-500 rounded focus:ring-blue-500 mt-1" className="w-4 h-4 text-blue-600 bg-slate-600 border-slate-500 rounded focus:ring-blue-500 mt-1"
/> />
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
@@ -396,7 +548,8 @@ const Users = () => {
onClick={() => { onClick={() => {
setShowForm(false) setShowForm(false)
setEditingUser(null) setEditingUser(null)
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] }) setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', isAdmin: false, enabled: true, groupIds: [] })
setShowAdminWarning(false)
setError('') setError('')
}} }}
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200" className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
@@ -437,11 +590,23 @@ const Users = () => {
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-xl font-semibold text-white mb-2"> <div className="flex items-center gap-2 mb-2">
{user.username} <h3 className="text-xl font-semibold text-white">
</h3> {user.username}
<p className="text-slate-300 mb-2">{user.email}</p> </h3>
{user.groupIds && user.groupIds.length > 0 && ( {user.isAdmin && (
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium border border-red-500/30">
ADMIN
</span>
)}
{user.id === 'admin' && user.enabled === false && (
<span className="px-2 py-0.5 bg-red-600/20 text-red-300 rounded text-xs font-medium border border-red-500/30">
DEAKTIVIERT
</span>
)}
</div>
<p className="text-slate-300 mb-2">{user.email}</p>
{!user.isAdmin && user.groupIds && user.groupIds.length > 0 && (
<div className="mb-2"> <div className="mb-2">
<p className="text-sm text-slate-300 mb-1">Berechtigungsgruppen:</p> <p className="text-sm text-slate-300 mb-1">Berechtigungsgruppen:</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@@ -472,12 +637,26 @@ const Users = () => {
> >
Bearbeiten Bearbeiten
</button> </button>
<button {user.id === 'admin' ? (
onClick={() => handleDelete(user)} <button
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors" onClick={() => handleToggleEnabled(user)}
> className={`px-4 py-2 text-white text-sm rounded-lg transition-colors ${
Löschen user.enabled
</button> ? 'bg-yellow-600 hover:bg-yellow-700'
: 'bg-green-600 hover:bg-green-700'
}`}
title={user.enabled ? "Admin-User deaktivieren" : "Admin-User aktivieren"}
>
{user.enabled ? 'Deaktivieren' : 'Aktivieren'}
</button>
) : (
<button
onClick={() => handleDelete(user)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors"
>
Löschen
</button>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -486,6 +665,66 @@ const Users = () => {
)} )}
</div> </div>
{/* Admin Warning Modal */}
{showAdminWarning && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-slate-800 rounded-xl shadow-2xl border border-red-600/50 max-w-md w-full p-6">
<div className="flex items-center mb-4">
<div className="flex-shrink-0 w-12 h-12 bg-red-500/20 rounded-full flex items-center justify-center mr-4">
<svg
className="w-6 h-6 text-red-400"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 className="text-xl font-bold text-white">
Administrator-Berechtigung
</h3>
</div>
<div className="mb-6">
<p className="text-slate-300 mb-3">
Sie sind dabei, diesem Benutzer <span className="font-semibold text-red-400">Administrator-Rechte</span> zu gewähren.
</p>
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 mb-4">
<p className="text-sm font-semibold text-red-300 mb-2"> Mögliche Gefahren:</p>
<ul className="text-xs text-slate-300 space-y-1 list-disc list-inside">
<li>Vollständiger Zugriff auf alle Funktionen und Einstellungen</li>
<li>Möglichkeit, andere Benutzer zu erstellen, zu bearbeiten oder zu löschen</li>
<li>Zugriff auf alle Spaces, FQDNs und Zertifikate</li>
<li>Möglichkeit, Berechtigungsgruppen zu verwalten</li>
<li>Keine Einschränkungen durch Berechtigungsgruppen</li>
</ul>
</div>
<p className="text-sm text-slate-400">
Möchten Sie wirklich fortfahren?
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowAdminWarning(false)}
className="flex-1 px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Abbrechen
</button>
<button
onClick={() => setShowAdminWarning(false)}
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Verstanden, fortfahren
</button>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{showDeleteModal && userToDelete && ( {showDeleteModal && userToDelete && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
@@ -548,6 +787,99 @@ const Users = () => {
</div> </div>
</div> </div>
)} )}
{/* Toggle Enabled Confirmation Modal */}
{showToggleModal && userToToggle && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className={`bg-slate-800 rounded-xl shadow-2xl border max-w-md w-full p-6 ${
userToToggle.enabled ? 'border-yellow-600/50' : 'border-green-600/50'
}`}>
<div className="flex items-center mb-4">
<div className={`flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center mr-4 ${
userToToggle.enabled ? 'bg-yellow-500/20' : 'bg-green-500/20'
}`}>
{userToToggle.enabled ? (
<svg
className="w-6 h-6 text-yellow-400"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
) : (
<svg
className="w-6 h-6 text-green-400"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
</div>
<h3 className="text-xl font-bold text-white">
Admin-User {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'}
</h3>
</div>
<div className="mb-6">
<p className="text-slate-300 mb-4">
Möchten Sie den Admin-User <span className="font-semibold text-white">{userToToggle.username}</span> (UID: {userToToggle.id}) wirklich {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'}?
</p>
<p className={`text-sm mb-4 ${
userToToggle.enabled ? 'text-yellow-400' : 'text-green-400'
}`}>
{userToToggle.enabled
? 'Der Admin-User kann sich nach der Deaktivierung nicht mehr anmelden und keine API-Calls durchführen.'
: 'Der Admin-User kann sich nach der Aktivierung wieder anmelden und API-Calls durchführen.'}
</p>
<label className="flex items-start cursor-pointer group">
<input
type="checkbox"
checked={confirmToggleChecked}
onChange={(e) => setConfirmToggleChecked(e.target.checked)}
className={`mt-1 w-5 h-5 bg-slate-700 border-slate-600 rounded focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-800 cursor-pointer ${
userToToggle.enabled
? 'text-yellow-600 focus:ring-yellow-500'
: 'text-green-600 focus:ring-green-500'
}`}
/>
<span className="ml-3 text-sm text-slate-300 group-hover:text-white transition-colors">
Ich bestätige, dass ich den Admin-User {userToToggle.enabled ? 'deaktivieren' : 'aktivieren'} möchte
</span>
</label>
</div>
<div className="flex gap-3">
<button
onClick={confirmToggle}
disabled={!confirmToggleChecked}
className={`flex-1 px-4 py-2 disabled:bg-slate-700 disabled:text-slate-500 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors duration-200 ${
userToToggle.enabled
? 'bg-yellow-600 hover:bg-yellow-700'
: 'bg-green-600 hover:bg-green-700'
}`}
>
{userToToggle.enabled ? 'Deaktivieren' : 'Aktivieren'}
</button>
<button
onClick={cancelToggle}
className="flex-1 px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
) )