Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/collab/handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/collab/handler.go |
| Original SHA-256 | f26c6fd33da99a0d9bd6856895b13ad81f41951361f7f7f6e60a9a502beaf4b7 |
| Git blob SHA | 138036073cb78959aa61e1fefcd36655955001a4 |
| Reading-copy SHA-256 | f26c6fd33da99a0d9bd6856895b13ad81f41951361f7f7f6e60a9a502beaf4b7 |
Recovered source transcript
Download complete source text ↓159 lines · 5.3 KB
package collab
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Handler serves the who is on this surface.
//
// THE CAPABILITY CHOICE. Reads are gated on candidate.read, the tier every role holds, because a viewer
// seeing that Dana is already on a candidate is exactly the collision this prevents and there is no reason to
// withhold it from them. Writes are gated on candidate.write, the tier that runs a desk, because claiming
// work you cannot do is not a thing anyone needs. Neither adds a capability to the frozen matrix: the two
// existing tiers already describe this authority correctly, and inventing work.claim would have meant
// touching the exhaustively tested matrix to say something it already says.
type Handler struct{ svc *Service }
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
func (h *Handler) Routes(r chi.Router) {
read := security.RequireCapability(security.CapCandidateRead)
write := security.RequireCapability(security.CapCandidateWrite)
r.With(read).Get("/work/mine", h.mine)
r.With(read).Get("/work/{entityType}/{entityID}", h.get)
// A touch WRITES a presence row, so it is a POST, but it is gated at the READ tier: looking at a record
// you are allowed to read is not a privileged act, and requiring the write tier would blind every viewer
// on the floor to the fact that they are looking at the same person.
r.With(read).Post("/work/{entityType}/{entityID}/touch", h.touch)
r.With(write).Post("/work/{entityType}/{entityID}/claim", h.claim)
r.With(write).Delete("/work/{entityType}/{entityID}/claim", h.release)
}
func (h *Handler) params(w http.ResponseWriter, r *http.Request) (string, uuid.UUID, bool) {
t := chi.URLParam(r, "entityType")
if !ValidEntityType(t) {
// 404 rather than 422: an unknown type is an unknown route, and this keeps the surface consistent
// with the rest of the API where a thing you may not reach is indistinguishable from absent.
writeErr(w, http.StatusNotFound, "no such record type, expected one of: "+typeList())
return "", uuid.Nil, false
}
id, err := uuid.Parse(chi.URLParam(r, "entityID"))
if err != nil {
writeErr(w, http.StatusBadRequest, "that is not a record identifier")
return "", uuid.Nil, false
}
return t, id, true
}
func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
t, id, ok := h.params(w, r)
if !ok {
return
}
a, err := h.svc.GetActivity(r.Context(), t, id)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, a)
}
func (h *Handler) touch(w http.ResponseWriter, r *http.Request) {
t, id, ok := h.params(w, r)
if !ok {
return
}
a, err := h.svc.Touch(r.Context(), t, id)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, a)
}
func (h *Handler) claim(w http.ResponseWriter, r *http.Request) {
t, id, ok := h.params(w, r)
if !ok {
return
}
var in ClaimInput
if r.Body != nil {
// An empty body is a valid claim with the defaults, so a decode failure on an absent body is not an
// error. A malformed body is.
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&in); err != nil && !errors.Is(err, http.ErrBodyReadAfterClose) {
in = ClaimInput{}
}
}
a, err := h.svc.TakeClaim(r.Context(), t, id, in, clientIP(r), r.UserAgent())
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, a)
}
func (h *Handler) release(w http.ResponseWriter, r *http.Request) {
t, id, ok := h.params(w, r)
if !ok {
return
}
a, err := h.svc.ReleaseClaim(r.Context(), t, id, clientIP(r), r.UserAgent())
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, a)
}
func (h *Handler) mine(w http.ResponseWriter, r *http.Request) {
cs, err := h.svc.MyClaims(r.Context())
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"claims": cs, "count": len(cs)})
}
func (h *Handler) fail(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrUnknownType):
writeErr(w, http.StatusNotFound, "no such record type")
case errors.Is(err, ErrNotFound):
writeErr(w, http.StatusNotFound, "no such record")
case errors.Is(err, ErrClaimedByOther):
// 409, and the message names the alternative. A recruiter who genuinely needs the record can take it
// explicitly, which is audited, rather than being stuck.
writeErr(w, http.StatusConflict,
"somebody else is already working this record, take it over explicitly if you need it")
case errors.Is(err, ErrNotYours):
writeErr(w, http.StatusForbidden, "that claim belongs to somebody else, a manager can release it")
case errors.Is(err, ErrNoDatabase):
writeErr(w, http.StatusServiceUnavailable, "unavailable")
case errors.Is(err, tenant.ErrNoTenant):
writeErr(w, http.StatusForbidden, "forbidden")
default:
writeErr(w, http.StatusInternalServerError, "could not read who is working this record")
}
}
func clientIP(r *http.Request) string { return r.Header.Get("X-Forwarded-For") }
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})
}