chldapasswd/static.go
Pierre-Olivier Mercier 99def55e80
All checks were successful
continuous-integration/drone/push Build is passing
feat: replace Bootstrap with custom CSS and add profile page
- Add self-hosted style.css replacing Bootstrap CDN dependency
- Add profile.html with tabbed view (account info, emails/aliases, API token)
- Refactor login handler to pass structured data to template instead of building HTML strings
- Add brand-name and brand-logo flags/env vars for UI customization
- Update CSP to allow brand logo domain and remove CDN references
- Update all templates to pass template vars to header/footer and use new CSS classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 11:49:51 +07:00

92 lines
2.5 KiB
Go

package main
import (
"embed"
"html/template"
"log"
"net/http"
"net/url"
"strings"
)
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
imgSrc := "'self' data:"
if strings.HasPrefix(brandLogo, "http://") || strings.HasPrefix(brandLogo, "https://") {
if u, err := url.Parse(brandLogo); err == nil {
imgSrc += " " + u.Scheme + "://" + u.Host
}
}
csp := "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline'; style-src 'self' 'sha256-W6z8OR2iqpPyNGe72eRXH58H75H3UVJDuwHoKA6pX98='; img-src " + imgSrc + "; worker-src blob:"
w.Header().Set("Content-Security-Policy", csp)
if !devMode {
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
next.ServeHTTP(w, r)
})
}
//go:embed all:static
var assets embed.FS
func serveStyleCSS(w http.ResponseWriter, r *http.Request) {
data, err := assets.ReadFile("static/style.css")
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(data)
}
func displayTmpl(w http.ResponseWriter, page string, vars map[string]any) {
if vars == nil {
vars = map[string]any{}
}
vars["brand_name"] = brandName
if brandLogo != "" {
vars["brand_logo"] = brandLogo
}
data, err := assets.ReadFile("static/" + page)
if err != nil {
log.Fatalf("Unable to find %q: %s", page, err.Error())
}
footer, err := assets.ReadFile("static/footer.html")
if err != nil {
log.Fatalf("Unable to find %q: %s", "footer.html", err.Error())
}
header, err := assets.ReadFile("static/header.html")
if err != nil {
log.Fatalf("Unable to find %q: %s", "header.html", err.Error())
}
tpl := template.Must(template.New("page").Parse(string(data)))
tpl.New("footer.html").Parse(string(footer))
tpl.New("header.html").Parse(string(header))
tpl.ExecuteTemplate(w, "page", vars)
}
func displayTmplError(w http.ResponseWriter, statusCode int, page string, vars map[string]any) {
w.WriteHeader(statusCode)
displayTmpl(w, page, vars)
}
func displayMsg(w http.ResponseWriter, msg string, statusCode int) {
w.WriteHeader(statusCode)
label := "error"
if statusCode < 400 {
label = "message"
}
displayTmpl(w, "message.html", map[string]any{label: msg})
}