Files
certigo/backend/main.go

6321 lines
203 KiB
Go

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