Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/org/handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/org/handler.go |
| Original SHA-256 | 68f55951f2461c6052e88c8edb4458bff219007d465a3fea47f71443fabc0b6e |
| Git blob SHA | 68b0c1b99f297dc3032c5bcee88ba14ac540c572 |
| Reading-copy SHA-256 | 68f55951f2461c6052e88c8edb4458bff219007d465a3fea47f71443fabc0b6e |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/org/handler.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/org/handler.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓145 lines · 5.1 KB
package org
import (
"encoding/json"
"errors"
"io"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Handler serves the branding surface. Reads are open to every role because the shell renders the mark
// for everyone in the organization. Writes are gated on org.settings.write, which the capability matrix
// grants to the owner alone.
type Handler struct{ svc *Service }
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
func (h *Handler) Routes(r chi.Router) {
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/org/branding", h.branding)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/org/logo", h.getLogo)
r.With(security.RequireCapability(security.CapOrgSettings)).Put("/org/logo", h.putLogo)
r.With(security.RequireCapability(security.CapOrgSettings)).Delete("/org/logo", h.deleteLogo)
r.With(security.RequireCapability(security.CapOrgSettings)).Patch("/org/branding", h.patchBranding)
}
func (h *Handler) branding(w http.ResponseWriter, r *http.Request) {
b, err := h.svc.Branding(r.Context())
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, b)
}
// getLogo serves the stored bytes. Every header here is load bearing.
//
// The Content-Type is the MIME the SERVER derived at upload time by decoding the file, never anything a
// caller supplied. nosniff stops a browser second guessing that and interpreting the body as something
// executable. The restrictive Content-Security-Policy means that even if a future format slipped through
// validation, the response could not load a script, a frame or a remote resource. Cache-Control is
// private because a logo is tenant data and must never land in a shared cache.
func (h *Handler) getLogo(w http.ResponseWriter, r *http.Request) {
logo, err := h.svc.Logo(r.Context())
if err != nil {
h.fail(w, err)
return
}
w.Header().Set("Content-Type", logo.MIME)
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
w.Header().Set("Cache-Control", "private, max-age=300, must-revalidate")
w.Header().Set("ETag", logo.ETag)
// A conditional request that still matches costs no body. The ETag is a hash of the bytes, so it
// changes the moment the logo does and never goes stale.
if match := r.Header.Get("If-None-Match"); match != "" && match == logo.ETag {
w.WriteHeader(http.StatusNotModified)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(logo.Bytes)
}
// putLogo accepts a raw image body. The request is capped one byte above the limit so an oversized
// upload is refused by the reader rather than buffered in full and then measured.
func (h *Handler) putLogo(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, MaxLogoBytes+1)
raw, err := io.ReadAll(r.Body)
if err != nil {
writeErr(w, http.StatusRequestEntityTooLarge, "logo exceeds the maximum size")
return
}
b, err := h.svc.SetLogo(r.Context(), raw)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, b)
}
func (h *Handler) deleteLogo(w http.ResponseWriter, r *http.Request) {
b, err := h.svc.ClearLogo(r.Context())
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, b)
}
func (h *Handler) patchBranding(w http.ResponseWriter, r *http.Request) {
var in struct {
DisplayName string `json:"display_name"`
}
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10))
dec.DisallowUnknownFields()
if err := dec.Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid body")
return
}
b, err := h.svc.SetDisplayName(r.Context(), in.DisplayName)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, b)
}
// fail maps a service error onto a status. The wrapped error can carry table and column names, so it is
// never returned to the caller.
func (h *Handler) fail(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrNoLogo):
writeErr(w, http.StatusNotFound, "no logo set")
case errors.Is(err, ErrLogoTooLarge):
writeErr(w, http.StatusRequestEntityTooLarge, "logo exceeds the maximum size")
case errors.Is(err, ErrLogoFormat):
writeErr(w, http.StatusUnsupportedMediaType, "logo must be a PNG or JPEG image")
case errors.Is(err, ErrLogoTooBig):
writeErr(w, http.StatusUnprocessableEntity, "logo dimensions exceed the maximum")
case errors.Is(err, ErrDisplayName):
writeErr(w, http.StatusBadRequest, "display name must be between 1 and 120 characters")
case errors.Is(err, tenant.ErrNoTenant):
writeErr(w, http.StatusForbidden, "no tenant context")
case errors.Is(err, ErrNoDatabase):
writeErr(w, http.StatusServiceUnavailable, "database unavailable")
default:
writeErr(w, http.StatusInternalServerError, "internal error")
}
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
func writeErr(w http.ResponseWriter, code int, msg string) {
writeJSON(w, code, map[string]string{"error": msg})
}