feature/permissionsAndRoles #2
717
backend/main.go
717
backend/main.go
@@ -5,9 +5,9 @@ import (
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -258,25 +258,63 @@ type CSR struct {
|
||||
|
||||
// User struct für Benutzer
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
GroupIDs []string `json:"groupIds,omitempty"`
|
||||
}
|
||||
|
||||
// CreateUserRequest struct für Benutzer-Erstellung
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
// PermissionLevel definiert die Berechtigungsstufen
|
||||
type PermissionLevel string
|
||||
|
||||
const (
|
||||
PermissionRead PermissionLevel = "READ"
|
||||
PermissionReadWrite PermissionLevel = "READ_WRITE"
|
||||
PermissionFullAccess PermissionLevel = "FULL_ACCESS"
|
||||
)
|
||||
|
||||
// PermissionGroup struct für Berechtigungsgruppen
|
||||
type PermissionGroup struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permission PermissionLevel `json:"permission"`
|
||||
SpaceIDs []string `json:"spaceIds"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// CreatePermissionGroupRequest struct für Gruppen-Erstellung
|
||||
type CreatePermissionGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permission PermissionLevel `json:"permission"`
|
||||
SpaceIDs []string `json:"spaceIds"`
|
||||
}
|
||||
|
||||
// UpdatePermissionGroupRequest struct für Gruppen-Update
|
||||
type UpdatePermissionGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permission PermissionLevel `json:"permission"`
|
||||
SpaceIDs []string `json:"spaceIds"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest struct für Benutzer-Update
|
||||
type UpdateUserRequest struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
OldPassword string `json:"oldPassword,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
OldPassword string `json:"oldPassword,omitempty"`
|
||||
GroupIDs []string `json:"groupIds,omitempty"`
|
||||
}
|
||||
|
||||
// CreateUserRequest struct für Benutzer-Erstellung
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
GroupIDs []string `json:"groupIds,omitempty"`
|
||||
}
|
||||
|
||||
// MessageResponse struct für einfache Nachrichten
|
||||
@@ -318,7 +356,7 @@ func initDB() {
|
||||
log.Println("Teste Datenbank-Verbindung...")
|
||||
maxRetries := 5
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
err := db.PingContext(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -570,10 +608,75 @@ func initDB() {
|
||||
} else {
|
||||
log.Printf("Avatar-Ordner erstellt: %s", avatarDir)
|
||||
}
|
||||
|
||||
// Erstelle Permission Groups-Tabelle
|
||||
log.Println("Erstelle permission_groups-Tabelle...")
|
||||
createPermissionGroupsTableSQL := `
|
||||
CREATE TABLE IF NOT EXISTS permission_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
permission TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL
|
||||
);`
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||
_, err = db.ExecContext(ctx, createPermissionGroupsTableSQL)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "database is locked") {
|
||||
log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.")
|
||||
}
|
||||
log.Fatal("Fehler beim Erstellen der permission_groups-Tabelle:", err)
|
||||
}
|
||||
|
||||
// Erstelle group_spaces-Tabelle für Space-Zuweisungen
|
||||
log.Println("Erstelle group_spaces-Tabelle...")
|
||||
createGroupSpacesTableSQL := `
|
||||
CREATE TABLE IF NOT EXISTS group_spaces (
|
||||
group_id TEXT NOT NULL,
|
||||
space_id TEXT NOT NULL,
|
||||
PRIMARY KEY (group_id, space_id),
|
||||
FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE
|
||||
);`
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||
_, err = db.ExecContext(ctx, createGroupSpacesTableSQL)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "database is locked") {
|
||||
log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.")
|
||||
}
|
||||
log.Fatal("Fehler beim Erstellen der group_spaces-Tabelle:", err)
|
||||
}
|
||||
|
||||
// Erstelle user_groups-Tabelle für Benutzer-Gruppen-Zuweisungen
|
||||
log.Println("Erstelle user_groups-Tabelle...")
|
||||
createUserGroupsTableSQL := `
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
user_id TEXT NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, group_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE
|
||||
);`
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||
_, err = db.ExecContext(ctx, createUserGroupsTableSQL)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "database is locked") {
|
||||
log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.")
|
||||
}
|
||||
log.Fatal("Fehler beim Erstellen der user_groups-Tabelle:", err)
|
||||
}
|
||||
|
||||
log.Println("Berechtigungssystem-Tabellen erfolgreich erstellt")
|
||||
}
|
||||
|
||||
func createDefaultAdmin() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Prüfe ob bereits ein Admin-User existiert
|
||||
@@ -699,11 +802,11 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
response := StatsResponse{
|
||||
Spaces: spacesCount,
|
||||
FQDNs: fqdnsCount,
|
||||
CSRs: csrsCount,
|
||||
Spaces: spacesCount,
|
||||
FQDNs: fqdnsCount,
|
||||
CSRs: csrsCount,
|
||||
Certificates: certificatesCount,
|
||||
Users: usersCount,
|
||||
Users: usersCount,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
@@ -2410,9 +2513,10 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Lade alle Benutzer
|
||||
rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC")
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError)
|
||||
@@ -2422,6 +2526,7 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
defer rows.Close()
|
||||
|
||||
var users []User
|
||||
var userIDs []string
|
||||
for rows.Next() {
|
||||
var user User
|
||||
err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
|
||||
@@ -2430,7 +2535,42 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
|
||||
return
|
||||
}
|
||||
user.GroupIDs = []string{} // Initialisiere als leeres Array
|
||||
users = append(users, user)
|
||||
userIDs = append(userIDs, user.ID)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Lade alle Gruppen-Zuweisungen in einem einzigen Query
|
||||
if len(userIDs) > 0 {
|
||||
// Erstelle Platzhalter für IN-Clause
|
||||
placeholders := make([]string, len(userIDs))
|
||||
args := make([]interface{}, len(userIDs))
|
||||
for i, id := range userIDs {
|
||||
placeholders[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT user_id, group_id FROM user_groups WHERE user_id IN (%s)", strings.Join(placeholders, ","))
|
||||
groupRows, err := db.QueryContext(ctx, query, args...)
|
||||
if err == nil {
|
||||
// Erstelle eine Map von user_id zu group_ids
|
||||
groupMap := make(map[string][]string)
|
||||
for groupRows.Next() {
|
||||
var userID, groupID string
|
||||
if err := groupRows.Scan(&userID, &groupID); err == nil {
|
||||
groupMap[userID] = append(groupMap[userID], groupID)
|
||||
}
|
||||
}
|
||||
groupRows.Close()
|
||||
|
||||
// Weise Gruppen-IDs den Benutzern zu
|
||||
for i := range users {
|
||||
if groupIDs, exists := groupMap[users[i].ID]; exists {
|
||||
users[i].GroupIDs = groupIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(users)
|
||||
@@ -2450,7 +2590,7 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["id"]
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
var user User
|
||||
@@ -2466,6 +2606,22 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Lade Gruppen-IDs für diesen Benutzer
|
||||
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
|
||||
if err == nil {
|
||||
var groupIDs []string
|
||||
for groupRows.Next() {
|
||||
var groupID string
|
||||
if err := groupRows.Scan(&groupID); err == nil {
|
||||
groupIDs = append(groupIDs, groupID)
|
||||
}
|
||||
}
|
||||
groupRows.Close()
|
||||
user.GroupIDs = groupIDs
|
||||
} else {
|
||||
user.GroupIDs = []string{} // Initialisiere als leeres Array bei Fehler
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
@@ -2511,7 +2667,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := uuid.New().String()
|
||||
createdAt := time.Now().Format(time.RFC3339)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
_, err = db.ExecContext(ctx,
|
||||
@@ -2531,11 +2687,27 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Weise Gruppen zu, falls angegeben
|
||||
if len(req.GroupIDs) > 0 {
|
||||
for _, groupID := range req.GroupIDs {
|
||||
// Prüfe ob Gruppe existiert
|
||||
var exists bool
|
||||
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists)
|
||||
if err == nil && exists {
|
||||
_, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user := User{
|
||||
ID: userID,
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
CreatedAt: createdAt,
|
||||
GroupIDs: req.GroupIDs,
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
@@ -2547,6 +2719,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{
|
||||
"username": req.Username,
|
||||
"email": req.Email,
|
||||
"groupIds": req.GroupIDs,
|
||||
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email),
|
||||
}, ipAddress, userAgent)
|
||||
}
|
||||
@@ -2571,7 +2744,7 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Prüfe ob Benutzer existiert
|
||||
@@ -2659,6 +2832,28 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Aktualisiere Gruppen-Zuweisungen, falls angegeben
|
||||
if req.GroupIDs != nil {
|
||||
// Lösche alle bestehenden Gruppen-Zuweisungen
|
||||
_, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err)
|
||||
}
|
||||
|
||||
// Füge neue Gruppen-Zuweisungen hinzu
|
||||
for _, groupID := range req.GroupIDs {
|
||||
// Prüfe ob Gruppe existiert
|
||||
var exists bool
|
||||
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists)
|
||||
if err == nil && exists {
|
||||
_, err = db.ExecContext(ctx, "INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lade aktualisierten Benutzer
|
||||
var user User
|
||||
err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
|
||||
@@ -2669,10 +2864,28 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Lade Gruppen-IDs
|
||||
if req.GroupIDs != nil {
|
||||
user.GroupIDs = req.GroupIDs
|
||||
} else {
|
||||
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
|
||||
if err == nil {
|
||||
var groupIDs []string
|
||||
for groupRows.Next() {
|
||||
var groupID string
|
||||
if err := groupRows.Scan(&groupID); err == nil {
|
||||
groupIDs = append(groupIDs, groupID)
|
||||
}
|
||||
}
|
||||
groupRows.Close()
|
||||
user.GroupIDs = groupIDs
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
|
||||
// Audit-Log: User aktualisiert
|
||||
userID, username := getUserFromRequest(r)
|
||||
requestUserID, requestUsername := getUserFromRequest(r)
|
||||
ipAddress, userAgent := getRequestInfo(r)
|
||||
details := map[string]interface{}{}
|
||||
if req.Username != "" {
|
||||
@@ -2684,7 +2897,10 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if req.Password != "" {
|
||||
details["passwordChanged"] = true
|
||||
}
|
||||
auditService.Track(r.Context(), "UPDATE", "user", vars["id"], userID, username, details, ipAddress, userAgent)
|
||||
if req.GroupIDs != nil {
|
||||
details["groupIds"] = req.GroupIDs
|
||||
}
|
||||
auditService.Track(r.Context(), "UPDATE", "user", vars["id"], requestUserID, requestUsername, details, ipAddress, userAgent)
|
||||
}
|
||||
|
||||
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -2701,7 +2917,7 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["id"]
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID)
|
||||
@@ -2921,10 +3137,400 @@ func getAvatarHandler(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, file)
|
||||
}
|
||||
|
||||
// Permission Groups Handler Functions
|
||||
|
||||
func getPermissionGroupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Lade alle Gruppen
|
||||
rows, err := db.QueryContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups ORDER BY created_at DESC")
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppen", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Abrufen der Berechtigungsgruppen: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groups []PermissionGroup
|
||||
var groupIDs []string
|
||||
for rows.Next() {
|
||||
var group PermissionGroup
|
||||
err := rows.Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Lesen der Gruppen-Daten", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Lesen der Gruppen-Daten: %v", err)
|
||||
return
|
||||
}
|
||||
group.SpaceIDs = []string{} // Initialisiere als leeres Array
|
||||
groups = append(groups, group)
|
||||
groupIDs = append(groupIDs, group.ID)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Lade alle Space-Zuweisungen in einem einzigen Query
|
||||
if len(groupIDs) > 0 {
|
||||
// Erstelle Platzhalter für IN-Clause
|
||||
placeholders := make([]string, len(groupIDs))
|
||||
args := make([]interface{}, len(groupIDs))
|
||||
for i, id := range groupIDs {
|
||||
placeholders[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT group_id, space_id FROM group_spaces WHERE group_id IN (%s)", strings.Join(placeholders, ","))
|
||||
spaceRows, err := db.QueryContext(ctx, query, args...)
|
||||
if err == nil {
|
||||
// Erstelle eine Map von group_id zu space_ids
|
||||
spaceMap := make(map[string][]string)
|
||||
for spaceRows.Next() {
|
||||
var groupID, spaceID string
|
||||
if err := spaceRows.Scan(&groupID, &spaceID); err == nil {
|
||||
spaceMap[groupID] = append(spaceMap[groupID], spaceID)
|
||||
}
|
||||
}
|
||||
spaceRows.Close()
|
||||
|
||||
// Weise Space-IDs den Gruppen zu
|
||||
for i := range groups {
|
||||
if spaceIDs, exists := spaceMap[groups[i].ID]; exists {
|
||||
groups[i].SpaceIDs = spaceIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(groups)
|
||||
}
|
||||
|
||||
func getPermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
groupID := vars["id"]
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
var group PermissionGroup
|
||||
err := db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID).
|
||||
Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppe", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Abrufen der Berechtigungsgruppe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Lade Space-IDs für diese Gruppe
|
||||
spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID)
|
||||
if err == nil {
|
||||
var spaceIDs []string
|
||||
for spaceRows.Next() {
|
||||
var spaceID string
|
||||
if err := spaceRows.Scan(&spaceID); err == nil {
|
||||
spaceIDs = append(spaceIDs, spaceID)
|
||||
}
|
||||
}
|
||||
spaceRows.Close()
|
||||
group.SpaceIDs = spaceIDs
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(group)
|
||||
}
|
||||
|
||||
func createPermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
var req CreatePermissionGroupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validierung
|
||||
if req.Name == "" {
|
||||
http.Error(w, "Name ist erforderlich", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validiere Berechtigungsstufe
|
||||
if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess {
|
||||
http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Erstelle Gruppe
|
||||
groupID := uuid.New().String()
|
||||
createdAt := time.Now().Format(time.RFC3339)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.ExecContext(ctx,
|
||||
"INSERT INTO permission_groups (id, name, description, permission, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
groupID, req.Name, req.Description, string(req.Permission), createdAt)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Fehler beim Erstellen der Berechtigungsgruppe", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Erstellen der Berechtigungsgruppe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Weise Spaces zu, falls angegeben
|
||||
if len(req.SpaceIDs) > 0 {
|
||||
for _, spaceID := range req.SpaceIDs {
|
||||
// Prüfe ob Space existiert
|
||||
var exists bool
|
||||
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
|
||||
if err == nil && exists {
|
||||
_, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group := PermissionGroup{
|
||||
ID: groupID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Permission: req.Permission,
|
||||
SpaceIDs: req.SpaceIDs,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(group)
|
||||
|
||||
// Audit-Log
|
||||
requestUserID, requestUsername := getUserFromRequest(r)
|
||||
ipAddress, userAgent := getRequestInfo(r)
|
||||
auditService.Track(r.Context(), "CREATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"permission": req.Permission,
|
||||
"spaceIds": req.SpaceIDs,
|
||||
"message": fmt.Sprintf("Berechtigungsgruppe erstellt: %s", req.Name),
|
||||
}, ipAddress, userAgent)
|
||||
}
|
||||
|
||||
func updatePermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
groupID := vars["id"]
|
||||
|
||||
var req UpdatePermissionGroupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// Prüfe ob Gruppe existiert
|
||||
var exists bool
|
||||
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists)
|
||||
if err != nil || !exists {
|
||||
http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Validiere Berechtigungsstufe, falls angegeben
|
||||
if req.Permission != "" {
|
||||
if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess {
|
||||
http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Update Felder
|
||||
updates := []string{}
|
||||
args := []interface{}{}
|
||||
|
||||
if req.Name != "" {
|
||||
updates = append(updates, "name = ?")
|
||||
args = append(args, req.Name)
|
||||
}
|
||||
if req.Description != "" {
|
||||
updates = append(updates, "description = ?")
|
||||
args = append(args, req.Description)
|
||||
}
|
||||
if req.Permission != "" {
|
||||
updates = append(updates, "permission = ?")
|
||||
args = append(args, string(req.Permission))
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
args = append(args, groupID)
|
||||
query := fmt.Sprintf("UPDATE permission_groups SET %s WHERE id = ?", strings.Join(updates, ", "))
|
||||
_, err = db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Fehler beim Aktualisieren der Berechtigungsgruppe", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Aktualisieren der Berechtigungsgruppe: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Aktualisiere Space-Zuweisungen, falls angegeben
|
||||
if req.SpaceIDs != nil {
|
||||
// Lösche alle bestehenden Space-Zuweisungen
|
||||
_, err = db.ExecContext(ctx, "DELETE FROM group_spaces WHERE group_id = ?", groupID)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Löschen der Space-Zuweisungen: %v", err)
|
||||
}
|
||||
|
||||
// Füge neue Space-Zuweisungen hinzu
|
||||
for _, spaceID := range req.SpaceIDs {
|
||||
// Prüfe ob Space existiert
|
||||
var exists bool
|
||||
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
|
||||
if err == nil && exists {
|
||||
_, err = db.ExecContext(ctx, "INSERT INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lade aktualisierte Gruppe
|
||||
var group PermissionGroup
|
||||
err = db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID).
|
||||
Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Abrufen der aktualisierten Gruppe", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Abrufen der aktualisierten Gruppe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Lade Space-IDs
|
||||
if req.SpaceIDs != nil {
|
||||
group.SpaceIDs = req.SpaceIDs
|
||||
} else {
|
||||
spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID)
|
||||
if err == nil {
|
||||
var spaceIDs []string
|
||||
for spaceRows.Next() {
|
||||
var spaceID string
|
||||
if err := spaceRows.Scan(&spaceID); err == nil {
|
||||
spaceIDs = append(spaceIDs, spaceID)
|
||||
}
|
||||
}
|
||||
spaceRows.Close()
|
||||
group.SpaceIDs = spaceIDs
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(group)
|
||||
|
||||
// Audit-Log
|
||||
requestUserID, requestUsername := getUserFromRequest(r)
|
||||
ipAddress, userAgent := getRequestInfo(r)
|
||||
auditService.Track(r.Context(), "UPDATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"permission": req.Permission,
|
||||
"spaceIds": req.SpaceIDs,
|
||||
"message": fmt.Sprintf("Berechtigungsgruppe aktualisiert: %s", groupID),
|
||||
}, ipAddress, userAgent)
|
||||
}
|
||||
|
||||
func deletePermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
groupID := vars["id"]
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
result, err := db.ExecContext(ctx, "DELETE FROM permission_groups WHERE id = ?", groupID)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Löschen der Berechtigungsgruppe", http.StatusInternalServerError)
|
||||
log.Printf("Fehler beim Löschen der Berechtigungsgruppe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := MessageResponse{Message: "Berechtigungsgruppe erfolgreich gelöscht"}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
// Audit-Log
|
||||
requestUserID, requestUsername := getUserFromRequest(r)
|
||||
ipAddress, userAgent := getRequestInfo(r)
|
||||
auditService.Track(r.Context(), "DELETE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{
|
||||
"message": fmt.Sprintf("Berechtigungsgruppe gelöscht: %s", groupID),
|
||||
}, ipAddress, userAgent)
|
||||
}
|
||||
|
||||
// Passwortvalidierung nach Richtlinien
|
||||
func validatePassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("Passwort muss mindestens 8 Zeichen lang sein")
|
||||
return fmt.Errorf("passwort muss mindestens 8 Zeichen lang sein")
|
||||
}
|
||||
|
||||
hasUpper := false
|
||||
@@ -2963,7 +3569,7 @@ func validatePassword(password string) error {
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("Passwort muss enthalten: %s", strings.Join(missing, ", "))
|
||||
return fmt.Errorf("passwort muss enthalten: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -2990,12 +3596,16 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
|
||||
username = parts[0]
|
||||
|
||||
// Hole User-ID aus der Datenbank
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
var id string
|
||||
err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id)
|
||||
if err != nil {
|
||||
// Logge Fehler nur wenn es nicht "no rows" ist
|
||||
if err != sql.ErrNoRows {
|
||||
log.Printf("Fehler beim Abrufen der User-ID für %s: %v", username, err)
|
||||
}
|
||||
return "", username
|
||||
}
|
||||
|
||||
@@ -3201,11 +3811,11 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Zeilen gelesen: %d, Scan-Fehler: %d, Logs hinzugefügt: %d", rowCount, scanErrors, len(logs))
|
||||
|
||||
response := map[string]interface{}{
|
||||
"logs": logs,
|
||||
"total": totalCount,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"hasMore": offset+limit < totalCount,
|
||||
"logs": logs,
|
||||
"total": totalCount,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"hasMore": offset+limit < totalCount,
|
||||
}
|
||||
|
||||
log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset)
|
||||
@@ -3275,14 +3885,14 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Action string `json:"action"`
|
||||
Entity string `json:"entity"`
|
||||
EntityID string `json:"entityID"`
|
||||
UserID string `json:"userID"`
|
||||
Username string `json:"username"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Action string `json:"action"`
|
||||
Entity string `json:"entity"`
|
||||
EntityID string `json:"entityID"`
|
||||
UserID string `json:"userID"`
|
||||
Username string `json:"username"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -3383,7 +3993,7 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
password := parts[1]
|
||||
|
||||
// Validiere Benutzer
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
var storedHash string
|
||||
@@ -3474,7 +4084,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Login-Versuch für Benutzer: %s", username)
|
||||
|
||||
// Validiere Benutzer
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
var user User
|
||||
@@ -3571,6 +4181,13 @@ func main() {
|
||||
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
|
||||
|
||||
// Permission Groups Routes (Protected)
|
||||
api.HandleFunc("/permission-groups", basicAuthMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/permission-groups", basicAuthMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS")
|
||||
api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS")
|
||||
|
||||
// Provider Routes (Protected)
|
||||
api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/providers/{id}", basicAuthMiddleware(getProviderHandler)).Methods("GET", "OPTIONS")
|
||||
@@ -3965,12 +4582,12 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID, username := getUserFromRequest(r)
|
||||
ipAddress, userAgent := getRequestInfo(r)
|
||||
auditService.Track(r.Context(), "SIGN", "csr", csrID, userID, username, map[string]interface{}{
|
||||
"providerId": req.ProviderID,
|
||||
"fqdnId": fqdnID,
|
||||
"spaceId": spaceID,
|
||||
"providerId": req.ProviderID,
|
||||
"fqdnId": fqdnID,
|
||||
"spaceId": spaceID,
|
||||
"certificateId": result.OrderID,
|
||||
"status": result.Status,
|
||||
"message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID),
|
||||
"status": result.Status,
|
||||
"message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID),
|
||||
}, ipAddress, userAgent)
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -9,6 +9,7 @@ import SpaceDetail from './pages/SpaceDetail'
|
||||
import Impressum from './pages/Impressum'
|
||||
import Profile from './pages/Profile'
|
||||
import Users from './pages/Users'
|
||||
import Permissions from './pages/Permissions'
|
||||
import Login from './pages/Login'
|
||||
import AuditLogs from './pages/AuditLogs'
|
||||
|
||||
@@ -71,6 +72,7 @@ const AppContent = () => {
|
||||
<Route path="/impressum" element={<ProtectedRoute><Impressum /></ProtectedRoute>} />
|
||||
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
||||
<Route path="/settings/users" element={<ProtectedRoute><Users /></ProtectedRoute>} />
|
||||
<Route path="/settings/permissions" element={<ProtectedRoute><Permissions /></ProtectedRoute>} />
|
||||
<Route path="/audit-logs" element={<ProtectedRoute><AuditLogs /></ProtectedRoute>} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => {
|
||||
path: '/settings',
|
||||
subItems: [
|
||||
{ path: '/settings/users', label: 'User', icon: '👥' },
|
||||
{ path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' },
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
550
frontend/src/pages/Permissions.jsx
Normal file
550
frontend/src/pages/Permissions.jsx
Normal file
@@ -0,0 +1,550 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
const Permissions = () => {
|
||||
const { authFetch } = useAuth()
|
||||
const [groups, setGroups] = useState([])
|
||||
const [spaces, setSpaces] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fetching, setFetching] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingGroup, setEditingGroup] = useState(null)
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
permission: 'READ',
|
||||
spaceIds: []
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Lade Daten parallel beim Mount und beim Zurückkehren zur Seite
|
||||
let isMounted = true
|
||||
|
||||
const loadData = async () => {
|
||||
if (isMounted) {
|
||||
setFetching(true)
|
||||
}
|
||||
await Promise.all([fetchGroups(), fetchSpaces()])
|
||||
}
|
||||
|
||||
loadData()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
setError('')
|
||||
const response = await authFetch('/api/permission-groups')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setGroups(Array.isArray(data) ? data : [])
|
||||
} else {
|
||||
setError('Fehler beim Abrufen der Berechtigungsgruppen')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Fehler beim Abrufen der Berechtigungsgruppen')
|
||||
console.error('Error fetching permission groups:', err)
|
||||
} finally {
|
||||
setFetching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSpaces = async () => {
|
||||
try {
|
||||
const response = await authFetch('/api/spaces')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setSpaces(Array.isArray(data) ? data : [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching spaces:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
setError('Bitte geben Sie einen Namen ein')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.permission) {
|
||||
setError('Bitte wählen Sie eine Berechtigungsstufe')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = editingGroup
|
||||
? `/api/permission-groups/${editingGroup.id}`
|
||||
: '/api/permission-groups'
|
||||
const method = editingGroup ? 'PUT' : 'POST'
|
||||
|
||||
const body = {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
permission: formData.permission,
|
||||
spaceIds: formData.spaceIds
|
||||
}
|
||||
|
||||
const response = await authFetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await fetchGroups()
|
||||
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
||||
setShowForm(false)
|
||||
setEditingGroup(null)
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Fehler beim Speichern der Berechtigungsgruppe')
|
||||
console.error('Error saving permission group:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (group) => {
|
||||
setEditingGroup(group)
|
||||
setFormData({
|
||||
name: group.name,
|
||||
description: group.description || '',
|
||||
permission: group.permission,
|
||||
spaceIds: group.spaceIds || []
|
||||
})
|
||||
setShowForm(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (groupId) => {
|
||||
if (!window.confirm('Möchten Sie diese Berechtigungsgruppe wirklich löschen?')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authFetch(`/api/permission-groups/${groupId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await fetchGroups()
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe')
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Fehler beim Löschen der Berechtigungsgruppe')
|
||||
console.error('Error deleting permission group:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleSpaceToggle = (spaceId) => {
|
||||
setFormData(prev => {
|
||||
const spaceIds = prev.spaceIds || []
|
||||
if (spaceIds.includes(spaceId)) {
|
||||
return { ...prev, spaceIds: spaceIds.filter(id => id !== spaceId) }
|
||||
} else {
|
||||
return { ...prev, spaceIds: [...spaceIds, spaceId] }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPermissionLabel = (permission) => {
|
||||
switch (permission) {
|
||||
case 'READ':
|
||||
return 'Lesen'
|
||||
case 'READ_WRITE':
|
||||
return 'Lesen/Schreiben'
|
||||
case 'FULL_ACCESS':
|
||||
return 'Vollzugriff'
|
||||
default:
|
||||
return permission
|
||||
}
|
||||
}
|
||||
|
||||
const getPermissionBadgeColor = (permission) => {
|
||||
switch (permission) {
|
||||
case 'READ':
|
||||
return 'bg-green-600/20 text-green-300 border-green-500/30'
|
||||
case 'READ_WRITE':
|
||||
return 'bg-yellow-600/20 text-yellow-300 border-yellow-500/30'
|
||||
case 'FULL_ACCESS':
|
||||
return 'bg-purple-600/20 text-purple-300 border-purple-500/30'
|
||||
default:
|
||||
return 'bg-blue-600/20 text-blue-300 border-blue-500/30'
|
||||
}
|
||||
}
|
||||
|
||||
const getPermissionIcon = (permission) => {
|
||||
switch (permission) {
|
||||
case 'READ':
|
||||
return '👁️'
|
||||
case 'READ_WRITE':
|
||||
return '✏️'
|
||||
case 'FULL_ACCESS':
|
||||
return '🔓'
|
||||
default:
|
||||
return '🔐'
|
||||
}
|
||||
}
|
||||
|
||||
const getPermissionDescription = (permission) => {
|
||||
switch (permission) {
|
||||
case 'READ':
|
||||
return 'Nur CSRs und Zertifikate ansehen. Keine Requests, keine Lösch-/Erstellrechte.'
|
||||
case 'READ_WRITE':
|
||||
return 'FQDNs innerhalb eines Spaces erstellen (nicht löschen), CSRs requesten und ansehen. Keine Spaces löschen/erstellen.'
|
||||
case 'FULL_ACCESS':
|
||||
return 'Vollzugriff: Alles darf gemacht werden. Löschen, Erstellen, CSR requesten und ansehen.'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-white mb-2 flex items-center gap-3">
|
||||
<span className="text-5xl">🔐</span>
|
||||
Berechtigungsgruppen
|
||||
</h1>
|
||||
<p className="text-slate-300">Verwalten Sie Berechtigungsgruppen und weisen Sie Spaces zu</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForm(true)
|
||||
setEditingGroup(null)
|
||||
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
||||
}}
|
||||
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 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Neue Gruppe
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
<div className="mb-8 bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-2xl border border-slate-600/50 p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<span className="text-3xl">{editingGroup ? '✏️' : '➕'}</span>
|
||||
{editingGroup ? 'Berechtigungsgruppe bearbeiten' : 'Neue Berechtigungsgruppe'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForm(false)
|
||||
setEditingGroup(null)
|
||||
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
||||
}}
|
||||
className="text-slate-400 hover:text-white transition-colors"
|
||||
title="Schließen"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
|
||||
<span>📝</span>
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-2.5 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 transition-all"
|
||||
placeholder="z.B. Entwickler, Administratoren"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
|
||||
<span>📄</span>
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows="3"
|
||||
className="w-full px-4 py-2.5 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 transition-all resize-none"
|
||||
placeholder="Beschreibung der Berechtigungsgruppe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-3 flex items-center gap-2">
|
||||
<span>🔑</span>
|
||||
Berechtigungsstufe *
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ value: 'READ', label: 'Lesen', icon: '👁️', activeClass: 'bg-green-600/20 border-green-500 text-green-300', inactiveClass: 'bg-slate-700/50 border-slate-600 text-slate-300' },
|
||||
{ value: 'READ_WRITE', label: 'Lesen/Schreiben', icon: '✏️', activeClass: 'bg-yellow-600/20 border-yellow-500 text-yellow-300', inactiveClass: 'bg-slate-700/50 border-slate-600 text-slate-300' },
|
||||
{ value: 'FULL_ACCESS', label: 'Vollzugriff', icon: '🔓', activeClass: 'bg-purple-600/20 border-purple-500 text-purple-300', inactiveClass: 'bg-slate-700/50 border-slate-600 text-slate-300' }
|
||||
].map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, permission: option.value })}
|
||||
className={`p-4 rounded-lg border-2 transition-all duration-200 ${
|
||||
formData.permission === option.value
|
||||
? `${option.activeClass} shadow-lg`
|
||||
: `${option.inactiveClass} hover:border-slate-500`
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-2">{option.icon}</div>
|
||||
<div className="font-semibold text-sm">{option.label}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 p-3 bg-slate-700/30 border border-slate-600/50 rounded-lg">
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
{getPermissionDescription(formData.permission)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
|
||||
<span>📁</span>
|
||||
Spaces zuweisen
|
||||
{formData.spaceIds && formData.spaceIds.length > 0 && (
|
||||
<span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 rounded-full text-xs font-medium">
|
||||
{formData.spaceIds.length} ausgewählt
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto">
|
||||
{spaces.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-slate-400 text-sm">Keine Spaces vorhanden</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{spaces.map(space => (
|
||||
<label
|
||||
key={space.id}
|
||||
className={`flex items-center cursor-pointer p-3 rounded-lg border transition-all duration-200 ${
|
||||
formData.spaceIds?.includes(space.id)
|
||||
? 'bg-blue-600/20 border-blue-500/50 hover:bg-blue-600/30'
|
||||
: 'bg-slate-600/30 border-slate-600 hover:bg-slate-600/50 hover:border-slate-500'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.spaceIds?.includes(space.id) || false}
|
||||
onChange={() => handleSpaceToggle(space.id)}
|
||||
className="w-5 h-5 text-blue-600 bg-slate-700 border-slate-500 rounded focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
<div className="ml-3 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-300 font-medium">{space.name}</span>
|
||||
{formData.spaceIds?.includes(space.id) && (
|
||||
<span className="text-blue-400">✓</span>
|
||||
)}
|
||||
</div>
|
||||
{space.description && (
|
||||
<p className="text-xs text-slate-400 mt-0.5">{space.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6 pt-6 border-t border-slate-600/50">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin h-5 w-5" 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>
|
||||
Wird gespeichert...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{editingGroup ? '💾' : '➕'}</span>
|
||||
{editingGroup ? 'Aktualisieren' : 'Erstellen'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowForm(false)
|
||||
setEditingGroup(null)
|
||||
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
||||
}}
|
||||
className="px-6 py-2.5 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-all duration-200"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fetching ? (
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-8 text-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<svg className="animate-spin h-8 w-8 text-blue-500 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 Berechtigungsgruppen...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : groups.length === 0 ? (
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-xl border border-slate-600/50 p-12 text-center">
|
||||
<div className="text-6xl mb-4">🔐</div>
|
||||
<p className="text-slate-300 text-lg mb-2">
|
||||
Noch keine Berechtigungsgruppen vorhanden
|
||||
</p>
|
||||
<p className="text-slate-400 text-sm mb-6">
|
||||
Erstellen Sie Ihre erste Gruppe, um Berechtigungen zu verwalten
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForm(true)
|
||||
setEditingGroup(null)
|
||||
setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] })
|
||||
}}
|
||||
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"
|
||||
>
|
||||
+ Erste Gruppe erstellen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{groups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className="bg-slate-800/80 backdrop-blur-sm rounded-xl shadow-xl border border-slate-600/50 p-6 hover:border-slate-500/50 transition-all duration-200 group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white group-hover:text-blue-300 transition-colors mb-2">
|
||||
{group.name}
|
||||
</h3>
|
||||
{group.description && (
|
||||
<p className="text-sm text-slate-400 mb-2">{group.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(group)}
|
||||
className="p-2 bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 rounded-lg transition-all duration-200"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(group.id)}
|
||||
className="p-2 bg-red-600/20 hover:bg-red-600/30 text-red-300 rounded-lg transition-all duration-200"
|
||||
title="Löschen"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-medium mb-4 ${getPermissionBadgeColor(group.permission)}`}>
|
||||
<span className="text-lg">{getPermissionIcon(group.permission)}</span>
|
||||
{getPermissionLabel(group.permission)}
|
||||
</div>
|
||||
|
||||
{group.spaceIds && group.spaceIds.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-slate-600/50">
|
||||
<p className="text-sm font-medium text-slate-300 mb-2 flex items-center gap-2">
|
||||
<span>📁</span>
|
||||
Zugewiesene Spaces ({group.spaceIds.length})
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.spaceIds.map(spaceId => {
|
||||
const space = spaces.find(s => s.id === spaceId)
|
||||
return space ? (
|
||||
<span
|
||||
key={spaceId}
|
||||
className="px-3 py-1.5 bg-slate-700/50 text-slate-300 rounded-lg text-xs font-medium border border-slate-600/50 hover:border-slate-500 transition-colors"
|
||||
>
|
||||
{space.name}
|
||||
</span>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-slate-600/50">
|
||||
<p className="text-xs text-slate-500">
|
||||
Erstellt: {group.createdAt ? new Date(group.createdAt).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}) : 'Unbekannt'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Permissions
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
|
||||
const Users = () => {
|
||||
const { authFetch } = useAuth()
|
||||
const [users, setUsers] = useState([])
|
||||
const [groups, setGroups] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
@@ -13,11 +14,13 @@ const Users = () => {
|
||||
email: '',
|
||||
oldPassword: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
confirmPassword: '',
|
||||
groupIds: []
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
fetchGroups()
|
||||
}, [])
|
||||
|
||||
const fetchUsers = async () => {
|
||||
@@ -36,6 +39,18 @@ const Users = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
const response = await authFetch('/api/permission-groups')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setGroups(Array.isArray(data) ? data : [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching permission groups:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
@@ -68,12 +83,14 @@ const Users = () => {
|
||||
...(formData.password && {
|
||||
password: formData.password,
|
||||
oldPassword: formData.oldPassword
|
||||
})
|
||||
}),
|
||||
...(formData.groupIds !== undefined && { groupIds: formData.groupIds })
|
||||
}
|
||||
: {
|
||||
username: formData.username,
|
||||
email: formData.email,
|
||||
password: formData.password
|
||||
password: formData.password,
|
||||
groupIds: formData.groupIds || []
|
||||
}
|
||||
|
||||
const response = await authFetch(url, {
|
||||
@@ -86,7 +103,7 @@ const Users = () => {
|
||||
|
||||
if (response.ok) {
|
||||
await fetchUsers()
|
||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
|
||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
|
||||
setShowForm(false)
|
||||
setEditingUser(null)
|
||||
} else {
|
||||
@@ -108,7 +125,8 @@ const Users = () => {
|
||||
email: user.email,
|
||||
oldPassword: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
confirmPassword: '',
|
||||
groupIds: user.groupIds || []
|
||||
})
|
||||
setShowForm(true)
|
||||
}
|
||||
@@ -142,6 +160,30 @@ const Users = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleGroupToggle = (groupId) => {
|
||||
setFormData(prev => {
|
||||
const groupIds = prev.groupIds || []
|
||||
if (groupIds.includes(groupId)) {
|
||||
return { ...prev, groupIds: groupIds.filter(id => id !== groupId) }
|
||||
} else {
|
||||
return { ...prev, groupIds: [...groupIds, groupId] }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getPermissionLabel = (permission) => {
|
||||
switch (permission) {
|
||||
case 'READ':
|
||||
return 'Lesen'
|
||||
case 'READ_WRITE':
|
||||
return 'Lesen/Schreiben'
|
||||
case 'FULL_ACCESS':
|
||||
return 'Vollzugriff'
|
||||
default:
|
||||
return permission
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
@@ -156,7 +198,7 @@ const Users = () => {
|
||||
onClick={() => {
|
||||
setShowForm(!showForm)
|
||||
setEditingUser(null)
|
||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
|
||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
|
||||
}}
|
||||
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"
|
||||
>
|
||||
@@ -284,6 +326,40 @@ const Users = () => {
|
||||
<p className="mt-1 text-xs text-green-400">✓ Passwörter stimmen überein</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-200 mb-2">
|
||||
Berechtigungsgruppen
|
||||
</label>
|
||||
<div className="bg-slate-700/50 border border-slate-600 rounded-lg p-4 max-h-60 overflow-y-auto">
|
||||
{groups.length === 0 ? (
|
||||
<p className="text-slate-400 text-sm">Keine Berechtigungsgruppen vorhanden</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{groups.map(group => (
|
||||
<label key={group.id} className="flex items-start cursor-pointer hover:bg-slate-600/50 p-2 rounded">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.groupIds?.includes(group.id) || false}
|
||||
onChange={() => handleGroupToggle(group.id)}
|
||||
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="flex items-center gap-2">
|
||||
<span className="text-slate-300 font-medium">{group.name}</span>
|
||||
<span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 rounded text-xs">
|
||||
{getPermissionLabel(group.permission)}
|
||||
</span>
|
||||
</div>
|
||||
{group.description && (
|
||||
<p className="text-xs text-slate-400 mt-1">{group.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm">
|
||||
{error}
|
||||
@@ -302,7 +378,7 @@ const Users = () => {
|
||||
onClick={() => {
|
||||
setShowForm(false)
|
||||
setEditingUser(null)
|
||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '' })
|
||||
setFormData({ username: '', email: '', oldPassword: '', password: '', confirmPassword: '', groupIds: [] })
|
||||
setError('')
|
||||
}}
|
||||
className="px-6 py-2 bg-slate-600 hover:bg-slate-700 text-white font-semibold rounded-lg transition-colors duration-200"
|
||||
@@ -347,6 +423,21 @@ const Users = () => {
|
||||
{user.username}
|
||||
</h3>
|
||||
<p className="text-slate-300 mb-2">{user.email}</p>
|
||||
{user.groupIds && user.groupIds.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<p className="text-sm text-slate-300 mb-1">Berechtigungsgruppen:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.groupIds.map(groupId => {
|
||||
const group = groups.find(g => g.id === groupId)
|
||||
return group ? (
|
||||
<span key={groupId} className="px-2 py-1 bg-blue-600/20 text-blue-300 rounded text-xs">
|
||||
{group.name} ({getPermissionLabel(group.permission)})
|
||||
</span>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-slate-400">
|
||||
Erstellt: {user.createdAt ? new Date(user.createdAt).toLocaleString('de-DE') : 'Unbekannt'}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user