package main import ( "context" "crypto/tls" "crypto/x509" "fmt" "os" "os/signal" "syscall" "github.com/gofiber/fiber/v2" "github.com/rs/zerolog" "github.com/gosec/gsc-ops-api/internal/client" "github.com/gosec/gsc-ops-api/internal/config" "github.com/gosec/gsc-ops-api/internal/database" "github.com/gosec/gsc-ops-api/internal/handler" "github.com/gosec/gsc-ops-api/internal/router" "github.com/gosec/gsc-ops-api/internal/schema" "github.com/gosec/gsc-ops-api/internal/service" ) const ( appName = "gsc-ops-api" appVersion = "1.0.0" ) func main() { // Logger logger := zerolog.New(os.Stdout).With(). Timestamp(). Str("app", appName). Str("version", appVersion). Logger() // Load configuration configPath := os.Getenv("GSC_OPS_API_CONFIG") if configPath == "" { configPath = "/etc/gsc-ops-api/config.yaml" } cfg, err := config.Load(configPath) if err != nil { logger.Fatal().Err(err).Msg("Failed to load config") } // Set log level level, err := zerolog.ParseLevel(cfg.Logging.Level) if err == nil { zerolog.SetGlobalLevel(level) } // Load secrets from Infisical if err := cfg.LoadSecretsFromInfisical(); err != nil { logger.Warn().Err(err).Msg("Failed to load secrets from Infisical") } // Connect to databases var coreDB, pbxDB, voiceAgentDB, personaDB *database.DB if cfg.HasDatabase("core") { coreDB, err = database.NewNamed(cfg, "core") if err != nil { logger.Fatal().Err(err).Str("db", "core").Msg("Failed to connect to database") } defer coreDB.Close() logger.Info().Str("db", "gsc_core").Msg("Connected to database") } if cfg.HasDatabase("pbx") { pbxDB, err = database.NewNamed(cfg, "pbx") if err != nil { logger.Fatal().Err(err).Str("db", "pbx").Msg("Failed to connect to database") } defer pbxDB.Close() logger.Info().Str("db", "gsc_pbx").Msg("Connected to database") } if cfg.HasDatabase("voice_agent") { voiceAgentDB, err = database.NewNamed(cfg, "voice_agent") if err != nil { logger.Fatal().Err(err).Str("db", "voice_agent").Msg("Failed to connect to database") } defer voiceAgentDB.Close() logger.Info().Str("db", "gsc_voice_agent").Msg("Connected to database") } if cfg.HasDatabase("persona") { personaDB, err = database.NewNamed(cfg, "persona") if err != nil { logger.Fatal().Err(err).Str("db", "persona").Msg("Failed to connect to database") } defer personaDB.Close() logger.Info().Str("db", "gsc_persona").Msg("Connected to database") } // Fallback to legacy single database if named databases not configured var db *database.DB if coreDB == nil || pbxDB == nil || voiceAgentDB == nil { db, err = database.New(cfg) if err != nil { logger.Fatal().Err(err).Msg("Failed to connect to legacy database") } defer db.Close() logger.Info().Msg("Connected to legacy database (gsc_admin)") if coreDB == nil { coreDB = db } if pbxDB == nil { pbxDB = db } if voiceAgentDB == nil { voiceAgentDB = db } } else { // Use coreDB as the primary for health checks db = coreDB } // Initialize LDAP client var ldapClient *client.LDAPClient if len(cfg.LDAP.Servers) > 0 { ldapClient, err = client.NewLDAPClient(cfg.LDAP, logger) if err != nil { logger.Fatal().Err(err).Msg("Failed to create LDAP client") } defer ldapClient.Close() logger.Info().Int("poolSize", cfg.LDAP.PoolSize).Msg("Connected to LDAP") } // Initialize PowerDNS client var pdnsClient *client.PowerDNSClient if cfg.PowerDNS.BaseURL != "" { pdnsClient = client.NewPowerDNSClient(cfg.PowerDNS, logger) logger.Info().Str("url", cfg.PowerDNS.BaseURL).Msg("PowerDNS client initialized") } // Initialize EJBCA client var ejbcaClient *client.EJBCAClient if cfg.EJBCA.BaseURL != "" { ejbcaClient, err = client.NewEJBCAClient(cfg.EJBCA, logger) if err != nil { logger.Warn().Err(err).Msg("Failed to create EJBCA client (certificates disabled)") } else { logger.Info().Str("url", cfg.EJBCA.BaseURL).Msg("EJBCA client initialized") } } // Initialize Hockeypuck client var hkpClient *client.HockeypuckClient if len(cfg.Hockeypuck.Servers) > 0 { hkpClient = client.NewHockeypuckClient(cfg.Hockeypuck, logger) logger.Info().Int("servers", len(cfg.Hockeypuck.Servers)).Msg("Hockeypuck client initialized") } // Initialize CardDAV client var carddavClient *client.CardDAVClient if cfg.CardDAV.Host != "" { carddavClient, err = client.NewCardDAVClient(cfg.CardDAV, cfg.CardDAVDSN(), logger) if err != nil { logger.Warn().Err(err).Msg("Failed to create CardDAV client (carddav disabled)") } else { defer carddavClient.Close() logger.Info().Str("host", cfg.CardDAV.Host).Str("database", cfg.CardDAV.Database).Msg("CardDAV client initialized") } } // Initialize Asterisk AMI client var asteriskClient *client.AsteriskClient if len(cfg.Asterisk.Servers) > 0 { astServers := make([]client.AsteriskServer, len(cfg.Asterisk.Servers)) for i, s := range cfg.Asterisk.Servers { astServers[i] = client.AsteriskServer{Host: s.Host, AMIPort: s.AMIPort} } asteriskClient = client.NewAsteriskClient(astServers, cfg.Asterisk.AMIUser, cfg.Asterisk.AMISecret, logger) logger.Info().Int("servers", len(cfg.Asterisk.Servers)).Msg("Asterisk AMI client initialized") } // Initialize Kamailio client var kamailioClient *client.KamailioClient if len(cfg.Kamailio.Servers) > 0 { var sshKey []byte if cfg.Kamailio.SSHKey != "" { sshKey, _ = os.ReadFile(cfg.Kamailio.SSHKey) } kamailioClient = client.NewKamailioClient(cfg.Kamailio.Servers, cfg.Kamailio.SSHUser, sshKey, logger) logger.Info().Int("servers", len(cfg.Kamailio.Servers)).Msg("Kamailio SSH client initialized") } // Create schema registry registry := schema.NewRegistry() logger.Info().Int("attrs", len(registry.AllUserAttrs())).Int("entities", len(registry.AllEntityTypes())).Msg("Schema registry initialized") // Create services ldapSvc := service.NewLDAPService(ldapClient, cfg.LDAP.BaseDN, logger, registry) ldapEntitySvc := service.NewLDAPEntityService(ldapClient, cfg.LDAP.BaseDN, registry, logger) dnsSvc := service.NewDNSService(pdnsClient, logger) dbSvc := service.NewDatabaseService(coreDB.Pool(), logger) certSvc := service.NewCertificateService(ejbcaClient, logger) pgpSvc := service.NewPGPService(hkpClient, logger) var carddavSvc *service.CardDAVService if carddavClient != nil { carddavSvc = service.NewCardDAVService(carddavClient.Pool(), logger) } pbxSvc := service.NewPBXService(pbxDB.Pool(), asteriskClient, kamailioClient, logger) voiceAgentSvc := service.NewVoiceAgentService(voiceAgentDB.Pool(), logger) personalAgentSvc := service.NewPersonalAgentService(coreDB.Pool(), logger) var personaSvc *service.PersonaService if personaDB != nil { personaSvc = service.NewPersonaService(personaDB.Pool(), logger) } // Dynamic API-key store (self-managed keys, validated alongside the static // keys loaded from Infisical). Backed by the core DB. A failure here is // non-fatal: the service stays up on the static keys and picks up dynamic // keys once the table is reachable (the cache reloads periodically). apiKeySvc := service.NewAPIKeyService(coreDB.Pool(), logger) if err := apiKeySvc.EnsureSchema(context.Background()); err != nil { logger.Error().Err(err).Msg("API-key store unavailable — dynamic keys disabled until the table is provisioned") } else { logger.Info().Msg("Dynamic API-key store initialized") } // Create handlers healthHandler := handler.NewHealthHandler(db, ldapClient, pdnsClient, carddavClient) ldapUserHandler := handler.NewLDAPUserHandler(ldapSvc) ldapGroupHandler := handler.NewLDAPGroupHandler(ldapSvc) ldapEntityHandler := handler.NewLDAPEntityHandler(ldapEntitySvc, registry) dnsZoneHandler := handler.NewDNSZoneHandler(dnsSvc) dnsRecordHandler := handler.NewDNSRecordHandler(dnsSvc) dbTenantHandler := handler.NewDBTenantHandler(dbSvc) dbUserHandler := handler.NewDBUserHandler(dbSvc) certHandler := handler.NewCertHandler(certSvc) pgpHandler := handler.NewPGPHandler(pgpSvc) var carddavHandler *handler.CardDAVHandler if carddavSvc != nil { carddavHandler = handler.NewCardDAVHandler(carddavSvc) } pbxHandler := handler.NewPBXHandler(pbxSvc) voiceAgentHandler := handler.NewVoiceAgentHandler(voiceAgentSvc) personalAgentHandler := handler.NewPersonalAgentHandler(personalAgentSvc) var personaHandler *handler.PersonaHandler if personaSvc != nil { personaHandler = handler.NewPersonaHandler(personaSvc) } apiKeyHandler := handler.NewAPIKeyHandler(apiKeySvc) // Create Fiber app app := fiber.New(fiber.Config{ ReadTimeout: cfg.Server.ReadTimeout, WriteTimeout: cfg.Server.WriteTimeout, AppName: appName, }) // Setup routes router.Setup(app, &router.Config{ Logger: logger, APIKeys: cfg.Auth.APIKeys, APIKeyValidate: apiKeySvc.Validate, APIKeysAdmin: apiKeyHandler, Health: healthHandler, LDAPUsers: ldapUserHandler, LDAPGroups: ldapGroupHandler, LDAPEntities: ldapEntityHandler, DNSZones: dnsZoneHandler, DNSRecords: dnsRecordHandler, DBTenants: dbTenantHandler, DBUsers: dbUserHandler, Certs: certHandler, PGP: pgpHandler, CardDAV: carddavHandler, PBX: pbxHandler, VoiceAgent: voiceAgentHandler, PersonalAgent: personalAgentHandler, Persona: personaHandler, }) // Load TLS config tlsConfig, err := loadTLSConfig(cfg) if err != nil { logger.Fatal().Err(err).Msg("Failed to load TLS config") } // Start server addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) logger.Info().Str("addr", addr).Msg("Starting server") // Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh logger.Info().Str("signal", sig.String()).Msg("Shutting down") app.Shutdown() }() // Start HTTPS with mTLS ln, err := tls.Listen("tcp", addr, tlsConfig) if err != nil { logger.Fatal().Err(err).Msg("Failed to create TLS listener") } if err := app.Listener(ln); err != nil { logger.Fatal().Err(err).Msg("Server error") } } func loadTLSConfig(cfg *config.Config) (*tls.Config, error) { cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile) if err != nil { return nil, fmt.Errorf("failed to load server certificate: %w", err) } caCert, err := os.ReadFile(cfg.TLS.CAFile) if err != nil { return nil, fmt.Errorf("failed to read CA certificate: %w", err) } caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCert) { return nil, fmt.Errorf("failed to parse CA certificate") } return &tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: caCertPool, MinVersion: tls.VersionTLS12, }, nil }