package web import ( "io/fs" "net/http" "strings" "github.com/gin-gonic/gin" ) const notBuiltHTML = `Admin UI not built

Admin UI not built

Run npm --prefix web ci && npm --prefix web run build and rebuild the binary.

The JSON API is available under /admin/api.

` const csp = "default-src 'self'; img-src 'self' https: data:; style-src 'self' 'unsafe-inline'; " + "script-src 'self'; connect-src 'self'; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" func securityHeaders(c *gin.Context) { c.Header("Content-Security-Policy", csp) c.Header("X-Content-Type-Options", "nosniff") c.Header("Referrer-Policy", "no-referrer") c.Header("X-Frame-Options", "DENY") } // Handler returns the SPA handler: real files are served from the embedded // build, every other path gets index.html so client-side routes deep-link. func Handler() gin.HandlerFunc { sub, err := fs.Sub(dist, "dist") if err != nil { panic(err) } index, err := fs.ReadFile(sub, "index.html") if err != nil { index = []byte(notBuiltHTML) } files := http.FileServer(http.FS(sub)) return func(c *gin.Context) { securityHeaders(c) p := strings.TrimPrefix(c.Param("filepath"), "/") if p != "" && p != "index.html" { if st, err := fs.Stat(sub, p); err == nil && !st.IsDir() { if strings.HasPrefix(p, "assets/") { c.Header("Cache-Control", "public, max-age=31536000, immutable") } else { c.Header("Cache-Control", "no-cache") } req := c.Request.Clone(c.Request.Context()) req.URL.Path = "/" + p files.ServeHTTP(c.Writer, req) return } // a missing hashed asset must not turn into an HTML page if strings.HasPrefix(p, "assets/") { c.Status(http.StatusNotFound) return } } // serve the bytes directly; FileServer would redirect /index.html to ./ c.Header("Cache-Control", "no-cache") c.Data(http.StatusOK, "text/html; charset=utf-8", index) } } // Register mounts the dashboard at /admin/ui and redirects /admin there. func Register(r *gin.Engine) { h := Handler() r.GET("/admin/ui/*filepath", h) r.HEAD("/admin/ui/*filepath", h) redirect := func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/ui/") } r.GET("/admin", redirect) r.GET("/admin/ui", redirect) }