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
This commit is contained in:
parent
e49882b8a8
commit
bb2bc223b9
16 changed files with 1292 additions and 0 deletions
92
internal/web/render.go
Normal file
92
internal/web/render.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
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())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue