Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/auth/handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 09015496d565a172bcaa15cfe9a8e4b072b13df0 |
| Branch | workstream-b-screenmatch |
| Path or archive member | internal/modules/auth/handler.go |
| Original SHA-256 | 9d8e215980ad34091dd3be2a9a538cbf4377c7ba91160408fb88be80b6b65c10 |
| Git blob SHA | e886f3886c98227c523f7e96822abc69b8b64109 |
| Reading-copy SHA-256 | 9d8e215980ad34091dd3be2a9a538cbf4377c7ba91160408fb88be80b6b65c10 |
Every recorded occurrence
These 57 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/auth/handler.go | workstream-b-screenmatch | 09015496d565a172bcaa15cfe9a8e4b072b13df0 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-d-missionmatch | 3be46ba89edb737182075a2b320509b42dc916f8 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-e-dataflow-deploy | 433815a042f9741411e60fa553fe72143e9b10db | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-c-slatematch | 5cd60c918301b46ecd6ed3e03e20c395da0062e2 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ruthless-debug-sweep | b7d742f8f032984be3817efffa3eb8b13b651c1f | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-a-shared-core | e0de03ac3a1ec826bd15d0e048e3b02baa23eeff | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-a-shared-core | e0de03ac3a1ec826bd15d0e048e3b02baa23eeff | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-c-slatematch | 5cd60c918301b46ecd6ed3e03e20c395da0062e2 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | w3-ai-staffing-recommendations | 43d56387270ef4da4cabd794d97ca6a49909bc7d | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-e-dataflow-deploy | 433815a042f9741411e60fa553fe72143e9b10db | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ruthless-debug-sweep | b7d742f8f032984be3817efffa3eb8b13b651c1f | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ai-intelligence-layer | 5d5ddff70393a197c5f54a45280bb5dd42b24755 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-b-screenmatch | 09015496d565a172bcaa15cfe9a8e4b072b13df0 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | workstream-d-missionmatch | 3be46ba89edb737182075a2b320509b42dc916f8 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/auth/handler.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓414 lines · 13.3 KB
package auth
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Handler is the HTTP surface for auth. It owns cookie issuance and the public/tenant route split.
type Handler struct {
svc *Service
csrf *security.CSRF
cookieSecure bool
allowedOrigins []string
trustedProxyHeader string
}
// NewHandler builds the auth handler. trustedProxyHeader names the header carrying the real client IP
// when the app sits behind a trusted reverse proxy (the app must not be directly exposed); empty uses
// the TCP peer address.
func NewHandler(svc *Service, csrf *security.CSRF, cookieSecure bool, allowedOrigins []string, trustedProxyHeader string) *Handler {
return &Handler{svc: svc, csrf: csrf, cookieSecure: cookieSecure, allowedOrigins: allowedOrigins, trustedProxyHeader: trustedProxyHeader}
}
// SessionCookieName and CSRFCookieName return the cookie names that match the Secure flag, so the
// __Host- prefix is only used when Secure is set (the prefix requires it).
func (h *Handler) SessionCookieName() string {
if h.cookieSecure {
return SessionCookie
}
return sessionCookieInsecure
}
// CSRFCookieName returns the CSRF cookie name; the CSRF middleware must be built with the same value.
func (h *Handler) CSRFCookieName() string {
if h.cookieSecure {
return CSRFCookie
}
return csrfCookieInsecure
}
// PublicRoutes mounts the pre tenant endpoints. They run OUTSIDE the session middleware (they operate
// on a pre MFA session or no session) but the caller wraps them in the CSRF Origin allowlist. The
// login, mfa, and accept handlers read the session cookie directly where they need a pending session.
func (h *Handler) PublicRoutes(r chi.Router) {
r.Get("/auth/csrf", h.issueCSRF)
r.Post("/auth/login", h.login)
r.Post("/auth/mfa/enroll", h.mfaEnroll)
r.Post("/auth/mfa/activate", h.mfaActivate)
r.Post("/auth/mfa/verify", h.mfaVerify)
r.Post("/auth/mfa/recover", h.mfaRecover)
r.Post("/auth/invite/accept", h.inviteAccept)
r.Post("/auth/logout", h.logout)
}
// TenantRoutes mounts the authenticated endpoints. The caller mounts these INSIDE the session
// middleware and the CSRF middleware; per route capabilities are applied here.
func (h *Handler) TenantRoutes(r chi.Router) {
r.Get("/auth/session", h.session)
r.Post("/auth/logout-all", h.logoutAll)
r.Post("/auth/password/change", h.passwordChange)
r.With(security.RequireRole(security.RoleAdmin)).Get("/users", h.listUsers)
r.With(security.RequireCapability(security.CapUserInvite)).Post("/users/invite", h.invite)
r.With(security.RequireCapability(security.CapUserRoleChange)).Patch("/users/{id}/role", h.changeRole)
r.With(security.RequireCapability(security.CapUserDisable)).Patch("/users/{id}/status", h.changeStatus)
r.With(security.RequireCapability(security.CapUserDisable)).Post("/users/{id}/mfa/reset", h.resetMFA)
r.With(security.RequireCapability(security.CapUserInvite)).Post("/users/{id}/recovery", h.reissueRecovery)
}
// SessionMiddleware resolves a validated active session into tenant context, replacing the dev header
// resolver. It reads the __Host- session cookie, requires an active MFA satisfied session, and stashes
// the authoritative org and the principal. Any failure is 403 before a handler runs (fail closed).
func (h *Handler) SessionMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(h.SessionCookieName())
if err != nil || cookie.Value == "" {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
principal, org, verr := h.svc.Validate(r.Context(), cookie.Value)
if verr != nil {
// The request is denied regardless, but the rejection audit must not vanish silently.
if aerr := h.svc.auditSessionRejected(r.Context(), h.clientIP(r), r.UserAgent()); aerr != nil {
slog.Error("auth: session rejection audit write failed", "error", aerr)
}
http.Error(w, "forbidden", http.StatusForbidden)
return
}
ctx := tenant.WithUser(tenant.WithOrg(r.Context(), org), principal)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ---- public handlers ----
func (h *Handler) issueCSRF(w http.ResponseWriter, r *http.Request) {
token, err := h.csrf.Issue()
if err != nil {
writeError(w, http.StatusInternalServerError, "internal error")
return
}
h.setCSRFCookie(w, token)
writeJSON(w, http.StatusOK, map[string]string{"csrf_token": token})
}
func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
var in LoginInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.Login(r.Context(), in, h.clientIP(r), r.UserAgent())
if err != nil {
writeAuthError(w, err)
return
}
h.setSessionCookie(w, res.Token)
writeJSON(w, http.StatusOK, res)
}
func (h *Handler) mfaEnroll(w http.ResponseWriter, r *http.Request) {
token, ok := h.sessionToken(r)
if !ok {
writeError(w, http.StatusForbidden, "no session")
return
}
res, err := h.svc.EnrollMFA(r.Context(), token)
if err != nil {
writeAuthError(w, err)
return
}
writeJSON(w, http.StatusOK, res)
}
func (h *Handler) mfaActivate(w http.ResponseWriter, r *http.Request) {
token, ok := h.sessionToken(r)
if !ok {
writeError(w, http.StatusForbidden, "no session")
return
}
var in CodeInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.ActivateMFA(r.Context(), token, in, h.clientIP(r), r.UserAgent())
if err != nil {
writeAuthError(w, err)
return
}
h.setSessionCookie(w, res.Token)
writeJSON(w, http.StatusOK, res)
}
func (h *Handler) mfaVerify(w http.ResponseWriter, r *http.Request) {
token, ok := h.sessionToken(r)
if !ok {
writeError(w, http.StatusForbidden, "no session")
return
}
var in CodeInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.VerifyTOTP(r.Context(), token, in, h.clientIP(r), r.UserAgent())
if err != nil {
writeAuthError(w, err)
return
}
h.setSessionCookie(w, res.Token)
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (h *Handler) mfaRecover(w http.ResponseWriter, r *http.Request) {
token, ok := h.sessionToken(r)
if !ok {
writeError(w, http.StatusForbidden, "no session")
return
}
var in CodeInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.RecoverMFA(r.Context(), token, in, h.clientIP(r), r.UserAgent())
if err != nil {
writeAuthError(w, err)
return
}
h.setSessionCookie(w, res.Token)
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (h *Handler) inviteAccept(w http.ResponseWriter, r *http.Request) {
var in AcceptInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.AcceptInvite(r.Context(), in, h.clientIP(r), r.UserAgent())
if err != nil {
writeAuthError(w, err)
return
}
h.setSessionCookie(w, res.Token)
writeJSON(w, http.StatusOK, res)
}
func (h *Handler) logout(w http.ResponseWriter, r *http.Request) {
if token, ok := h.sessionToken(r); ok {
_ = h.svc.Logout(r.Context(), token, h.clientIP(r), r.UserAgent())
}
h.clearSessionCookie(w)
writeJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
}
// ---- tenant handlers ----
func (h *Handler) session(w http.ResponseWriter, r *http.Request) {
info, err := h.svc.SessionInfo(r.Context())
writeOrError(w, info, err)
}
func (h *Handler) logoutAll(w http.ResponseWriter, r *http.Request) {
token, _ := h.sessionToken(r)
if err := h.svc.LogoutAll(r.Context(), token); err != nil {
writeAuthError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *Handler) passwordChange(w http.ResponseWriter, r *http.Request) {
token, ok := h.sessionToken(r)
if !ok {
writeError(w, http.StatusForbidden, "no session")
return
}
var in ChangePasswordInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.ChangePassword(r.Context(), token, in, h.clientIP(r), r.UserAgent())
if err != nil {
writeAuthError(w, err)
return
}
h.setSessionCookie(w, res.Token)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *Handler) listUsers(w http.ResponseWriter, r *http.Request) {
users, err := h.svc.ListUsers(r.Context())
writeOrError(w, users, err)
}
func (h *Handler) invite(w http.ResponseWriter, r *http.Request) {
var in InviteInput
if !decode(w, r, &in) {
return
}
res, err := h.svc.Invite(r.Context(), in, h.clientIP(r), r.UserAgent())
writeOrError(w, res, err)
}
func (h *Handler) changeRole(w http.ResponseWriter, r *http.Request) {
var in RoleChangeInput
if !decode(w, r, &in) {
return
}
err := h.svc.ChangeRole(r.Context(), chi.URLParam(r, "id"), in, h.clientIP(r), r.UserAgent())
writeStatusOrError(w, err)
}
func (h *Handler) changeStatus(w http.ResponseWriter, r *http.Request) {
var in StatusChangeInput
if !decode(w, r, &in) {
return
}
err := h.svc.ChangeStatus(r.Context(), chi.URLParam(r, "id"), in, h.clientIP(r), r.UserAgent())
writeStatusOrError(w, err)
}
func (h *Handler) resetMFA(w http.ResponseWriter, r *http.Request) {
err := h.svc.ResetMFA(r.Context(), chi.URLParam(r, "id"), h.clientIP(r), r.UserAgent())
writeStatusOrError(w, err)
}
func (h *Handler) reissueRecovery(w http.ResponseWriter, r *http.Request) {
res, err := h.svc.ReissueRecovery(r.Context(), chi.URLParam(r, "id"), h.clientIP(r), r.UserAgent())
writeOrError(w, res, err)
}
// ---- cookies ----
func (h *Handler) setSessionCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: h.SessionCookieName(), Value: token, Path: "/", HttpOnly: true,
Secure: h.cookieSecure, SameSite: http.SameSiteStrictMode,
})
}
func (h *Handler) clearSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: h.SessionCookieName(), Value: "", Path: "/", HttpOnly: true,
Secure: h.cookieSecure, SameSite: http.SameSiteStrictMode, MaxAge: -1,
})
}
// setCSRFCookie sets the companion CSRF cookie. It is NOT HttpOnly because the SPA reads it to echo
// the token into the X-CSRF-Token header (the signed double submit pattern).
func (h *Handler) setCSRFCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: h.CSRFCookieName(), Value: token, Path: "/", HttpOnly: false,
Secure: h.cookieSecure, SameSite: http.SameSiteStrictMode,
})
}
// ---- helpers ----
func (h *Handler) sessionToken(r *http.Request) (string, bool) {
c, err := r.Cookie(h.SessionCookieName())
if err != nil || c.Value == "" {
return "", false
}
return c.Value, true
}
func decode(w http.ResponseWriter, r *http.Request, v any) bool {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(v); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return false
}
return true
}
func writeOrError(w http.ResponseWriter, v any, err error) {
if err != nil {
writeAuthError(w, err)
return
}
writeJSON(w, http.StatusOK, v)
}
func writeStatusOrError(w http.ResponseWriter, err error) {
if err != nil {
writeAuthError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// writeAuthError maps service errors to status codes. Credential and session failures are 401, never
// leaking which factor failed; authorization failures are 403; missing tenant is 403; no database is
// 503; bad input is 400.
func writeAuthError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrInvalidCredentials), errors.Is(err, ErrInvalidSession), errors.Is(err, ErrInvalidState):
writeError(w, http.StatusUnauthorized, "unauthorized")
case errors.Is(err, ErrAccountLocked):
writeError(w, http.StatusTooManyRequests, "account temporarily locked")
case errors.Is(err, ErrForbidden):
writeError(w, http.StatusForbidden, "forbidden")
case errors.Is(err, tenant.ErrNoTenant):
writeError(w, http.StatusForbidden, "no validated tenant")
case errors.Is(err, ErrNotFound):
writeError(w, http.StatusNotFound, "not found")
case errors.Is(err, ErrEmailInUse):
writeError(w, http.StatusConflict, "email already in use")
case errors.Is(err, security.ErrPasswordTooLong), errors.Is(err, ErrBadRequest):
writeError(w, http.StatusBadRequest, "bad request")
case errors.Is(err, ErrNoDatabase):
writeError(w, http.StatusServiceUnavailable, "database not configured")
default:
writeError(w, http.StatusInternalServerError, "internal error")
}
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// clientIP returns the caller's IP for throttling and the audit trail. When trustedProxyHeader is set
// (the app is behind a trusted reverse proxy that overwrites it, and is not directly exposed) it reads
// that header, otherwise it uses the unspoofable TCP peer address. It does NOT trust the leftmost
// X-Forwarded-For (the spoofable value chi RealIP used).
func (h *Handler) clientIP(r *http.Request) string {
if h.trustedProxyHeader != "" {
if v := strings.TrimSpace(r.Header.Get(h.trustedProxyHeader)); v != "" {
return v
}
}
host := r.RemoteAddr
if i := strings.LastIndex(host, ":"); i >= 0 && !strings.Contains(host[i:], "]") {
host = host[:i]
}
return strings.Trim(host, "[]")
}