248 lines
7.4 KiB
Go
248 lines
7.4 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/insertr/insertr/internal/api"
|
|
"github.com/insertr/insertr/internal/auth"
|
|
"github.com/insertr/insertr/internal/config"
|
|
"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", 0, "Server port")
|
|
serveCmd.Flags().BoolVar(&devMode, "dev-mode", false, "Enable development mode features")
|
|
}
|
|
|
|
func runServe(cmd *cobra.Command, args []string) {
|
|
// Load configuration
|
|
cfg, err := loadConfig()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load configuration: %v", err)
|
|
}
|
|
|
|
// Override with flags if provided
|
|
if port != 0 {
|
|
cfg.Server.Port = port
|
|
}
|
|
if devMode {
|
|
cfg.Auth.DevMode = true
|
|
}
|
|
|
|
// Initialize database
|
|
database, err := db.NewDatabase(cfg.Database.Path)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize database: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
// Support environment variables for sensitive values
|
|
if clientSecret := os.Getenv("AUTHENTIK_CLIENT_SECRET"); clientSecret != "" && cfg.Auth.OIDC != nil {
|
|
cfg.Auth.OIDC.ClientSecret = clientSecret
|
|
}
|
|
if endpoint := os.Getenv("AUTHENTIK_ENDPOINT"); endpoint != "" && cfg.Auth.OIDC != nil {
|
|
cfg.Auth.OIDC.Endpoint = endpoint
|
|
}
|
|
|
|
// Set redirect URL if not configured
|
|
if cfg.Auth.OIDC != nil && cfg.Auth.OIDC.RedirectURL == "" {
|
|
cfg.Auth.OIDC.RedirectURL = fmt.Sprintf("http://%s:%d/auth/callback", cfg.Server.Host, cfg.Server.Port)
|
|
}
|
|
|
|
// Create legacy auth config for compatibility
|
|
authConfig := &auth.AuthConfig{
|
|
DevMode: cfg.Auth.DevMode,
|
|
Provider: cfg.Auth.Provider,
|
|
JWTSecret: cfg.Auth.JWTSecret,
|
|
}
|
|
|
|
if cfg.Auth.OIDC != nil {
|
|
authConfig.OIDC = &auth.OIDCConfig{
|
|
Endpoint: cfg.Auth.OIDC.Endpoint,
|
|
ClientID: cfg.Auth.OIDC.ClientID,
|
|
ClientSecret: cfg.Auth.OIDC.ClientSecret,
|
|
RedirectURL: cfg.Auth.OIDC.RedirectURL,
|
|
Scopes: cfg.Auth.OIDC.Scopes,
|
|
}
|
|
}
|
|
|
|
log.Printf("🔑 Using auth provider: %s", cfg.Auth.Provider)
|
|
if cfg.Auth.Provider == "authentik" && cfg.Auth.OIDC != nil {
|
|
log.Printf("🔐 Using Authentik OIDC provider: %s", cfg.Auth.OIDC.Endpoint)
|
|
}
|
|
|
|
authService, err := auth.NewAuthService(authConfig)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize authentication service: %v", err)
|
|
}
|
|
|
|
// Initialize content client for site manager
|
|
contentClient := database.NewContentRepository()
|
|
|
|
// Initialize site manager with auth provider
|
|
authProvider := &engine.AuthProvider{Type: cfg.Auth.Provider}
|
|
siteManager := content.NewSiteManagerWithAuth(contentClient, cfg.Auth.DevMode, authProvider)
|
|
|
|
// Convert config sites to legacy format and register
|
|
var legacySites []*config.SiteConfig
|
|
for _, site := range cfg.Server.Sites {
|
|
legacySite := &config.SiteConfig{
|
|
SiteID: site.SiteID,
|
|
Path: site.Path,
|
|
SourcePath: site.SourcePath,
|
|
Domain: site.Domain,
|
|
AutoEnhance: site.AutoEnhance,
|
|
}
|
|
if site.Discovery != nil {
|
|
legacySite.Discovery = &content.DiscoveryConfig{
|
|
Enabled: site.Discovery.Enabled,
|
|
Aggressive: site.Discovery.Aggressive,
|
|
Containers: site.Discovery.Containers,
|
|
Individual: site.Discovery.Individual,
|
|
}
|
|
}
|
|
legacySites = append(legacySites, legacySite)
|
|
}
|
|
|
|
if len(legacySites) > 0 {
|
|
if err := siteManager.RegisterSites(legacySites); err != nil {
|
|
log.Printf("⚠️ Failed to register some sites: %v", err)
|
|
}
|
|
}
|
|
|
|
// Auto-enhance sites if enabled
|
|
if cfg.Auth.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{"*"},
|
|
AllowCredentials: true,
|
|
}))
|
|
|
|
// Health check
|
|
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("OK"))
|
|
})
|
|
|
|
// Authentication routes
|
|
router.Route("/auth", func(r chi.Router) {
|
|
r.Post("/login", authService.HandleOAuthLogin)
|
|
r.Get("/callback", authService.HandleOAuthCallback)
|
|
})
|
|
|
|
// Register all Content API routes
|
|
contentHandler.RegisterRoutes(router)
|
|
|
|
// Serve insertr library assets (development only)
|
|
if cfg.Auth.DevMode {
|
|
router.Get("/insertr.js", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/javascript")
|
|
http.ServeFile(w, r, "./lib/dist/insertr.js")
|
|
})
|
|
router.Get("/insertr.min.js", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/javascript")
|
|
http.ServeFile(w, r, "./lib/dist/insertr.min.js")
|
|
})
|
|
router.Get("/insertr.css", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/css")
|
|
http.ServeFile(w, r, "./lib/dist/insertr.css")
|
|
})
|
|
log.Printf("📦 Serving insertr library assets from ./lib/dist/ (dev mode)")
|
|
}
|
|
|
|
// Serve static sites
|
|
for _, siteConfig := range siteManager.GetAllSites() {
|
|
log.Printf("📁 Serving site %s from %s at /sites/%s/", siteConfig.SiteID, siteConfig.Path, siteConfig.SiteID)
|
|
|
|
// Create a file server for each site
|
|
fileServer := http.FileServer(http.Dir(siteConfig.Path))
|
|
|
|
// Handle both /sites/{siteID}/ and /{siteID}/ patterns
|
|
router.Mount(fmt.Sprintf("/sites/%s/", siteConfig.SiteID), http.StripPrefix(fmt.Sprintf("/sites/%s/", siteConfig.SiteID), fileServer))
|
|
|
|
// Optionally serve at root for primary site
|
|
if siteConfig.Domain != "" {
|
|
log.Printf("🌐 Site %s available at domain: %s", siteConfig.SiteID, siteConfig.Domain)
|
|
}
|
|
}
|
|
|
|
// Catch-all for serving sites by domain or default
|
|
router.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
|
// Try to match by domain first
|
|
host := strings.Split(r.Host, ":")[0] // Remove port if present
|
|
for _, siteConfig := range siteManager.GetAllSites() {
|
|
if siteConfig.Domain == host {
|
|
fileServer := http.FileServer(http.Dir(siteConfig.Path))
|
|
fileServer.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Default 404
|
|
http.NotFound(w, r)
|
|
})
|
|
|
|
// Start server
|
|
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
|
log.Printf("🚀 Server starting on %s", addr)
|
|
log.Printf("📝 Content API available at http://%s/api/content/{site_id}", addr)
|
|
log.Printf("🔐 Authentication at http://%s/auth/login", addr)
|
|
|
|
// Graceful shutdown
|
|
go func() {
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
<-sigChan
|
|
log.Printf("🛑 Shutting down server...")
|
|
os.Exit(0)
|
|
}()
|
|
|
|
if err := http.ListenAndServe(addr, router); err != nil {
|
|
log.Fatalf("Server failed to start: %v", err)
|
|
}
|
|
}
|