Files
where_woof/frontend/internal/handlers/handlers.go

109 lines
3.4 KiB
Go

// Package handlers wires templates, auth, and database queries for the
// WhereWoof GOAT front-end.
package handlers
import (
"html/template"
"net/http"
"strings"
"wherewoof/frontend/internal/auth"
"wherewoof/frontend/internal/db"
"wherewoof/frontend/internal/sms"
"wherewoof/frontend/internal/storage"
)
// Templates maps a page key to its parsed template set (base + page + partials).
// Each page is parsed as its own set so the shared "content" block name
// doesn't collide across pages.
type Templates map[string]*template.Template
// App holds dependencies shared by all handlers.
type App struct {
Queries *db.Queries
Tpl Templates
Sender sms.Sender
Storage *storage.Client
}
// New returns an App with the given query layer, template sets, SMS sender, and object storage.
func New(queries *db.Queries, tpl Templates, sender sms.Sender, store *storage.Client) *App {
return &App{Queries: queries, Tpl: tpl, Sender: sender, Storage: store}
}
// PageData is the root data passed to the base layout.
type PageData struct {
CurrentUser *db.User
Title string
Error string
Data any
}
func titleCase(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}
// LoadTemplates parses every page's template set from templates/.
func LoadTemplates() (Templates, error) {
const dir = "templates"
base := dir + "/base.html"
pages := map[string][]string{
"index": {dir + "/index.html"},
"register": {dir + "/register.html"},
"login": {dir + "/login.html"},
"account": {dir + "/account.html", dir + "/account-panel.html", dir + "/tag-list.html", dir + "/tag-edit-inline.html"},
"edit": {dir + "/tag-edit.html"},
"public": {dir + "/tag-public.html"},
"location": {dir + "/location.html"},
"notfound": {dir + "/not-found.html"},
}
funcs := template.FuncMap{"title": titleCase}
tpl := make(Templates, len(pages))
for name, files := range pages {
paths := append([]string{base}, files...)
t, err := template.New(name).Funcs(funcs).ParseFiles(paths...)
if err != nil {
return nil, err
}
tpl[name] = t
}
return tpl, nil
}
// render executes the page's base layout with a PageData populated from the
// authenticated user (if any).
func (a *App) render(w http.ResponseWriter, r *http.Request, page, title string, data any, errMsg string) {
pd := PageData{Title: title, Data: data, Error: errMsg}
if uid, ok := auth.GetUserID(r); ok {
if u, err := a.Queries.GetUserByID(r.Context(), uid); err == nil {
pd.CurrentUser = &u
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := a.Tpl[page].ExecuteTemplate(w, "base", pd); err != nil {
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
}
}
// renderPartial executes a named partial (e.g. "account-panel") for HTMX swaps.
func (a *App) renderPartial(w http.ResponseWriter, page, partial string, data any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := a.Tpl[page].ExecuteTemplate(w, partial, data); err != nil {
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
}
}
// RequireAuth redirects unauthenticated requests to /login.
func (a *App) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, ok := auth.GetUserID(r); !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
next(w, r)
}
}