Ctrl K
ComunidadGuías…

view/template.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package view

import (
	"crypto/sha256"
	"embed"
	"encoding/hex"
	"html/template"
	"io/fs"
	"net/http"

	"github.com/eugenioenko/autentico/pkg/config"
	"github.com/eugenioenko/autentico/pkg/cspnonce"
)

//go:embed *.html static/*
var FS embed.FS

// ParseTemplate parses layout.html together with the named page template,
// returning a template set where executing "layout" renders the full page.
func ParseTemplate(name string) (*template.Template, error) {
	tmpl := template.New("layout").Funcs(template.FuncMap{
		"authURL": func(path string) string {
			return config.GetBootstrap().AppOAuthPath + path
		},
		"hasThemeCss": func() bool {
			return config.Get().ThemeCssResolved != ""
		},
		"footerLinks": func() []config.FooterLink {
			return config.Get().FooterLinks
		},
		"brandColor": func() string {
			return config.Get().Theme.BrandColor
		},
		"logoURL": func(v any) template.URL {
			raw, _ := v.(string)
			if raw == "" || config.ValidateLogoURL(raw) != nil {
				return template.URL(config.GetBootstrap().AppOAuthPath + "/static/logo.svg")
			}
			return template.URL(raw)
		},
	})
	return tmpl.ParseFS(FS, "layout.html", name+".html")
}

// InjectNonce reads the CSP nonce from the request context and adds it to the
// template data map under the key "CspNonce". Call this before ExecuteTemplate
// so that templates can render nonce="{{.CspNonce}}" on <script> tags.
func InjectNonce(r *http.Request, data map[string]any) {
	data["CspNonce"] = cspnonce.Get(r.Context())
}

// RenderError renders a branded error page using the error template.
// Use this for user-facing errors on browser endpoints that should not return JSON.
func RenderError(w http.ResponseWriter, r *http.Request, status int, errorMsg string) {
	cfg := config.Get()
	tmpl, err := ParseTemplate("error")
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	data := map[string]any{
		"Error":        errorMsg,
		"ThemeTitle":   cfg.Theme.Title,
		"ThemeLogoUrl": cfg.Theme.LogoUrl,
	}
	InjectNonce(r, data)
	w.WriteHeader(status)
	if err = tmpl.ExecuteTemplate(w, "layout", data); err != nil {
		http.Error(w, "Template Execution Error", http.StatusInternalServerError)
	}
}

// StaticHandler returns an http.Handler that serves files from view/static/.
// Mount it with http.StripPrefix so the handler receives bare file names.
func StaticHandler() http.Handler {
	sub, err := fs.Sub(FS, "static")
	if err != nil {
		panic(err)
	}
	return http.FileServer(http.FS(sub))
}

// ThemeCSSHandler serves admin-supplied theme CSS (theme_css_inline +
// theme_css_file content) with text/css content-type. Serving as an external
// stylesheet — instead of injecting into a page <style> block — eliminates
// the </style> breakout vector that turned admin-controlled CSS into XSS.
func ThemeCSSHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		css := config.Get().ThemeCssResolved
		sum := sha256.Sum256([]byte(css))
		etag := `"` + hex.EncodeToString(sum[:8]) + `"`
		w.Header().Set("Content-Type", "text/css; charset=utf-8")
		w.Header().Set("ETag", etag)
		w.Header().Set("Cache-Control", "public, max-age=60, must-revalidate")
		if r.Header.Get("If-None-Match") == etag {
			w.WriteHeader(http.StatusNotModified)
			return
		}
		_, _ = w.Write([]byte(css))
	})
}