- Add SiteManager for registering and managing static sites with file-based enhancement - Implement EnhanceInPlace method for in-place file modification using database content - Integrate automatic file enhancement triggers in UpdateContent API handler - Add comprehensive site configuration support in insertr.yaml with auto-enhancement - Extend serve command to automatically register and manage configured sites - Add backup system for original files before enhancement - Support multi-site hosting with individual auto-enhancement settings - Update documentation for server-hosted enhancement workflow This enables real-time content deployment where database content changes immediately update static files without requiring rebuilds or redeployment. The database remains the single source of truth while maintaining static file performance benefits.
193 lines
5.9 KiB
Go
193 lines
5.9 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, "./insertr-backups")
|
|
|
|
// 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 domain, ok := configMap["domain"].(string); ok {
|
|
site.Domain = domain
|
|
}
|
|
if autoEnhance, ok := configMap["auto_enhance"].(bool); ok {
|
|
site.AutoEnhance = autoEnhance
|
|
}
|
|
if backupOriginals, ok := configMap["backup_originals"].(bool); ok {
|
|
site.BackupOriginals = backupOriginals
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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/content").Subrouter()
|
|
|
|
// Content endpoints matching the expected API contract
|
|
apiRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
|
|
apiRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET")
|
|
apiRouter.HandleFunc("/{id}", contentHandler.UpdateContent).Methods("PUT")
|
|
apiRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET")
|
|
apiRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")
|
|
|
|
// Version control endpoints
|
|
apiRouter.HandleFunc("/{id}/versions", contentHandler.GetContentVersions).Methods("GET")
|
|
apiRouter.HandleFunc("/{id}/rollback", contentHandler.RollbackContent).Methods("POST")
|
|
|
|
// Handle CORS preflight requests explicitly
|
|
apiRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS")
|
|
apiRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS")
|
|
apiRouter.HandleFunc("/bulk", api.CORSPreflightHandler).Methods("OPTIONS")
|
|
apiRouter.HandleFunc("/{id}/versions", api.CORSPreflightHandler).Methods("OPTIONS")
|
|
apiRouter.HandleFunc("/{id}/rollback", 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("\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")
|
|
}
|