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) } log.Printf("Rufe RequestCertificate auf...") result, err := RequestCertificate(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 := `