// Package handler contains all HTTP request handlers. package handler import ( "fmt" "html/template" "log" "net/http" "os" "path/filepath" "strings" "time" "ridgwaysystems.org/website/internal/blog" "ridgwaysystems.org/website/internal/gitea" "ridgwaysystems.org/website/internal/newsletter" "ridgwaysystems.org/website/internal/ratelimit" "ridgwaysystems.org/website/internal/status" ) // Handler holds shared dependencies for all HTTP handlers. type Handler struct { store *blog.Store news *newsletter.Store dataDir string templates map[string]*template.Template siteURL string contactEmail string devMode bool postLimit *ratelimit.Limiter // rate-limits contact + newsletter POSTs gitea *gitea.Client giteaOwner string giteaRepo string giteaLabel string } // New creates a Handler. dataDir is the path to the data/ directory. func New(store *blog.Store, news *newsletter.Store, dataDir string) *Handler { h := &Handler{ store: store, news: news, dataDir: dataDir, siteURL: getenv("SITE_URL", "https://ridgwaysystems.org"), contactEmail: getenv("CONTACT_EMAIL", "hire@ridgwaysystems.org"), devMode: os.Getenv("DEV") == "1", postLimit: ratelimit.New(10*time.Minute, 5), giteaOwner: getenv("GITEA_OWNER", ""), giteaRepo: getenv("GITEA_REPO", ""), giteaLabel: getenv("GITEA_LABEL", "planned-outage"), } if url := os.Getenv("GITEA_URL"); url != "" { h.gitea = gitea.New(url, os.Getenv("GITEA_TOKEN")) } if !h.devMode { h.templates = mustLoadTemplates() } return h } func getenv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } // tmpl returns the template set for a given name, reloading in dev mode. func (h *Handler) tmpl(name string) *template.Template { if h.devMode { return mustLoadTemplates()[name] } return h.templates[name] } func mustLoadTemplates() map[string]*template.Template { m := make(map[string]*template.Template) funcMap := template.FuncMap{ "formatDate": formatDate, "joinTags": joinTags, } base := "templates/base.html" pages := []struct { name string file string }{ {"index", "templates/index.html"}, {"blog", "templates/blog.html"}, {"post", "templates/post.html"}, {"infrastructure", "templates/infrastructure.html"}, {"status", "templates/status.html"}, {"about", "templates/about.html"}, {"hire", "templates/hire.html"}, {"resume", "templates/resume.html"}, {"uses", "templates/uses.html"}, {"projects", "templates/projects.html"}, {"error", "templates/error.html"}, {"changelog", "templates/changelog.html"}, {"admin-login", "templates/admin/login.html"}, {"admin-dashboard", "templates/admin/dashboard.html"}, {"admin-editor", "templates/admin/editor.html"}, {"admin-status", "templates/admin/status.html"}, {"admin-uploads", "templates/admin/uploads.html"}, {"admin-newsletter", "templates/admin/newsletter.html"}, {"admin-changelog", "templates/admin/changelog.html"}, {"admin-changelog-editor", "templates/admin/changelog-editor.html"}, {"admin-outages", "templates/admin/outages.html"}, } for _, p := range pages { t, err := template.New(filepath.Base(p.file)).Funcs(funcMap).ParseFiles(base, p.file) if err != nil { log.Fatalf("template %s: %v", p.name, err) } m[p.name] = t } return m } // baseEnvelope wraps page-specific data with shared layout data for the base template. type baseEnvelope struct { Banner *siteBanner Inner any } // siteBanner holds the data for the site-wide status banner. type siteBanner struct { Level string // "danger" | "warning" Message string } // computeBanner loads status.json and returns a banner if any services are down or degraded. func (h *Handler) computeBanner() *siteBanner { p, err := status.Load(filepath.Join(h.dataDir, "status.json")) if err != nil || p == nil { return nil } var down, degraded []string for _, s := range p.Services { switch s.Status { case "down": down = append(down, s.Name) case "degraded": degraded = append(degraded, s.Name) } } switch { case len(down) > 0: noun := "are unavailable" if len(down) == 1 { noun = "is unavailable" } return &siteBanner{ Level: "danger", Message: "Major Outage \u2014 " + strings.Join(down, ", ") + " " + noun + ".", } case len(degraded) > 0: noun := "are experiencing issues" if len(degraded) == 1 { noun = "is experiencing issues" } return &siteBanner{ Level: "warning", Message: "Partial Outage \u2014 " + strings.Join(degraded, ", ") + " " + noun + ".", } } return nil } func (h *Handler) render(w http.ResponseWriter, name string, data any) { t := h.tmpl(name) if t == nil { http.Error(w, "template not found: "+name, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := t.ExecuteTemplate(w, "base", baseEnvelope{Banner: h.computeBanner(), Inner: data}); err != nil { log.Printf("render %s: %v", name, err) } } // errorData is passed to the error template. type errorData struct { Code int Title string Message string } func (h *Handler) renderErr(w http.ResponseWriter, code int, msg string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(code) data := errorData{Code: code, Title: http.StatusText(code), Message: msg} t := h.tmpl("error") if t == nil { fmt.Fprintf(w, "

%d %s

%s

Home", code, http.StatusText(code), msg) return } if err := t.ExecuteTemplate(w, "base", baseEnvelope{Banner: h.computeBanner(), Inner: data}); err != nil { log.Printf("renderErr %d: %v", code, err) } }