package cmd import ( "fmt" "log" "net/http" "os" "os/signal" "strings" "syscall" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/spf13/cobra" "github.com/insertr/insertr/internal/api" "github.com/insertr/insertr/internal/auth" "github.com/insertr/insertr/internal/content" "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/engine" ) var serveCmd = &cobra.Command{ Use: "serve", Short: "Start the content API server", Long: `Start the HTTP API server that provides content storage and retrieval. Supports both development and production modes with SQLite or PostgreSQL databases.`, Run: runServe, } var ( port int devMode bool ) func init() { serveCmd.Flags().IntVarP(&port, "port", "p", 0, "Server port") serveCmd.Flags().BoolVar(&devMode, "dev-mode", false, "Enable development mode features") } func runServe(cmd *cobra.Command, args []string) { // Load configuration cfg, err := loadConfig() if err != nil { log.Fatalf("Failed to load configuration: %v", err) } // Override with flags if provided if port != 0 { cfg.Server.Port = port } if devMode { cfg.Auth.DevMode = true } // Initialize database database, err := db.NewDatabase(cfg.Database.Path) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } defer database.Close() // Support environment variables for sensitive values if clientSecret := os.Getenv("AUTHENTIK_CLIENT_SECRET"); clientSecret != "" && cfg.Auth.OIDC != nil { cfg.Auth.OIDC.ClientSecret = clientSecret } if endpoint := os.Getenv("AUTHENTIK_ENDPOINT"); endpoint != "" && cfg.Auth.OIDC != nil { cfg.Auth.OIDC.Endpoint = endpoint } // Set redirect URL if not configured if cfg.Auth.OIDC != nil && cfg.Auth.OIDC.RedirectURL == "" { cfg.Auth.OIDC.RedirectURL = fmt.Sprintf("http://%s:%d/auth/callback", cfg.Server.Host, cfg.Server.Port) } // Create legacy auth config for compatibility authConfig := &auth.AuthConfig{ DevMode: cfg.Auth.DevMode, Provider: cfg.Auth.Provider, JWTSecret: cfg.Auth.JWTSecret, } if cfg.Auth.OIDC != nil { authConfig.OIDC = &auth.OIDCConfig{ Endpoint: cfg.Auth.OIDC.Endpoint, ClientID: cfg.Auth.OIDC.ClientID, ClientSecret: cfg.Auth.OIDC.ClientSecret, RedirectURL: cfg.Auth.OIDC.RedirectURL, Scopes: cfg.Auth.OIDC.Scopes, } } log.Printf("🔑 Using auth provider: %s", cfg.Auth.Provider) if cfg.Auth.Provider == "authentik" && cfg.Auth.OIDC != nil { log.Printf("🔐 Using Authentik OIDC provider: %s", cfg.Auth.OIDC.Endpoint) } authService, err := auth.NewAuthService(authConfig) if err != nil { log.Fatalf("Failed to initialize authentication service: %v", err) } // Initialize content client for site manager contentClient := engine.NewDatabaseClient(database) // Initialize site manager with auth provider authProvider := &engine.AuthProvider{Type: cfg.Auth.Provider} siteManager := content.NewSiteManagerWithAuth(contentClient, cfg.Auth.DevMode, authProvider) // Convert config sites to legacy format and register var legacySites []*content.SiteConfig for _, site := range cfg.Server.Sites { legacySite := &content.SiteConfig{ SiteID: site.SiteID, Path: site.Path, SourcePath: site.SourcePath, Domain: site.Domain, AutoEnhance: site.AutoEnhance, } if site.Discovery != nil { legacySite.Discovery = &content.DiscoveryConfig{ Enabled: site.Discovery.Enabled, Aggressive: site.Discovery.Aggressive, Containers: site.Discovery.Containers, Individual: site.Discovery.Individual, } } legacySites = append(legacySites, legacySite) } if len(legacySites) > 0 { if err := siteManager.RegisterSites(legacySites); err != nil { log.Printf("⚠️ Failed to register some sites: %v", err) } } // Auto-enhance sites if enabled if cfg.Auth.DevMode { log.Printf("🔄 Auto-enhancing sites in development mode...") if err := siteManager.EnhanceAllSites(); err != nil { log.Printf("⚠️ Some sites failed to enhance: %v", err) } } // Initialize handlers contentHandler := api.NewContentHandler(database, authService) contentHandler.SetSiteManager(siteManager) // Setup Chi router router := chi.NewRouter() // Add Chi middleware router.Use(middleware.Logger) router.Use(middleware.Recoverer) router.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"*"}, // In dev mode, allow all origins AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"*"}, ExposedHeaders: []string{"*"}, AllowCredentials: true, })) // Health check router.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) // Authentication routes router.Route("/auth", func(r chi.Router) { r.Post("/login", authService.HandleOAuthLogin) r.Get("/callback", authService.HandleOAuthCallback) }) // Content API routes router.Route("/api", func(r chi.Router) { // Public routes r.Get("/content/{siteID}/{id}", contentHandler.GetContent) r.Get("/content/{siteID}", contentHandler.GetAllContent) // Protected routes (require authentication) r.Group(func(r chi.Router) { r.Use(authService.RequireAuth) r.Post("/content/{siteID}", contentHandler.CreateContent) r.Put("/content/{siteID}/{id}", contentHandler.UpdateContent) r.Delete("/content/{siteID}/{id}", contentHandler.DeleteContent) // Version management r.Get("/content/{siteID}/{id}/versions", contentHandler.GetContentVersions) r.Post("/content/{siteID}/{id}/rollback/{version}", contentHandler.RollbackContent) }) }) // Serve static sites for _, siteConfig := range siteManager.GetAllSites() { log.Printf("📁 Serving site %s from %s at /sites/%s/", siteConfig.SiteID, siteConfig.Path, siteConfig.SiteID) // Create a file server for each site fileServer := http.FileServer(http.Dir(siteConfig.Path)) // Handle both /sites/{siteID}/ and /{siteID}/ patterns router.Mount(fmt.Sprintf("/sites/%s/", siteConfig.SiteID), http.StripPrefix(fmt.Sprintf("/sites/%s/", siteConfig.SiteID), fileServer)) // Optionally serve at root for primary site if siteConfig.Domain != "" { log.Printf("🌐 Site %s available at domain: %s", siteConfig.SiteID, siteConfig.Domain) } } // Catch-all for serving sites by domain or default router.NotFound(func(w http.ResponseWriter, r *http.Request) { // Try to match by domain first host := strings.Split(r.Host, ":")[0] // Remove port if present for _, siteConfig := range siteManager.GetAllSites() { if siteConfig.Domain == host { fileServer := http.FileServer(http.Dir(siteConfig.Path)) fileServer.ServeHTTP(w, r) return } } // Default 404 http.NotFound(w, r) }) // Start server addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) log.Printf("🚀 Server starting on %s", addr) log.Printf("📝 Content API available at http://%s/api/content/{site_id}", addr) log.Printf("🔐 Authentication at http://%s/auth/login", addr) // Graceful shutdown go func() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) <-sigChan log.Printf("🛑 Shutting down server...") os.Exit(0) }() if err := http.ListenAndServe(addr, router); err != nil { log.Fatalf("Server failed to start: %v", err) } }