package web import ( "net/http" "os" "strconv" "strings" "github.com/go-chi/chi/v5" "hearthforge/internal/avatar" "hearthforge/internal/highlight" ) // immutableCache caches a response for one year. Both routes are safe to // cache that long because their URLs carry a version or change only with the // binary. const immutableCache = "public, max-age=31536000, immutable" // avatarRoutes serves user avatars and the generated syntax highlighting CSS. func (s *Server) avatarRoutes(r chi.Router) { r.Get("/avatars/{id}", s.serveAvatar) // The literal path wins over the /assets/* static file handler in chi's // trie, so the order of registration does not matter. // TestCodeThemeBeatsStaticMount covers that. r.Get("/assets/code-theme.css", serveCodeTheme) } func (s *Server) serveAvatar(w http.ResponseWriter, r *http.Request) { name := strings.TrimSuffix(chi.URLParam(r, "id"), ".png") userID, err := strconv.ParseInt(name, 10, 64) if err != nil { http.Error(w, "Not found", http.StatusNotFound) return } // Without the version parameter the answer could be cached forever under // a URL that never changes, so refuse it. if !r.URL.Query().Has("v") { http.Error(w, "Not found", http.StatusNotFound) return } path := avatar.Path(s.Cfg.AvatarsDir(), userID) if _, err := os.Stat(path); err != nil { // Users created before avatars existed have no file yet. user, err := s.DB.UserByID(r.Context(), userID) if err != nil || user == nil { http.Error(w, "Not found", http.StatusNotFound) return } if err := avatar.SaveDefault(s.Cfg.AvatarsDir(), userID, user.Username); err != nil { http.Error(w, "Not found", http.StatusNotFound) return } } w.Header().Set("Content-Type", "image/png") w.Header().Set("Cache-Control", immutableCache) http.ServeFile(w, r, path) } func serveCodeTheme(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Cache-Control", immutableCache) w.Write([]byte(highlight.CSS())) }