Files
egutierrez 7849e1cc8e feat(registry): random_hex_id_go_core + spa_handler_go_infra
Dos primitivas reutilizables para apps web del registry:
- random_hex_id_go_core: IDs hex aleatorios (apps con SQLite + IDs string)
- spa_handler_go_infra: http.Handler que sirve embed.FS con fallback
  a index.html (patron SPA para React Router/dnd-kit)

Ambas creadas via fn-constructor durante apps/kanban (issue 0053).
Tests pasan, fn index OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:54:01 +02:00

50 lines
1.4 KiB
Go

package infra
import (
"io/fs"
"net/http"
"strings"
)
// SPAHandler retorna un http.Handler que sirve los archivos estáticos de fsys
// y hace fallback a indexFile para cualquier ruta que no corresponda a un
// archivo existente — patrón SPA para React Router y similares.
//
// fsys es típicamente un iofs.Sub(embed.FS, "frontend/dist").
// indexFile es la ruta dentro de fsys del HTML de fallback (ej. "index.html").
func SPAHandler(fsys fs.FS, indexFile string) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Defensa contra path traversal
if strings.Contains(r.URL.Path, "..") {
http.Error(w, "invalid path", http.StatusBadRequest)
return
}
// Normalizar: quitar la barra inicial para fs.Stat
path := strings.TrimPrefix(r.URL.Path, "/")
if path == "" {
path = "."
}
info, err := fs.Stat(fsys, path)
if err == nil && !info.IsDir() {
// El archivo existe y no es directorio: servir normalmente
fileServer.ServeHTTP(w, r)
return
}
// Fallback a indexFile (ruta no encontrada o directorio)
data, err := fs.ReadFile(fsys, indexFile)
if err != nil {
http.Error(w, "could not read index file", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data) //nolint:errcheck
})
}