diff --git a/DB_COMMANDS.md b/DB_COMMANDS.md new file mode 100644 index 0000000..506735d --- /dev/null +++ b/DB_COMMANDS.md @@ -0,0 +1,198 @@ +# SQLite Datenbank-Befehle + +## Verbindung zur Datenbank + +Die Datenbank liegt in: `./backend/spaces.db` + +### Verbindung herstellen: +```bash +cd /home/nick/Development/certigo-addon/backend +sqlite3 spaces.db +``` + +## Nützliche SQLite-Befehle + +### Tabellen auflisten: +```sql +.tables +``` + +### Schema einer Tabelle anzeigen: +```sql +.schema audit_logs +``` + +### Alle Audit-Logs anzeigen: +```sql +SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 10; +``` + +### Anzahl der Audit-Logs: +```sql +SELECT COUNT(*) FROM audit_logs; +``` + +### Letzte 5 Audit-Logs mit Details: +```sql +SELECT + timestamp, + username, + action, + resource_type, + details +FROM audit_logs +ORDER BY timestamp DESC +LIMIT 5; +``` + +### Einen spezifischen Audit-Log anzeigen (nach ID): +```sql +SELECT * FROM audit_logs WHERE id = 'LOG_ID_HIER'; +``` + +### Einen spezifischen Audit-Log anzeigen (nach Timestamp): +```sql +SELECT * FROM audit_logs WHERE timestamp = '2025-11-20 16:20:00'; +``` + +### Audit-Logs nach Aktion filtern: +```sql +SELECT * FROM audit_logs WHERE action = 'CREATE' ORDER BY timestamp DESC; +``` + +### Audit-Logs nach Ressourcentyp filtern: +```sql +SELECT * FROM audit_logs WHERE resource_type = 'user' ORDER BY timestamp DESC; +``` + +### Audit-Logs nach Benutzer filtern: +```sql +SELECT * FROM audit_logs WHERE username = 'admin' ORDER BY timestamp DESC; +``` + +### Vollständige Details eines Logs (formatiert): +```sql +SELECT + id, + timestamp, + user_id, + username, + action, + resource_type, + resource_id, + details, + ip_address, + user_agent +FROM audit_logs +WHERE id = 'LOG_ID_HIER'; +``` + +### Neueste Logs mit formatierten Details: +```sql +SELECT + datetime(timestamp) as zeit, + username, + action, + resource_type, + json_extract(details, '$.message') as nachricht, + ip_address +FROM audit_logs +ORDER BY datetime(timestamp) DESC +LIMIT 10; +``` + +### Logs der letzten Stunde: +```sql +SELECT * FROM audit_logs +WHERE datetime(timestamp) >= datetime('now', '-1 hour') +ORDER BY timestamp DESC; +``` + +### Logs von heute: +```sql +SELECT * FROM audit_logs +WHERE date(timestamp) = date('now') +ORDER BY timestamp DESC; +``` + +### Alle Tabellen auflisten: +```sql +SELECT name FROM sqlite_master WHERE type='table'; +``` + +### Prüfen ob audit_logs Tabelle existiert: +```sql +SELECT name FROM sqlite_master WHERE type='table' AND name='audit_logs'; +``` + +### Alle Spalten der audit_logs Tabelle: +```sql +PRAGMA table_info(audit_logs); +``` + +### SQLite verlassen: +```sql +.quit +``` +oder +```sql +.exit +``` + +## Direkte Befehle (ohne interaktive Shell) + +### Anzahl der Logs: +```bash +sqlite3 backend/spaces.db "SELECT COUNT(*) FROM audit_logs;" +``` + +### Letzte 5 Logs: +```bash +sqlite3 backend/spaces.db "SELECT timestamp, username, action, resource_type, details FROM audit_logs ORDER BY timestamp DESC LIMIT 5;" +``` + +### Prüfen ob Tabelle existiert: +```bash +sqlite3 backend/spaces.db "SELECT name FROM sqlite_master WHERE type='table' AND name='audit_logs';" +``` + +### Alle Logs als CSV exportieren: +```bash +sqlite3 -header -csv backend/spaces.db "SELECT * FROM audit_logs ORDER BY timestamp DESC;" > audit_logs.csv +``` + +### Einen spezifischen Log anzeigen (Beispiel): +```bash +sqlite3 backend/spaces.db "SELECT * FROM audit_logs WHERE id = '5d424293-14c1-48ef-a34c-5555d843d289';" +``` + +### Neueste 10 Logs mit Details: +```bash +sqlite3 -header -column backend/spaces.db "SELECT timestamp, username, action, resource_type, details FROM audit_logs ORDER BY timestamp DESC LIMIT 10;" +``` + +### Logs nach Aktion filtern: +```bash +sqlite3 -header -column backend/spaces.db "SELECT * FROM audit_logs WHERE action = 'CREATE' ORDER BY timestamp DESC LIMIT 10;" +``` + +### Logs nach Benutzer filtern: +```bash +sqlite3 -header -column backend/spaces.db "SELECT * FROM audit_logs WHERE username = 'admin' ORDER BY timestamp DESC LIMIT 10;" +``` + +### Logs von heute: +```bash +sqlite3 -header -column backend/spaces.db "SELECT * FROM audit_logs WHERE date(timestamp) = date('now') ORDER BY timestamp DESC;" +``` + +### JSON-Details eines Logs formatiert anzeigen: +```bash +sqlite3 backend/spaces.db "SELECT json_pretty(details) FROM audit_logs WHERE id = 'LOG_ID_HIER';" +``` + +**Hinweis**: Falls `json_pretty` nicht verfügbar ist, verwende stattdessen: +```bash +sqlite3 backend/spaces.db "SELECT details FROM audit_logs WHERE id = 'LOG_ID_HIER';" | python3 -m json.tool +``` + diff --git a/backend/go.mod b/backend/go.mod index b9219e3..879097f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,9 +1,13 @@ module certigo-addon-backend -go 1.21 +go 1.24.0 + +toolchain go1.24.10 require ( github.com/google/uuid v1.5.0 github.com/gorilla/mux v1.8.1 github.com/mattn/go-sqlite3 v1.14.18 ) + +require golang.org/x/crypto v0.45.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index e5d0ccc..15da475 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -4,3 +4,5 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= diff --git a/backend/internal/core/audit.go b/backend/internal/core/audit.go new file mode 100644 index 0000000..ad7637b --- /dev/null +++ b/backend/internal/core/audit.go @@ -0,0 +1,126 @@ +package core + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/google/uuid" +) + +var berlinLocation *time.Location + +func init() { + var err error + berlinLocation, err = time.LoadLocation("Europe/Berlin") + if err != nil { + // Fallback zu UTC falls Europe/Berlin nicht verfügbar ist + log.Printf("Warnung: Europe/Berlin Zeitzone nicht verfügbar, verwende UTC: %v", err) + berlinLocation = time.UTC + } +} + +// AuditService handles audit logging +type AuditService struct { + db *sql.DB +} + +// NewAuditService creates a new AuditService instance +func NewAuditService(db *sql.DB) *AuditService { + return &AuditService{ + db: db, + } +} + +// Track logs an audit event asynchronously +// ctx: context for the operation +// action: the action performed (e.g., "CREATE", "UPDATE", "DELETE") +// entity: the entity type (e.g., "user", "space", "fqdn", "csr") +// entityID: the ID of the entity (optional) +// userID: the ID of the user performing the action (optional) +// username: the username of the user performing the action (optional) +// details: additional details as a map (will be stored as JSON) +// ipAddress: IP address of the request (optional) +// userAgent: User-Agent header (optional) +func (s *AuditService) Track(ctx context.Context, action, entity, entityID, userID, username string, details map[string]interface{}, ipAddress, userAgent string) { + // Check if service is nil (should not happen, but safety check) + if s == nil { + log.Printf("Warnung: AuditService ist nil, kann kein Audit-Log schreiben") + return + } + + // Check if database is available + if s.db == nil { + log.Printf("Warnung: Datenbank ist nil im AuditService, kann kein Audit-Log schreiben") + return + } + + // Execute asynchronously in a goroutine to not block the main operation + go func() { + if err := s.trackSync(ctx, action, entity, entityID, userID, username, details, ipAddress, userAgent); err != nil { + // Log errors but don't fail the main operation + log.Printf("Fehler beim Schreiben des Audit-Logs: %v", err) + } + }() +} + +// trackSync performs the actual database write synchronously +func (s *AuditService) trackSync(ctx context.Context, action, entity, entityID, userID, username string, details map[string]interface{}, ipAddress, userAgent string) error { + // Safety check: ensure service and database are not nil + if s == nil { + return fmt.Errorf("AuditService ist nil") + } + if s.db == nil { + return fmt.Errorf("Datenbank ist nil im AuditService") + } + + // Generate unique ID for the log entry + logID := uuid.New().String() + + // Format timestamp for SQLite - verwende Europe/Berlin Zeitzone + // Speichere als ISO-String mit Zeitzone für bessere Kompatibilität + // Format: YYYY-MM-DD HH:MM:SS (SQLite DATETIME Format) + // Die Zeit wird in Europe/Berlin Zeitzone gespeichert + now := time.Now().In(berlinLocation) + timestamp := now.Format("2006-01-02 15:04:05") + + // Marshal details to JSON + var detailsJSON string + if details != nil && len(details) > 0 { + jsonBytes, err := json.Marshal(details) + if err != nil { + return fmt.Errorf("Fehler beim Marshalling der Details: %w", err) + } + detailsJSON = string(jsonBytes) + } + + // Create context with timeout for database operation + dbCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + // Insert audit log entry + result, err := s.db.ExecContext(dbCtx, + "INSERT INTO audit_logs (id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + logID, timestamp, userID, username, action, entity, entityID, detailsJSON, ipAddress, userAgent) + + if err != nil { + return fmt.Errorf("Fehler beim INSERT: %w", err) + } + + // Verify that the insert was successful + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Fehler beim Prüfen der RowsAffected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("Keine Zeile eingefügt") + } + + log.Printf("Audit-Log geschrieben: %s %s %s (ID: %s)", action, entity, entityID, logID) + return nil +} + diff --git a/backend/main.go b/backend/main.go index 1f8359f..28c51ca 100644 --- a/backend/main.go +++ b/backend/main.go @@ -7,18 +7,24 @@ import ( "encoding/asn1" "encoding/hex" "encoding/json" + "encoding/base64" "encoding/pem" "fmt" "io" "log" "net/http" + "os" + "path/filepath" + "strconv" "strings" "time" "github.com/google/uuid" "github.com/gorilla/mux" _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" + "certigo-addon-backend/internal/core" "certigo-addon-backend/providers" ) @@ -249,7 +255,50 @@ type CSR struct { CreatedAt string `json:"createdAt"` } +// User struct für Benutzer +type User struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + CreatedAt string `json:"createdAt"` +} + +// CreateUserRequest struct für Benutzer-Erstellung +type CreateUserRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +// 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"` +} + +// MessageResponse struct für einfache Nachrichten +type MessageResponse struct { + Message string `json:"message"` +} + +// AuditLog struct für Audit-Logs +type AuditLog struct { + ID string `json:"id"` + Timestamp string `json:"timestamp"` + UserID string `json:"userId,omitempty"` + Username string `json:"username,omitempty"` + Action string `json:"action"` + ResourceType string `json:"resourceType"` + ResourceID string `json:"resourceId,omitempty"` + Details string `json:"details,omitempty"` + IPAddress string `json:"ipAddress,omitempty"` + UserAgent string `json:"userAgent,omitempty"` +} + var db *sql.DB +var auditService *core.AuditService func initDB() { var err error @@ -439,7 +488,140 @@ func initDB() { log.Fatal("Fehler beim Erstellen der Zertifikat-Tabelle:", err) } + // Erstelle Users-Tabelle + log.Println("Erstelle users-Tabelle...") + createUsersTableSQL := ` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at DATETIME NOT NULL + );` + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, createUsersTableSQL) + 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 (z.B. andere go run main.go Instanzen).") + } + log.Fatal("Fehler beim Erstellen der Users-Tabelle:", err) + } + log.Println("Datenbank erfolgreich initialisiert") + + // Erstelle Audit-Log-Tabelle + log.Println("Erstelle audit_logs-Tabelle...") + createAuditLogsTableSQL := ` + CREATE TABLE IF NOT EXISTS audit_logs ( + id TEXT PRIMARY KEY, + timestamp DATETIME NOT NULL, + user_id TEXT, + username TEXT, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + details TEXT, + ip_address TEXT, + user_agent TEXT + );` + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, createAuditLogsTableSQL) + 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 (z.B. andere go run main.go Instanzen).") + } + log.Fatal("Fehler beim Erstellen der Audit-Log-Tabelle:", err) + } + + // Erstelle Index für bessere Performance + log.Println("Erstelle Indizes für audit_logs...") + createIndexSQL := ` + CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp DESC); + CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id); + CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action); + CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_type ON audit_logs(resource_type);` + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + _, err = db.ExecContext(ctx, createIndexSQL) + cancel() + if err != nil { + log.Printf("Warnung: Fehler beim Erstellen der Indizes: %v", err) + } + + // Erstelle Default Admin-User falls nicht vorhanden + createDefaultAdmin() + + // Initialisiere AuditService (muss nach DB-Initialisierung passieren) + auditService = core.NewAuditService(db) + if auditService == nil { + log.Fatal("Fehler: AuditService konnte nicht initialisiert werden") + } + log.Println("AuditService erfolgreich initialisiert") + + // Erstelle Upload-Ordner für Profilbilder + avatarDir := "uploads/avatars" + if err := os.MkdirAll(avatarDir, 0755); err != nil { + log.Printf("Warnung: Konnte Avatar-Ordner nicht erstellen: %v", err) + } else { + log.Printf("Avatar-Ordner erstellt: %s", avatarDir) + } +} + +func createDefaultAdmin() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + // Prüfe ob bereits ein Admin-User existiert + var count int + err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count) + if err != nil { + log.Printf("Fehler beim Prüfen des Admin-Users: %v", err) + return + } + + if count > 0 { + log.Println("Admin-User existiert bereits") + // Prüfe ob das Passwort noch "admin" ist (für Debugging) + var storedHash string + err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = 'admin'").Scan(&storedHash) + if err == nil { + // Teste ob das Passwort "admin" ist + testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin")) + if testErr == nil { + log.Println("Admin-User Passwort ist korrekt gesetzt") + } else { + log.Println("Warnung: Admin-User Passwort ist nicht 'admin'") + } + } + return + } + + // Erstelle Default Admin-User + adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) + if err != nil { + log.Printf("Fehler beim Hashen des Admin-Passworts: %v", err) + return + } + + adminID := uuid.New().String() + createdAt := time.Now().Format(time.RFC3339) + + _, err = db.ExecContext(ctx, + "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)", + adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt) + if err != nil { + log.Printf("Fehler beim Erstellen des Admin-Users: %v", err) + return + } + + log.Println("✓ Default Admin-User erstellt: username='admin', password='admin'") + log.Printf(" User ID: %s", adminID) + log.Printf(" Email: admin@certigo.local") } func healthHandler(w http.ResponseWriter, r *http.Request) { @@ -628,6 +810,17 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newSpace) + + // Audit-Log: Space erstellt + if auditService != nil { + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "CREATE", "space", id, userID, username, map[string]interface{}{ + "name": req.Name, + "description": req.Description, + "message": fmt.Sprintf("Space erstellt: %s", req.Name), + }, ipAddress, userAgent) + } } func deleteSpaceHandler(w http.ResponseWriter, r *http.Request) { @@ -732,6 +925,18 @@ func deleteSpaceHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Space erfolgreich gelöscht"}) + + // Audit-Log: Space gelöscht + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + details := map[string]interface{}{ + "message": fmt.Sprintf("Space gelöscht: %s", id), + } + if deleteFqdns && fqdnCount > 0 { + details["fqdnsDeleted"] = fqdnCount + details["message"] = fmt.Sprintf("Space gelöscht: %s (mit %d FQDNs)", id, fqdnCount) + } + auditService.Track(r.Context(), "DELETE", "space", id, userID, username, details, ipAddress, userAgent) } func getSpaceFqdnCountHandler(w http.ResponseWriter, r *http.Request) { @@ -921,6 +1126,16 @@ func createFqdnHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newFqdn) + + // Audit-Log: FQDN erstellt + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "CREATE", "fqdn", id, userID, username, map[string]interface{}{ + "fqdn": req.FQDN, + "spaceId": spaceID, + "description": req.Description, + "message": fmt.Sprintf("FQDN erstellt: %s (Space: %s)", req.FQDN, spaceID), + }, ipAddress, userAgent) } func deleteFqdnHandler(w http.ResponseWriter, r *http.Request) { @@ -1007,6 +1222,14 @@ func deleteFqdnHandler(w http.ResponseWriter, r *http.Request) { log.Printf("FQDN %s und zugehörige CSRs erfolgreich gelöscht", fqdnID) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "FQDN erfolgreich gelöscht"}) + + // Audit-Log: FQDN gelöscht + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "DELETE", "fqdn", fqdnID, userID, username, map[string]interface{}{ + "spaceId": spaceID, + "message": fmt.Sprintf("FQDN gelöscht (Space: %s)", spaceID), + }, ipAddress, userAgent) } func deleteAllFqdnsHandler(w http.ResponseWriter, r *http.Request) { @@ -1423,6 +1646,15 @@ func uploadCSRHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newCSR) + + // Audit-Log: CSR hochgeladen + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "UPLOAD", "csr", csrID, userID, username, map[string]interface{}{ + "fqdnId": fqdnID, + "spaceId": spaceID, + "message": fmt.Sprintf("CSR hochgeladen für FQDN: %s (Space: %s)", fqdnID, spaceID), + }, ipAddress, userAgent) } func getCSRByFQDNHandler(w http.ResponseWriter, r *http.Request) { @@ -2149,6 +2381,1125 @@ components: w.Write([]byte(openAPIContent)) } +// User Handler Functions + +func getUsersHandler(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*5) + defer cancel() + + 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) + log.Printf("Fehler beim Abrufen der Benutzer: %v", err) + return + } + defer rows.Close() + + var users []User + for rows.Next() { + var user User + err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) + if err != nil { + http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError) + log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err) + return + } + users = append(users, user) + } + + json.NewEncoder(w).Encode(users) +} + +func getUserHandler(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) + userID := vars["id"] + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + var user User + err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). + Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) + 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 + } + + json.NewEncoder(w).Encode(user) +} + +func createUserHandler(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 CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) + return + } + + // Validierung + if req.Username == "" || req.Email == "" || req.Password == "" { + http.Error(w, "Benutzername, E-Mail und Passwort sind erforderlich", http.StatusBadRequest) + return + } + + // Passwortrichtlinie prüfen + if err := validatePassword(req.Password); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + // Passwort hashen + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError) + log.Printf("Fehler beim Hashen des Passworts: %v", err) + return + } + + // Erstelle Benutzer + userID := uuid.New().String() + createdAt := time.Now().Format(time.RFC3339) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, err = db.ExecContext(ctx, + "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)", + userID, req.Username, req.Email, string(hashedPassword), createdAt) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + if strings.Contains(err.Error(), "username") { + http.Error(w, "Benutzername bereits vergeben", http.StatusConflict) + } else { + http.Error(w, "E-Mail-Adresse bereits vergeben", http.StatusConflict) + } + return + } + http.Error(w, "Fehler beim Erstellen des Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Erstellen des Benutzers: %v", err) + return + } + + user := User{ + ID: userID, + Username: req.Username, + Email: req.Email, + CreatedAt: createdAt, + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(user) + + // Audit-Log: User erstellt + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{ + "username": req.Username, + "email": req.Email, + "message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email), + }, ipAddress, userAgent) +} + +func updateUserHandler(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) + userID := vars["id"] + + var req UpdateUserRequest + 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*5) + defer cancel() + + // Prüfe ob Benutzer existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists) + if err != nil || !exists { + http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) + return + } + + // Update Felder + updates := []string{} + args := []interface{}{} + + if req.Username != "" { + updates = append(updates, "username = ?") + args = append(args, req.Username) + } + if req.Email != "" { + updates = append(updates, "email = ?") + args = append(args, req.Email) + } + if req.Password != "" { + // Altes Passwort ist erforderlich, wenn Passwort geändert wird + if req.OldPassword == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Altes Passwort ist erforderlich, um das Passwort zu ändern"}) + return + } + + // Hole aktuelles Passwort-Hash aus der Datenbank + var storedHash string + err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = ?", userID).Scan(&storedHash) + if err != nil { + http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen des Benutzers: %v", err) + return + } + + // Validiere altes Passwort + err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(req.OldPassword)) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Altes Passwort ist falsch"}) + return + } + + // Passwortrichtlinie prüfen + if err := validatePassword(req.Password); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError) + log.Printf("Fehler beim Hashen des Passworts: %v", err) + return + } + updates = append(updates, "password_hash = ?") + args = append(args, string(hashedPassword)) + } + + if len(updates) == 0 { + http.Error(w, "Keine Felder zum Aktualisieren", http.StatusBadRequest) + return + } + + args = append(args, userID) + query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", strings.Join(updates, ", ")) + + _, err = db.ExecContext(ctx, query, args...) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + if strings.Contains(err.Error(), "username") { + http.Error(w, "Benutzername bereits vergeben", http.StatusConflict) + } else { + http.Error(w, "E-Mail-Adresse bereits vergeben", http.StatusConflict) + } + return + } + http.Error(w, "Fehler beim Aktualisieren des Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Aktualisieren des Benutzers: %v", err) + return + } + + // Lade aktualisierten Benutzer + var user User + err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID). + Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt) + if err != nil { + http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err) + return + } + + json.NewEncoder(w).Encode(user) + + // Audit-Log: User aktualisiert + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + details := map[string]interface{}{} + if req.Username != "" { + details["username"] = req.Username + } + if req.Email != "" { + details["email"] = req.Email + } + if req.Password != "" { + details["passwordChanged"] = true + } + auditService.Track(r.Context(), "UPDATE", "user", vars["id"], userID, username, details, ipAddress, userAgent) +} + +func deleteUserHandler(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) + userID := vars["id"] + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID) + if err != nil { + http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError) + log.Printf("Fehler beim Löschen des Benutzers: %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, "Benutzer nicht gefunden", http.StatusNotFound) + return + } + + response := MessageResponse{Message: "Benutzer erfolgreich gelöscht"} + json.NewEncoder(w).Encode(response) + + // Audit-Log: User gelöscht + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "DELETE", "user", vars["id"], userID, username, map[string]interface{}{ + "message": fmt.Sprintf("User gelöscht: %s", vars["id"]), + }, ipAddress, userAgent) +} + +// Profilbild-Upload Handler +func uploadAvatarHandler(w http.ResponseWriter, r *http.Request) { + 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 + } + + // Prüfe ob Benutzer authentifiziert ist + userID, username := getUserFromRequest(r) + if userID == "" { + http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + requestedUserID := vars["id"] + + // Prüfe ob Benutzer sein eigenes Profilbild hochlädt + if userID != requestedUserID { + http.Error(w, "Sie können nur Ihr eigenes Profilbild ändern", http.StatusForbidden) + return + } + + // Parse multipart form (max 10MB) + err := r.ParseMultipartForm(10 << 20) // 10MB + if err != nil { + http.Error(w, "Fehler beim Parsen des Formulars", http.StatusBadRequest) + return + } + + file, handler, err := r.FormFile("avatar") + if err != nil { + http.Error(w, "Keine Datei gefunden", http.StatusBadRequest) + return + } + defer file.Close() + + // Validiere Dateityp (nur Bilder) + allowedTypes := []string{"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"} + fileType := handler.Header.Get("Content-Type") + isAllowed := false + for _, allowedType := range allowedTypes { + if fileType == allowedType { + isAllowed = true + break + } + } + + if !isAllowed { + http.Error(w, "Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt", http.StatusBadRequest) + return + } + + // Bestimme Dateiendung basierend auf Content-Type + var ext string + switch fileType { + case "image/jpeg", "image/jpg": + ext = ".jpg" + case "image/png": + ext = ".png" + case "image/gif": + ext = ".gif" + case "image/webp": + ext = ".webp" + default: + ext = ".jpg" + } + + // Erstelle Dateiname basierend auf User-ID + avatarDir := "uploads/avatars" + filename := userID + ext + avatarPath := filepath.Join(avatarDir, filename) + + // Lösche alle vorhandenen Avatar-Dateien für diesen Benutzer + extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} + for _, oldExt := range extensions { + oldPath := filepath.Join(avatarDir, userID+oldExt) + if _, err := os.Stat(oldPath); err == nil { + // Datei existiert, lösche sie + if err := os.Remove(oldPath); err != nil { + log.Printf("Warnung: Konnte alte Avatar-Datei nicht löschen: %v", err) + // Weiter machen, auch wenn Löschen fehlschlägt + } else { + log.Printf("Alte Avatar-Datei gelöscht: %s", oldPath) + } + } + } + + // Erstelle Datei + dst, err := os.Create(avatarPath) + if err != nil { + http.Error(w, "Fehler beim Erstellen der Datei", http.StatusInternalServerError) + log.Printf("Fehler beim Erstellen der Avatar-Datei: %v", err) + return + } + defer dst.Close() + + // Kopiere Dateiinhalt + _, err = io.Copy(dst, file) + if err != nil { + http.Error(w, "Fehler beim Speichern der Datei", http.StatusInternalServerError) + log.Printf("Fehler beim Speichern der Avatar-Datei: %v", err) + return + } + + // Audit-Log + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "UPDATE", "user", userID, userID, username, map[string]interface{}{ + "action": "avatar_uploaded", + "filename": filename, + }, ipAddress, userAgent) + + // Erfolgreiche Antwort + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Profilbild erfolgreich hochgeladen", + "filename": filename, + "url": fmt.Sprintf("/api/users/%s/avatar", userID), + }) +} + +// Profilbild-Abruf Handler +func getAvatarHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + userID := vars["id"] + + // Suche nach Avatar-Datei (versuche verschiedene Formate) + avatarDir := "uploads/avatars" + extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} + + var avatarPath string + var found bool + for _, ext := range extensions { + path := filepath.Join(avatarDir, userID+ext) + if _, err := os.Stat(path); err == nil { + avatarPath = path + found = true + break + } + } + + if !found { + http.Error(w, "Profilbild nicht gefunden", http.StatusNotFound) + return + } + + // Öffne Datei + file, err := os.Open(avatarPath) + if err != nil { + http.Error(w, "Fehler beim Öffnen der Datei", http.StatusInternalServerError) + return + } + defer file.Close() + + // Bestimme Content-Type basierend auf Dateiendung + ext := filepath.Ext(avatarPath) + var contentType string + switch ext { + case ".jpg", ".jpeg": + contentType = "image/jpeg" + case ".png": + contentType = "image/png" + case ".gif": + contentType = "image/gif" + case ".webp": + contentType = "image/webp" + default: + contentType = "image/jpeg" + } + + w.Header().Set("Content-Type", contentType) + w.Header().Set("Cache-Control", "public, max-age=3600") + io.Copy(w, file) +} + +// Passwortvalidierung nach Richtlinien +func validatePassword(password string) error { + if len(password) < 8 { + return fmt.Errorf("Passwort muss mindestens 8 Zeichen lang sein") + } + + hasUpper := false + hasLower := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + switch { + case 'A' <= char && char <= 'Z': + hasUpper = true + case 'a' <= char && char <= 'z': + hasLower = true + case '0' <= char && char <= '9': + hasDigit = true + default: + // Prüfe auf Sonderzeichen (alles was nicht Buchstabe oder Zahl ist) + if !(('A' <= char && char <= 'Z') || ('a' <= char && char <= 'z') || ('0' <= char && char <= '9')) { + hasSpecial = true + } + } + } + + var missing []string + if !hasUpper { + missing = append(missing, "Großbuchstaben") + } + if !hasLower { + missing = append(missing, "Kleinbuchstaben") + } + if !hasDigit { + missing = append(missing, "Zahlen") + } + if !hasSpecial { + missing = append(missing, "Sonderzeichen") + } + + if len(missing) > 0 { + return fmt.Errorf("Passwort muss enthalten: %s", strings.Join(missing, ", ")) + } + + return nil +} + +// Helper-Funktion zum Extrahieren des Benutzers aus dem Request (für Basic Auth) +func getUserFromRequest(r *http.Request) (userID, username string) { + auth := r.Header.Get("Authorization") + if auth == "" || !strings.HasPrefix(auth, "Basic ") { + return "", "" + } + + encoded := strings.TrimPrefix(auth, "Basic ") + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return "", "" + } + + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + return "", "" + } + + username = parts[0] + + // Hole User-ID aus der Datenbank + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + + var id string + err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id) + if err != nil { + return "", username + } + + return id, username +} + +// Helper-Funktion zum Extrahieren von IP-Adresse und User-Agent aus Request +func getRequestInfo(r *http.Request) (ipAddress, userAgent string) { + // Hole IP-Adresse + ipAddress = r.RemoteAddr + if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { + ipAddress = forwarded + } else if realIP := r.Header.Get("X-Real-IP"); realIP != "" { + ipAddress = realIP + } + + // Hole User-Agent + userAgent = r.Header.Get("User-Agent") + + // Prüfe, ob es sich um einen API-Aufruf handelt + // API-Aufrufe haben entweder keinen User-Agent oder einen speziellen Header + if userAgent == "" || r.Header.Get("X-API-Request") == "true" || r.Header.Get("X-Request-Source") == "api" { + userAgent = "API" + } + + return ipAddress, userAgent +} + +// Audit Log Handler +func getAuditLogsHandler(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 + } + + // Query-Parameter für Filterung und Pagination + query := r.URL.Query() + limitStr := query.Get("limit") + offsetStr := query.Get("offset") + actionFilter := query.Get("action") + resourceTypeFilter := query.Get("resourceType") + userIdFilter := query.Get("userId") + + // Standardwerte für Pagination + limit := 100 + offset := 0 + + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { + limit = l + } + } + + if offsetStr != "" { + if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { + offset = o + } + } + + // Baue SQL-Query mit Filtern + whereClauses := []string{} + args := []interface{}{} + + if actionFilter != "" { + whereClauses = append(whereClauses, "action = ?") + args = append(args, actionFilter) + } + + if resourceTypeFilter != "" { + whereClauses = append(whereClauses, "resource_type = ?") + args = append(args, resourceTypeFilter) + } + + if userIdFilter != "" { + whereClauses = append(whereClauses, "user_id = ?") + args = append(args, userIdFilter) + } + + whereSQL := "" + if len(whereClauses) > 0 { + whereSQL = "WHERE " + strings.Join(whereClauses, " AND ") + } + + // Zähle Gesamtanzahl für Pagination + var totalCount int + countSQL := "SELECT COUNT(*) FROM audit_logs " + whereSQL + countCtx, countCancel := context.WithTimeout(context.Background(), time.Second*5) + defer countCancel() + err := db.QueryRowContext(countCtx, countSQL, args...).Scan(&totalCount) + if err != nil { + http.Error(w, "Fehler beim Zählen der Logs", http.StatusInternalServerError) + log.Printf("Fehler beim Zählen der Logs: %v", err) + return + } + + // Hole Logs - Verwende Prepared Statement für bessere Kompatibilität + var querySQL string + var queryArgs []interface{} + + if whereSQL != "" { + querySQL = fmt.Sprintf(` + SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent + FROM audit_logs + %s + ORDER BY datetime(timestamp) DESC, id DESC + LIMIT ? OFFSET ? + `, whereSQL) + queryArgs = make([]interface{}, len(args)) + copy(queryArgs, args) + queryArgs = append(queryArgs, limit, offset) + } else { + querySQL = ` + SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent + FROM audit_logs + ORDER BY datetime(timestamp) DESC, id DESC + LIMIT ? OFFSET ? + ` + queryArgs = []interface{}{limit, offset} + } + + queryCtx, queryCancel := context.WithTimeout(context.Background(), time.Second*10) + defer queryCancel() // Cancel wird erst aufgerufen, wenn die Funktion beendet ist + + rows, err := db.QueryContext(queryCtx, querySQL, queryArgs...) + if err != nil { + http.Error(w, "Fehler beim Abrufen der Logs", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Logs: %v", err) + return + } + defer rows.Close() + + log.Printf("SQL Query: %s, Args: %v (Count: %d)", querySQL, queryArgs, len(queryArgs)) + + var logs []AuditLog + rowCount := 0 + scanErrors := 0 + for rows.Next() { + rowCount++ + var logEntry AuditLog + var userID, username, resourceID, detailsJSON, ipAddress, userAgent sql.NullString + + err := rows.Scan( + &logEntry.ID, + &logEntry.Timestamp, + &userID, + &username, + &logEntry.Action, + &logEntry.ResourceType, + &resourceID, + &detailsJSON, + &ipAddress, + &userAgent, + ) + if err != nil { + scanErrors++ + log.Printf("Fehler beim Scannen der Log-Zeile %d: %v", rowCount, err) + continue + } + + if userID.Valid { + logEntry.UserID = userID.String + } + if username.Valid { + logEntry.Username = username.String + } + if resourceID.Valid { + logEntry.ResourceID = resourceID.String + } + // Parse JSON details + if detailsJSON.Valid && detailsJSON.String != "" { + var detailsMap map[string]interface{} + if err := json.Unmarshal([]byte(detailsJSON.String), &detailsMap); err == nil { + // Convert map to JSON string for display + if jsonBytes, err := json.Marshal(detailsMap); err == nil { + logEntry.Details = string(jsonBytes) + } else { + logEntry.Details = detailsJSON.String + } + } else { + // Fallback: use raw string if JSON parsing fails + logEntry.Details = detailsJSON.String + } + } + if ipAddress.Valid { + logEntry.IPAddress = ipAddress.String + } + if userAgent.Valid { + logEntry.UserAgent = userAgent.String + } + + logs = append(logs, logEntry) + } + + if err := rows.Err(); err != nil { + log.Printf("Fehler beim Iterieren über die Zeilen: %v", err) + } + + 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, + } + + log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// Handler zum Löschen aller Audit-Logs +func deleteAllAuditLogsHandler(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 + } + + // Prüfe Bestätigung + confirm := r.URL.Query().Get("confirm") + if confirm != "true" { + http.Error(w, "Bestätigung erforderlich. Verwende ?confirm=true", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // Lösche alle Audit-Logs + result, err := db.ExecContext(ctx, "DELETE FROM audit_logs") + if err != nil { + http.Error(w, "Fehler beim Löschen der Audit-Logs", http.StatusInternalServerError) + log.Printf("Fehler beim Löschen der Audit-Logs: %v", err) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) + log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) + return + } + + log.Printf("Alle Audit-Logs gelöscht: %d Einträge", rowsAffected) + + response := map[string]interface{}{ + "message": "Alle Audit-Logs erfolgreich gelöscht", + "deletedCount": rowsAffected, + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// Handler zum Erstellen eines Test-Audit-Logs (für Testskripte) +func createTestAuditLogHandler(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 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"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) + return + } + + if req.Action == "" || req.Entity == "" { + http.Error(w, "action und entity sind erforderlich", http.StatusBadRequest) + return + } + + // Verwende AuditService zum Erstellen des Logs + if auditService == nil { + http.Error(w, "AuditService nicht initialisiert", http.StatusInternalServerError) + return + } + + // Erstelle Context für die Anfrage + ctx := r.Context() + + // Track das Event + auditService.Track(ctx, req.Action, req.Entity, req.EntityID, req.UserID, req.Username, req.Details, req.IPAddress, req.UserAgent) + + response := map[string]interface{}{ + "success": true, + "message": "Test-Audit-Log erstellt", + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// Basic Auth Middleware +func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // OPTIONS-Requests erlauben (für CORS) + if r.Method == "OPTIONS" { + next(w, r) + return + } + + // Prüfe ob es ein AJAX/Fetch-Request ist (kein Browser-Basic-Auth-Dialog) + isAjaxRequest := r.Header.Get("X-Requested-With") == "XMLHttpRequest" || + strings.Contains(r.Header.Get("Content-Type"), "application/json") || + r.Header.Get("Accept") == "application/json" || + strings.HasPrefix(r.URL.Path, "/api/") + + // Prüfe Authorization Header + auth := r.Header.Get("Authorization") + if auth == "" { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Authentifizierung erforderlich"}) + return + } + + // Parse Basic Auth + if !strings.HasPrefix(auth, "Basic ") { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) + return + } + + // Decode Base64 + encoded := strings.TrimPrefix(auth, "Basic ") + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) + return + } + + // Split username:password + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) + return + } + + username := parts[0] + password := parts[1] + + // Validiere Benutzer + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + var storedHash string + err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(&storedHash) + if err != nil { + if err == sql.ErrNoRows { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Fehler bei der Authentifizierung"}) + log.Printf("Fehler bei der Authentifizierung: %v", err) + return + } + + // Prüfe Passwort + err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) + if err != nil { + if !isAjaxRequest { + w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) + return + } + + // Authentifizierung erfolgreich - weiterleiten + next(w, r) + } +} + +// Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück) +func loginHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + 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, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + // Prüfe Authorization Header + auth := r.Header.Get("Authorization") + if auth == "" { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Authentifizierung erforderlich"}) + return + } + + // Parse Basic Auth + if !strings.HasPrefix(auth, "Basic ") { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) + return + } + + // Decode Base64 + encoded := strings.TrimPrefix(auth, "Basic ") + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + log.Printf("Fehler beim Decodieren der Basic Auth: %v", err) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) + return + } + + // Split username:password + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + log.Printf("Ungültiges Format in Basic Auth: %s", string(decoded)) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) + return + } + + username := parts[0] + password := parts[1] + + log.Printf("Login-Versuch für Benutzer: %s", username) + + // Validiere Benutzer + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + var user User + var storedHash string + err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username). + Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt) + if err != nil { + if err == sql.ErrNoRows { + log.Printf("Benutzer nicht gefunden: %s", username) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) + return + } + log.Printf("Fehler beim Abrufen des Benutzers: %v", err) + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Fehler bei der Authentifizierung"}) + return + } + + // Prüfe Passwort + err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) + if err != nil { + log.Printf("Passwort-Validierung fehlgeschlagen für Benutzer: %s", username) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) + return + } + + log.Printf("Login erfolgreich für Benutzer: %s", username) + + // Login erfolgreich + response := map[string]interface{}{ + "success": true, + "user": user, + "message": "Login erfolgreich", + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + func main() { log.Println("Starte certigo-addon Backend...") @@ -2175,8 +3526,13 @@ func main() { // API Routes api := r.PathPrefix("/api").Subrouter() + + // Public Routes (keine Auth erforderlich) api.HandleFunc("/health", healthHandler).Methods("GET", "OPTIONS") - api.HandleFunc("/stats", getStatsHandler).Methods("GET", "OPTIONS") + api.HandleFunc("/login", loginHandler).Methods("POST", "OPTIONS") + + // Protected Routes (Basic Auth erforderlich) + api.HandleFunc("/stats", basicAuthMiddleware(getStatsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", getSpacesHandler).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", createSpaceHandler).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{id}", deleteSpaceHandler).Methods("DELETE", "OPTIONS") @@ -2190,15 +3546,29 @@ func main() { api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", getCSRByFQDNHandler).Methods("GET", "OPTIONS") api.HandleFunc("/csrs", deleteAllCSRsHandler).Methods("DELETE", "OPTIONS") - // Provider Routes - api.HandleFunc("/providers", getProvidersHandler).Methods("GET", "OPTIONS") - api.HandleFunc("/providers/{id}", getProviderHandler).Methods("GET", "OPTIONS") - api.HandleFunc("/providers/{id}/enabled", setProviderEnabledHandler).Methods("PUT", "OPTIONS") - api.HandleFunc("/providers/{id}/config", updateProviderConfigHandler).Methods("PUT", "OPTIONS") - api.HandleFunc("/providers/{id}/test", testProviderConnectionHandler).Methods("POST", "OPTIONS") - api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr/sign", signCSRHandler).Methods("POST", "OPTIONS") - api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates", getCertificatesHandler).Methods("GET", "OPTIONS") - api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates/{certId}/refresh", refreshCertificateHandler).Methods("POST", "OPTIONS") + // User Routes + api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS") + api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS") + api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS") + api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS") + api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "OPTIONS") + api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS") + + // Provider Routes (Protected) + api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/providers/{id}", basicAuthMiddleware(getProviderHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/providers/{id}/enabled", basicAuthMiddleware(setProviderEnabledHandler)).Methods("PUT", "OPTIONS") + api.HandleFunc("/providers/{id}/config", basicAuthMiddleware(updateProviderConfigHandler)).Methods("PUT", "OPTIONS") + api.HandleFunc("/providers/{id}/test", basicAuthMiddleware(testProviderConnectionHandler)).Methods("POST", "OPTIONS") + api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr/sign", basicAuthMiddleware(signCSRHandler)).Methods("POST", "OPTIONS") + api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates", basicAuthMiddleware(getCertificatesHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates/{certId}/refresh", basicAuthMiddleware(refreshCertificateHandler)).Methods("POST", "OPTIONS") + + // Audit Log Routes + api.HandleFunc("/audit-logs", basicAuthMiddleware(getAuditLogsHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/audit-logs", basicAuthMiddleware(deleteAllAuditLogsHandler)).Methods("DELETE", "OPTIONS") + api.HandleFunc("/audit-logs/test", basicAuthMiddleware(createTestAuditLogHandler)).Methods("POST", "OPTIONS") // Start server port := ":8080" @@ -2356,6 +3726,18 @@ func setProviderEnabledHandler(w http.ResponseWriter, r *http.Request) { "message": "Provider-Status erfolgreich aktualisiert", "enabled": req.Enabled, }) + + // Audit-Log: Provider aktiviert/deaktiviert + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + action := "DISABLE" + if req.Enabled { + action = "ENABLE" + } + auditService.Track(r.Context(), action, "provider", id, userID, username, map[string]interface{}{ + "enabled": req.Enabled, + "message": fmt.Sprintf("Provider %s %s", id, strings.ToLower(action)), + }, ipAddress, userAgent) } func updateProviderConfigHandler(w http.ResponseWriter, r *http.Request) { @@ -2393,6 +3775,13 @@ func updateProviderConfigHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Konfiguration erfolgreich aktualisiert", }) + + // Audit-Log: Provider-Konfiguration aktualisiert + userID, username := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "UPDATE", "provider", id, userID, username, map[string]interface{}{ + "message": fmt.Sprintf("Provider-Konfiguration aktualisiert: %s", id), + }, ipAddress, userAgent) } func testProviderConnectionHandler(w http.ResponseWriter, r *http.Request) { @@ -2555,6 +3944,18 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) { "status": result.Status, "csrId": csrID, }) + + // Audit-Log: CSR signiert + 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, + "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), + }, ipAddress, userAgent) } func getCertificatesHandler(w http.ResponseWriter, r *http.Request) { diff --git a/backend/scripts/README.md b/backend/scripts/README.md new file mode 100644 index 0000000..0bd70d9 --- /dev/null +++ b/backend/scripts/README.md @@ -0,0 +1,42 @@ +# Test-Skripte für Audit-Logs + +## Test-Logs generieren + +Das Skript `generate_test_logs.go` erstellt 3000 Test-Audit-Logs für Testzwecke. + +### Verwendung: + +```bash +cd backend/scripts +go run generate_test_logs.go +``` + +### Konfiguration: + +Das Skript verwendet standardmäßig: +- URL: `http://localhost:8080` +- Username: `admin` +- Password: `admin` + +Diese können im Skript geändert werden, falls nötig. + +### Was wird erstellt: + +- 3000 verschiedene Audit-Log-Einträge +- Verschiedene Aktionen: CREATE, UPDATE, DELETE, UPLOAD, SIGN, ENABLE, DISABLE +- Verschiedene Ressourcentypen: user, space, fqdn, csr, provider, certificate +- Realistische Testdaten mit verschiedenen Details +- Fortschrittsanzeige alle 100 Logs + +## Alle Logs löschen + +Verwende die API, um alle Audit-Logs zu löschen: + +```bash +curl -X DELETE "http://localhost:8080/api/audit-logs?confirm=true" \ + -u admin:admin \ + -H "Content-Type: application/json" +``` + +**Wichtig**: Der `confirm=true` Query-Parameter ist erforderlich, um versehentliches Löschen zu verhindern. + diff --git a/backend/scripts/generate_test_logs.go b/backend/scripts/generate_test_logs.go new file mode 100644 index 0000000..2ff8bd0 --- /dev/null +++ b/backend/scripts/generate_test_logs.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net/http" + "time" +) + +func main() { + baseURL := "http://localhost:8080" + username := "admin" + password := "admin" + + // Erstelle Basic Auth Header + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) + + // Verschiedene Aktionen und Ressourcen für realistische Testdaten + actions := []string{"CREATE", "UPDATE", "DELETE", "UPLOAD", "SIGN", "ENABLE", "DISABLE"} + resourceTypes := []string{"user", "space", "fqdn", "csr", "provider", "certificate"} + usernames := []string{"admin", "user1", "user2", "operator", "manager"} + + fmt.Printf("Generiere 3000 Test-Audit-Logs...\n") + + client := &http.Client{ + Timeout: 30 * time.Second, + } + + successCount := 0 + errorCount := 0 + + for i := 0; i < 3000; i++ { + // Wähle zufällige Werte für realistische Testdaten + action := actions[i%len(actions)] + resourceType := resourceTypes[i%len(resourceTypes)] + username := usernames[i%len(usernames)] + + // Erstelle Details mit verschiedenen Informationen + details := map[string]interface{}{ + "message": fmt.Sprintf("Test-Log Eintrag #%d", i+1), + "iteration": i + 1, + "timestamp": time.Now().Format(time.RFC3339), + "testData": true, + "resourceId": fmt.Sprintf("test-resource-%d", i+1), + "description": fmt.Sprintf("Dies ist ein Test-Log-Eintrag für %s %s", action, resourceType), + } + + // Füge spezifische Details basierend auf Resource-Type hinzu + switch resourceType { + case "user": + details["username"] = fmt.Sprintf("testuser%d", i+1) + details["email"] = fmt.Sprintf("test%d@example.com", i+1) + case "space": + details["name"] = fmt.Sprintf("Test Space %d", i+1) + details["description"] = "Test Space Description" + case "fqdn": + details["fqdn"] = fmt.Sprintf("test%d.example.com", i+1) + details["spaceId"] = fmt.Sprintf("space-%d", i%100) + case "csr": + details["fqdnId"] = fmt.Sprintf("fqdn-%d", i%200) + details["keySize"] = 2048 + case "provider": + details["providerId"] = fmt.Sprintf("provider-%d", i%10) + details["enabled"] = i%2 == 0 + case "certificate": + details["certificateId"] = fmt.Sprintf("cert-%d", i+1) + details["status"] = "issued" + } + + // Erstelle Request Body + requestBody := map[string]interface{}{ + "action": action, + "entity": resourceType, + "entityID": fmt.Sprintf("test-id-%d", i+1), + "userID": fmt.Sprintf("user-id-%d", i%5+1), + "username": username, + "details": details, + "ipAddress": fmt.Sprintf("192.168.1.%d", i%255+1), + "userAgent": "Test-Script/1.0", + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + log.Printf("Fehler beim Marshalling: %v", err) + errorCount++ + continue + } + + // Erstelle HTTP Request + req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/audit-logs/test", baseURL), bytes.NewBuffer(jsonData)) + if err != nil { + log.Printf("Fehler beim Erstellen des Requests: %v", err) + errorCount++ + continue + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) + + // Sende Request + resp, err := client.Do(req) + if err != nil { + log.Printf("Fehler beim Senden des Requests: %v", err) + errorCount++ + continue + } + resp.Body.Close() + + if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { + successCount++ + if (i+1)%100 == 0 { + fmt.Printf("Progress: %d/3000 Logs erstellt\n", i+1) + } + } else { + errorCount++ + log.Printf("Unerwarteter Status Code: %d für Log %d", resp.StatusCode, i+1) + } + + // Kleine Pause, um die Datenbank nicht zu überlasten + if i%50 == 0 && i > 0 { + time.Sleep(10 * time.Millisecond) + } + } + + fmt.Printf("\nFertig!\n") + fmt.Printf("Erfolgreich: %d\n", successCount) + fmt.Printf("Fehler: %d\n", errorCount) +} + diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm index c2dafbd..d9f70fb 100644 Binary files a/backend/spaces.db-shm and b/backend/spaces.db-shm differ diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal index 7979005..647eeae 100644 Binary files a/backend/spaces.db-wal and b/backend/spaces.db-wal differ diff --git a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png new file mode 100644 index 0000000..803e549 Binary files /dev/null and b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4b5df92..5764df8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,33 +1,92 @@ import { useState } from 'react' -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider, useAuth } from './contexts/AuthContext' import Sidebar from './components/Sidebar' import Footer from './components/Footer' import Home from './pages/Home' import Spaces from './pages/Spaces' import SpaceDetail from './pages/SpaceDetail' import Impressum from './pages/Impressum' +import Profile from './pages/Profile' +import Users from './pages/Users' +import Login from './pages/Login' +import AuditLogs from './pages/AuditLogs' -function App() { +// Protected Route Component +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth() + + if (loading) { + return ( +
Lade...
+Lade...
+