package main
import (
"context"
"crypto/x509"
"database/sql"
"encoding/asn1"
"encoding/hex"
"encoding/json"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
"certigo-addon-backend/internal/core"
"certigo-addon-backend/providers"
)
// 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"`
}
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"`
}
type CreateFQDNRequest struct {
FQDN string `json:"fqdn"`
Description string `json:"description"`
}
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"`
CreatedAt string `json:"createdAt"`
}
// CreateUserRequest struct für Benutzer-Erstellung
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
// UpdateUserRequest struct für Benutzer-Update
type UpdateUserRequest struct {
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
OldPassword string `json:"oldPassword,omitempty"`
Password string `json:"password,omitempty"`
}
// MessageResponse struct für einfache Nachrichten
type MessageResponse struct {
Message string `json:"message"`
}
// AuditLog struct für Audit-Logs
type AuditLog struct {
ID string `json:"id"`
Timestamp string `json:"timestamp"`
UserID string `json:"userId,omitempty"`
Username string `json:"username,omitempty"`
Action string `json:"action"`
ResourceType string `json:"resourceType"`
ResourceID string `json:"resourceId,omitempty"`
Details string `json:"details,omitempty"`
IPAddress string `json:"ipAddress,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
}
var db *sql.DB
var auditService *core.AuditService
func initDB() {
var err error
// 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*5)
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
log.Println("Führe WAL-Checkpoint aus...")
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
_, err = db.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)")
cancel()
if err != nil {
log.Printf("Warnung: WAL-Checkpoint fehlgeschlagen: %v", err)
}
// 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)
}
// 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...")
createCertificateTableSQL := `
CREATE TABLE IF NOT EXISTS certificates (
id TEXT PRIMARY KEY,
fqdn_id TEXT NOT NULL,
space_id TEXT NOT NULL,
csr_id TEXT NOT NULL,
certificate_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
certificate_pem TEXT,
status TEXT NOT NULL,
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 (csr_id) REFERENCES csrs(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,
created_at DATETIME NOT NULL
);`
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
_, err = db.ExecContext(ctx, createUsersTableSQL)
cancel()
if err != nil {
if strings.Contains(err.Error(), "database is locked") {
log.Fatal("Datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden (z.B. andere go run main.go Instanzen).")
}
log.Fatal("Fehler beim Erstellen der Users-Tabelle:", err)
}
log.Println("Datenbank erfolgreich initialisiert")
// Erstelle Audit-Log-Tabelle
log.Println("Erstelle audit_logs-Tabelle...")
createAuditLogsTableSQL := `
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
timestamp DATETIME NOT NULL,
user_id TEXT,
username TEXT,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
details TEXT,
ip_address TEXT,
user_agent TEXT
);`
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
_, err = db.ExecContext(ctx, createAuditLogsTableSQL)
cancel()
if err != nil {
if strings.Contains(err.Error(), "database is locked") {
log.Fatal("Datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden (z.B. andere go run main.go Instanzen).")
}
log.Fatal("Fehler beim Erstellen der Audit-Log-Tabelle:", err)
}
// Erstelle Index für bessere Performance
log.Println("Erstelle Indizes für audit_logs...")
createIndexSQL := `
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_type ON audit_logs(resource_type);`
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
_, err = db.ExecContext(ctx, createIndexSQL)
cancel()
if err != nil {
log.Printf("Warnung: Fehler beim Erstellen der Indizes: %v", err)
}
// Erstelle Default Admin-User falls nicht vorhanden
createDefaultAdmin()
// Initialisiere AuditService (muss nach DB-Initialisierung passieren)
auditService = core.NewAuditService(db)
if auditService == nil {
log.Fatal("Fehler: AuditService konnte nicht initialisiert werden")
}
log.Println("AuditService erfolgreich initialisiert")
// Erstelle Upload-Ordner für Profilbilder
avatarDir := "uploads/avatars"
if err := os.MkdirAll(avatarDir, 0755); err != nil {
log.Printf("Warnung: Konnte Avatar-Ordner nicht erstellen: %v", err)
} else {
log.Printf("Avatar-Ordner erstellt: %s", avatarDir)
}
}
func createDefaultAdmin() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Prüfe ob bereits ein Admin-User existiert
var count int
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count)
if err != nil {
log.Printf("Fehler beim Prüfen des Admin-Users: %v", err)
return
}
if count > 0 {
log.Println("Admin-User existiert bereits")
// Prüfe ob das Passwort noch "admin" ist (für Debugging)
var storedHash string
err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = 'admin'").Scan(&storedHash)
if err == nil {
// Teste ob das Passwort "admin" ist
testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin"))
if testErr == nil {
log.Println("Admin-User Passwort ist korrekt gesetzt")
} else {
log.Println("Warnung: Admin-User Passwort ist nicht 'admin'")
}
}
return
}
// Erstelle Default Admin-User
adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
log.Printf("Fehler beim Hashen des Admin-Passworts: %v", err)
return
}
adminID := uuid.New().String()
createdAt := time.Now().Format(time.RFC3339)
_, err = db.ExecContext(ctx,
"INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt)
if err != nil {
log.Printf("Fehler beim Erstellen des Admin-Users: %v", err)
return
}
log.Println("✓ Default Admin-User erstellt: username='admin', password='admin'")
log.Printf(" User ID: %s", adminID)
log.Printf(" Email: admin@certigo.local")
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
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 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
}
response := StatsResponse{
Spaces: spacesCount,
FQDNs: fqdnsCount,
CSRs: csrsCount,
Certificates: certificatesCount,
}
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
}
// Verwende Prepared Statement für bessere Performance und Sicherheit
stmt, err := db.Prepare("SELECT id, name, description, created_at FROM spaces ORDER BY created_at DESC")
if err != nil {
http.Error(w, "Fehler beim Vorbereiten der Abfrage", http.StatusInternalServerError)
log.Printf("Fehler beim Vorbereiten der Abfrage: %v", err)
return
}
defer stmt.Close()
rows, err := stmt.Query()
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
}
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 {
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "CREATE", "space", id, userID, username, map[string]interface{}{
"name": req.Name,
"description": req.Description,
"message": fmt.Sprintf("Space erstellt: %s", req.Name),
}, ipAddress, userAgent)
}
}
func deleteSpaceHandler(w http.ResponseWriter, r *http.Request) {
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 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
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
details := map[string]interface{}{
"message": fmt.Sprintf("Space gelöscht: %s", id),
}
if deleteFqdns && fqdnCount > 0 {
details["fqdnsDeleted"] = fqdnCount
details["message"] = fmt.Sprintf("Space gelöscht: %s (mit %d FQDNs)", id, fqdnCount)
}
auditService.Track(r.Context(), "DELETE", "space", id, userID, username, details, ipAddress, userAgent)
}
func getSpaceFqdnCountHandler(w http.ResponseWriter, r *http.Request) {
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
}
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 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 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
err := rows.Scan(&fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN, &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 {
fqdn.Description = description.String
} else {
fqdn.Description = ""
}
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 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()
// Speichere in Datenbank
_, err = db.Exec(
"INSERT INTO fqdns (id, space_id, fqdn, description, created_at) VALUES (?, ?, ?, ?, ?)",
id, spaceID, req.FQDN, req.Description, createdAt,
)
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),
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newFqdn)
// Audit-Log: FQDN erstellt
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "CREATE", "fqdn", id, userID, username, map[string]interface{}{
"fqdn": req.FQDN,
"spaceId": spaceID,
"description": req.Description,
"message": fmt.Sprintf("FQDN erstellt: %s (Space: %s)", req.FQDN, spaceID),
}, ipAddress, userAgent)
}
func deleteFqdnHandler(w http.ResponseWriter, r *http.Request) {
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 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
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "DELETE", "fqdn", fqdnID, userID, username, map[string]interface{}{
"spaceId": spaceID,
"message": fmt.Sprintf("FQDN gelöscht (Space: %s)", spaceID),
}, ipAddress, userAgent)
}
func deleteAllFqdnsHandler(w http.ResponseWriter, r *http.Request) {
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 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 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 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 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
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "UPLOAD", "csr", csrID, userID, username, map[string]interface{}{
"fqdnId": fqdnID,
"spaceId": spaceID,
"message": fmt.Sprintf("CSR hochgeladen für FQDN: %s (Space: %s)", fqdnID, spaceID),
}, ipAddress, userAgent)
}
func getCSRByFQDNHandler(w http.ResponseWriter, r *http.Request) {
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 ob nur der neueste CSR gewünscht ist
latestOnly := r.URL.Query().Get("latest") == "true"
if latestOnly {
// Hole nur den neuesten CSR
var csr CSR
var createdAt time.Time
var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON, extensionsJSON sql.NullString
err := db.QueryRow(`
SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject,
public_key_algorithm, signature_algorithm, key_size,
dns_names, email_addresses, ip_addresses, uris, extensions, created_at
FROM csrs
WHERE fqdn_id = ? AND space_id = ?
ORDER BY created_at DESC
LIMIT 1
`, fqdnID, spaceID).Scan(
&csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject,
&csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize,
&dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt,
)
if err != nil {
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(nil)
return
}
http.Error(w, "Fehler beim Abrufen des CSR", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des CSR: %v", err)
return
}
// Parse JSON-Strings zurück zu Slices
json.Unmarshal([]byte(dnsNamesJSON.String), &csr.DNSNames)
json.Unmarshal([]byte(emailAddressesJSON.String), &csr.EmailAddresses)
json.Unmarshal([]byte(ipAddressesJSON.String), &csr.IPAddresses)
json.Unmarshal([]byte(urisJSON.String), &csr.URIs)
if extensionsJSON.Valid {
json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions)
} else {
csr.Extensions = []Extension{}
}
csr.CreatedAt = createdAt.Format(time.RFC3339)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(csr)
} else {
// Hole alle CSRs für diesen FQDN
rows, err := db.Query(`
SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject,
public_key_algorithm, signature_algorithm, key_size,
dns_names, email_addresses, ip_addresses, uris, extensions, created_at
FROM csrs
WHERE fqdn_id = ? AND space_id = ?
ORDER BY created_at DESC
`, fqdnID, spaceID)
if err != nil {
http.Error(w, "Fehler beim Abrufen der CSRs", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der CSRs: %v", err)
return
}
defer rows.Close()
var csrs []CSR
for rows.Next() {
var csr CSR
var createdAt time.Time
var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON string
var extensionsJSON sql.NullString
err := rows.Scan(
&csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject,
&csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize,
&dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt,
)
if err != nil {
http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Daten: %v", err)
return
}
// Parse JSON-Strings zurück zu Slices
json.Unmarshal([]byte(dnsNamesJSON), &csr.DNSNames)
json.Unmarshal([]byte(emailAddressesJSON), &csr.EmailAddresses)
json.Unmarshal([]byte(ipAddressesJSON), &csr.IPAddresses)
json.Unmarshal([]byte(urisJSON), &csr.URIs)
if extensionsJSON.Valid {
json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions)
} else {
csr.Extensions = []Extension{}
}
csr.CreatedAt = createdAt.Format(time.RFC3339)
csrs = append(csrs, csr)
}
if err = rows.Err(); err != nil {
http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError)
log.Printf("Fehler beim Verarbeiten der Daten: %v", err)
return
}
if csrs == nil {
csrs = []CSR{}
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(csrs)
}
}
func swaggerUIHandler(w http.ResponseWriter, r *http.Request) {
html := `
Certigo Addon API - Swagger UI
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func openAPIHandler(w http.ResponseWriter, r *http.Request) {
// Lese die OpenAPI YAML Datei
openAPIContent := `openapi: 3.0.3
info:
title: Certigo Addon API
description: API für die Verwaltung von Spaces, FQDNs und Certificate Signing Requests (CSRs)
version: 1.0.0
contact:
name: Certigo Addon
servers:
- url: http://localhost:8080/api
description: Local development server
paths:
/health:
get:
summary: System Health Check
description: Prüft den Systemstatus des Backends
tags: [System]
responses:
'200':
description: System ist erreichbar
content:
application/json:
schema:
$ref: '#/components/schemas/HealthResponse'
/stats:
get:
summary: Statistiken abrufen
description: Ruft Statistiken über die Anzahl der Spaces, FQDNs und CSRs ab
tags: [System]
responses:
'200':
description: Statistiken erfolgreich abgerufen
content:
application/json:
schema:
$ref: '#/components/schemas/StatsResponse'
/spaces:
get:
summary: Alle Spaces abrufen
description: Ruft eine Liste aller Spaces ab
tags: [Spaces]
responses:
'200':
description: Liste der Spaces
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Space'
post:
summary: Space erstellen
description: Erstellt einen neuen Space
tags: [Spaces]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateSpaceRequest'
responses:
'201':
description: Space erfolgreich erstellt
content:
application/json:
schema:
$ref: '#/components/schemas/Space'
'400':
description: Ungültige Anfrage
/spaces/{id}:
delete:
summary: Space löschen
description: Löscht einen Space. Wenn der Space FQDNs enthält, muss der Parameter deleteFqdns=true gesetzt werden.
tags: [Spaces]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
- name: deleteFqdns
in: query
required: false
schema:
type: boolean
default: false
description: Wenn true, werden alle FQDNs des Spaces mitgelöscht
responses:
'200':
description: Space erfolgreich gelöscht
content:
application/json:
schema:
$ref: '#/components/schemas/MessageResponse'
'404':
description: Space nicht gefunden
'409':
description: Space enthält noch FQDNs
/spaces/{id}/fqdns/count:
get:
summary: FQDN-Anzahl abrufen
description: Ruft die Anzahl der FQDNs für einen Space ab
tags: [FQDNs]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Anzahl der FQDNs
content:
application/json:
schema:
$ref: '#/components/schemas/CountResponse'
/spaces/{id}/fqdns:
get:
summary: Alle FQDNs eines Spaces abrufen
description: Ruft alle FQDNs für einen Space ab
tags: [FQDNs]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Liste der FQDNs
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FQDN'
'404':
description: Space nicht gefunden
post:
summary: FQDN erstellen
description: Erstellt einen neuen FQDN innerhalb eines Spaces
tags: [FQDNs]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateFQDNRequest'
responses:
'201':
description: FQDN erfolgreich erstellt
content:
application/json:
schema:
$ref: '#/components/schemas/FQDN'
'400':
description: Ungültige Anfrage
'404':
description: Space nicht gefunden
'409':
description: FQDN existiert bereits in diesem Space
delete:
summary: Alle FQDNs eines Spaces löschen
description: Löscht alle FQDNs eines Spaces
tags: [FQDNs]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Alle FQDNs erfolgreich gelöscht
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteResponse'
/spaces/{id}/fqdns/{fqdnId}:
delete:
summary: FQDN löschen
description: Löscht einen einzelnen FQDN
tags: [FQDNs]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
- name: fqdnId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: FQDN erfolgreich gelöscht
content:
application/json:
schema:
$ref: '#/components/schemas/MessageResponse'
'404':
description: FQDN nicht gefunden
/fqdns:
delete:
summary: Alle FQDNs global löschen
description: Löscht alle FQDNs aus allen Spaces. Erfordert confirm=true Query-Parameter.
tags: [FQDNs]
parameters:
- name: confirm
in: query
required: true
schema:
type: boolean
description: Muss true sein, um die Operation auszuführen
responses:
'200':
description: Alle FQDNs erfolgreich gelöscht
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteResponse'
'400':
description: Bestätigung erforderlich
/csrs:
delete:
summary: Alle CSRs global löschen
description: Löscht alle CSRs aus allen Spaces. Erfordert confirm=true Query-Parameter.
tags: [CSRs]
parameters:
- name: confirm
in: query
required: true
schema:
type: string
description: Muss "true" sein, um die Operation auszuführen
example: "true"
responses:
'200':
description: Alle CSRs erfolgreich gelöscht
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteResponse'
'400':
description: Bestätigung erforderlich
/spaces/{spaceId}/fqdns/{fqdnId}/csr:
post:
summary: CSR hochladen
description: Lädt einen CSR (Certificate Signing Request) im PEM-Format hoch
tags: [CSRs]
parameters:
- name: spaceId
in: path
required: true
schema:
type: string
format: uuid
- name: fqdnId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [csr, spaceId, fqdn]
properties:
csr:
type: string
format: binary
description: CSR-Datei im PEM-Format
spaceId:
type: string
description: ID des Spaces
fqdn:
type: string
description: Name des FQDNs
responses:
'201':
description: CSR erfolgreich hochgeladen
content:
application/json:
schema:
$ref: '#/components/schemas/CSR'
'400':
description: Ungültige Anfrage oder ungültiges CSR-Format
'404':
description: Space oder FQDN nicht gefunden
get:
summary: CSR(s) abrufen
description: Ruft CSR(s) für einen FQDN ab. Mit latest=true wird nur der neueste CSR zurückgegeben.
tags: [CSRs]
parameters:
- name: spaceId
in: path
required: true
schema:
type: string
format: uuid
- name: fqdnId
in: path
required: true
schema:
type: string
format: uuid
- name: latest
in: query
required: false
schema:
type: boolean
default: false
description: Wenn true, wird nur der neueste CSR zurückgegeben
responses:
'200':
description: CSR(s) erfolgreich abgerufen
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/CSR'
- type: array
items:
$ref: '#/components/schemas/CSR'
'404':
description: FQDN nicht gefunden
components:
schemas:
HealthResponse:
type: object
properties:
status:
type: string
example: "ok"
message:
type: string
example: "Backend ist erreichbar"
time:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
StatsResponse:
type: object
properties:
spaces:
type: integer
example: 5
fqdns:
type: integer
example: 12
csrs:
type: integer
example: 7
Space:
type: object
properties:
id:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
name:
type: string
example: "Mein Space"
description:
type: string
example: "Beschreibung des Spaces"
createdAt:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
CreateSpaceRequest:
type: object
required: [name]
properties:
name:
type: string
example: "Mein Space"
description:
type: string
example: "Beschreibung des Spaces"
FQDN:
type: object
properties:
id:
type: string
format: uuid
example: "660e8400-e29b-41d4-a716-446655440000"
spaceId:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
fqdn:
type: string
example: "example.com"
description:
type: string
example: "Beschreibung des FQDN"
createdAt:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
CreateFQDNRequest:
type: object
required: [fqdn]
properties:
fqdn:
type: string
example: "example.com"
description:
type: string
example: "Beschreibung des FQDN"
Extension:
type: object
properties:
id:
type: string
example: "2.5.29.37"
oid:
type: string
example: "2.5.29.37"
name:
type: string
example: "X509v3 Extended Key Usage"
critical:
type: boolean
example: false
value:
type: string
example: "301406082b0601050507030106082b06010505070302"
description:
type: string
example: "TLS Web Server Authentication"
purposes:
type: array
items:
type: string
example: ["TLS Web Server Authentication", "TLS Web Client Authentication"]
CSR:
type: object
properties:
id:
type: string
format: uuid
example: "770e8400-e29b-41d4-a716-446655440000"
fqdnId:
type: string
format: uuid
example: "660e8400-e29b-41d4-a716-446655440000"
spaceId:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
fqdn:
type: string
example: "example.com"
csrPem:
type: string
example: "-----BEGIN CERTIFICATE REQUEST-----"
subject:
type: string
example: "CN=example.com"
publicKeyAlgorithm:
type: string
example: "RSA"
signatureAlgorithm:
type: string
example: "SHA256-RSA"
keySize:
type: integer
example: 2048
dnsNames:
type: array
items:
type: string
example: ["example.com", "www.example.com"]
emailAddresses:
type: array
items:
type: string
example: ["admin@example.com"]
ipAddresses:
type: array
items:
type: string
example: ["192.168.1.1"]
uris:
type: array
items:
type: string
example: ["https://example.com"]
extensions:
type: array
items:
$ref: '#/components/schemas/Extension'
createdAt:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
MessageResponse:
type: object
properties:
message:
type: string
example: "Operation erfolgreich"
CountResponse:
type: object
properties:
count:
type: integer
example: 5
DeleteResponse:
type: object
properties:
message:
type: string
example: "Alle FQDNs erfolgreich gelöscht"
deletedCount:
type: integer
example: 5`
w.Header().Set("Content-Type", "application/x-yaml")
w.Write([]byte(openAPIContent))
}
// User Handler Functions
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC")
if err != nil {
http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der Benutzer: %v", err)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
if err != nil {
http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
return
}
users = append(users, user)
}
json.NewEncoder(w).Encode(users)
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
userID := vars["id"]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var user User
err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
return
}
http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return
}
json.NewEncoder(w).Encode(user)
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
return
}
// Validierung
if req.Username == "" || req.Email == "" || req.Password == "" {
http.Error(w, "Benutzername, E-Mail und Passwort sind erforderlich", http.StatusBadRequest)
return
}
// Passwortrichtlinie prüfen
if err := validatePassword(req.Password); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Passwort hashen
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError)
log.Printf("Fehler beim Hashen des Passworts: %v", err)
return
}
// Erstelle Benutzer
userID := uuid.New().String()
createdAt := time.Now().Format(time.RFC3339)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err = db.ExecContext(ctx,
"INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
userID, req.Username, req.Email, string(hashedPassword), createdAt)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
if strings.Contains(err.Error(), "username") {
http.Error(w, "Benutzername bereits vergeben", http.StatusConflict)
} else {
http.Error(w, "E-Mail-Adresse bereits vergeben", http.StatusConflict)
}
return
}
http.Error(w, "Fehler beim Erstellen des Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Erstellen des Benutzers: %v", err)
return
}
user := User{
ID: userID,
Username: req.Username,
Email: req.Email,
CreatedAt: createdAt,
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
// Audit-Log: User erstellt
requestUserID, requestUsername := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{
"username": req.Username,
"email": req.Email,
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email),
}, ipAddress, userAgent)
}
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
userID := vars["id"]
var req UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Prüfe ob Benutzer existiert
var exists bool
err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists)
if err != nil || !exists {
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
return
}
// Update Felder
updates := []string{}
args := []interface{}{}
if req.Username != "" {
updates = append(updates, "username = ?")
args = append(args, req.Username)
}
if req.Email != "" {
updates = append(updates, "email = ?")
args = append(args, req.Email)
}
if req.Password != "" {
// Altes Passwort ist erforderlich, wenn Passwort geändert wird
if req.OldPassword == "" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Altes Passwort ist erforderlich, um das Passwort zu ändern"})
return
}
// Hole aktuelles Passwort-Hash aus der Datenbank
var storedHash string
err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = ?", userID).Scan(&storedHash)
if err != nil {
http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return
}
// Validiere altes Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(req.OldPassword))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Altes Passwort ist falsch"})
return
}
// Passwortrichtlinie prüfen
if err := validatePassword(req.Password); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError)
log.Printf("Fehler beim Hashen des Passworts: %v", err)
return
}
updates = append(updates, "password_hash = ?")
args = append(args, string(hashedPassword))
}
if len(updates) == 0 {
http.Error(w, "Keine Felder zum Aktualisieren", http.StatusBadRequest)
return
}
args = append(args, userID)
query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", strings.Join(updates, ", "))
_, err = db.ExecContext(ctx, query, args...)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
if strings.Contains(err.Error(), "username") {
http.Error(w, "Benutzername bereits vergeben", http.StatusConflict)
} else {
http.Error(w, "E-Mail-Adresse bereits vergeben", http.StatusConflict)
}
return
}
http.Error(w, "Fehler beim Aktualisieren des Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Aktualisieren des Benutzers: %v", err)
return
}
// Lade aktualisierten Benutzer
var user User
err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
if err != nil {
http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err)
return
}
json.NewEncoder(w).Encode(user)
// Audit-Log: User aktualisiert
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
details := map[string]interface{}{}
if req.Username != "" {
details["username"] = req.Username
}
if req.Email != "" {
details["email"] = req.Email
}
if req.Password != "" {
details["passwordChanged"] = true
}
auditService.Track(r.Context(), "UPDATE", "user", vars["id"], userID, username, details, ipAddress, userAgent)
}
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
userID := vars["id"]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID)
if err != nil {
http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Löschen des Benutzers: %v", err)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
return
}
response := MessageResponse{Message: "Benutzer erfolgreich gelöscht"}
json.NewEncoder(w).Encode(response)
// Audit-Log: User gelöscht
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "DELETE", "user", vars["id"], userID, username, map[string]interface{}{
"message": fmt.Sprintf("User gelöscht: %s", vars["id"]),
}, ipAddress, userAgent)
}
// Profilbild-Upload Handler
func uploadAvatarHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Prüfe ob Benutzer authentifiziert ist
userID, username := getUserFromRequest(r)
if userID == "" {
http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
requestedUserID := vars["id"]
// Prüfe ob Benutzer sein eigenes Profilbild hochlädt
if userID != requestedUserID {
http.Error(w, "Sie können nur Ihr eigenes Profilbild ändern", http.StatusForbidden)
return
}
// Parse multipart form (max 10MB)
err := r.ParseMultipartForm(10 << 20) // 10MB
if err != nil {
http.Error(w, "Fehler beim Parsen des Formulars", http.StatusBadRequest)
return
}
file, handler, err := r.FormFile("avatar")
if err != nil {
http.Error(w, "Keine Datei gefunden", http.StatusBadRequest)
return
}
defer file.Close()
// Validiere Dateityp (nur Bilder)
allowedTypes := []string{"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"}
fileType := handler.Header.Get("Content-Type")
isAllowed := false
for _, allowedType := range allowedTypes {
if fileType == allowedType {
isAllowed = true
break
}
}
if !isAllowed {
http.Error(w, "Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt", http.StatusBadRequest)
return
}
// Bestimme Dateiendung basierend auf Content-Type
var ext string
switch fileType {
case "image/jpeg", "image/jpg":
ext = ".jpg"
case "image/png":
ext = ".png"
case "image/gif":
ext = ".gif"
case "image/webp":
ext = ".webp"
default:
ext = ".jpg"
}
// Erstelle Dateiname basierend auf User-ID
avatarDir := "uploads/avatars"
filename := userID + ext
avatarPath := filepath.Join(avatarDir, filename)
// Lösche alle vorhandenen Avatar-Dateien für diesen Benutzer
extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
for _, oldExt := range extensions {
oldPath := filepath.Join(avatarDir, userID+oldExt)
if _, err := os.Stat(oldPath); err == nil {
// Datei existiert, lösche sie
if err := os.Remove(oldPath); err != nil {
log.Printf("Warnung: Konnte alte Avatar-Datei nicht löschen: %v", err)
// Weiter machen, auch wenn Löschen fehlschlägt
} else {
log.Printf("Alte Avatar-Datei gelöscht: %s", oldPath)
}
}
}
// Erstelle Datei
dst, err := os.Create(avatarPath)
if err != nil {
http.Error(w, "Fehler beim Erstellen der Datei", http.StatusInternalServerError)
log.Printf("Fehler beim Erstellen der Avatar-Datei: %v", err)
return
}
defer dst.Close()
// Kopiere Dateiinhalt
_, err = io.Copy(dst, file)
if err != nil {
http.Error(w, "Fehler beim Speichern der Datei", http.StatusInternalServerError)
log.Printf("Fehler beim Speichern der Avatar-Datei: %v", err)
return
}
// Audit-Log
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "UPDATE", "user", userID, userID, username, map[string]interface{}{
"action": "avatar_uploaded",
"filename": filename,
}, ipAddress, userAgent)
// Erfolgreiche Antwort
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Profilbild erfolgreich hochgeladen",
"filename": filename,
"url": fmt.Sprintf("/api/users/%s/avatar", userID),
})
}
// Profilbild-Abruf Handler
func getAvatarHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
userID := vars["id"]
// Suche nach Avatar-Datei (versuche verschiedene Formate)
avatarDir := "uploads/avatars"
extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
var avatarPath string
var found bool
for _, ext := range extensions {
path := filepath.Join(avatarDir, userID+ext)
if _, err := os.Stat(path); err == nil {
avatarPath = path
found = true
break
}
}
if !found {
http.Error(w, "Profilbild nicht gefunden", http.StatusNotFound)
return
}
// Öffne Datei
file, err := os.Open(avatarPath)
if err != nil {
http.Error(w, "Fehler beim Öffnen der Datei", http.StatusInternalServerError)
return
}
defer file.Close()
// Bestimme Content-Type basierend auf Dateiendung
ext := filepath.Ext(avatarPath)
var contentType string
switch ext {
case ".jpg", ".jpeg":
contentType = "image/jpeg"
case ".png":
contentType = "image/png"
case ".gif":
contentType = "image/gif"
case ".webp":
contentType = "image/webp"
default:
contentType = "image/jpeg"
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=3600")
io.Copy(w, file)
}
// Passwortvalidierung nach Richtlinien
func validatePassword(password string) error {
if len(password) < 8 {
return fmt.Errorf("Passwort muss mindestens 8 Zeichen lang sein")
}
hasUpper := false
hasLower := false
hasDigit := false
hasSpecial := false
for _, char := range password {
switch {
case 'A' <= char && char <= 'Z':
hasUpper = true
case 'a' <= char && char <= 'z':
hasLower = true
case '0' <= char && char <= '9':
hasDigit = true
default:
// Prüfe auf Sonderzeichen (alles was nicht Buchstabe oder Zahl ist)
if !(('A' <= char && char <= 'Z') || ('a' <= char && char <= 'z') || ('0' <= char && char <= '9')) {
hasSpecial = true
}
}
}
var missing []string
if !hasUpper {
missing = append(missing, "Großbuchstaben")
}
if !hasLower {
missing = append(missing, "Kleinbuchstaben")
}
if !hasDigit {
missing = append(missing, "Zahlen")
}
if !hasSpecial {
missing = append(missing, "Sonderzeichen")
}
if len(missing) > 0 {
return fmt.Errorf("Passwort muss enthalten: %s", strings.Join(missing, ", "))
}
return nil
}
// Helper-Funktion zum Extrahieren des Benutzers aus dem Request (für Basic Auth)
func getUserFromRequest(r *http.Request) (userID, username string) {
auth := r.Header.Get("Authorization")
if auth == "" || !strings.HasPrefix(auth, "Basic ") {
return "", ""
}
encoded := strings.TrimPrefix(auth, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return "", ""
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
return "", ""
}
username = parts[0]
// Hole User-ID aus der Datenbank
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
var id string
err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id)
if err != nil {
return "", username
}
return id, username
}
// Helper-Funktion zum Extrahieren von IP-Adresse und User-Agent aus Request
func getRequestInfo(r *http.Request) (ipAddress, userAgent string) {
// Hole IP-Adresse
ipAddress = r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ipAddress = forwarded
} else if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
ipAddress = realIP
}
// Hole User-Agent
userAgent = r.Header.Get("User-Agent")
// Prüfe, ob es sich um einen API-Aufruf handelt
// API-Aufrufe haben entweder keinen User-Agent oder einen speziellen Header
if userAgent == "" || r.Header.Get("X-API-Request") == "true" || r.Header.Get("X-Request-Source") == "api" {
userAgent = "API"
}
return ipAddress, userAgent
}
// Audit Log Handler
func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Query-Parameter für Filterung und Pagination
query := r.URL.Query()
limitStr := query.Get("limit")
offsetStr := query.Get("offset")
actionFilter := query.Get("action")
resourceTypeFilter := query.Get("resourceType")
userIdFilter := query.Get("userId")
// Standardwerte für Pagination
limit := 100
offset := 0
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 {
limit = l
}
}
if offsetStr != "" {
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
offset = o
}
}
// Baue SQL-Query mit Filtern
whereClauses := []string{}
args := []interface{}{}
if actionFilter != "" {
whereClauses = append(whereClauses, "action = ?")
args = append(args, actionFilter)
}
if resourceTypeFilter != "" {
whereClauses = append(whereClauses, "resource_type = ?")
args = append(args, resourceTypeFilter)
}
if userIdFilter != "" {
whereClauses = append(whereClauses, "user_id = ?")
args = append(args, userIdFilter)
}
whereSQL := ""
if len(whereClauses) > 0 {
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
}
// Zähle Gesamtanzahl für Pagination
var totalCount int
countSQL := "SELECT COUNT(*) FROM audit_logs " + whereSQL
countCtx, countCancel := context.WithTimeout(context.Background(), time.Second*5)
defer countCancel()
err := db.QueryRowContext(countCtx, countSQL, args...).Scan(&totalCount)
if err != nil {
http.Error(w, "Fehler beim Zählen der Logs", http.StatusInternalServerError)
log.Printf("Fehler beim Zählen der Logs: %v", err)
return
}
// Hole Logs - Verwende Prepared Statement für bessere Kompatibilität
var querySQL string
var queryArgs []interface{}
if whereSQL != "" {
querySQL = fmt.Sprintf(`
SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent
FROM audit_logs
%s
ORDER BY datetime(timestamp) DESC, id DESC
LIMIT ? OFFSET ?
`, whereSQL)
queryArgs = make([]interface{}, len(args))
copy(queryArgs, args)
queryArgs = append(queryArgs, limit, offset)
} else {
querySQL = `
SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent
FROM audit_logs
ORDER BY datetime(timestamp) DESC, id DESC
LIMIT ? OFFSET ?
`
queryArgs = []interface{}{limit, offset}
}
queryCtx, queryCancel := context.WithTimeout(context.Background(), time.Second*10)
defer queryCancel() // Cancel wird erst aufgerufen, wenn die Funktion beendet ist
rows, err := db.QueryContext(queryCtx, querySQL, queryArgs...)
if err != nil {
http.Error(w, "Fehler beim Abrufen der Logs", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der Logs: %v", err)
return
}
defer rows.Close()
log.Printf("SQL Query: %s, Args: %v (Count: %d)", querySQL, queryArgs, len(queryArgs))
var logs []AuditLog
rowCount := 0
scanErrors := 0
for rows.Next() {
rowCount++
var logEntry AuditLog
var userID, username, resourceID, detailsJSON, ipAddress, userAgent sql.NullString
err := rows.Scan(
&logEntry.ID,
&logEntry.Timestamp,
&userID,
&username,
&logEntry.Action,
&logEntry.ResourceType,
&resourceID,
&detailsJSON,
&ipAddress,
&userAgent,
)
if err != nil {
scanErrors++
log.Printf("Fehler beim Scannen der Log-Zeile %d: %v", rowCount, err)
continue
}
if userID.Valid {
logEntry.UserID = userID.String
}
if username.Valid {
logEntry.Username = username.String
}
if resourceID.Valid {
logEntry.ResourceID = resourceID.String
}
// Parse JSON details
if detailsJSON.Valid && detailsJSON.String != "" {
var detailsMap map[string]interface{}
if err := json.Unmarshal([]byte(detailsJSON.String), &detailsMap); err == nil {
// Convert map to JSON string for display
if jsonBytes, err := json.Marshal(detailsMap); err == nil {
logEntry.Details = string(jsonBytes)
} else {
logEntry.Details = detailsJSON.String
}
} else {
// Fallback: use raw string if JSON parsing fails
logEntry.Details = detailsJSON.String
}
}
if ipAddress.Valid {
logEntry.IPAddress = ipAddress.String
}
if userAgent.Valid {
logEntry.UserAgent = userAgent.String
}
logs = append(logs, logEntry)
}
if err := rows.Err(); err != nil {
log.Printf("Fehler beim Iterieren über die Zeilen: %v", err)
}
log.Printf("Zeilen gelesen: %d, Scan-Fehler: %d, Logs hinzugefügt: %d", rowCount, scanErrors, len(logs))
response := map[string]interface{}{
"logs": logs,
"total": totalCount,
"limit": limit,
"offset": offset,
"hasMore": offset+limit < totalCount,
}
log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// Handler zum Löschen aller Audit-Logs
func deleteAllAuditLogsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Prüfe Bestätigung
confirm := r.URL.Query().Get("confirm")
if confirm != "true" {
http.Error(w, "Bestätigung erforderlich. Verwende ?confirm=true", http.StatusBadRequest)
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// Lösche alle Audit-Logs
result, err := db.ExecContext(ctx, "DELETE FROM audit_logs")
if err != nil {
http.Error(w, "Fehler beim Löschen der Audit-Logs", http.StatusInternalServerError)
log.Printf("Fehler beim Löschen der Audit-Logs: %v", err)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError)
log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err)
return
}
log.Printf("Alle Audit-Logs gelöscht: %d Einträge", rowsAffected)
response := map[string]interface{}{
"message": "Alle Audit-Logs erfolgreich gelöscht",
"deletedCount": rowsAffected,
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// Handler zum Erstellen eines Test-Audit-Logs (für Testskripte)
func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
var req struct {
Action string `json:"action"`
Entity string `json:"entity"`
EntityID string `json:"entityID"`
UserID string `json:"userID"`
Username string `json:"username"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
return
}
if req.Action == "" || req.Entity == "" {
http.Error(w, "action und entity sind erforderlich", http.StatusBadRequest)
return
}
// Verwende AuditService zum Erstellen des Logs
if auditService == nil {
http.Error(w, "AuditService nicht initialisiert", http.StatusInternalServerError)
return
}
// Erstelle Context für die Anfrage
ctx := r.Context()
// Track das Event
auditService.Track(ctx, req.Action, req.Entity, req.EntityID, req.UserID, req.Username, req.Details, req.IPAddress, req.UserAgent)
response := map[string]interface{}{
"success": true,
"message": "Test-Audit-Log erstellt",
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// Basic Auth Middleware
func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// OPTIONS-Requests erlauben (für CORS)
if r.Method == "OPTIONS" {
next(w, r)
return
}
// Prüfe ob es ein AJAX/Fetch-Request ist (kein Browser-Basic-Auth-Dialog)
isAjaxRequest := r.Header.Get("X-Requested-With") == "XMLHttpRequest" ||
strings.Contains(r.Header.Get("Content-Type"), "application/json") ||
r.Header.Get("Accept") == "application/json" ||
strings.HasPrefix(r.URL.Path, "/api/")
// Prüfe Authorization Header
auth := r.Header.Get("Authorization")
if auth == "" {
if !isAjaxRequest {
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Authentifizierung erforderlich"})
return
}
// Parse Basic Auth
if !strings.HasPrefix(auth, "Basic ") {
if !isAjaxRequest {
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"})
return
}
// Decode Base64
encoded := strings.TrimPrefix(auth, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
if !isAjaxRequest {
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"})
return
}
// Split username:password
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
if !isAjaxRequest {
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"})
return
}
username := parts[0]
password := parts[1]
// Validiere Benutzer
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var storedHash string
err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(&storedHash)
if err != nil {
if err == sql.ErrNoRows {
if !isAjaxRequest {
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Fehler bei der Authentifizierung"})
log.Printf("Fehler bei der Authentifizierung: %v", err)
return
}
// Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil {
if !isAjaxRequest {
w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"})
return
}
// Authentifizierung erfolgreich - weiterleiten
next(w, r)
}
}
// Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück)
func loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Prüfe Authorization Header
auth := r.Header.Get("Authorization")
if auth == "" {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Authentifizierung erforderlich"})
return
}
// Parse Basic Auth
if !strings.HasPrefix(auth, "Basic ") {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"})
return
}
// Decode Base64
encoded := strings.TrimPrefix(auth, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
log.Printf("Fehler beim Decodieren der Basic Auth: %v", err)
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"})
return
}
// Split username:password
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
log.Printf("Ungültiges Format in Basic Auth: %s", string(decoded))
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"})
return
}
username := parts[0]
password := parts[1]
log.Printf("Login-Versuch für Benutzer: %s", username)
// Validiere Benutzer
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var user User
var storedHash string
err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username).
Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("Benutzer nicht gefunden: %s", username)
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"})
return
}
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Fehler bei der Authentifizierung"})
return
}
// Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil {
log.Printf("Passwort-Validierung fehlgeschlagen für Benutzer: %s", username)
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"})
return
}
log.Printf("Login erfolgreich für Benutzer: %s", username)
// Login erfolgreich
response := map[string]interface{}{
"success": true,
"user": user,
"message": "Login erfolgreich",
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
func main() {
log.Println("Starte certigo-addon Backend...")
// Initialisiere Datenbank
log.Println("Initialisiere Datenbank...")
initDB()
defer func() {
log.Println("Schließe Datenbankverbindung...")
db.Close()
}()
log.Println("Datenbank initialisiert")
// Initialisiere Provider
pm := providers.GetManager()
pm.RegisterProvider(providers.NewDummyCAProvider())
pm.RegisterProvider(providers.NewAutoDNSProvider())
pm.RegisterProvider(providers.NewHetznerProvider())
r := mux.NewRouter()
// Swagger UI Route
r.HandleFunc("/swagger", swaggerUIHandler).Methods("GET")
r.HandleFunc("/api/openapi.yaml", openAPIHandler).Methods("GET")
// API Routes
api := r.PathPrefix("/api").Subrouter()
// Public Routes (keine Auth erforderlich)
api.HandleFunc("/health", healthHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/login", loginHandler).Methods("POST", "OPTIONS")
// Protected Routes (Basic Auth erforderlich)
api.HandleFunc("/stats", basicAuthMiddleware(getStatsHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/spaces", getSpacesHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/spaces", createSpaceHandler).Methods("POST", "OPTIONS")
api.HandleFunc("/spaces/{id}", deleteSpaceHandler).Methods("DELETE", "OPTIONS")
api.HandleFunc("/spaces/{id}/fqdns/count", getSpaceFqdnCountHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/spaces/{id}/fqdns", getFqdnsHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/spaces/{id}/fqdns", createFqdnHandler).Methods("POST", "OPTIONS")
api.HandleFunc("/spaces/{id}/fqdns", deleteAllFqdnsHandler).Methods("DELETE", "OPTIONS")
api.HandleFunc("/spaces/{id}/fqdns/{fqdnId}", deleteFqdnHandler).Methods("DELETE", "OPTIONS")
api.HandleFunc("/fqdns", deleteAllFqdnsGlobalHandler).Methods("DELETE", "OPTIONS")
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", uploadCSRHandler).Methods("POST", "OPTIONS")
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", getCSRByFQDNHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/csrs", deleteAllCSRsHandler).Methods("DELETE", "OPTIONS")
// User Routes
api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS")
api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS")
api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
// Provider Routes (Protected)
api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/providers/{id}", basicAuthMiddleware(getProviderHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/providers/{id}/enabled", basicAuthMiddleware(setProviderEnabledHandler)).Methods("PUT", "OPTIONS")
api.HandleFunc("/providers/{id}/config", basicAuthMiddleware(updateProviderConfigHandler)).Methods("PUT", "OPTIONS")
api.HandleFunc("/providers/{id}/test", basicAuthMiddleware(testProviderConnectionHandler)).Methods("POST", "OPTIONS")
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr/sign", basicAuthMiddleware(signCSRHandler)).Methods("POST", "OPTIONS")
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates", basicAuthMiddleware(getCertificatesHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates/{certId}/refresh", basicAuthMiddleware(refreshCertificateHandler)).Methods("POST", "OPTIONS")
// Audit Log Routes
api.HandleFunc("/audit-logs", basicAuthMiddleware(getAuditLogsHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/audit-logs", basicAuthMiddleware(deleteAllAuditLogsHandler)).Methods("DELETE", "OPTIONS")
api.HandleFunc("/audit-logs/test", basicAuthMiddleware(createTestAuditLogHandler)).Methods("POST", "OPTIONS")
// Start server
port := ":8080"
log.Printf("Server läuft auf Port %s", port)
log.Fatal(http.ListenAndServe(port, r))
}
// Provider Handlers
func getProvidersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
pm := providers.GetManager()
allProviders := pm.GetAllProviders()
// Definiere feste Reihenfolge der Provider
providerOrder := []string{"dummy-ca", "autodns", "hetzner"}
// Erstelle Map für schnellen Zugriff
providerMap := make(map[string]providers.ProviderInfo)
for id, provider := range allProviders {
config, _ := pm.GetProviderConfig(id)
providerInfo := providers.ProviderInfo{
ID: id,
Name: provider.GetName(),
DisplayName: provider.GetDisplayName(),
Description: provider.GetDescription(),
Enabled: config.Enabled,
Settings: provider.GetRequiredSettings(),
}
providerMap[id] = providerInfo
}
// Sortiere nach definierter Reihenfolge
var providerInfos []providers.ProviderInfo
for _, id := range providerOrder {
if providerInfo, exists := providerMap[id]; exists {
providerInfos = append(providerInfos, providerInfo)
delete(providerMap, id)
}
}
// Füge alle anderen Provider hinzu, die nicht in der Liste sind
for _, providerInfo := range providerMap {
providerInfos = append(providerInfos, providerInfo)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(providerInfos)
}
func getProviderHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
id := vars["id"]
pm := providers.GetManager()
provider, exists := pm.GetProvider(id)
if !exists {
http.Error(w, "Provider nicht gefunden", http.StatusNotFound)
return
}
config, _ := pm.GetProviderConfig(id)
providerInfo := providers.ProviderInfo{
ID: id,
Name: provider.GetName(),
DisplayName: provider.GetDisplayName(),
Description: provider.GetDescription(),
Enabled: config.Enabled,
Settings: provider.GetRequiredSettings(),
}
// Füge aktuelle Konfigurationswerte hinzu (ohne Passwörter)
safeSettings := make(map[string]interface{})
for key, value := range config.Settings {
// Verstecke Passwörter und API Keys in der Antwort
if key == "password" || key == "apiKey" {
if str, ok := value.(string); ok && str != "" {
safeSettings[key] = "***"
} else {
safeSettings[key] = value
}
} else {
safeSettings[key] = value
}
}
// Konvertiere zu JSON für die Response
safeSettingsJSON, _ := json.Marshal(safeSettings)
var safeSettingsMap map[string]interface{}
json.Unmarshal(safeSettingsJSON, &safeSettingsMap)
response := map[string]interface{}{
"id": providerInfo.ID,
"name": providerInfo.Name,
"displayName": providerInfo.DisplayName,
"description": providerInfo.Description,
"enabled": providerInfo.Enabled,
"settings": providerInfo.Settings,
"config": safeSettingsMap,
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
func setProviderEnabledHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
id := vars["id"]
var req struct {
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
pm := providers.GetManager()
if err := pm.SetProviderEnabled(id, req.Enabled); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Provider-Status erfolgreich aktualisiert",
"enabled": req.Enabled,
})
// Audit-Log: Provider aktiviert/deaktiviert
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
action := "DISABLE"
if req.Enabled {
action = "ENABLE"
}
auditService.Track(r.Context(), action, "provider", id, userID, username, map[string]interface{}{
"enabled": req.Enabled,
"message": fmt.Sprintf("Provider %s %s", id, strings.ToLower(action)),
}, ipAddress, userAgent)
}
func updateProviderConfigHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
id := vars["id"]
var req struct {
Settings map[string]interface{} `json:"settings"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
pm := providers.GetManager()
config, _ := pm.GetProviderConfig(id)
config.Settings = req.Settings
if err := pm.UpdateProviderConfig(id, config); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Konfiguration erfolgreich aktualisiert",
})
// Audit-Log: Provider-Konfiguration aktualisiert
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "UPDATE", "provider", id, userID, username, map[string]interface{}{
"message": fmt.Sprintf("Provider-Konfiguration aktualisiert: %s", id),
}, ipAddress, userAgent)
}
func testProviderConnectionHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
id := vars["id"]
var req struct {
Settings map[string]interface{} `json:"settings"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
pm := providers.GetManager()
provider, exists := pm.GetProvider(id)
if !exists {
http.Error(w, "Provider nicht gefunden", http.StatusNotFound)
return
}
if err := provider.TestConnection(req.Settings); err != nil {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"message": err.Error(),
})
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Verbindung erfolgreich",
})
}
func signCSRHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
spaceID := vars["spaceId"]
fqdnID := vars["fqdnId"]
var req struct {
ProviderID string `json:"providerId"`
CSRID string `json:"csrId,omitempty"` // Optional: spezifischer CSR, sonst neuester
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.ProviderID == "" {
http.Error(w, "providerId ist erforderlich", http.StatusBadRequest)
return
}
// Hole neuesten CSR für den FQDN
var csrPEM string
var csrID string
err := db.QueryRow(`
SELECT id, csr_pem
FROM csrs
WHERE fqdn_id = ? AND space_id = ?
ORDER BY created_at DESC
LIMIT 1
`, fqdnID, spaceID).Scan(&csrID, &csrPEM)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Kein CSR für diesen FQDN gefunden", http.StatusNotFound)
return
}
http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError)
log.Printf("Fehler beim Laden des CSR: %v", err)
return
}
// Wenn spezifischer CSR angefordert wurde
if req.CSRID != "" && req.CSRID != csrID {
err := db.QueryRow(`
SELECT csr_pem
FROM csrs
WHERE id = ? AND fqdn_id = ? AND space_id = ?
`, req.CSRID, fqdnID, spaceID).Scan(&csrPEM)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "CSR nicht gefunden", http.StatusNotFound)
return
}
http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError)
return
}
csrID = req.CSRID
}
// Hole Provider
pm := providers.GetManager()
provider, exists := pm.GetProvider(req.ProviderID)
if !exists {
http.Error(w, "Provider nicht gefunden", http.StatusNotFound)
return
}
// Prüfe ob Provider aktiviert ist
config, err := pm.GetProviderConfig(req.ProviderID)
if err != nil || !config.Enabled {
http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest)
return
}
// Signiere CSR
result, err := provider.SignCSR(csrPEM, config.Settings)
if err != nil {
http.Error(w, fmt.Sprintf("Fehler beim Signieren des CSR: %v", err), http.StatusInternalServerError)
log.Printf("Fehler beim Signieren des CSR: %v", err)
return
}
// Speichere das Zertifikat in der DB
certID := uuid.New().String()
createdAt := time.Now()
_, err = db.Exec(`
INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, certID, fqdnID, spaceID, csrID, result.OrderID, req.ProviderID, result.CertificatePEM, result.Status, createdAt)
if err != nil {
log.Printf("Fehler beim Speichern des Zertifikats: %v", err)
// Weiterhin erfolgreich zurückgeben, auch wenn Speichern fehlschlägt
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": result.Message,
"certificateId": certID,
"orderId": result.OrderID,
"status": result.Status,
"csrId": csrID,
})
// Audit-Log: CSR signiert
userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "SIGN", "csr", csrID, userID, username, map[string]interface{}{
"providerId": req.ProviderID,
"fqdnId": fqdnID,
"spaceId": spaceID,
"certificateId": result.OrderID,
"status": result.Status,
"message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID),
}, ipAddress, userAgent)
}
func getCertificatesHandler(w http.ResponseWriter, r *http.Request) {
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"]
// Hole alle Zertifikate für diesen FQDN
rows, err := db.Query(`
SELECT id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at
FROM certificates
WHERE fqdn_id = ? AND space_id = ?
ORDER BY created_at DESC
`, fqdnID, spaceID)
if err != nil {
http.Error(w, "Fehler beim Laden der Zertifikate", http.StatusInternalServerError)
log.Printf("Fehler beim Laden der Zertifikate: %v", err)
return
}
defer rows.Close()
var certificates []map[string]interface{}
for rows.Next() {
var id, csrID, certID, providerID, certPEM, status, createdAt string
err := rows.Scan(&id, &csrID, &certID, &providerID, &certPEM, &status, &createdAt)
if err != nil {
log.Printf("Fehler beim Scannen der Zertifikat-Zeile: %v", err)
continue
}
certificates = append(certificates, map[string]interface{}{
"id": id,
"csrId": csrID,
"certificateId": certID,
"providerId": providerID,
"certificatePEM": certPEM,
"status": status,
"createdAt": createdAt,
})
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(certificates)
}
func refreshCertificateHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
vars := mux.Vars(r)
spaceID := vars["spaceId"]
fqdnID := vars["fqdnId"]
certID := vars["certId"]
// Hole Zertifikat aus DB
var certificateID, providerID string
err := db.QueryRow(`
SELECT certificate_id, provider_id
FROM certificates
WHERE id = ? AND fqdn_id = ? AND space_id = ?
`, certID, fqdnID, spaceID).Scan(&certificateID, &providerID)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Zertifikat nicht gefunden", http.StatusNotFound)
return
}
http.Error(w, "Fehler beim Laden des Zertifikats", http.StatusInternalServerError)
return
}
// Hole Provider
pm := providers.GetManager()
provider, exists := pm.GetProvider(providerID)
if !exists {
http.Error(w, "Provider nicht gefunden", http.StatusNotFound)
return
}
// Prüfe ob Provider aktiviert ist
config, err := pm.GetProviderConfig(providerID)
if err != nil || !config.Enabled {
http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest)
return
}
// Rufe Zertifikat von CA ab
certPEM, err := provider.GetCertificate(certificateID, config.Settings)
if err != nil {
http.Error(w, fmt.Sprintf("Fehler beim Abrufen des Zertifikats: %v", err), http.StatusInternalServerError)
return
}
// Aktualisiere Zertifikat in DB
_, err = db.Exec(`
UPDATE certificates
SET certificate_pem = ?
WHERE id = ? AND fqdn_id = ? AND space_id = ?
`, certPEM, certID, fqdnID, spaceID)
if err != nil {
log.Printf("Fehler beim Aktualisieren des Zertifikats: %v", err)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"certificatePEM": certPEM,
"certificateId": certificateID,
})
}