Major improvements: - Replace Gorilla Mux with Chi v5 router for better performance and cleaner code - Fix CSS/JS MIME type issues that prevented proper asset loading - Add built-in CORS middleware replacing manual OPTIONS handlers - Simplify routing with nested route syntax - Update URL parameter extraction from mux.Vars to chi.URLParam New Devigo demo: - Add production Norwegian sales training website (devigo.no) - Real-world Hugo-generated content with TailwindCSS - 39 insertr-enhanced elements for comprehensive CMS testing - Demonstrates international language support and B2B use cases - Fixed asset paths for localhost serving compatibility Technical benefits: - Automatic MIME type detection for static files - Reduced code complexity with built-in middleware - Better performance with lighter dependency stack - Production-ready CORS handling
223 lines
6.8 KiB
Go
223 lines
6.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"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"
|
|
)
|
|
|
|
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"),
|
|
JWTSecret: viper.GetString("jwt_secret"),
|
|
}
|
|
|
|
// Set default JWT secret if not configured
|
|
if authConfig.JWTSecret == "" {
|
|
authConfig.JWTSecret = "dev-secret-change-in-production"
|
|
if authConfig.DevMode {
|
|
log.Printf("🔑 Using default JWT secret for development")
|
|
}
|
|
}
|
|
|
|
authService := auth.NewAuthService(authConfig)
|
|
|
|
// Initialize content client for site manager
|
|
contentClient := content.NewDatabaseClient(database)
|
|
|
|
// Initialize site manager
|
|
siteManager := content.NewSiteManager(contentClient, devMode)
|
|
|
|
// 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
|
|
}
|
|
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)
|
|
|
|
// 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)
|
|
|
|
// Version control endpoints
|
|
contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions)
|
|
contentRouter.Post("/{id}/rollback", contentHandler.RollbackContent)
|
|
})
|
|
})
|
|
|
|
// Static site serving - serve registered sites at /sites/{site_id}
|
|
// This fixes the MIME type issues with Chi's FileServer
|
|
for siteID, siteConfig := range siteManager.GetAllSites() {
|
|
log.Printf("📁 Serving site %s from %s at /sites/%s/", siteID, siteConfig.Path, siteID)
|
|
fileServer := http.FileServer(http.Dir(siteConfig.Path))
|
|
router.Handle("/sites/"+siteID+"/*", http.StripPrefix("/sites/"+siteID+"/", fileServer))
|
|
}
|
|
|
|
// 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(" 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("🌐 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")
|
|
}
|