192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
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(`<script\s+id="dynamic-data"\s+type="application/json">\s*</script>`)
|
|
|
|
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 <script id="dynamic-data" type="application/json"></script>
|
|
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(`<script id="dynamic-data">window.__PARCOURSMOB_DATA__ = ` + string(jsonData) + `;</script>`)
|
|
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"}`))
|
|
}
|