109 lines
2.5 KiB
Go
109 lines
2.5 KiB
Go
package frontend
|
|
|
|
import (
|
|
"RenewCMS/internal/infrastructure/assets"
|
|
"bytes"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
const hashedAssetsPrefix = "assets/"
|
|
|
|
const (
|
|
hashedAssetCacheControl = "public, max-age=31536000, immutable"
|
|
staticFileCacheControl = "public, max-age=3600"
|
|
indexCacheControl = "no-store, no-cache, must-revalidate"
|
|
)
|
|
|
|
const devNotice = "RenewCMS was built with -tags nofrontend: the SPA is served by the Vite dev server (http://localhost:5173)."
|
|
|
|
type spaHandler struct {
|
|
staticFS fs.FS
|
|
modTime time.Time
|
|
}
|
|
|
|
func NewFrontendRouter() http.Handler {
|
|
r := chi.NewRouter()
|
|
|
|
if assets.Dist == nil {
|
|
r.Get("/*", serveDevNotice)
|
|
return r
|
|
}
|
|
|
|
handler := &spaHandler{
|
|
staticFS: assets.Dist,
|
|
modTime: time.Now(),
|
|
}
|
|
|
|
r.Get("/*", handler.ServeHTTP)
|
|
return r
|
|
}
|
|
|
|
func serveDevNotice(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = io.WriteString(w, devNotice)
|
|
}
|
|
|
|
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
path := h.cleanPath(r.URL.Path)
|
|
if h.fileExists(path) {
|
|
h.serveAsset(w, r, path)
|
|
return
|
|
}
|
|
h.serveIndex(w, r)
|
|
}
|
|
|
|
func (h *spaHandler) cleanPath(p string) string {
|
|
return strings.TrimPrefix(path.Clean("/"+p), "/")
|
|
}
|
|
|
|
func (h *spaHandler) fileExists(path string) bool {
|
|
if path == "" || path == "." {
|
|
return false
|
|
}
|
|
f, err := h.staticFS.Open(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer f.Close()
|
|
stat, err := f.Stat()
|
|
return err == nil && !stat.IsDir()
|
|
}
|
|
|
|
func (h *spaHandler) serveAsset(w http.ResponseWriter, r *http.Request, path string) {
|
|
if strings.HasPrefix(path, hashedAssetsPrefix) {
|
|
w.Header().Set("Cache-Control", hashedAssetCacheControl)
|
|
} else {
|
|
w.Header().Set("Cache-Control", staticFileCacheControl)
|
|
}
|
|
http.FileServer(http.FS(h.staticFS)).ServeHTTP(w, r)
|
|
}
|
|
|
|
func (h *spaHandler) serveIndex(w http.ResponseWriter, r *http.Request) {
|
|
f, err := h.staticFS.Open("index.html")
|
|
if err != nil {
|
|
http.Error(w, "Index not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
html, err := io.ReadAll(f)
|
|
if err != nil {
|
|
http.Error(w, "Index not readable", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", indexCacheControl)
|
|
if policy := w.Header().Get("Content-Security-Policy"); policy != "" {
|
|
w.Header().Set("Content-Security-Policy", AllowInlineScripts(policy, html))
|
|
}
|
|
http.ServeContent(w, r, "index.html", h.modTime, bytes.NewReader(html))
|
|
}
|