opnsense-portal/internal/web/render.go
Carsten Abele bb2bc223b9 feat(web): Sessions, Rate-Limiting, Security-Header, Templates und eingebettete Assets
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
2026-08-14 09:20:45 +02:00

92 lines
2.4 KiB
Go

package web
import (
"bytes"
"embed"
"fmt"
"html/template"
"net/http"
)
//go:embed templates/*.html
var templateFS embed.FS
// AssetsFS enthält CSS und JavaScript; wird vom Asset-Handler ausgeliefert.
//
//go:embed assets
var AssetsFS embed.FS
// Brand bündelt die White-Label-Angaben.
type Brand struct {
Title string
SupportContact string
Version string
HasLogo bool
}
// PageData ist das Wurzelobjekt jedes Templates.
type PageData struct {
Brand Brand
Flash string
FlashKind string // "error" oder "info"
CSRF string
User string
Data any
}
// errorPageData füllt error.html.
type errorPageData struct {
Title string
Message string
ShowLoginLink bool
ShowOverviewLink bool
}
// Renderer hält die beim Start geparsten Templates.
type Renderer struct {
brand Brand
pages map[string]*template.Template
}
// pageNames sind die Inhaltstemplates, die jeweils mit dem Layout kombiniert werden.
var pageNames = []string{"login", "overview", "guides", "error"}
// NewRenderer parst alle Templates beim Start; ein Fehler bricht den Start ab,
// damit ein Tippfehler im Template nicht erst im Betrieb auffällt.
func NewRenderer(brand Brand) (*Renderer, error) {
funcs := template.FuncMap{"t": T}
pages := make(map[string]*template.Template, len(pageNames))
for _, name := range pageNames {
tmpl, err := template.New("layout.html").Funcs(funcs).
ParseFS(templateFS, "templates/layout.html", "templates/"+name+".html")
if err != nil {
return nil, fmt.Errorf("Template %s: %w", name, err)
}
pages[name] = tmpl
}
return &Renderer{brand: brand, pages: pages}, nil
}
// Brand liefert die White-Label-Angaben für Handler.
func (rn *Renderer) Brand() Brand { return rn.brand }
// Render schreibt eine Seite. Es wird zuerst in einen Puffer gerendert, damit
// ein Template-Fehler nicht zu einer halb geschriebenen Antwort führt.
func (rn *Renderer) Render(w http.ResponseWriter, r *http.Request, status int, page string, data PageData) {
tmpl, ok := rn.pages[page]
if !ok {
http.Error(w, T("error_internal"), http.StatusInternalServerError)
return
}
if data.Brand.Title == "" {
data.Brand = rn.brand
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "layout", data); err != nil {
http.Error(w, T("error_internal"), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
w.Write(buf.Bytes())
}