Files
insertr/cmd/serve.go
Joakim 71561316da Fix demo site auto-enhancement and content persistence
- Restructure demo directory from test-sites/ to demos/ with flattened layout
- Add auto-enhancement on server startup for all sites with auto_enhance: true
- Fix inconsistent content ID generation that prevented dan-eden-portfolio content persistence
- Update server configuration to enhance from source to separate output directories
- Remove manual enhancement from justfile in favor of automatic server enhancement
- Clean up legacy test files and unused restore command
- Update build system to use CDN endpoint instead of file copying
2025-09-17 00:07:40 +02:00

222 lines
7.0 KiB
Go

package cmd
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/gorilla/mux"
"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 router
router := mux.NewRouter()
// Add middleware
router.Use(api.CORSMiddleware)
router.Use(api.LoggingMiddleware)
router.Use(api.ContentTypeMiddleware)
// Health check endpoint
router.HandleFunc("/health", api.HealthMiddleware())
// API routes
apiRouter := router.PathPrefix("/api").Subrouter()
// Content endpoints
contentRouter := apiRouter.PathPrefix("/content").Subrouter()
contentRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
contentRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET")
contentRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET")
contentRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")
// Version control endpoints
contentRouter.HandleFunc("/{id}/versions", contentHandler.GetContentVersions).Methods("GET")
contentRouter.HandleFunc("/{id}/rollback", contentHandler.RollbackContent).Methods("POST")
// Site enhancement endpoint
apiRouter.HandleFunc("/enhance", contentHandler.EnhanceSite).Methods("POST")
// Static library serving (for demo sites)
router.HandleFunc("/insertr.js", contentHandler.ServeInsertrJS).Methods("GET")
// Static site serving - serve registered sites at /sites/{site_id}
siteRouter := router.PathPrefix("/sites").Subrouter()
for siteID, siteConfig := range siteManager.GetAllSites() {
log.Printf("📁 Serving site %s from %s at /sites/%s/", siteID, siteConfig.Path, siteID)
siteRouter.PathPrefix("/" + siteID + "/").Handler(
http.StripPrefix("/sites/"+siteID+"/",
http.FileServer(http.Dir(siteConfig.Path))))
}
// Handle CORS preflight requests explicitly
contentRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS")
contentRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS")
contentRouter.HandleFunc("/bulk", api.CORSPreflightHandler).Methods("OPTIONS")
contentRouter.HandleFunc("/{id}/versions", api.CORSPreflightHandler).Methods("OPTIONS")
contentRouter.HandleFunc("/{id}/rollback", api.CORSPreflightHandler).Methods("OPTIONS")
apiRouter.HandleFunc("/enhance", api.CORSPreflightHandler).Methods("OPTIONS")
// 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")
}