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/spf13/viper" "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", 8080, "Server port") serveCmd.Flags().BoolVar(&devMode, "dev-mode", false, "Enable development mode features") // Bind flags to viper viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port")) viper.BindPFlag("dev_mode", serveCmd.Flags().Lookup("dev-mode")) } func runServe(cmd *cobra.Command, args []string) { // Get configuration values port := viper.GetInt("server.port") dbPath := viper.GetString("database.path") devMode := viper.GetBool("dev_mode") // Initialize database database, err := db.NewDatabase(dbPath) if err != nil { log.Fatalf("Failed to initialize database: %v", err) } defer database.Close() // Initialize authentication service authConfig := &auth.AuthConfig{ DevMode: viper.GetBool("dev_mode"), Provider: viper.GetString("auth.provider"), JWTSecret: viper.GetString("auth.jwt_secret"), } // Set default values if authConfig.Provider == "" { authConfig.Provider = "mock" } if authConfig.JWTSecret == "" { authConfig.JWTSecret = "dev-secret-change-in-production" if authConfig.DevMode { log.Printf("šŸ”‘ Using default JWT secret for development") } } // Configure OIDC if using authentik if authConfig.Provider == "authentik" { oidcConfig := &auth.OIDCConfig{ Endpoint: viper.GetString("auth.oidc.endpoint"), ClientID: viper.GetString("auth.oidc.client_id"), ClientSecret: viper.GetString("auth.oidc.client_secret"), RedirectURL: fmt.Sprintf("http://localhost:%d/auth/callback", port), } // Support environment variables for sensitive values if clientSecret := os.Getenv("AUTHENTIK_CLIENT_SECRET"); clientSecret != "" { oidcConfig.ClientSecret = clientSecret } if endpoint := os.Getenv("AUTHENTIK_ENDPOINT"); endpoint != "" { oidcConfig.Endpoint = endpoint } authConfig.OIDC = oidcConfig // Validate required OIDC config if oidcConfig.Endpoint == "" || oidcConfig.ClientID == "" || oidcConfig.ClientSecret == "" { log.Fatalf("āŒ Authentik OIDC configuration incomplete. Required: endpoint, client_id, client_secret") } log.Printf("šŸ” Using Authentik OIDC provider: %s", oidcConfig.Endpoint) } else { log.Printf("šŸ”‘ Using auth provider: %s", authConfig.Provider) } 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: authConfig.Provider} siteManager := content.NewSiteManagerWithAuth(contentClient, devMode, authProvider) // Load sites from configuration if siteConfigs := viper.Get("server.sites"); siteConfigs != nil { if configs, ok := siteConfigs.([]interface{}); ok { var sites []*content.SiteConfig for _, configInterface := range configs { if configMap, ok := configInterface.(map[string]interface{}); ok { site := &content.SiteConfig{} if siteID, ok := configMap["site_id"].(string); ok { site.SiteID = siteID } if path, ok := configMap["path"].(string); ok { site.Path = path } if sourcePath, ok := configMap["source_path"].(string); ok { site.SourcePath = sourcePath } if domain, ok := configMap["domain"].(string); ok { site.Domain = domain } if autoEnhance, ok := configMap["auto_enhance"].(bool); ok { site.AutoEnhance = autoEnhance } // Parse discovery config if present if discoveryMap, ok := configMap["discovery"].(map[string]interface{}); ok { discovery := &content.DiscoveryConfig{ Containers: true, // defaults Individual: true, } if enabled, ok := discoveryMap["enabled"].(bool); ok { discovery.Enabled = enabled } if aggressive, ok := discoveryMap["aggressive"].(bool); ok { discovery.Aggressive = aggressive } if containers, ok := discoveryMap["containers"].(bool); ok { discovery.Containers = containers } if individual, ok := discoveryMap["individual"].(bool); ok { discovery.Individual = individual } site.Discovery = discovery } if site.SiteID != "" && site.Path != "" { sites = append(sites, site) } } } if err := siteManager.RegisterSites(sites); err != nil { log.Printf("āš ļø Failed to register some sites: %v", err) } } } // Auto-enhance sites if enabled if 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{"Link"}, AllowCredentials: true, MaxAge: 300, // Maximum value not ignored by any of major browsers })) router.Use(api.ContentTypeMiddleware) // Health check endpoint router.Get("/health", api.HealthMiddleware()) // Static library serving (for demo sites) router.Get("/insertr.js", contentHandler.ServeInsertrJS) router.Get("/insertr.css", contentHandler.ServeInsertrCSS) // Auth routes router.Route("/auth", func(authRouter chi.Router) { authRouter.Get("/login", authService.HandleOAuthLogin) authRouter.Get("/callback", authService.HandleOAuthCallback) }) // API routes router.Route("/api", func(apiRouter chi.Router) { // Site enhancement endpoint apiRouter.Post("/enhance", contentHandler.EnhanceSite) // Content endpoints apiRouter.Route("/content", func(contentRouter chi.Router) { contentRouter.Get("/bulk", contentHandler.GetBulkContent) contentRouter.Get("/{id}", contentHandler.GetContent) contentRouter.Get("/", contentHandler.GetAllContent) contentRouter.Post("/", contentHandler.CreateContent) contentRouter.Put("/{id}", contentHandler.UpdateContent) // Version control endpoints contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions) contentRouter.Post("/{id}/rollback", contentHandler.RollbackContent) }) // Collection endpoints apiRouter.Route("/collections", func(collectionRouter chi.Router) { collectionRouter.Get("/", contentHandler.GetAllCollections) collectionRouter.Get("/{id}", contentHandler.GetCollection) // Collection item endpoints collectionRouter.Get("/{id}/items", contentHandler.GetCollectionItems) collectionRouter.Post("/{id}/items", contentHandler.CreateCollectionItem) collectionRouter.Put("/{id}/items/{item_id}", contentHandler.UpdateCollectionItem) collectionRouter.Delete("/{id}/items/{item_id}", contentHandler.DeleteCollectionItem) }) }) // Static site serving - serve registered sites at /sites/{site_id} // Custom file server that fixes CSS MIME types for siteID, siteConfig := range siteManager.GetAllSites() { log.Printf("šŸ“ Serving site %s from %s at /sites/%s/", siteID, siteConfig.Path, siteID) // Create custom file server with MIME type fixing fileServer := http.FileServer(http.Dir(siteConfig.Path)) customHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Fix MIME type for CSS files (including extensionless ones in css/ directory) if strings.Contains(r.URL.Path, "/css/") { w.Header().Set("Content-Type", "text/css; charset=utf-8") } fileServer.ServeHTTP(w, r) }) router.Handle("/sites/"+siteID+"/*", http.StripPrefix("/sites/"+siteID+"/", customHandler)) } // Start server addr := fmt.Sprintf(":%d", port) mode := "production" if devMode { mode = "development" } fmt.Printf("šŸš€ Insertr Content Server starting (%s mode)...\n", mode) fmt.Printf("šŸ“ Database: %s\n", dbPath) fmt.Printf("🌐 Server running at: http://localhost%s\n", addr) fmt.Printf("šŸ’š Health check: http://localhost%s/health\n", addr) fmt.Printf("šŸ“Š API endpoints:\n") fmt.Printf(" Content:\n") fmt.Printf(" GET /api/content?site_id={site}\n") fmt.Printf(" GET /api/content/{id}?site_id={site}\n") fmt.Printf(" GET /api/content/bulk?site_id={site}&ids[]={id1}&ids[]={id2}\n") fmt.Printf(" POST /api/content\n") fmt.Printf(" PUT /api/content/{id}\n") fmt.Printf(" GET /api/content/{id}/versions?site_id={site}\n") fmt.Printf(" POST /api/content/{id}/rollback\n") fmt.Printf(" Collections:\n") fmt.Printf(" GET /api/collections?site_id={site}\n") fmt.Printf(" GET /api/collections/{id}?site_id={site}\n") fmt.Printf(" GET /api/collections/{id}/items?site_id={site}\n") fmt.Printf(" POST /api/collections/{id}/items\n") fmt.Printf(" PUT /api/collections/{id}/items/{item_id}\n") fmt.Printf(" DELETE /api/collections/{id}/items/{item_id}\n") fmt.Printf("🌐 Static sites:\n") for siteID, _ := range siteManager.GetAllSites() { fmt.Printf(" %s: http://localhost%s/sites/%s/\n", siteID, addr, siteID) } fmt.Printf("\nšŸ”„ Press Ctrl+C to shutdown gracefully\n\n") // Setup graceful shutdown server := &http.Server{ Addr: addr, Handler: router, } // Start server in a goroutine go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed to start: %v", err) } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit fmt.Println("\nšŸ›‘ Shutting down server...") if err := server.Close(); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } fmt.Println("āœ… Server shutdown complete") }