- Rebuild JavaScript library with delayed control panel initialization - Update server assets to include latest UI behavior changes - Ensure built assets reflect invisible UI for regular visitors The control panel now only appears after gate activation, maintaining the invisible CMS principle for end users.
279 lines
8.8 KiB
Go
279 lines
8.8 KiB
Go
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 := content.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
|
|
}
|
|
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)
|
|
|
|
// 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}
|
|
// 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(" 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")
|
|
}
|