package main import ( "context" "crypto/x509" "database/sql" "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" "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" ) // OID zu Name Mapping (OpenSSL Format) var oidToName = map[string]string{ "2.5.29.15": "X509v3 Key Usage", "2.5.29.17": "X509v3 Subject Alternative Name", "2.5.29.19": "X509v3 Basic Constraints", "2.5.29.31": "X509v3 CRL Distribution Points", "2.5.29.32": "X509v3 Certificate Policies", "2.5.29.35": "X509v3 Authority Key Identifier", "2.5.29.37": "X509v3 Extended Key Usage", "2.5.29.14": "X509v3 Subject Key Identifier", "1.3.6.1.5.5.7.1.1": "Authority Information Access", "1.3.6.1.5.5.7.48.1": "OCSP", "1.3.6.1.5.5.7.48.2": "CA Issuers", } // Extended Key Usage OIDs (OpenSSL Format) var extendedKeyUsageOIDs = map[string]string{ "1.3.6.1.5.5.7.3.1": "TLS Web Server Authentication", "1.3.6.1.5.5.7.3.2": "TLS Web Client Authentication", "1.3.6.1.5.5.7.3.3": "Code Signing", "1.3.6.1.5.5.7.3.4": "E-mail Protection", "1.3.6.1.5.5.7.3.8": "Time Stamping", "1.3.6.1.5.5.7.3.9": "OCSP Signing", "1.3.6.1.5.5.7.3.5": "IPsec End System", "1.3.6.1.5.5.7.3.6": "IPsec Tunnel", "1.3.6.1.5.5.7.3.7": "IPsec User", } // Key Usage Flags var keyUsageFlags = map[int]string{ 0: "Digital Signature", 1: "Content Commitment", 2: "Key Encipherment", 3: "Data Encipherment", 4: "Key Agreement", 5: "Key Cert Sign", 6: "CRL Sign", 7: "Encipher Only", 8: "Decipher Only", } func getExtensionName(oid string) string { if name, ok := oidToName[oid]; ok { return name } return "Unknown Extension" } func parseExtensionValue(oid string, value []byte, csr *x509.CertificateRequest) (string, []string) { switch oid { case "2.5.29.37": // Extended Key Usage return parseExtendedKeyUsage(value) case "2.5.29.15": // Key Usage return parseKeyUsage(value) case "2.5.29.19": // Basic Constraints return parseBasicConstraints(value) case "2.5.29.17": // Subject Alternative Name return parseSubjectAlternativeName(csr) default: return hex.EncodeToString(value), nil } } func parseSubjectAlternativeName(csr *x509.CertificateRequest) (string, []string) { var parts []string // DNS Names for _, dns := range csr.DNSNames { parts = append(parts, fmt.Sprintf("DNS:%s", dns)) } // Email Addresses for _, email := range csr.EmailAddresses { parts = append(parts, fmt.Sprintf("email:%s", email)) } // IP Addresses for _, ip := range csr.IPAddresses { parts = append(parts, fmt.Sprintf("IP:%s", ip.String())) } // URIs for _, uri := range csr.URIs { parts = append(parts, fmt.Sprintf("URI:%s", uri.String())) } if len(parts) > 0 { return strings.Join(parts, ", "), parts } return "No Subject Alternative Name", nil } func parseExtendedKeyUsage(value []byte) (string, []string) { var oids []asn1.ObjectIdentifier _, err := asn1.Unmarshal(value, &oids) if err != nil { return hex.EncodeToString(value), nil } var purposes []string for _, oid := range oids { oidStr := oid.String() if purpose, ok := extendedKeyUsageOIDs[oidStr]; ok { purposes = append(purposes, purpose) } else { purposes = append(purposes, oidStr) } } if len(purposes) > 0 { // Format wie OpenSSL: jede Purpose auf eigener Zeile return strings.Join(purposes, "\n "), purposes } return hex.EncodeToString(value), nil } func parseKeyUsage(value []byte) (string, []string) { var bits asn1.BitString _, err := asn1.Unmarshal(value, &bits) if err != nil { return hex.EncodeToString(value), nil } var usages []string for i := 0; i < len(bits.Bytes)*8 && i < 9; i++ { if bits.At(i) == 1 { if usage, ok := keyUsageFlags[i]; ok { usages = append(usages, usage) } } } if len(usages) > 0 { return strings.Join(usages, ", "), usages } return "No key usage specified", nil } func parseBasicConstraints(value []byte) (string, []string) { var constraints struct { IsCA bool `asn1:"optional"` MaxPathLen int `asn1:"optional,default:-1"` } _, err := asn1.Unmarshal(value, &constraints) if err != nil { return hex.EncodeToString(value), nil } var parts []string if constraints.IsCA { parts = append(parts, "CA: true") } else { parts = append(parts, "CA: false") } if constraints.MaxPathLen >= 0 { parts = append(parts, fmt.Sprintf("Path Length: %d", constraints.MaxPathLen)) } return strings.Join(parts, ", "), parts } type HealthResponse struct { Status string `json:"status"` Message string `json:"message"` Time string `json:"time"` } type StatsResponse struct { Spaces int `json:"spaces"` FQDNs int `json:"fqdns"` CSRs int `json:"csrs"` Certificates int `json:"certificates"` Users int `json:"users"` } type Space struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` CreatedAt string `json:"createdAt"` } type CreateSpaceRequest struct { Name string `json:"name"` Description string `json:"description"` } type FQDN struct { ID string `json:"id"` SpaceID string `json:"spaceId"` FQDN string `json:"fqdn"` Description string `json:"description"` CreatedAt string `json:"createdAt"` AcmeProviderID string `json:"acmeProviderId,omitempty"` AcmeUsername string `json:"acmeUsername,omitempty"` AcmePassword string `json:"acmePassword,omitempty"` AcmeFulldomain string `json:"acmeFulldomain,omitempty"` AcmeSubdomain string `json:"acmeSubdomain,omitempty"` AcmeChallengeToken string `json:"acmeChallengeToken,omitempty"` AcmeEmail string `json:"acmeEmail,omitempty"` AcmeKeyID string `json:"acmeKeyId,omitempty"` RenewalEnabled bool `json:"renewalEnabled"` } type CreateFQDNRequest struct { FQDN string `json:"fqdn"` Description string `json:"description"` ProviderID string `json:"providerId,omitempty"` Acme bool `json:"acme,omitempty"` AcmeEmail string `json:"acmeEmail,omitempty"` } type Extension struct { ID string `json:"id"` OID string `json:"oid"` Name string `json:"name"` Critical bool `json:"critical"` Value string `json:"value"` Description string `json:"description"` Purposes []string `json:"purposes,omitempty"` } type CSR struct { ID string `json:"id"` FQDNID string `json:"fqdnId"` SpaceID string `json:"spaceId"` FQDN string `json:"fqdn"` CSRPEM string `json:"csrPem"` Subject string `json:"subject"` PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` SignatureAlgorithm string `json:"signatureAlgorithm"` KeySize int `json:"keySize"` DNSNames []string `json:"dnsNames"` EmailAddresses []string `json:"emailAddresses"` IPAddresses []string `json:"ipAddresses"` URIs []string `json:"uris"` Extensions []Extension `json:"extensions"` CreatedAt string `json:"createdAt"` } // User struct für Benutzer type User struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` IsAdmin bool `json:"isAdmin"` Enabled bool `json:"enabled"` CreatedAt string `json:"createdAt"` GroupIDs []string `json:"groupIds,omitempty"` } // 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"` Password string `json:"password,omitempty"` OldPassword string `json:"oldPassword,omitempty"` IsAdmin *bool `json:"isAdmin,omitempty"` Enabled *bool `json:"enabled,omitempty"` GroupIDs []string `json:"groupIds,omitempty"` } // CreateUserRequest struct für Benutzer-Erstellung type CreateUserRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` IsAdmin bool `json:"isAdmin,omitempty"` GroupIDs []string `json:"groupIds,omitempty"` } // 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 // SQLite Connection String mit Timeout und WAL Mode für bessere Concurrency // _busy_timeout erhöht die Wartezeit bei Locks db, err = sql.Open("sqlite3", "./spaces.db?_foreign_keys=1&_journal_mode=WAL&_timeout=10000&_busy_timeout=10000") if err != nil { log.Fatal("Fehler beim Öffnen der Datenbank:", err) } // Setze Connection Pool Settings db.SetMaxOpenConns(1) // SQLite unterstützt nur eine Verbindung gleichzeitig db.SetMaxIdleConns(1) // Teste die Verbindung mit Retry log.Println("Teste Datenbank-Verbindung...") maxRetries := 5 for i := 0; i < maxRetries; i++ { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) err := db.PingContext(ctx) cancel() if err != nil { if i < maxRetries-1 { log.Printf("Datenbank-Verbindung fehlgeschlagen, versuche erneut (%d/%d)...", i+1, maxRetries) time.Sleep(time.Second * 2) continue } log.Fatal("Fehler beim Verbinden mit der Datenbank nach mehreren Versuchen:", err) } log.Println("Datenbank-Verbindung erfolgreich") break } // Aktiviere Foreign Keys (auch über Connection String, aber zur Sicherheit nochmal) log.Println("Aktiviere Foreign Keys...") ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "PRAGMA foreign_keys = ON") cancel() if err != nil { log.Fatal("Fehler beim Aktivieren der Foreign Keys:", err) } // Prüfe und bereinige WAL-Dateien falls nötig // Verwende längeres Timeout und ignoriere Fehler, da dies optional ist log.Println("Führe WAL-Checkpoint aus...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)") cancel() if err != nil { log.Printf("Warnung: WAL-Checkpoint fehlgeschlagen (kann ignoriert werden): %v", err) // Prüfe ob die DB von einem anderen Prozess gesperrt ist if strings.Contains(err.Error(), "locked") || strings.Contains(err.Error(), "database is locked") { log.Printf("Hinweis: Die Datenbank wird möglicherweise von einem anderen Prozess verwendet.") log.Printf("Bitte schließen Sie alle anderen Programme, die die Datenbank öffnen (z.B. SQLite-Browser).") } } else { log.Println("WAL-Checkpoint erfolgreich") } // Erstelle Tabelle falls sie nicht existiert log.Println("Erstelle spaces-Tabelle...") createTableSQL := ` CREATE TABLE IF NOT EXISTS spaces ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, created_at DATETIME NOT NULL );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der Tabelle:", err) } // Erstelle FQDN-Tabelle log.Println("Erstelle fqdns-Tabelle...") createFQDNTableSQL := ` CREATE TABLE IF NOT EXISTS fqdns ( id TEXT PRIMARY KEY, space_id TEXT NOT NULL, fqdn TEXT NOT NULL, description TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createFQDNTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der FQDN-Tabelle:", err) } // Erweitere FQDN-Tabelle um ACME Challenge-Daten (Migration) log.Println("Erweitere fqdns-Tabelle um ACME Felder...") // SQLite unterstützt kein "IF NOT EXISTS" bei ALTER TABLE ADD COLUMN // Prüfe stattdessen, ob die Spalte bereits existiert var acmeProviderIDExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_provider_id'").Scan(&acmeProviderIDExists) if err == nil && acmeProviderIDExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_provider_id TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_provider_id: %v", err) } } var acmeUsernameExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_username'").Scan(&acmeUsernameExists) if err == nil && acmeUsernameExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_username TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_username: %v", err) } } var acmePasswordExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_password'").Scan(&acmePasswordExists) if err == nil && acmePasswordExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_password TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_password: %v", err) } } var acmeFulldomainExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_fulldomain'").Scan(&acmeFulldomainExists) if err == nil && acmeFulldomainExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_fulldomain TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_fulldomain: %v", err) } } var acmeSubdomainExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_subdomain'").Scan(&acmeSubdomainExists) if err == nil && acmeSubdomainExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_subdomain TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_subdomain: %v", err) } } var acmeChallengeTokenExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_challenge_token'").Scan(&acmeChallengeTokenExists) if err == nil && acmeChallengeTokenExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_challenge_token TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_challenge_token: %v", err) } } var acmeEmailExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_email'").Scan(&acmeEmailExists) if err == nil && acmeEmailExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_email TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_email: %v", err) } } var acmeKeyIDExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='acme_key_id'").Scan(&acmeKeyIDExists) if err == nil && acmeKeyIDExists == 0 { ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN acme_key_id TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von acme_key_id: %v", err) } } // Füge renewal_enabled Spalte hinzu (standardmäßig aktiviert) var renewalEnabledExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('fqdns') WHERE name='renewal_enabled'").Scan(&renewalEnabledExists) if err == nil && renewalEnabledExists == 0 { log.Println("Füge renewal_enabled Spalte zu fqdns-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE fqdns ADD COLUMN renewal_enabled INTEGER DEFAULT 1") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von renewal_enabled: %v", err) } else { log.Println("renewal_enabled Spalte erfolgreich hinzugefügt") } } else if err == nil && renewalEnabledExists > 0 { // Spalte existiert bereits - setze alle NULL-Werte auf 1 (Default) log.Println("Setze NULL-Werte in renewal_enabled auf 1 (Default)...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "UPDATE fqdns SET renewal_enabled = 1 WHERE renewal_enabled IS NULL") cancel() if err != nil { log.Printf("Warnung: Fehler beim Setzen von NULL-Werten in renewal_enabled: %v", err) } else { log.Println("NULL-Werte in renewal_enabled erfolgreich auf 1 gesetzt") } } // Erstelle CSR-Tabelle log.Println("Erstelle csrs-Tabelle...") createCSRTableSQL := ` CREATE TABLE IF NOT EXISTS csrs ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, fqdn TEXT NOT NULL, csr_pem TEXT NOT NULL, subject TEXT, public_key_algorithm TEXT, signature_algorithm TEXT, key_size INTEGER, dns_names TEXT, email_addresses TEXT, ip_addresses TEXT, uris TEXT, extensions TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(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, createCSRTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der CSR-Tabelle:", err) } // Füge Extensions-Spalte hinzu, falls sie nicht existiert (für bestehende Datenbanken) // Prüfe zuerst, ob die Spalte bereits existiert log.Println("Prüfe Extensions-Spalte...") var columnExists bool ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) rows, err := db.QueryContext(ctx, "PRAGMA table_info(csrs)") cancel() if err == nil { defer rows.Close() for rows.Next() { var cid int var name string var dataType string var notNull int var defaultValue interface{} var pk int if err := rows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &pk); err == nil { if name == "extensions" { columnExists = true break } } } rows.Close() } // Füge Spalte nur hinzu, wenn sie nicht existiert if !columnExists { log.Println("Füge Extensions-Spalte hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE csrs ADD COLUMN extensions TEXT") cancel() if err != nil { // Ignoriere "duplicate column" Fehler, da die Spalte möglicherweise zwischenzeitlich hinzugefügt wurde if !strings.Contains(err.Error(), "duplicate column") { log.Printf("Fehler beim Hinzufügen der Extensions-Spalte: %v", err) } } else { log.Println("Extensions-Spalte zur csrs-Tabelle hinzugefügt") } } else { log.Println("Extensions-Spalte existiert bereits") } // Erstelle Zertifikat-Tabelle log.Println("Erstelle certificates-Tabelle...") // Prüfe ob Tabelle bereits existiert var tableExists int err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='certificates'").Scan(&tableExists) if err != nil { log.Printf("Warnung: Fehler beim Prüfen der certificates-Tabelle: %v", err) tableExists = 0 } if tableExists > 0 { // Tabelle existiert bereits - prüfe ob Migration nötig ist log.Println("certificates-Tabelle existiert bereits, prüfe Migration...") // Prüfe ob csr_id NOT NULL ist oder ob Foreign Key Constraint existiert var csrIDNotNull int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='csr_id' AND \"notnull\"=1").Scan(&csrIDNotNull) needsMigration := false if err == nil && csrIDNotNull > 0 { needsMigration = true log.Println("Migriere certificates-Tabelle: csr_id ist NOT NULL, entferne Constraint...") } // Prüfe ob Foreign Key Constraint für csr_id existiert (durch Prüfung der CREATE TABLE Statement) var fkExists int err = db.QueryRow(` SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='certificates' AND sql LIKE '%FOREIGN KEY%csr_id%' `).Scan(&fkExists) if err == nil && fkExists > 0 { needsMigration = true log.Println("Migriere certificates-Tabelle: Foreign Key Constraint für csr_id gefunden, entferne...") } if needsMigration { // SQLite unterstützt kein ALTER COLUMN um NOT NULL zu entfernen oder Foreign Keys zu löschen // Wir müssen die Tabelle neu erstellen log.Println("Starte Migration der certificates-Tabelle...") // Erstelle temporäre Tabelle mit neuer Struktur (ohne Foreign Key für csr_id) ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, ` CREATE TABLE certificates_new ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, csr_id TEXT, certificate_id TEXT NOT NULL, provider_id TEXT NOT NULL, certificate_pem TEXT, private_key_pem TEXT, status TEXT NOT NULL, expires_at DATETIME, is_intermediate INTEGER DEFAULT 0, parent_certificate_id TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(id) ON DELETE CASCADE, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, FOREIGN KEY (parent_certificate_id) REFERENCES certificates(id) ON DELETE CASCADE ) `) cancel() if err == nil { // Prüfe welche Spalten in alter Tabelle existieren var hasPrivateKey int db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='private_key_pem'").Scan(&hasPrivateKey) // Prüfe welche Spalten in alter Tabelle existieren var hasExpiresAt int var hasIsIntermediate int db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='expires_at'").Scan(&hasExpiresAt) db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='is_intermediate'").Scan(&hasIsIntermediate) // Kopiere Daten ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) if hasPrivateKey > 0 && hasExpiresAt > 0 && hasIsIntermediate > 0 { // Kopiere mit allen neuen Spalten _, err = db.ExecContext(ctx, ` INSERT INTO certificates_new (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at) SELECT id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at FROM certificates `) } else if hasPrivateKey > 0 { // Kopiere mit private_key_pem, aber ohne expires_at/is_intermediate _, err = db.ExecContext(ctx, ` INSERT INTO certificates_new (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, created_at) SELECT id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, created_at FROM certificates `) } else { // Kopiere ohne private_key_pem, expires_at, is_intermediate _, err = db.ExecContext(ctx, ` INSERT INTO certificates_new (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at) SELECT id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at FROM certificates `) } cancel() if err == nil { // Lösche alte Tabelle ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "DROP TABLE certificates") cancel() if err == nil { // Benenne neue Tabelle um ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates_new RENAME TO certificates") cancel() if err == nil { log.Println("Migration erfolgreich: certificates-Tabelle migriert (csr_id optional, Foreign Key entfernt)") } else { log.Printf("Warnung: Fehler beim Umbenennen der Tabelle: %v", err) } } else { log.Printf("Warnung: Fehler beim Löschen der alten Tabelle: %v", err) } } else { log.Printf("Warnung: Fehler beim Kopieren der Daten: %v", err) // Lösche neue Tabelle db.ExecContext(context.Background(), "DROP TABLE certificates_new") } } else { log.Printf("Warnung: Fehler beim Erstellen der neuen Tabelle: %v", err) } } // Prüfe ob private_key_pem existiert var privateKeyExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='private_key_pem'").Scan(&privateKeyExists) if err == nil && privateKeyExists == 0 { log.Println("Füge private_key_pem Spalte zu certificates-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates ADD COLUMN private_key_pem TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von private_key_pem: %v", err) } } // Prüfe ob expires_at existiert var expiresAtExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='expires_at'").Scan(&expiresAtExists) if err == nil && expiresAtExists == 0 { log.Println("Füge expires_at Spalte zu certificates-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates ADD COLUMN expires_at DATETIME") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von expires_at: %v", err) } } // Prüfe ob is_intermediate existiert var isIntermediateExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='is_intermediate'").Scan(&isIntermediateExists) if err == nil && isIntermediateExists == 0 { log.Println("Füge is_intermediate Spalte zu certificates-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates ADD COLUMN is_intermediate INTEGER DEFAULT 0") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von is_intermediate: %v", err) } } // Prüfe ob parent_certificate_id existiert var parentCertIDExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='parent_certificate_id'").Scan(&parentCertIDExists) if err == nil && parentCertIDExists == 0 { log.Println("Füge parent_certificate_id Spalte zu certificates-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates ADD COLUMN parent_certificate_id TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von parent_certificate_id: %v", err) } else { log.Println("parent_certificate_id-Spalte erfolgreich hinzugefügt") } } // Prüfe ob cert_id_base64 existiert var certIDBase64Exists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='cert_id_base64'").Scan(&certIDBase64Exists) if err == nil && certIDBase64Exists == 0 { log.Println("Füge cert_id_base64 Spalte zu certificates-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates ADD COLUMN cert_id_base64 TEXT") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von cert_id_base64: %v", err) } else { log.Println("cert_id_base64-Spalte erfolgreich hinzugefügt") } } // Prüfe ob renewal_scheduled_at existiert var renewalScheduledAtExists int err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('certificates') WHERE name='renewal_scheduled_at'").Scan(&renewalScheduledAtExists) if err == nil && renewalScheduledAtExists == 0 { log.Println("Füge renewal_scheduled_at Spalte zu certificates-Tabelle hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*30) _, err = db.ExecContext(ctx, "ALTER TABLE certificates ADD COLUMN renewal_scheduled_at DATETIME") cancel() if err != nil { log.Printf("Warnung: Fehler beim Hinzufügen von renewal_scheduled_at: %v", err) } else { log.Println("renewal_scheduled_at-Spalte erfolgreich hinzugefügt") } } } else { // Tabelle existiert nicht - erstelle sie neu createCertificateTableSQL := ` CREATE TABLE certificates ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, csr_id TEXT, certificate_id TEXT NOT NULL, provider_id TEXT NOT NULL, certificate_pem TEXT, private_key_pem TEXT, status TEXT NOT NULL, expires_at DATETIME, is_intermediate INTEGER DEFAULT 0, parent_certificate_id TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(id) ON DELETE CASCADE, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, FOREIGN KEY (parent_certificate_id) REFERENCES certificates(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createCertificateTableSQL) 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 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, is_admin INTEGER DEFAULT 0, enabled INTEGER DEFAULT 1, 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 Index auf username für schnellere Lookups (falls nicht bereits vorhanden) log.Println("Erstelle Index auf username...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)") cancel() if err != nil { log.Printf("Warnung: Fehler beim Erstellen des username-Index: %v (kann ignoriert werden)", err) } else { log.Println("username-Index erfolgreich erstellt") } // Füge is_admin Spalte hinzu falls nicht vorhanden ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0") cancel() if err != nil && !strings.Contains(err.Error(), "duplicate column") { log.Printf("Hinweis: is_admin-Spalte könnte bereits existieren: %v", err) } // Füge enabled Spalte hinzu falls nicht vorhanden ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN enabled INTEGER DEFAULT 1") cancel() if err != nil && !strings.Contains(err.Error(), "duplicate column") { log.Printf("Hinweis: enabled-Spalte könnte bereits existieren: %v", err) } // Erstelle Audit-Log-Tabelle log.Println("Erstelle audit_logs-Tabelle...") createAuditLogsTableSQL := ` 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*30) _, err = db.ExecContext(ctx, createIndexSQL) cancel() if err != nil { if strings.Contains(err.Error(), "locked") || strings.Contains(err.Error(), "database is locked") { log.Printf("Warnung: Fehler beim Erstellen der Indizes (Datenbank gesperrt): %v", err) log.Printf("Hinweis: Die Indizes werden beim nächsten Start erstellt.") } else { 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) } // 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*30) _, err = db.ExecContext(ctx, createPermissionGroupsTableSQL) cancel() if err != nil { if strings.Contains(err.Error(), "locked") || strings.Contains(err.Error(), "database is locked") { log.Printf("Warnung: Datenbank gesperrt beim Erstellen der permission_groups-Tabelle: %v", err) log.Printf("Hinweis: Die Tabelle wird beim nächsten Start erstellt.") log.Printf("Bitte schließen Sie alle anderen Programme, die die Datenbank öffnen (z.B. SQLite-Browser).") } else { 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") // Erstelle renewal_queue-Tabelle für geplante Zertifikatserneuerungen log.Println("Erstelle renewal_queue-Tabelle...") createRenewalQueueTableSQL := ` CREATE TABLE IF NOT EXISTS renewal_queue ( id TEXT PRIMARY KEY, certificate_id TEXT NOT NULL, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, scheduled_at DATETIME NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at DATETIME NOT NULL, processed_at DATETIME, error_message TEXT, FOREIGN KEY (certificate_id) REFERENCES certificates(id) ON DELETE CASCADE, FOREIGN KEY (fqdn_id) REFERENCES fqdns(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, createRenewalQueueTableSQL) cancel() if err != nil { log.Printf("Warnung: Fehler beim Erstellen der renewal_queue-Tabelle: %v", err) } else { log.Println("renewal_queue-Tabelle erfolgreich erstellt") } // Erstelle Index für schnelle Abfragen createRenewalQueueIndexSQL := ` CREATE INDEX IF NOT EXISTS idx_renewal_queue_scheduled_at ON renewal_queue(scheduled_at); CREATE INDEX IF NOT EXISTS idx_renewal_queue_status ON renewal_queue(status); ` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createRenewalQueueIndexSQL) cancel() if err != nil { log.Printf("Warnung: Fehler beim Erstellen der Indizes für renewal_queue: %v", err) } } func createDefaultAdmin() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() // Prüfe ob bereits ein Admin-User mit UID "admin" existiert var count int err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = 'admin'").Scan(&count) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Users: %v", err) return } if count > 0 { log.Println("Admin-User mit UID 'admin' existiert bereits") // Stelle sicher, dass der Admin-User als Admin markiert ist // Versuche Admin-Status mit Retry-Logik zu setzen maxRetries := 3 for i := 0; i < maxRetries; i++ { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) _, err = db.ExecContext(ctx, "UPDATE users SET is_admin = 1 WHERE id = 'admin'") cancel() if err != nil { if strings.Contains(err.Error(), "locked") || strings.Contains(err.Error(), "database is locked") { if i < maxRetries-1 { log.Printf("Warnung: Datenbank gesperrt, versuche erneut (%d/%d)...", i+1, maxRetries) time.Sleep(time.Second * 2) continue } log.Printf("Warnung: Konnte Admin-Status nicht setzen (Datenbank gesperrt): %v", err) log.Printf("Hinweis: Die Datenbank wird möglicherweise von einem anderen Prozess verwendet.") log.Printf("Bitte schließen Sie alle anderen Programme, die die Datenbank öffnen (z.B. SQLite-Browser).") } else { log.Printf("Warnung: Konnte Admin-Status nicht setzen: %v", err) } } else { log.Println("Admin-User ist als Administrator markiert") break } } // Prüfe ob das Passwort noch "admin" ist (für Debugging) var storedHash string err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = 'admin'").Scan(&storedHash) if err == nil { // Teste ob das Passwort "admin" ist testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin")) if testErr == nil { log.Println("Admin-User Passwort ist korrekt gesetzt") } else { log.Println("Warnung: Admin-User Passwort ist nicht 'admin'") } } return } // Migration: Falls ein Admin-User mit username='admin' aber anderer UID existiert, migriere ihn var existingAdminID string err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = 'admin' AND id != 'admin' LIMIT 1").Scan(&existingAdminID) if err == nil { log.Printf("Migriere bestehenden Admin-User von UID '%s' zu UID 'admin'", existingAdminID) // Hole alle Daten des alten Admin-Users var oldUsername, oldEmail, oldPasswordHash string var oldIsAdmin, oldEnabled int var oldCreatedAt string err = db.QueryRowContext(ctx, "SELECT username, email, password_hash, is_admin, enabled, created_at FROM users WHERE id = ?", existingAdminID). Scan(&oldUsername, &oldEmail, &oldPasswordHash, &oldIsAdmin, &oldEnabled, &oldCreatedAt) if err == nil { // Erstelle neuen Admin-User mit UID 'admin' _, err = db.ExecContext(ctx, "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", "admin", oldUsername, oldEmail, oldPasswordHash, oldIsAdmin, oldEnabled, oldCreatedAt) if err == nil { // Migriere user_groups Zuweisungen _, err = db.ExecContext(ctx, "UPDATE user_groups SET user_id = 'admin' WHERE user_id = ?", existingAdminID) if err != nil { log.Printf("Warnung: Konnte user_groups nicht migrieren: %v", err) } // Lösche den alten User (CASCADE sollte user_groups automatisch löschen) _, err = db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", existingAdminID) if err == nil { log.Printf("✓ Admin-User erfolgreich zu UID 'admin' migriert") return } else { log.Printf("Warnung: Konnte alten Admin-User nicht löschen: %v", err) } } else { log.Printf("Warnung: Konnte neuen Admin-User nicht erstellen: %v", err) } } else { log.Printf("Warnung: Konnte Daten des alten Admin-Users nicht lesen: %v", err) } } // Erstelle Default Admin-User mit fester UID "admin" adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) if err != nil { log.Printf("Fehler beim Hashen des Admin-Passworts: %v", err) return } adminID := "admin" // Feste UID statt UUID createdAt := time.Now().Format(time.RFC3339) _, err = db.ExecContext(ctx, "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", adminID, "admin", "admin@certigo.local", string(hashedPassword), 1, 1, createdAt) if err != nil { log.Printf("Fehler beim Erstellen des Admin-Users: %v", err) return } log.Println("✓ Default Admin-User erstellt: UID='admin', username='admin', password='admin'") log.Printf(" User ID: %s", adminID) log.Printf(" Email: admin@certigo.local") } func healthHandler(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 } response := HealthResponse{ Status: "ok", Message: "Backend ist erreichbar", Time: time.Now().Format(time.RFC3339), } json.NewEncoder(w).Encode(response) } func getStatsHandler(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 } var spacesCount, fqdnsCount, csrsCount, certificatesCount, usersCount int // Zähle Spaces err := db.QueryRow("SELECT COUNT(*) FROM spaces").Scan(&spacesCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Spaces: %v", err) return } // Zähle FQDNs err = db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&fqdnsCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der FQDNs: %v", err) return } // Zähle CSRs err = db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&csrsCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der CSRs: %v", err) return } // Zähle Zertifikate err = db.QueryRow("SELECT COUNT(*) FROM certificates").Scan(&certificatesCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Zertifikate: %v", err) return } // Zähle Benutzer err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&usersCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Benutzer: %v", err) return } response := StatsResponse{ Spaces: spacesCount, FQDNs: fqdnsCount, CSRs: csrsCount, Certificates: certificatesCount, Users: usersCount, } json.NewEncoder(w).Encode(response) } func getSpacesHandler(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 } // Hole Benutzer-ID userID, _ := getUserFromRequest(r) // Hole alle Spaces, auf die der Benutzer Zugriff hat accessibleSpaceIDs, err := getAccessibleSpaceIDs(userID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigungen: %v", err) return } // Wenn der Benutzer keinen Zugriff auf Spaces hat, gebe leeres Array zurück if len(accessibleSpaceIDs) == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode([]Space{}) return } // Baue Query mit IN-Klausel für die zugänglichen Spaces placeholders := make([]string, len(accessibleSpaceIDs)) args := make([]interface{}, len(accessibleSpaceIDs)) for i, spaceID := range accessibleSpaceIDs { placeholders[i] = "?" args[i] = spaceID } query := fmt.Sprintf("SELECT id, name, description, created_at FROM spaces WHERE id IN (%s) ORDER BY created_at DESC", strings.Join(placeholders, ",")) rows, err := db.Query(query, args...) if err != nil { http.Error(w, "Fehler beim Abrufen der Spaces", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Spaces: %v", err) return } defer rows.Close() spaces := make([]Space, 0) for rows.Next() { var space Space var createdAt time.Time var description sql.NullString err := rows.Scan(&space.ID, &space.Name, &description, &createdAt) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } if description.Valid { space.Description = description.String } else { space.Description = "" } space.CreatedAt = createdAt.Format(time.RFC3339) spaces = append(spaces, space) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } // Stelle sicher, dass immer ein Array zurückgegeben wird if spaces == nil { spaces = []Space{} } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(spaces) } func createSpaceHandler(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 } // Prüfe Berechtigung: Nur FULL_ACCESS darf Spaces erstellen userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } // Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung) permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Admin oder HasFullAccess erlaubt Space-Erstellung hasFullAccess := isAdmin || permissions.HasFullAccess // Wenn nicht Admin, prüfe auch Gruppen if !isAdmin && len(permissions.Groups) > 0 { for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccess = true break } } } if !hasFullAccess { http.Error(w, "Keine Berechtigung zum Erstellen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden) return } var req CreateSpaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" { http.Error(w, "Name is required", http.StatusBadRequest) return } // Generiere eindeutige UUID id := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec( "INSERT INTO spaces (id, name, description, created_at) VALUES (?, ?, ?, ?)", id, req.Name, req.Description, createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des Space", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des Space: %v", err) return } newSpace := Space{ ID: id, Name: req.Name, Description: req.Description, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newSpace) // Audit-Log: Space erstellt if auditService != nil { 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) { 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) id := vars["id"] if id == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: Nur FULL_ACCESS darf Spaces löschen userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasPermission, err := hasPermission(userID, id, PermissionFullAccess) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasPermission { http.Error(w, "Keine Berechtigung zum Löschen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe ob der Space existiert var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", id).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Prüfe ob FQDNs vorhanden sind var fqdnCount int err = db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", id).Scan(&fqdnCount) if err != nil { http.Error(w, "Fehler beim Prüfen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der FQDNs: %v", err) return } // Prüfe Query-Parameter für Mitlöschen deleteFqdns := r.URL.Query().Get("deleteFqdns") == "true" if fqdnCount > 0 && !deleteFqdns { http.Error(w, "Space enthält noch FQDNs. Bitte löschen Sie zuerst die FQDNs oder wählen Sie die Option zum Mitlöschen.", http.StatusConflict) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche FQDNs zuerst, wenn gewünscht if deleteFqdns && fqdnCount > 0 { _, err = tx.Exec("DELETE FROM fqdns WHERE space_id = ?", id) if err != nil { http.Error(w, "Fehler beim Löschen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der FQDNs: %v", err) return } log.Printf("Gelöscht: %d FQDNs für Space %s", fqdnCount, id) } // Lösche den Space result, err := tx.Exec("DELETE FROM spaces WHERE id = ?", id) if err != nil { http.Error(w, "Fehler beim Löschen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Löschen des Space: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() 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 } if rowsAffected == 0 { tx.Rollback() http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Space erfolgreich gelöscht"}) // Audit-Log: Space gelöscht 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) { 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) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasAccess, err := hasSpaceAccess(userID, spaceID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasAccess { http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden) return } var count int err = db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", spaceID).Scan(&count) if err != nil { http.Error(w, "Fehler beim Abrufen der FQDN-Anzahl", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der FQDN-Anzahl: %v", err) return } json.NewEncoder(w).Encode(map[string]int{"count": count}) } func getFqdnsHandler(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) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasAccess, err := hasSpaceAccess(userID, spaceID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasAccess { http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden) return } // Prüfe ob der Space existiert var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } rows, err := db.Query("SELECT id, space_id, fqdn, description, created_at, acme_provider_id, acme_username, acme_password, acme_fulldomain, acme_subdomain, acme_challenge_token, acme_email, acme_key_id, renewal_enabled FROM fqdns WHERE space_id = ? ORDER BY created_at DESC", spaceID) if err != nil { http.Error(w, "Fehler beim Abrufen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der FQDNs: %v", err) return } defer rows.Close() var fqdns []FQDN for rows.Next() { var fqdn FQDN var createdAt time.Time var description sql.NullString var acmeProviderID sql.NullString var acmeUsername sql.NullString var acmePassword sql.NullString var acmeFulldomain sql.NullString var acmeSubdomain sql.NullString var acmeChallengeToken sql.NullString var acmeEmail sql.NullString var acmeKeyID sql.NullString var renewalEnabled sql.NullInt64 err := rows.Scan(&fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN, &description, &createdAt, &acmeProviderID, &acmeUsername, &acmePassword, &acmeFulldomain, &acmeSubdomain, &acmeChallengeToken, &acmeEmail, &acmeKeyID, &renewalEnabled) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } if description.Valid { fqdn.Description = description.String } else { fqdn.Description = "" } if acmeProviderID.Valid { fqdn.AcmeProviderID = acmeProviderID.String } if acmeUsername.Valid { fqdn.AcmeUsername = acmeUsername.String } if acmePassword.Valid { fqdn.AcmePassword = acmePassword.String } if acmeFulldomain.Valid { fqdn.AcmeFulldomain = acmeFulldomain.String } if acmeSubdomain.Valid { fqdn.AcmeSubdomain = acmeSubdomain.String } if acmeChallengeToken.Valid { fqdn.AcmeChallengeToken = acmeChallengeToken.String } if acmeEmail.Valid { fqdn.AcmeEmail = acmeEmail.String } if acmeKeyID.Valid { fqdn.AcmeKeyID = acmeKeyID.String } // Setze renewalEnabled (Standard: true wenn nicht gesetzt oder NULL) if renewalEnabled.Valid { fqdn.RenewalEnabled = renewalEnabled.Int64 == 1 } else { // Wenn NULL, dann ist es ein alter Eintrag ohne renewal_enabled Spalte -> Default true fqdn.RenewalEnabled = true } fqdn.CreatedAt = createdAt.Format(time.RFC3339) fqdns = append(fqdns, fqdn) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } if fqdns == nil { fqdns = []FQDN{} } json.NewEncoder(w).Encode(fqdns) } func createFqdnHandler(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 } vars := mux.Vars(r) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: READ_WRITE oder FULL_ACCESS erforderlich zum Erstellen von FQDNs userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasPermission, err := hasPermission(userID, spaceID, PermissionReadWrite) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasPermission { http.Error(w, "Keine Berechtigung zum Erstellen von FQDNs. Lesen/Schreiben oder Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe ob der Space existiert var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } var req CreateFQDNRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.FQDN == "" { http.Error(w, "FQDN is required", http.StatusBadRequest) return } // Prüfe ob der FQDN bereits existiert (case-insensitive) var fqdnExists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE LOWER(fqdn) = LOWER(?))", req.FQDN).Scan(&fqdnExists) if err != nil { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des FQDN: %v", err) return } if fqdnExists { http.Error(w, "Dieser FQDN existiert bereits", http.StatusConflict) return } // Generiere eindeutige UUID id := uuid.New().String() createdAt := time.Now() // ACME Challenge-Domain registrieren, falls ACME aktiviert ist var acmeProviderID, acmeUsername, acmePassword, acmeFulldomain, acmeSubdomain, acmeEmail string log.Printf("FQDN Creation Request - Acme: %v, ProviderID: %s, FQDN: %s, Email: %s", req.Acme, req.ProviderID, req.FQDN, req.AcmeEmail) if req.Acme && req.ProviderID == "certigo-acmeproxy" { // Prüfe ob Email angegeben wurde if req.AcmeEmail == "" { http.Error(w, "Email-Adresse ist für ACME FQDN erforderlich", http.StatusBadRequest) return } acmeEmail = req.AcmeEmail log.Printf("ACME FQDN erkannt, starte Registrierung...") pm := providers.GetManager() provider, exists := pm.GetProvider(req.ProviderID) if !exists { log.Printf("ACME Provider nicht gefunden: %s", req.ProviderID) http.Error(w, "ACME Provider nicht gefunden", http.StatusBadRequest) return } // Prüfe ob Provider ACME-fähig ist config, err := pm.GetProviderConfig(req.ProviderID) if err != nil { http.Error(w, "Fehler beim Abrufen der Provider-Konfiguration", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Provider-Konfiguration: %v", err) return } if !config.AcmeReady { http.Error(w, "Provider ist nicht ACME-fähig", http.StatusBadRequest) return } // Prüfe ob Provider aktiviert ist if !config.Enabled { http.Error(w, "ACME Provider ist nicht aktiviert", http.StatusBadRequest) return } // Rufe RegisterChallengeDomain auf acmeProxyProvider, ok := provider.(*providers.CertigoACMEProxyProvider) if !ok { http.Error(w, "Ungültiger ACME Provider-Typ", http.StatusInternalServerError) return } challengeResponse, err := acmeProxyProvider.RegisterChallengeDomain(config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler bei der ACME Challenge-Domain Registrierung: %v", err), http.StatusInternalServerError) log.Printf("Fehler bei der ACME Challenge-Domain Registrierung: %v", err) return } acmeProviderID = req.ProviderID acmeUsername = challengeResponse.Username acmePassword = challengeResponse.Password acmeFulldomain = challengeResponse.Fulldomain acmeSubdomain = challengeResponse.Subdomain log.Printf("ACME Challenge-Domain registriert für FQDN %s: %s (Subdomain: %s)", req.FQDN, acmeFulldomain, acmeSubdomain) } else { log.Printf("Kein ACME FQDN - Acme: %v, ProviderID: %s", req.Acme, req.ProviderID) } // Speichere in Datenbank _, err = db.Exec( "INSERT INTO fqdns (id, space_id, fqdn, description, created_at, acme_provider_id, acme_username, acme_password, acme_fulldomain, acme_subdomain, acme_email) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", id, spaceID, req.FQDN, req.Description, createdAt, acmeProviderID, acmeUsername, acmePassword, acmeFulldomain, acmeSubdomain, acmeEmail, ) if err != nil { http.Error(w, "Fehler beim Speichern des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des FQDN: %v", err) return } newFqdn := FQDN{ ID: id, SpaceID: spaceID, FQDN: req.FQDN, Description: req.Description, CreatedAt: createdAt.Format(time.RFC3339), AcmeProviderID: acmeProviderID, AcmeUsername: acmeUsername, AcmePassword: acmePassword, AcmeFulldomain: acmeFulldomain, AcmeSubdomain: acmeSubdomain, AcmeEmail: acmeEmail, } log.Printf("Returning FQDN with ACME data - ProviderID: %s, Fulldomain: %s, Subdomain: %s", newFqdn.AcmeProviderID, newFqdn.AcmeFulldomain, newFqdn.AcmeSubdomain) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newFqdn) // Audit-Log: FQDN erstellt 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 requestCertificateHandler(w http.ResponseWriter, r *http.Request) { log.Printf("===== REQUEST CERTIFICATE HANDLER AUFGERUFEN =====") log.Printf("Method: %s", r.Method) log.Printf("URL: %s", r.URL.String()) 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" { log.Printf("OPTIONS Request - beende Handler") w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] log.Printf("SpaceID: %s, FQDNID: %s", spaceID, fqdnID) if spaceID == "" || fqdnID == "" { log.Printf("FEHLER: Space ID oder FQDN ID fehlt") http.Error(w, "Space ID und FQDN ID sind erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasAccess, err := hasSpaceAccess(userID, spaceID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasAccess { http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden) return } // Lade FQDN aus Datenbank var fqdn FQDN var createdAt time.Time var description sql.NullString var acmeProviderID sql.NullString var acmeUsername sql.NullString var acmePassword sql.NullString var acmeFulldomain sql.NullString var acmeSubdomain sql.NullString var acmeChallengeToken sql.NullString var acmeEmail sql.NullString var acmeKeyID sql.NullString var renewalEnabled sql.NullInt64 err = db.QueryRow( "SELECT id, space_id, fqdn, description, created_at, acme_provider_id, acme_username, acme_password, acme_fulldomain, acme_subdomain, acme_challenge_token, acme_email, acme_key_id, renewal_enabled FROM fqdns WHERE id = ? AND space_id = ?", fqdnID, spaceID, ).Scan(&fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN, &description, &createdAt, &acmeProviderID, &acmeUsername, &acmePassword, &acmeFulldomain, &acmeSubdomain, &acmeChallengeToken, &acmeEmail, &acmeKeyID, &renewalEnabled) if err == sql.ErrNoRows { http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } if err != nil { http.Error(w, "Fehler beim Laden des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Laden des FQDN: %v", err) return } // Setze nullable Felder if description.Valid { fqdn.Description = description.String } if acmeProviderID.Valid { fqdn.AcmeProviderID = acmeProviderID.String } if acmeUsername.Valid { fqdn.AcmeUsername = acmeUsername.String } if acmePassword.Valid { fqdn.AcmePassword = acmePassword.String } if acmeFulldomain.Valid { fqdn.AcmeFulldomain = acmeFulldomain.String } if acmeSubdomain.Valid { fqdn.AcmeSubdomain = acmeSubdomain.String } if acmeChallengeToken.Valid { fqdn.AcmeChallengeToken = acmeChallengeToken.String } if acmeEmail.Valid { fqdn.AcmeEmail = acmeEmail.String } if acmeKeyID.Valid { fqdn.AcmeKeyID = acmeKeyID.String } // Setze renewalEnabled (Standard: true wenn nicht gesetzt oder NULL) if renewalEnabled.Valid { fqdn.RenewalEnabled = renewalEnabled.Int64 == 1 } else { // Wenn NULL, dann ist es ein alter Eintrag ohne renewal_enabled Spalte -> Default true fqdn.RenewalEnabled = true } fqdn.CreatedAt = createdAt.Format(time.RFC3339) // Prüfe ob ACME-Daten vorhanden sind log.Printf("Prüfe ACME-Daten: ProviderID=%s, Username=%s, Password=%s, Email=%s", fqdn.AcmeProviderID, fqdn.AcmeUsername, fqdn.AcmePassword, fqdn.AcmeEmail) if fqdn.AcmeProviderID != "certigo-acmeproxy" || fqdn.AcmeUsername == "" || fqdn.AcmePassword == "" || fqdn.AcmeEmail == "" { log.Printf("FEHLER: FQDN hat keine gültigen ACME-Daten (ProviderID: %s, Username: %s, Password: %s, Email: %s)", fqdn.AcmeProviderID, fqdn.AcmeUsername, fqdn.AcmePassword, fqdn.AcmeEmail) http.Error(w, "FQDN hat keine gültigen ACME-Daten", http.StatusBadRequest) return } log.Printf("ACME-Daten OK") // Prüfe ob bereits ein gültiges Zertifikat existiert (nur wenn nicht bestätigt) log.Printf("Lese Request Body...") var reqBody struct { Confirmed bool `json:"confirmed"` } if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { log.Printf("FEHLER beim Lesen des Request Body: %v", err) // Setze default auf false wenn Body leer ist reqBody.Confirmed = false } log.Printf("Request Body gelesen: Confirmed=%v", reqBody.Confirmed) if !reqBody.Confirmed { log.Printf("Prüfe auf existierende gültige Zertifikate...") hasValidCert, expiresAt, err := CheckExistingValidCertificate(fqdnID, spaceID) if err != nil { log.Printf("Fehler beim Prüfen bestehender Zertifikate: %v", err) // Weiter mit Request, da Prüfung fehlgeschlagen ist } else if hasValidCert { log.Printf("Gültiges Zertifikat gefunden, läuft ab: %v", expiresAt) // Prüfe ob Zertifikat noch gültig ist now := time.Now() if expiresAt.After(now) { log.Printf("Zertifikat ist noch gültig - sende Bestätigungsanfrage an Frontend") // Zertifikat ist noch gültig - sende Bestätigungsanfrage an Frontend w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "requiresConfirmation": true, "message": "Es existiert bereits ein gültiges Zertifikat für diesen FQDN", "expiresAt": expiresAt.Format(time.RFC3339), "expiresAtFormatted": expiresAt.Format("02.01.2006 15:04:05"), }) return } } else { log.Printf("Kein gültiges Zertifikat gefunden - fahre fort mit Request") // Kein Zertifikat vorhanden - fahre direkt mit Request fort // (Request wird unten ausgeführt) } } else { log.Printf("Request wurde bestätigt - fahre fort") } // Lade Provider-Konfiguration log.Printf("Lade Provider-Konfiguration...") pm := providers.GetManager() provider, exists := pm.GetProvider("certigo-acmeproxy") if !exists || provider == nil { log.Printf("FEHLER: ACME Provider nicht gefunden") http.Error(w, "ACME Provider nicht gefunden", http.StatusInternalServerError) return } log.Printf("Provider gefunden") config, err := pm.GetProviderConfig("certigo-acmeproxy") if err != nil { log.Printf("FEHLER beim Laden der Provider-Konfiguration: %v", err) http.Error(w, "Fehler beim Laden der Provider-Konfiguration", http.StatusInternalServerError) return } log.Printf("Provider-Konfiguration geladen") // Type-Assertion für CertigoACMEProxyProvider log.Printf("Führe Type-Assertion für Provider durch...") acmeProxyProvider, ok := provider.(*providers.CertigoACMEProxyProvider) if !ok { log.Printf("FEHLER: Ungültiger Provider-Typ") http.Error(w, "Ungültiger Provider-Typ", http.StatusInternalServerError) return } log.Printf("Type-Assertion erfolgreich") // Erstelle Update-Funktion für den Token log.Printf("Erstelle Update- und Cleanup-Funktionen...") updateTokenFunc := func(token string) error { log.Printf("[updateTokenFunc] Speichere Token in Datenbank...") // Speichere Token in Datenbank _, err := db.Exec("UPDATE fqdns SET acme_challenge_token = ? WHERE id = ?", token, fqdnID) if err != nil { return fmt.Errorf("fehler beim Speichern des Challenge-Tokens: %v", err) } // Sende Token an certigo-acmeproxy err = acmeProxyProvider.UpdateChallengeToken(fqdn.AcmeUsername, fqdn.AcmePassword, token, config.Settings) if err != nil { return fmt.Errorf("fehler beim Senden des Tokens an certigo-acmeproxy: %v", err) } return nil } // Erstelle Cleanup-Funktion für den Token (wird aufgerufen, wenn Challenge invalid ist) cleanupTokenFunc := func() error { // Entferne Token aus Datenbank _, err := db.Exec("UPDATE fqdns SET acme_challenge_token = NULL WHERE id = ?", fqdnID) if err != nil { return fmt.Errorf("fehler beim Entfernen des Challenge-Tokens: %v", err) } return nil } // Beantrage Zertifikat baseFqdn := strings.TrimPrefix(fqdn.FQDN, "*.") // Generiere TraceID für diesen Request traceID := generateTraceID() logCertStatus(traceID, fqdnID, "ZERTIFIKATSANFRAGE_START", "OK", "") // Initialisiere Schritt-Status für Frontend stepStatus := make(map[string]string) stepStatus["ZERTIFIKATSANFRAGE_START"] = "success" log.Printf("===== STARTE ZERTIFIKATSANFRAGE =====") log.Printf("FQDN: %s", baseFqdn) log.Printf("FQDN ID: %s", fqdnID) log.Printf("TraceID: %s", traceID) log.Printf("Email: %s", fqdn.AcmeEmail) log.Printf("Existing KeyID: %s", fqdn.AcmeKeyID) // Status-Callback für Live-Updates (mit Console-Log für Debugging) var statusMessages []string statusCallback := func(status string) { statusMessages = append(statusMessages, status) log.Printf("[STATUS] %s", status) } // Erstelle ACME-Client-Kontext // Standardmäßig verwenden wir Let's Encrypt Staging, aber in Zukunft könnte dies aus der FQDN-Konfiguration kommen acmeProviderIDStr := "letsencrypt-staging" // TODO: Aus FQDN-Konfiguration lesen acmeCtx, err := NewACMEClientContext(acmeProviderIDStr) if err != nil { log.Printf("FEHLER beim Erstellen des ACME-Client-Kontexts: %v", err) http.Error(w, fmt.Sprintf("Fehler beim Initialisieren des ACME-Providers: %v", err), http.StatusInternalServerError) return } log.Printf("Rufe RequestCertificate auf...") result, err := RequestCertificate(acmeCtx, baseFqdn, fqdn.AcmeEmail, fqdnID, fqdn.AcmeKeyID, traceID, updateTokenFunc, cleanupTokenFunc, statusCallback) if err != nil { logCertStatus(traceID, fqdnID, "ZERTIFIKATSANFRAGE_GESAMT", "FAILED", err.Error()) stepStatus["ZERTIFIKATSANFRAGE_GESAMT"] = "error" log.Printf("===== FEHLER BEIM ZERTIFIKATSANFRAGE =====") log.Printf("Fehler: %v", err) http.Error(w, fmt.Sprintf("Fehler beim Beantragen des Zertifikats: %v", err), http.StatusInternalServerError) return } logCertStatus(traceID, fqdnID, "ZERTIFIKATSANFRAGE_GESAMT", "OK", "") stepStatus["ZERTIFIKATSANFRAGE_GESAMT"] = "success" // Merge stepStatus from result with our initial stepStatus if result.StepStatus != nil { for k, v := range result.StepStatus { stepStatus[k] = v } } log.Printf("===== ZERTIFIKATSANFRAGE ERFOLGREICH =====") log.Printf("KeyID: %s", result.KeyID) log.Printf("OrderURL: %s", result.OrderURL) log.Printf("Certificate vorhanden: %v", result.Certificate != "") log.Printf("PrivateKey vorhanden: %v", result.PrivateKey != "") // Speichere KeyID in Datenbank (falls neu erstellt) if result.KeyID != "" && result.KeyID != fqdn.AcmeKeyID { _, err = db.Exec("UPDATE fqdns SET acme_key_id = ? WHERE id = ?", result.KeyID, fqdnID) if err != nil { log.Printf("Warnung: Fehler beim Speichern der KeyID: %v", err) } } // Speichere Zertifikat in Datenbank (falls erfolgreich erstellt) if result.Certificate != "" && result.PrivateKey != "" { certID := uuid.New().String() certificateID := result.OrderURL if certificateID == "" { certificateID = certID // Fallback falls keine Order-URL vorhanden } // Speichere createdAt in UTC createdAt := time.Now().UTC().Format("2006-01-02 15:04:05") // Parse Zertifikat um Ablaufdatum und CA-Status zu extrahieren expiresAt, isIntermediate, parseErr := ParseCertificate(result.Certificate) var expiresAtStr string var isIntermediateInt int if parseErr == nil { // Speichere expiresAt in UTC expiresAtStr = expiresAt.UTC().Format("2006-01-02 15:04:05") if isIntermediate { isIntermediateInt = 1 } } // Speichere Zertifikat (komplette Kette) _, err = db.Exec(` INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, certID, fqdnID, spaceID, nil, certificateID, "certigo-acmeproxy", result.Certificate, result.PrivateKey, "issued", expiresAtStr, isIntermediateInt, createdAt) if err != nil { log.Printf("Warnung: Fehler beim Speichern des Zertifikats in der Datenbank: %v", err) // Prüfe ob es ein NOT NULL oder Foreign Key Problem ist if strings.Contains(err.Error(), "NOT NULL") || strings.Contains(err.Error(), "FOREIGN KEY") { // Versuche ohne csr_id Spalte (falls sie nicht existiert oder optional ist) _, err = db.Exec(` INSERT INTO certificates (id, fqdn_id, space_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, certID, fqdnID, spaceID, certificateID, "certigo-acmeproxy", result.Certificate, result.PrivateKey, "issued", expiresAtStr, isIntermediateInt, createdAt) if err != nil { log.Printf("Warnung: Fehler beim Speichern ohne csr_id: %v", err) // Letzter Versuch: Verwende leeren String (falls NULL nicht erlaubt, aber leerer String OK ist) _, err = db.Exec(` INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, certID, fqdnID, spaceID, "", certificateID, "certigo-acmeproxy", result.Certificate, result.PrivateKey, "issued", expiresAtStr, isIntermediateInt, createdAt) if err != nil { log.Printf("Fehler: Konnte Zertifikat nicht in Datenbank speichern: %v", err) } else { log.Printf("Zertifikat erfolgreich in Datenbank gespeichert (ID: %s, mit leerem csr_id)", certID) } } else { log.Printf("Zertifikat erfolgreich in Datenbank gespeichert (ID: %s, ohne csr_id)", certID) } } else { log.Printf("Fehler: Unerwarteter Fehler beim Speichern des Zertifikats: %v", err) } } else { log.Printf("Zertifikat erfolgreich in Datenbank gespeichert (ID: %s)", certID) } // Prüfe ob RenewalInfo aktiviert ist und verarbeite RenewalInfo im Hintergrund renewalEnabledValue := true // Default if renewalEnabled.Valid { renewalEnabledValue = renewalEnabled.Int64 == 1 } if renewalEnabledValue { // Verarbeite RenewalInfo im Hintergrund (asynchron) go func() { if err := ProcessRenewalInfoForCertificate(result.Certificate, certID, fqdnID, spaceID, true); err != nil { log.Printf("Fehler beim Verarbeiten der RenewalInfo (wird ignoriert): %v", err) } }() } else { log.Printf("RenewalInfo wird übersprungen - renewal_enabled ist für FQDN %s deaktiviert", fqdnID) } } // Erfolgreiche Antwort response := map[string]interface{}{ "success": true, "message": "Zertifikat erfolgreich beantragt", "certificate": result.Certificate, "privateKey": result.PrivateKey, "keyId": result.KeyID, "status": result.Status, "stepStatus": stepStatus, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) // Audit-Log ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "CREATE", "certificate", fqdnID, userID, username, map[string]interface{}{ "fqdn": fqdn.FQDN, "spaceId": spaceID, "message": fmt.Sprintf("Zertifikat beantragt für FQDN: %s", fqdn.FQDN), }, ipAddress, userAgent) } func deleteFqdnHandler(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) spaceID := vars["id"] fqdnID := vars["fqdnId"] if spaceID == "" || fqdnID == "" { http.Error(w, "Space ID und FQDN ID sind erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: Nur FULL_ACCESS darf FQDNs löschen userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasPermission, err := hasPermission(userID, spaceID, PermissionFullAccess) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasPermission { http.Error(w, "Keine Berechtigung zum Löschen von FQDNs. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe ob der FQDN existiert und zum Space gehört var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE id = ? AND space_id = ?)", fqdnID, spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des FQDN: %v", err) return } if !exists { http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs für diesen FQDN (falls CASCADE nicht funktioniert) _, err = tx.Exec("DELETE FROM csrs WHERE fqdn_id = ? AND space_id = ?", fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der CSRs: %v", err) return } // Lösche den FQDN result, err := tx.Exec("DELETE FROM fqdns WHERE id = ? AND space_id = ?", fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Löschen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Löschen des FQDN: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() 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 } if rowsAffected == 0 { tx.Rollback() http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } 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 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) { 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) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: Nur FULL_ACCESS darf alle FQDNs eines Spaces löschen userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasPermission, err := hasPermission(userID, spaceID, PermissionFullAccess) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasPermission { http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe ob der Space existiert var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs für alle FQDNs dieses Spaces _, err = tx.Exec("DELETE FROM csrs WHERE space_id = ?", spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der CSRs: %v", err) return } // Lösche alle FQDNs des Spaces result, err := tx.Exec("DELETE FROM fqdns WHERE space_id = ?", spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der FQDNs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() 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 } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d FQDNs und zugehörige CSRs aus Space %s", rowsAffected, spaceID) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle FQDNs und zugehörige CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func deleteAllFqdnsGlobalHandler(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 Berechtigung: Nur FULL_ACCESS darf alle FQDNs global löschen userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Admin oder HasFullAccess erlaubt Löschen aller FQDNs hasFullAccess := isAdmin || permissions.HasFullAccess // Wenn nicht Admin, prüfe auch Gruppen if !isAdmin && len(permissions.Groups) > 0 { for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccess = true break } } } if !hasFullAccess { http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme) confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwenden Sie ?confirm=true", http.StatusBadRequest) return } // Zähle zuerst die Anzahl der FQDNs var totalCount int err = db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&totalCount) if err != nil { http.Error(w, "Fehler beim Zählen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der FQDNs: %v", err) return } if totalCount == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Keine FQDNs zum Löschen vorhanden", "deletedCount": 0, }) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs _, err = tx.Exec("DELETE FROM csrs") if err != nil { http.Error(w, "Fehler beim Löschen aller CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller CSRs: %v", err) return } // Lösche alle FQDNs result, err := tx.Exec("DELETE FROM fqdns") if err != nil { http.Error(w, "Fehler beim Löschen aller FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller FQDNs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() 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 } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d FQDNs und alle zugehörigen CSRs aus allen Spaces", rowsAffected) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle FQDNs und zugehörige CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func deleteAllCSRsHandler(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 Berechtigung: Nur FULL_ACCESS darf alle CSRs löschen userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Admin oder HasFullAccess erlaubt Löschen aller CSRs hasFullAccess := isAdmin || permissions.HasFullAccess // Wenn nicht Admin, prüfe auch Gruppen if !isAdmin && len(permissions.Groups) > 0 { for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccess = true break } } } if !hasFullAccess { http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme) confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwenden Sie ?confirm=true", http.StatusBadRequest) return } // Zähle zuerst die Anzahl der CSRs var totalCount int err = db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&totalCount) if err != nil { http.Error(w, "Fehler beim Zählen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der CSRs: %v", err) return } if totalCount == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Keine CSRs zum Löschen vorhanden", "deletedCount": 0, }) return } // Beginne Transaktion für atomare Operation tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche alle CSRs result, err := tx.Exec("DELETE FROM csrs") if err != nil { http.Error(w, "Fehler beim Löschen aller CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller CSRs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() 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 } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d CSRs aus allen Spaces", rowsAffected) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func uploadCSRHandler(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 } // Parse multipart form err := r.ParseMultipartForm(10 << 20) // 10 MB max if err != nil { http.Error(w, "Fehler beim Parsen des Formulars", http.StatusBadRequest) return } spaceID := r.FormValue("spaceId") fqdnName := r.FormValue("fqdn") if spaceID == "" || fqdnName == "" { http.Error(w, "spaceId und fqdn sind erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: READ_WRITE oder FULL_ACCESS erforderlich zum Hochladen von CSRs userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasPermission, err := hasPermission(userID, spaceID, PermissionReadWrite) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasPermission { http.Error(w, "Keine Berechtigung zum Hochladen von CSRs. Lesen/Schreiben oder Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe ob Space existiert var spaceExists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&spaceExists) if err != nil || !spaceExists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Prüfe ob FQDN existiert und zum Space gehört var fqdnID string err = db.QueryRow("SELECT id FROM fqdns WHERE fqdn = ? AND space_id = ?", fqdnName, spaceID).Scan(&fqdnID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "FQDN nicht gefunden oder gehört nicht zu diesem Space", http.StatusNotFound) } else { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) } return } // Hole die CSR-Datei file, header, err := r.FormFile("csr") if err != nil { http.Error(w, "Fehler beim Lesen der CSR-Datei", http.StatusBadRequest) return } defer file.Close() // Lese den Dateiinhalt csrBytes := make([]byte, header.Size) _, err = io.ReadFull(file, csrBytes) if err != nil { http.Error(w, "Fehler beim Lesen der CSR-Datei", http.StatusBadRequest) return } csrPEM := string(csrBytes) // Parse CSR block, _ := pem.Decode(csrBytes) if block == nil { http.Error(w, "Ungültiges PEM-Format", http.StatusBadRequest) return } csr, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { http.Error(w, "Fehler beim Parsen des CSR: "+err.Error(), http.StatusBadRequest) return } // Extrahiere Informationen subject := csr.Subject.String() publicKeyAlgorithm := csr.PublicKeyAlgorithm.String() signatureAlgorithm := csr.SignatureAlgorithm.String() // Bestimme Key Size keySize := 0 if csr.PublicKey != nil { switch pub := csr.PublicKey.(type) { case interface{ Size() int }: keySize = pub.Size() * 8 // Convert bytes to bits } } // Extrahiere SANs dnsNames := csr.DNSNames emailAddresses := csr.EmailAddresses ipAddresses := make([]string, len(csr.IPAddresses)) for i, ip := range csr.IPAddresses { ipAddresses[i] = ip.String() } uris := make([]string, len(csr.URIs)) for i, uri := range csr.URIs { uris[i] = uri.String() } // Extrahiere Extensions extensions := make([]Extension, 0) for _, ext := range csr.Extensions { oidStr := ext.Id.String() name := getExtensionName(oidStr) description, purposes := parseExtensionValue(oidStr, ext.Value, csr) extension := Extension{ ID: ext.Id.String(), OID: oidStr, Name: name, Critical: ext.Critical, Value: hex.EncodeToString(ext.Value), Description: description, Purposes: purposes, } extensions = append(extensions, extension) } // Konvertiere Slices zu JSON-Strings für DB dnsNamesJSON, _ := json.Marshal(dnsNames) emailAddressesJSON, _ := json.Marshal(emailAddresses) ipAddressesJSON, _ := json.Marshal(ipAddresses) urisJSON, _ := json.Marshal(uris) extensionsJSON, _ := json.Marshal(extensions) // Generiere eindeutige ID csrID := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec(` INSERT INTO csrs ( id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, csrID, fqdnID, spaceID, fqdnName, csrPEM, subject, publicKeyAlgorithm, signatureAlgorithm, keySize, string(dnsNamesJSON), string(emailAddressesJSON), string(ipAddressesJSON), string(urisJSON), string(extensionsJSON), createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des CSR: %v", err) return } newCSR := CSR{ ID: csrID, FQDNID: fqdnID, SpaceID: spaceID, FQDN: fqdnName, CSRPEM: csrPEM, Subject: subject, PublicKeyAlgorithm: publicKeyAlgorithm, SignatureAlgorithm: signatureAlgorithm, KeySize: keySize, DNSNames: dnsNames, EmailAddresses: emailAddresses, IPAddresses: ipAddresses, URIs: uris, Extensions: extensions, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newCSR) // Audit-Log: CSR hochgeladen 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) { 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) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] if spaceID == "" || fqdnID == "" { http.Error(w, "spaceId und fqdnId sind erforderlich", http.StatusBadRequest) return } // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben (READ, READ_WRITE oder FULL_ACCESS) userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasAccess, err := hasSpaceAccess(userID, spaceID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasAccess { http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden) return } // Prüfe ob nur der neueste CSR gewünscht ist latestOnly := r.URL.Query().Get("latest") == "true" if latestOnly { // Hole nur den neuesten CSR var csr CSR var createdAt time.Time var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON, extensionsJSON sql.NullString err := db.QueryRow(` SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC LIMIT 1 `, fqdnID, spaceID).Scan( &csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject, &csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize, &dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt, ) if err != nil { if err == sql.ErrNoRows { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(nil) return } http.Error(w, "Fehler beim Abrufen des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des CSR: %v", err) return } // Parse JSON-Strings zurück zu Slices json.Unmarshal([]byte(dnsNamesJSON.String), &csr.DNSNames) json.Unmarshal([]byte(emailAddressesJSON.String), &csr.EmailAddresses) json.Unmarshal([]byte(ipAddressesJSON.String), &csr.IPAddresses) json.Unmarshal([]byte(urisJSON.String), &csr.URIs) if extensionsJSON.Valid { json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions) } else { csr.Extensions = []Extension{} } csr.CreatedAt = createdAt.Format(time.RFC3339) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(csr) } else { // Hole alle CSRs für diesen FQDN rows, err := db.Query(` SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC `, fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Abrufen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der CSRs: %v", err) return } defer rows.Close() var csrs []CSR for rows.Next() { var csr CSR var createdAt time.Time var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON string var extensionsJSON sql.NullString err := rows.Scan( &csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject, &csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize, &dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt, ) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } // Parse JSON-Strings zurück zu Slices json.Unmarshal([]byte(dnsNamesJSON), &csr.DNSNames) json.Unmarshal([]byte(emailAddressesJSON), &csr.EmailAddresses) json.Unmarshal([]byte(ipAddressesJSON), &csr.IPAddresses) json.Unmarshal([]byte(urisJSON), &csr.URIs) if extensionsJSON.Valid { json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions) } else { csr.Extensions = []Extension{} } csr.CreatedAt = createdAt.Format(time.RFC3339) csrs = append(csrs, csr) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } if csrs == nil { csrs = []CSR{} } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(csrs) } } func swaggerUIHandler(w http.ResponseWriter, r *http.Request) { html := ` Certigo Addon API - Swagger UI
` w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func openAPIHandler(w http.ResponseWriter, r *http.Request) { // Lese die OpenAPI YAML Datei vom Dateisystem openAPIContent, err := os.ReadFile("openapi.yaml") if err != nil { log.Printf("Fehler beim Lesen der openapi.yaml Datei: %v", err) http.Error(w, "OpenAPI Spezifikation nicht gefunden", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-yaml") w.Write(openAPIContent) } // User Handler Functions func getUserPermissionsHandler(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 } // Hole Benutzer-ID userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) isAdmin = false } // Hole Berechtigungen permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Erstelle vereinfachte Antwort für Frontend canCreateFqdn := make(map[string]bool) canDeleteFqdn := make(map[string]bool) canUploadCSR := make(map[string]bool) canSignCSR := make(map[string]bool) response := map[string]interface{}{ "userId": userID, "isAdmin": isAdmin, "hasFullAccess": permissions.HasFullAccess || isAdmin, "accessibleSpaces": []string{}, "permissions": map[string]interface{}{ "canCreateSpace": permissions.HasFullAccess || isAdmin, "canDeleteSpace": permissions.HasFullAccess || isAdmin, "canCreateFqdn": canCreateFqdn, "canDeleteFqdn": canDeleteFqdn, "canUploadCSR": canUploadCSR, "canSignCSR": canSignCSR, }, } // Hole alle Spaces ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() spaceRows, err := db.QueryContext(ctx, "SELECT id FROM spaces") if err == nil { defer spaceRows.Close() var allSpaceIDs []string for spaceRows.Next() { var spaceID string if err := spaceRows.Scan(&spaceID); err == nil { allSpaceIDs = append(allSpaceIDs, spaceID) } } spaceRows.Close() // Prüfe für jeden Space die Berechtigungen accessibleSpaces := []string{} for _, spaceID := range allSpaceIDs { hasAccess, _ := hasSpaceAccess(userID, spaceID) if hasAccess { accessibleSpaces = append(accessibleSpaces, spaceID) // Prüfe READ_WRITE für FQDN erstellen und CSR upload/sign hasReadWrite, _ := hasPermission(userID, spaceID, PermissionReadWrite) canCreateFqdn[spaceID] = hasReadWrite canUploadCSR[spaceID] = hasReadWrite canSignCSR[spaceID] = hasReadWrite // Prüfe FULL_ACCESS für FQDN löschen hasFullAccess, _ := hasPermission(userID, spaceID, PermissionFullAccess) canDeleteFqdn[spaceID] = hasFullAccess } } response["accessibleSpaces"] = accessibleSpaces perms := response["permissions"].(map[string]interface{}) perms["canCreateFqdn"] = canCreateFqdn perms["canDeleteFqdn"] = canDeleteFqdn perms["canUploadCSR"] = canUploadCSR perms["canSignCSR"] = canSignCSR } // Prüfe globale Berechtigungen (Space erstellen/löschen) // Admins haben immer Vollzugriff if isAdmin { perms := response["permissions"].(map[string]interface{}) perms["canCreateSpace"] = true perms["canDeleteSpace"] = true // Alle Spaces sind zugänglich für Admins spaceRows, err := db.QueryContext(ctx, "SELECT id FROM spaces") if err == nil { defer spaceRows.Close() var allSpaceIDs []string for spaceRows.Next() { var spaceID string if err := spaceRows.Scan(&spaceID); err == nil { allSpaceIDs = append(allSpaceIDs, spaceID) canCreateFqdn[spaceID] = true canDeleteFqdn[spaceID] = true canUploadCSR[spaceID] = true canSignCSR[spaceID] = true } } spaceRows.Close() response["accessibleSpaces"] = allSpaceIDs perms["canCreateFqdn"] = canCreateFqdn perms["canDeleteFqdn"] = canDeleteFqdn perms["canUploadCSR"] = canUploadCSR perms["canSignCSR"] = canSignCSR } } else { hasFullAccessGlobal := false for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccessGlobal = true break } } perms := response["permissions"].(map[string]interface{}) perms["canCreateSpace"] = hasFullAccessGlobal perms["canDeleteSpace"] = hasFullAccessGlobal } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } 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*10) defer cancel() // Lade alle Benutzer rows, err := db.QueryContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users ORDER BY created_at DESC") if err != nil { http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Benutzer: %v", err) return } defer rows.Close() var users []User var userIDs []string for rows.Next() { var user User var isAdmin, enabled int err := rows.Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt) if err != nil { http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err) return } user.IsAdmin = isAdmin == 1 user.Enabled = enabled == 1 user.GroupIDs = []string{} // Initialisiere als leeres Array users = append(users, user) userIDs = append(userIDs, user.ID) } 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) } 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*10) defer cancel() var user User var isAdmin, enabled int err := db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID). Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des Benutzers: %v", err) return } user.IsAdmin = isAdmin == 1 user.Enabled = enabled == 1 // Lade Gruppen-IDs für diesen Benutzer groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) 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) } 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*10) defer cancel() isAdmin := 0 if req.IsAdmin { isAdmin = 1 } // Admin muss immer enabled sein enabledValue := 1 if req.IsAdmin { enabledValue = 1 // Admin immer enabled } _, err = db.ExecContext(ctx, "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", userID, req.Username, req.Email, string(hashedPassword), isAdmin, enabledValue, createdAt) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "username") { 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 } // 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, IsAdmin: req.IsAdmin, Enabled: true, CreatedAt: createdAt, GroupIDs: req.GroupIDs, } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) // Audit-Log: User erstellt requestUserID, requestUsername := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) auditDetails := map[string]interface{}{ "username": req.Username, "email": req.Email, "groupIds": req.GroupIDs, "message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email), } if req.IsAdmin { auditDetails["isAdmin"] = true auditDetails["message"] = fmt.Sprintf("Administrator erstellt: %s (%s)", req.Username, req.Email) } auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, auditDetails, ipAddress, userAgent) } func updateUserHandler(w http.ResponseWriter, r *http.Request) { 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*10) defer cancel() // Prüfe ob Benutzer existiert var isAdmin int var currentUsername, currentEmail string err := db.QueryRowContext(ctx, "SELECT is_admin, username, email FROM users WHERE id = ?", userID). Scan(&isAdmin, ¤tUsername, ¤tEmail) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des Benutzers: %v", err) return } // Nur der spezielle Admin-User mit UID "admin": Username und Email sind unveränderbar // Andere Admin-User können ihre Daten ändern if userID == "admin" { if req.Username != "" && req.Username != currentUsername { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden"}) return } if req.Email != "" && req.Email != currentEmail { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden"}) return } } // Update Felder updates := []string{} args := []interface{}{} // Nur Username/Email updaten wenn nicht der spezielle Admin-User mit UID "admin" // Andere Admin-User können ihre Daten ändern if req.Username != "" && (userID != "admin" || req.Username == currentUsername) { updates = append(updates, "username = ?") args = append(args, req.Username) } if req.Email != "" && (userID != "admin" || req.Email == currentEmail) { updates = append(updates, "email = ?") args = append(args, req.Email) } // isAdmin aktualisieren, falls angegeben // UID 'admin' kann seinen Admin-Status nicht ändern if req.IsAdmin != nil { // UID 'admin' ist immer Admin und kann nicht geändert werden if userID == "admin" { w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"error": "Der Admin-Status des Users mit UID 'admin' kann nicht geändert werden"}) return } adminValue := 0 if *req.IsAdmin { adminValue = 1 } updates = append(updates, "is_admin = ?") args = append(args, adminValue) // Wenn Admin aktiviert wird, entferne alle Gruppen-Zuweisungen if *req.IsAdmin { _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID) if err != nil { log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen für Admin: %v", err) } } } // enabled aktualisieren, falls angegeben // Nur der spezielle Admin-User mit UID "admin" kann enabled geändert werden if req.Enabled != nil { // Nur UID "admin" kann enabled geändert werden if userID != "admin" { w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"error": "Nur der Admin-User mit UID 'admin' kann aktiviert/deaktiviert werden"}) return } // Prüfe ob der anfragende User ein Admin ist (für Deaktivierung) if !*req.Enabled { requestUserID, _ := getUserFromRequest(r) isRequestingAdmin, err := isUserAdmin(requestUserID) if err != nil || !isRequestingAdmin { w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren können den Admin-User mit UID 'admin' deaktivieren"}) return } } enabledValue := 0 if *req.Enabled { enabledValue = 1 } updates = append(updates, "enabled = ?") args = append(args, enabledValue) } if req.Password != "" { // Altes Passwort ist erforderlich, wenn Passwort geändert wird if req.OldPassword == "" { 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 } // Aktualisiere Gruppen-Zuweisungen, falls angegeben // Nur wenn User nicht Admin ist oder Admin deaktiviert wird if req.GroupIDs != nil { // Prüfe ob User nach Update Admin ist var willBeAdmin int if req.IsAdmin != nil { if *req.IsAdmin { willBeAdmin = 1 } } else { willBeAdmin = isAdmin } // Nur Gruppen zuweisen wenn User nicht Admin ist if willBeAdmin == 0 { // Lösche alle bestehenden Gruppen-Zuweisungen _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID) if err != nil { log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err) } // Füge neue Gruppen-Zuweisungen hinzu for _, groupID := range req.GroupIDs { // Prüfe ob Gruppe existiert var exists bool err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists) if err == nil && exists { _, err = db.ExecContext(ctx, "INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID) if err != nil { log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err) } } } } } // Lade aktualisierten Benutzer var user User var isAdminUpdated, enabledUpdated int err = db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID). Scan(&user.ID, &user.Username, &user.Email, &isAdminUpdated, &enabledUpdated, &user.CreatedAt) if err != nil { http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err) return } user.IsAdmin = isAdminUpdated == 1 user.Enabled = enabledUpdated == 1 // Lade Gruppen-IDs (nur wenn nicht Admin) if user.IsAdmin { user.GroupIDs = []string{} // Admins haben keine Gruppen } else if req.GroupIDs != nil { user.GroupIDs = req.GroupIDs } else { groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) 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{} } } json.NewEncoder(w).Encode(user) // Audit-Log: User aktualisiert requestUserID, requestUsername := 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 } if req.IsAdmin != nil { details["isAdmin"] = *req.IsAdmin if *req.IsAdmin { details["message"] = "Benutzer wurde zum Administrator ernannt" } else { details["message"] = "Administrator-Rechte wurden entfernt" } } if req.Enabled != nil { details["enabled"] = *req.Enabled if *req.Enabled { details["message"] = "Benutzer wurde aktiviert" } else { details["message"] = "Benutzer wurde deaktiviert" } } if req.GroupIDs != nil { details["groupIds"] = req.GroupIDs } auditService.Track(r.Context(), "UPDATE", "user", vars["id"], requestUserID, requestUsername, 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*10) defer cancel() // Prüfe ob der zu löschende User der spezielle Admin-User mit UID "admin" ist // Nur dieser User kann nicht gelöscht werden, andere Admin-User können gelöscht werden if userID == "admin" { w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{ "error": "Der Administrator-Benutzer mit UID 'admin' kann nicht gelöscht werden. Verwenden Sie stattdessen die Deaktivierungsfunktion.", }) return } // Prüfe ob User existiert var exists bool err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Benutzers: %v", err) return } if !exists { http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound) return } result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID) if err != nil { http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError) 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 requestUserID, username := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "DELETE", "user", vars["id"], requestUserID, 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) } // 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") } 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*10) defer cancel() var id string var enabled int err = db.QueryRowContext(ctx, "SELECT id, enabled FROM users WHERE username = ?", username).Scan(&id, &enabled) if err != nil { // Logge Fehler nur wenn es nicht "no rows" ist if err != sql.ErrNoRows { log.Printf("Fehler beim Abrufen der User-ID für %s: %v", username, err) } return "", username } // Prüfe ob User aktiviert ist if enabled == 0 { log.Printf("API-Zugriff für deaktivierten Benutzer: %s", username) return "", username } return id, username } // UserPermissionInfo enthält die Berechtigungsinformationen eines Benutzers type UserPermissionInfo struct { UserID string Groups []PermissionGroupInfo HasFullAccess bool // true wenn der Benutzer mindestens eine FULL_ACCESS Gruppe hat } // PermissionGroupInfo enthält Informationen über eine Berechtigungsgruppe type PermissionGroupInfo struct { GroupID string Permission PermissionLevel SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces } // isUserAdmin prüft, ob ein Benutzer Admin ist func isUserAdmin(userID string) (bool, error) { if userID == "" { return false, nil } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var isAdmin int err := db.QueryRowContext(ctx, "SELECT is_admin FROM users WHERE id = ?", userID).Scan(&isAdmin) if err != nil { if err == sql.ErrNoRows { return false, nil } return false, err } return isAdmin == 1, nil } // getUserPermissions ruft die Berechtigungen eines Benutzers ab func getUserPermissions(userID string) (*UserPermissionInfo, error) { if userID == "" { return nil, fmt.Errorf("userID ist leer") } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) } if isAdmin { return &UserPermissionInfo{ UserID: userID, Groups: []PermissionGroupInfo{}, HasFullAccess: true, }, nil } // Hole alle Gruppen des Benutzers mit ihren Berechtigungen query := ` SELECT pg.id, pg.permission FROM permission_groups pg INNER JOIN user_groups ug ON pg.id = ug.group_id WHERE ug.user_id = ? ` rows, err := db.QueryContext(ctx, query, userID) if err != nil { return nil, fmt.Errorf("fehler beim Abrufen der Benutzergruppen: %w", err) } defer rows.Close() info := &UserPermissionInfo{ UserID: userID, Groups: []PermissionGroupInfo{}, } var groupIDs []string for rows.Next() { var groupID string var permission string if err := rows.Scan(&groupID, &permission); err != nil { continue } groupIDs = append(groupIDs, groupID) groupInfo := PermissionGroupInfo{ GroupID: groupID, Permission: PermissionLevel(permission), SpaceIDs: []string{}, } if PermissionLevel(permission) == PermissionFullAccess { info.HasFullAccess = true } info.Groups = append(info.Groups, groupInfo) } // Hole Space-Zuweisungen für alle Gruppen if len(groupIDs) > 0 { placeholders := make([]string, len(groupIDs)) args := make([]interface{}, len(groupIDs)) for i, id := range groupIDs { placeholders[i] = "?" args[i] = id } spaceQuery := fmt.Sprintf(` SELECT group_id, space_id FROM group_spaces WHERE group_id IN (%s) `, strings.Join(placeholders, ",")) spaceRows, err := db.QueryContext(ctx, spaceQuery, args...) if err == nil { 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() // Aktualisiere SpaceIDs für jede Gruppe for i := range info.Groups { if spaceIDs, exists := spaceMap[info.Groups[i].GroupID]; exists { info.Groups[i].SpaceIDs = spaceIDs } } } } return info, nil } // hasSpaceAccess prüft, ob ein Benutzer Zugriff auf einen bestimmten Space hat func hasSpaceAccess(userID, spaceID string) (bool, error) { if userID == "" { return false, nil } // Admins haben immer Zugriff isAdmin, err := isUserAdmin(userID) if err == nil && isAdmin { return true, nil } permissions, err := getUserPermissions(userID) if err != nil { return false, err } // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keinen Zugriff // Admins haben immer Zugriff (wird bereits oben geprüft) if !isAdmin && len(permissions.Groups) == 0 { return false, nil } // Prüfe für jede Gruppe, ob der Benutzer Zugriff auf den Space hat for _, group := range permissions.Groups { // Wenn die Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces if len(group.SpaceIDs) == 0 { return true, nil } // Prüfe, ob der Space in der Liste der zugewiesenen Spaces ist for _, assignedSpaceID := range group.SpaceIDs { if assignedSpaceID == spaceID { return true, nil } } } return false, nil } // hasPermission prüft, ob ein Benutzer eine bestimmte Berechtigung für einen Space hat // requiredPermission kann READ, READ_WRITE oder FULL_ACCESS sein func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (bool, error) { if userID == "" { return false, nil } // Admins haben immer alle Berechtigungen isAdmin, err := isUserAdmin(userID) if err == nil && isAdmin { return true, nil } permissions, err := getUserPermissions(userID) if err != nil { return false, err } // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keine Berechtigung // Admins haben immer alle Berechtigungen (wird bereits oben geprüft) if !isAdmin && len(permissions.Groups) == 0 { return false, nil } // Prüfe für jede Gruppe for _, group := range permissions.Groups { hasAccess := false // Prüfe, ob der Benutzer Zugriff auf den Space hat if len(group.SpaceIDs) == 0 { // Keine Space-Zuweisungen = Zugriff auf alle Spaces hasAccess = true } else { // Prüfe, ob der Space in der Liste ist for _, assignedSpaceID := range group.SpaceIDs { if assignedSpaceID == spaceID { hasAccess = true break } } } if !hasAccess { continue } // Prüfe die Berechtigungsstufe switch requiredPermission { case PermissionRead: // READ, READ_WRITE und FULL_ACCESS haben alle READ-Berechtigung return true, nil case PermissionReadWrite: // READ_WRITE und FULL_ACCESS haben READ_WRITE-Berechtigung if group.Permission == PermissionReadWrite || group.Permission == PermissionFullAccess { return true, nil } case PermissionFullAccess: // Nur FULL_ACCESS hat FULL_ACCESS-Berechtigung if group.Permission == PermissionFullAccess { return true, nil } } } return false, nil } // getAccessibleSpaceIDs gibt alle Space-IDs zurück, auf die der Benutzer Zugriff hat func getAccessibleSpaceIDs(userID string) ([]string, error) { if userID == "" { return []string{}, nil } // Prüfe ob User Admin ist - Admins haben Zugriff auf alle Spaces isAdmin, err := isUserAdmin(userID) if err == nil && isAdmin { // Hole alle Spaces für Admin ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM spaces") if err != nil { return []string{}, err } defer rows.Close() var spaceIDs []string for rows.Next() { var spaceID string if err := rows.Scan(&spaceID); err == nil { spaceIDs = append(spaceIDs, spaceID) } } return spaceIDs, nil } permissions, err := getUserPermissions(userID) if err != nil { return []string{}, err } // Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff // (Admin wurde bereits oben behandelt) if len(permissions.Groups) == 0 { return []string{}, nil } // Sammle alle zugewiesenen Spaces spaceIDMap := make(map[string]bool) hasUnrestrictedAccess := false for _, group := range permissions.Groups { // Wenn eine Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces if len(group.SpaceIDs) == 0 { hasUnrestrictedAccess = true break } // Sammle alle zugewiesenen Spaces for _, spaceID := range group.SpaceIDs { spaceIDMap[spaceID] = true } } // Wenn der Benutzer Zugriff auf alle Spaces hat, hole alle Spaces aus der DB if hasUnrestrictedAccess { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM spaces") if err != nil { return []string{}, err } defer rows.Close() var spaceIDs []string for rows.Next() { var spaceID string if err := rows.Scan(&spaceID); err == nil { spaceIDs = append(spaceIDs, spaceID) } } return spaceIDs, nil } // Konvertiere Map zu Slice spaceIDs := make([]string, 0, len(spaceIDMap)) for spaceID := range spaceIDMap { spaceIDs = append(spaceIDs, spaceID) } return spaceIDs, nil } // 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*10) defer cancel() var storedHash string var enabled int err = db.QueryRowContext(ctx, "SELECT password_hash, enabled FROM users WHERE username = ?", username).Scan(&storedHash, &enabled) if err != nil { if err == sql.ErrNoRows { if !isAjaxRequest { 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 ob User aktiviert ist if enabled == 0 { if !isAjaxRequest { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"}) return } // Prüfe Passwort err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) if err != nil { 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) } } // adminOnlyMiddleware prüft, ob der Benutzer ein Admin ist func adminOnlyMiddleware(next http.HandlerFunc) http.HandlerFunc { return basicAuthMiddleware(func(w http.ResponseWriter, r *http.Request) { userID, _ := getUserFromRequest(r) if userID == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Nicht authentifiziert"}) return } isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "Fehler beim Prüfen der Berechtigung"}) return } if !isAdmin { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren haben Zugriff auf diese Funktion"}) return } next(w, r) }) } // Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück) func loginHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") 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*10) defer cancel() var user User var storedHash string var enabled int var isAdmin int err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, enabled, is_admin, created_at FROM users WHERE username = ?", username). Scan(&user.ID, &user.Username, &user.Email, &storedHash, &enabled, &isAdmin, &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 } // Setze isAdmin und enabled Felder user.IsAdmin = isAdmin == 1 user.Enabled = enabled == 1 // Prüfe ob User aktiviert ist if enabled == 0 { log.Printf("Login-Versuch für deaktivierten Benutzer: %s", username) w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"}) return } // Prüfe Passwort err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) if err != nil { 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...") // Initialisiere Logging-System if err := initCertLogger(); err != nil { log.Printf("Warnung: Fehler beim Initialisieren des Logging-Systems: %v", err) } defer closeCertLogger() // Initialisiere Datenbank log.Println("Initialisiere Datenbank...") initDB() defer func() { log.Println("Schließe Datenbankverbindung...") db.Close() }() log.Println("Datenbank initialisiert") // Initialisiere Provider pm := providers.GetManager() pm.RegisterProvider(providers.NewDummyCAProvider()) pm.RegisterProvider(providers.NewAutoDNSProvider()) pm.RegisterProvider(providers.NewHetznerProvider()) pm.RegisterProvider(providers.NewCertigoACMEProxyProvider()) // Initialisiere ACME-Provider acmeManager := providers.GetACMEManager() acmeManager.RegisterACMEProvider(providers.NewLetsEncryptProvider("production")) acmeManager.RegisterACMEProvider(providers.NewLetsEncryptProvider("staging")) // Starte Renewal Scheduler StartRenewalScheduler() r := mux.NewRouter() // Swagger UI Route r.HandleFunc("/swagger", swaggerUIHandler).Methods("GET") r.HandleFunc("/api/openapi.yaml", openAPIHandler).Methods("GET") // API Routes api := r.PathPrefix("/api").Subrouter() // Public Routes (keine Auth erforderlich) api.HandleFunc("/health", healthHandler).Methods("GET", "OPTIONS") api.HandleFunc("/login", loginHandler).Methods("POST", "OPTIONS") // Protected Routes (Basic Auth erforderlich) api.HandleFunc("/stats", basicAuthMiddleware(getStatsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", basicAuthMiddleware(getSpacesHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", basicAuthMiddleware(createSpaceHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{id}", basicAuthMiddleware(deleteSpaceHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns/count", basicAuthMiddleware(getSpaceFqdnCountHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns", basicAuthMiddleware(getFqdnsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns", basicAuthMiddleware(createFqdnHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns", basicAuthMiddleware(deleteAllFqdnsHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns/{fqdnId}", basicAuthMiddleware(deleteFqdnHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/fqdns", basicAuthMiddleware(deleteAllFqdnsGlobalHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(uploadCSRHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS") // User Routes (Admin only) api.HandleFunc("/users", adminOnlyMiddleware(getUsersHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/users", adminOnlyMiddleware(createUserHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/users/{id}", adminOnlyMiddleware(getUserHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/users/{id}", adminOnlyMiddleware(updateUserHandler)).Methods("PUT", "OPTIONS") api.HandleFunc("/users/{id}", adminOnlyMiddleware(deleteUserHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS") // Permission Groups Routes (Admin only) api.HandleFunc("/permission-groups", adminOnlyMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/permission-groups", adminOnlyMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS") api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS") // Provider Routes (Protected) api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS") 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") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/request-certificate", basicAuthMiddleware(requestCertificateHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/renewal-enabled", basicAuthMiddleware(updateFqdnRenewalEnabledHandler)).Methods("PUT", "OPTIONS") // Renewal Queue Routes api.HandleFunc("/renewal-queue", basicAuthMiddleware(getRenewalQueueHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/renewal-queue", basicAuthMiddleware(deleteAllRenewalQueueEntriesHandler)).Methods("DELETE", "OPTIONS") // Renewal Queue Test Routes (nur für Administratoren) api.HandleFunc("/renewal-queue/test/create", basicAuthMiddleware(createTestRenewalQueueEntryHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/renewal-queue/test/trigger", basicAuthMiddleware(triggerRenewalQueueHandler)).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" log.Printf("Server läuft auf Port %s", port) log.Fatal(http.ListenAndServe(port, r)) } // Provider Handlers func getProvidersHandler(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 } pm := providers.GetManager() allProviders := pm.GetAllProviders() // Definiere feste Reihenfolge der Provider providerOrder := []string{"dummy-ca", "autodns", "hetzner"} // Erstelle Map für schnellen Zugriff providerMap := make(map[string]providers.ProviderInfo) for id, provider := range allProviders { config, _ := pm.GetProviderConfig(id) providerInfo := providers.ProviderInfo{ ID: id, Name: provider.GetName(), DisplayName: provider.GetDisplayName(), Description: provider.GetDescription(), Enabled: config.Enabled, AcmeReady: config.AcmeReady, Settings: provider.GetRequiredSettings(), } providerMap[id] = providerInfo } // Sortiere nach definierter Reihenfolge var providerInfos []providers.ProviderInfo for _, id := range providerOrder { if providerInfo, exists := providerMap[id]; exists { providerInfos = append(providerInfos, providerInfo) delete(providerMap, id) } } // Füge alle anderen Provider hinzu, die nicht in der Liste sind for _, providerInfo := range providerMap { providerInfos = append(providerInfos, providerInfo) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(providerInfos) } func getProviderHandler(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) id := vars["id"] pm := providers.GetManager() provider, exists := pm.GetProvider(id) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } config, _ := pm.GetProviderConfig(id) providerInfo := providers.ProviderInfo{ ID: id, Name: provider.GetName(), DisplayName: provider.GetDisplayName(), Description: provider.GetDescription(), Enabled: config.Enabled, Settings: provider.GetRequiredSettings(), } // Füge aktuelle Konfigurationswerte hinzu (ohne Passwörter) safeSettings := make(map[string]interface{}) for key, value := range config.Settings { // Verstecke Passwörter und API Keys in der Antwort if key == "password" || key == "apiKey" { if str, ok := value.(string); ok && str != "" { safeSettings[key] = "***" } else { safeSettings[key] = value } } else { safeSettings[key] = value } } // Konvertiere zu JSON für die Response safeSettingsJSON, _ := json.Marshal(safeSettings) var safeSettingsMap map[string]interface{} json.Unmarshal(safeSettingsJSON, &safeSettingsMap) response := map[string]interface{}{ "id": providerInfo.ID, "name": providerInfo.Name, "displayName": providerInfo.DisplayName, "description": providerInfo.Description, "enabled": providerInfo.Enabled, "settings": providerInfo.Settings, "config": safeSettingsMap, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } func setProviderEnabledHandler(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) id := vars["id"] var req struct { Enabled bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() if err := pm.SetProviderEnabled(id, req.Enabled); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "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) { 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) id := vars["id"] var req struct { Settings map[string]interface{} `json:"settings"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() config, _ := pm.GetProviderConfig(id) config.Settings = req.Settings if err := pm.UpdateProviderConfig(id, config); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) 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) { 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 } vars := mux.Vars(r) id := vars["id"] var req struct { Settings map[string]interface{} `json:"settings"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() provider, exists := pm.GetProvider(id) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } if err := provider.TestConnection(req.Settings); err != nil { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": err.Error(), }) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Verbindung erfolgreich", }) } func signCSRHandler(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 } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] // Prüfe Berechtigung: READ_WRITE oder FULL_ACCESS erforderlich zum Signieren von CSRs userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasPermission, err := hasPermission(userID, spaceID, PermissionReadWrite) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasPermission { http.Error(w, "Keine Berechtigung zum Signieren von CSRs. Lesen/Schreiben oder Vollzugriff erforderlich.", http.StatusForbidden) return } var req struct { ProviderID string `json:"providerId"` CSRID string `json:"csrId,omitempty"` // Optional: spezifischer CSR, sonst neuester } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.ProviderID == "" { http.Error(w, "providerId ist erforderlich", http.StatusBadRequest) return } // Hole neuesten CSR für den FQDN var csrPEM string var csrID string err = db.QueryRow(` SELECT id, csr_pem FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC LIMIT 1 `, fqdnID, spaceID).Scan(&csrID, &csrPEM) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Kein CSR für diesen FQDN gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Laden des CSR: %v", err) return } // Wenn spezifischer CSR angefordert wurde if req.CSRID != "" && req.CSRID != csrID { err := db.QueryRow(` SELECT csr_pem FROM csrs WHERE id = ? AND fqdn_id = ? AND space_id = ? `, req.CSRID, fqdnID, spaceID).Scan(&csrPEM) if err != nil { if err == sql.ErrNoRows { http.Error(w, "CSR nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError) return } csrID = req.CSRID } // Hole Provider pm := providers.GetManager() provider, exists := pm.GetProvider(req.ProviderID) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } // Prüfe ob Provider aktiviert ist config, err := pm.GetProviderConfig(req.ProviderID) if err != nil || !config.Enabled { http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest) return } // Signiere CSR result, err := provider.SignCSR(csrPEM, config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler beim Signieren des CSR: %v", err), http.StatusInternalServerError) log.Printf("Fehler beim Signieren des CSR: %v", err) return } // Speichere das Zertifikat in der DB certID := uuid.New().String() createdAt := time.Now() _, err = db.Exec(` INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, certID, fqdnID, spaceID, csrID, result.OrderID, req.ProviderID, result.CertificatePEM, result.Status, createdAt) if err != nil { log.Printf("Fehler beim Speichern des Zertifikats: %v", err) // Weiterhin erfolgreich zurückgeben, auch wenn Speichern fehlschlägt } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": result.Message, "certificateId": certID, "orderId": result.OrderID, "status": result.Status, "csrId": csrID, }) // Audit-Log: CSR signiert 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) { 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) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben (READ, READ_WRITE oder FULL_ACCESS) userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasAccess, err := hasSpaceAccess(userID, spaceID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasAccess { http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden) return } // Hole alle Zertifikate für diesen FQDN, sortiert nach Ablaufdatum (neueste zuerst) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() rows, err := db.QueryContext(ctx, ` SELECT id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at FROM certificates WHERE fqdn_id = ? AND space_id = ? ORDER BY expires_at DESC, created_at DESC `, fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Laden der Zertifikate", http.StatusInternalServerError) log.Printf("Fehler beim Laden der Zertifikate: %v", err) return } defer rows.Close() var certificates []map[string]interface{} for rows.Next() { var id, certID, providerID, certPEM, status, createdAt string var csrID sql.NullString var privateKeyPEM sql.NullString var expiresAtStr sql.NullString var isIntermediateInt int err := rows.Scan(&id, &csrID, &certID, &providerID, &certPEM, &privateKeyPEM, &status, &expiresAtStr, &isIntermediateInt, &createdAt) if err != nil { log.Printf("Fehler beim Scannen der Zertifikat-Zeile: %v", err) continue } // Parse und formatiere createdAt als ISO 8601 mit Europe/Berlin Zeitzone // Annahme: Zeiten in DB sind in UTC gespeichert (Format: "2006-01-02 15:04:05") var createdAtISO string if createdAt != "" { if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil { // Parse erstellt eine Zeit ohne Zeitzone, interpretiere sie explizit als UTC tUTC := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.UTC) berlinLocation, err := time.LoadLocation("Europe/Berlin") if err != nil { log.Printf("Warnung: Konnte Zeitzone Europe/Berlin nicht laden: %v, verwende UTC", err) createdAtISO = tUTC.Format(time.RFC3339) } else { // Konvertiere von UTC nach Europe/Berlin berlinTime := tUTC.In(berlinLocation) createdAtISO = berlinTime.Format(time.RFC3339) } } else { // Fallback: Verwende Original-String createdAtISO = createdAt } } // Extrahiere Leaf-Zertifikat für Metadaten (expiresAt, issuer), aber sende komplettes PEM leafPEM, _, splitErr := SplitCertificateChain(certPEM) // Verwende Leaf-PEM für Metadaten-Extraktion, falls verfügbar certPEMForMetadata := certPEM if splitErr == nil && leafPEM != "" { certPEMForMetadata = leafPEM } // Extrahiere Issuer aus dem Leaf-Zertifikat (für "Issued by" Anzeige) var issuerName string if certPEMForMetadata != "" { issuer, err := GetCertificateIssuer(certPEMForMetadata) if err == nil { issuerName = GetProviderNameFromIssuer(issuer) } } // Erstelle Zertifikat-Eintrag (komplettes PEM, aber nur Leaf-Metadaten) certData := map[string]interface{}{ "id": id, "certificateId": certID, "providerId": providerID, // Ursprünglicher Provider aus Certigo "issuer": issuerName, // Issuer aus dem Leaf-Zertifikat "certificatePEM": certPEM, // Komplettes PEM (mit Intermediate) "status": status, "createdAt": createdAtISO, } // Füge csrId nur hinzu, wenn vorhanden (kann NULL sein für ACME-Zertifikate) if csrID.Valid && csrID.String != "" { certData["csrId"] = csrID.String } // Füge privateKeyPEM hinzu, wenn vorhanden if privateKeyPEM.Valid && privateKeyPEM.String != "" { certData["privateKeyPEM"] = privateKeyPEM.String } // Füge expiresAt hinzu (nur für Leaf-Zertifikate, aus DB oder aus Leaf-Zertifikat extrahiert) // Ignoriere Intermediate-Zertifikate (isIntermediateInt == 1) if isIntermediateInt == 0 { // Zuerst versuche es aus der DB - sende direkt als String ohne Zeitzonen-Konvertierung if expiresAtStr.Valid && expiresAtStr.String != "" { // Sende die Zeit direkt aus der DB ohne Konvertierung certData["expiresAt"] = expiresAtStr.String } else { // Falls nicht in DB, extrahiere aus Leaf-Zertifikat if certPEMForMetadata != "" { certExpiresAt, _, parseErr := ParseCertificate(certPEMForMetadata) if parseErr == nil { // Format als "YYYY-MM-DD HH:MM:SS" ohne Zeitzone certData["expiresAt"] = certExpiresAt.UTC().Format("2006-01-02 15:04:05") } } } } // Nur Leaf-Zertifikate anzeigen (ignoriere Intermediate) if isIntermediateInt == 0 { certificates = append(certificates, certData) } } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(certificates) } func refreshCertificateHandler(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 } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] certID := vars["certId"] // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben (READ, READ_WRITE oder FULL_ACCESS) userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } hasAccess, err := hasSpaceAccess(userID, spaceID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigung: %v", err) return } if !hasAccess { http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden) return } // Hole Zertifikat aus DB var certificateID, providerID string err = db.QueryRow(` SELECT certificate_id, provider_id FROM certificates WHERE id = ? AND fqdn_id = ? AND space_id = ? `, certID, fqdnID, spaceID).Scan(&certificateID, &providerID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Zertifikat nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des Zertifikats", http.StatusInternalServerError) return } // Hole Provider pm := providers.GetManager() provider, exists := pm.GetProvider(providerID) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } // Prüfe ob Provider aktiviert ist config, err := pm.GetProviderConfig(providerID) if err != nil || !config.Enabled { http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest) return } // Rufe Zertifikat von CA ab certPEM, err := provider.GetCertificate(certificateID, config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler beim Abrufen des Zertifikats: %v", err), http.StatusInternalServerError) return } // Aktualisiere Zertifikat in DB _, err = db.Exec(` UPDATE certificates SET certificate_pem = ? WHERE id = ? AND fqdn_id = ? AND space_id = ? `, certPEM, certID, fqdnID, spaceID) if err != nil { log.Printf("Fehler beim Aktualisieren des Zertifikats: %v", err) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "certificatePEM": certPEM, "certificateId": certificateID, }) }