package publicweb import ( "encoding/json" "net/http" "os" "path/filepath" "regexp" "strings" "time" "github.com/gorilla/mux" "github.com/rs/zerolog/log" "github.com/spf13/viper" "git.coopgo.io/coopgo-apps/parcoursmob/core/application" cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage" "git.coopgo.io/coopgo-apps/parcoursmob/services" ) // DataProvider returns data to hydrate a page type DataProvider func(r *http.Request) (any, error) // DynamicRoute defines a route with its HTML file and data provider type DynamicRoute struct { HTMLFile string DataProvider DataProvider } // Regex to find the placeholder script tag var dynamicDataRegex = regexp.MustCompile(`\s*`) type PublicWebServer struct { cfg *viper.Viper services *services.ServicesHandler kv cache.KVHandler filestorage cache.FileStorage applicationHandler *application.ApplicationHandler rootDir string dynamicRoutes map[string]DynamicRoute } func Run( cfg *viper.Viper, svc *services.ServicesHandler, applicationHandler *application.ApplicationHandler, kv cache.KVHandler, filestorage cache.FileStorage, ) { address := cfg.GetString("server.publicweb.listen") rootDir := cfg.GetString("server.publicweb.root_dir") serviceName := cfg.GetString("service_name") server := &PublicWebServer{ cfg: cfg, services: svc, kv: kv, filestorage: filestorage, applicationHandler: applicationHandler, rootDir: rootDir, dynamicRoutes: make(map[string]DynamicRoute), } server.registerDynamicRoutes() r := mux.NewRouter() r.HandleFunc("/health", server.healthHandler).Methods("GET") for pattern := range server.dynamicRoutes { r.HandleFunc(pattern, server.dynamicHandler).Methods("GET", "POST") } r.PathPrefix("/").Handler(server.fileServerHandler()) srv := &http.Server{ Handler: r, Addr: address, WriteTimeout: 30 * time.Second, ReadTimeout: 15 * time.Second, } log.Info(). Str("service_name", serviceName). Str("address", address). Str("root_dir", rootDir). Msg("Running Public Web HTTP server") err := srv.ListenAndServe() log.Error().Err(err).Msg("Public Web server error") } func (s *PublicWebServer) registerDynamicRoutes() { s.RegisterDynamicRoute("/recherche/", "recherche/index.html", s.journeySearchDataProvider) } func (s *PublicWebServer) RegisterDynamicRoute(pattern, htmlFile string, provider DataProvider) { s.dynamicRoutes[pattern] = DynamicRoute{ HTMLFile: htmlFile, DataProvider: provider, } } func (s *PublicWebServer) dynamicHandler(w http.ResponseWriter, r *http.Request) { route := mux.CurrentRoute(r) pattern, _ := route.GetPathTemplate() dynRoute, exists := s.dynamicRoutes[pattern] if !exists { http.NotFound(w, r) return } data, err := dynRoute.DataProvider(r) if err != nil { log.Error().Err(err).Str("route", pattern).Msg("Error getting data") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if err := s.hydrate(w, dynRoute.HTMLFile, data); err != nil { http.NotFound(w, r) } } // hydrate reads an HTML file and injects JSON data into func (s *PublicWebServer) hydrate(w http.ResponseWriter, htmlFile string, data any) error { htmlPath := filepath.Join(s.rootDir, htmlFile) htmlContent, err := os.ReadFile(htmlPath) if err != nil { log.Error().Err(err).Str("file", htmlPath).Msg("Error reading HTML file") return err } jsonData, err := json.Marshal(data) if err != nil { log.Error().Err(err).Msg("Error marshaling data to JSON") return err } // Replace the placeholder with a script that assigns data to window.__PARCOURSMOB_DATA__ replacement := []byte(``) modifiedHTML := dynamicDataRegex.ReplaceAll(htmlContent, replacement) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(modifiedHTML) return nil } func (s *PublicWebServer) fileServerHandler() http.Handler { fs := http.FileServer(http.Dir(s.rootDir)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := filepath.Join(s.rootDir, r.URL.Path) if _, err := os.Stat(path); os.IsNotExist(err) { if filepath.Ext(path) == "" { if idx := filepath.Join(path, "index.html"); fileExists(idx) { http.ServeFile(w, r, idx) return } if idx := filepath.Join(s.rootDir, "index.html"); fileExists(idx) { http.ServeFile(w, r, idx) return } } http.NotFound(w, r) return } if info, _ := os.Stat(path); info != nil && info.IsDir() { if idx := filepath.Join(path, "index.html"); fileExists(idx) && strings.HasSuffix(r.URL.Path, "/") { http.ServeFile(w, r, idx) return } } fs.ServeHTTP(w, r) }) } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func (s *PublicWebServer) healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"healthy"}`)) }