ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/missionmatch / internal/modules/matching/handler.go

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

Source identity

FieldRecorded value
Source typeGitHub source
Original recordJanady13/missionmatch / internal/modules/matching/handler.go
DateNot specified in source
RepositoryJanady13/missionmatch
Revision720d47fae476b0d38477ae4401697d372b1ea2bc
Branchfeature/opportunity-to-outcome
Path or archive memberinternal/modules/matching/handler.go
Original SHA-25659ff914e460bf12f54a340cbbd463eb42adb7d5e1b58d6a572c3183e8a8d141e
Git blob SHAffc6befa26c8fd236ef4504dfdf8b1d8e70b6fe5
Reading-copy SHA-25659ff914e460bf12f54a340cbbd463eb42adb7d5e1b58d6a572c3183e8a8d141e

Original source ↗

Recovered source transcript

Download complete source text ↓1,160 lines · 42.3 KB
package matching import ( "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/security" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // Handler exposes the matching/slate API. Routes are mounted under a tenant-scoped group. type Handler struct{ svc *Service } // NewHandler builds the HTTP handler. func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // Routes registers EVERY matching endpoint on r at its flat path (already wrapped by the session // resolver). It is the alias surface: it keeps the pre split paths (POST /match, /slates/..., etc.) // working unchanged while the three tool routers mount the same handlers under their namespaces. It is // the sum of the four tool route groups below, so a path appears in exactly one group and the flat and // namespaced mounts share one handler. Every person scoring surface is an AEDT output and carries a // capability check, so authorization is enforced at the API from the validated session, never hidden // in the UI. Scoring and ranking need search.run; the slate write and the human decision need // slate.write; reads need candidate.read. func (h *Handler) Routes(r chi.Router) { h.ScreenRoutes(r) h.SlateRoutes(r) h.ReadRoutes(r) h.IntelRoutes(r) h.MissionRoutes(r) } // ScreenRoutes are the screenMATCH endpoints: single candidate qualification, the pool ranked against // one requirement, the Opportunity Fit for one candidate, the Mission Check resume flow, and the // candidate writes that flow saves. POST /match is the original alias of POST /screen's raw result. func (h *Handler) ScreenRoutes(r chi.Router) { r.With(security.RequireCapability(security.CapSearchRun)).Post("/match", h.postMatch) r.With(security.RequireCapability(security.CapSearchRun)).Post("/screen", h.postScreen) r.With(security.RequireCapability(security.CapSearchRun)).Get("/requirements/{requirementID}/candidates", h.getRankedCandidates) r.With(security.RequireCapability(security.CapSearchRun)).Get("/candidates/{candidateID}/opportunities", h.getOpportunities) // Mission Check (W1): scoring an in memory resume is a search.run AEDT output; the optional Save // Candidate is a recruiter write. Both ride the same session auth, RBAC, and audit as every surface. r.With(security.RequireCapability(security.CapSearchRun)).Post("/mission-check/opportunities", h.postMissionCheckOpportunities) r.With(security.RequireCapability(security.CapSearchRun)).Post("/mission-check/screen", h.postMissionCheckJD) r.With(security.RequireCapability(security.CapCandidateWrite)).Post("/candidates", h.postCreateCandidate) r.With(security.RequireCapability(security.CapCandidateWrite)).Patch("/candidates/{candidateID}/clearance-provenance", h.patchClearanceProvenance) // The access history: continuous vetting enrollment and the dates the break in access window turns on. // candidate.write, the same tier as the provenance beside it, because both change how placeable a person // is presented as being. r.With(security.RequireCapability(security.CapCandidateWrite)).Patch("/candidates/{candidateID}/access", h.patchCandidateAccess) // Assigning a candidate to a desk. candidate.write rather than a management capability, because putting // a candidate on your own desk is ordinary recruiting work, and under a narrowed record scope a // recruiter who could not assign would be unable to take responsibility for anybody they sourced. r.With(security.RequireCapability(security.CapCandidateWrite)).Put("/candidates/{candidateID}/owner", h.putCandidateOwner) } // SlateRoutes are the slateMATCH endpoints: generate a slate, the human approve or reject of a member, // and the frozen decision trace for a member. func (h *Handler) SlateRoutes(r chi.Router) { r.With(security.RequireCapability(security.CapSlateEdit)).Post("/slates/generate", h.postGenerate) r.With(security.RequireCapability(security.CapSlateEdit)).Patch("/slates/{slateID}/members/{candidateID}/decision", h.postDecision) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}/members/{candidateID}/trace", h.getTrace) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements/{requirementID}/slates", h.getSlates) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}", h.getSlateDetail) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}/proposal-readiness", h.getProposalReadiness) // The branded CSV is a bulk egress of candidate PII, so it needs export.run (owner, admin, recruiter), // not the read capability every role holds. The egress is also recorded in the audit (ExportSlate). r.With(security.RequireCapability(security.CapExportRun)).Get("/slates/{slateID}/export", h.exportSlate) } // ReadRoutes are the shared candidate and requirement pickers every tool needs to choose a target. func (h *Handler) ReadRoutes(r chi.Router) { r.With(security.RequireCapability(security.CapCandidateRead)).Get("/candidates", h.getCandidates) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/candidates/{candidateID}", h.getCandidate) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements", h.getRequirements) r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements/{requirementID}", h.getRequirement) } // IntelRoutes is the missionMATCH Candidates Intelligence View (the whole pool scored once, a // screening stage AEDT output, so it needs search.run). func (h *Handler) IntelRoutes(r chi.Router) { r.With(security.RequireCapability(security.CapSearchRun)).Get("/candidates/intelligence", h.getCandidateIntelligence) } // MissionRoutes are the missionMATCH reads that consume the stored artifacts: who can support a mission // today, read from the persisted screenMATCH evaluations (never a rescore). It surfaces frozen scores, // so it needs search.run like the other AEDT read surfaces. func (h *Handler) MissionRoutes(r chi.Router) { r.With(security.RequireCapability(security.CapSearchRun)).Get("/requirements/{requirementID}/support", h.getWhoCanSupport) } type matchRequest struct { CandidateID uuid.UUID `json:"candidate_id"` RequirementID uuid.UUID `json:"requirement_id"` } func (h *Handler) postMatch(w http.ResponseWriter, r *http.Request) { var req matchRequest if !decodeJSON(w, r, &req) { return } if req.CandidateID == uuid.Nil || req.RequirementID == uuid.Nil { writeError(w, http.StatusBadRequest, "candidate_id and requirement_id are required") return } res, err := h.svc.ScreenCandidate(r.Context(), req.CandidateID, req.RequirementID) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, matchResultView(res)) } func (h *Handler) postScreen(w http.ResponseWriter, r *http.Request) { var req matchRequest if !decodeJSON(w, r, &req) { return } if req.CandidateID == uuid.Nil || req.RequirementID == uuid.Nil { writeError(w, http.StatusBadRequest, "candidate_id and requirement_id are required") return } view, err := h.svc.Screen(r.Context(), req.CandidateID, req.RequirementID) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, view) } func (h *Handler) getRankedCandidates(w http.ResponseWriter, r *http.Request) { reqID, err := uuid.Parse(chi.URLParam(r, "requirementID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid requirement id") return } out, err := h.svc.RankCandidates(r.Context(), reqID) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, out) } func (h *Handler) getOpportunities(w http.ResponseWriter, r *http.Request) { candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid candidate id") return } limit := 10 if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 100 { limit = v } fits, err := h.svc.OpportunitiesForCandidate(r.Context(), candidateID, limit) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, fits) } type generateRequest struct { RequirementID uuid.UUID `json:"requirement_id"` Name string `json:"name"` MaxCandidates int32 `json:"max_candidates"` } func (h *Handler) postGenerate(w http.ResponseWriter, r *http.Request) { var req generateRequest if !decodeJSON(w, r, &req) { return } if req.RequirementID == uuid.Nil { writeError(w, http.StatusBadRequest, "requirement_id is required") return } out, err := h.svc.GenerateSlate(r.Context(), GenerateSlateInput{ RequirementID: req.RequirementID, Name: req.Name, MaxCandidates: req.MaxCandidates, CreatedBy: principalID(r), }) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusCreated, out) } type decisionRequest struct { Decision string `json:"decision"` // approved or rejected Note string `json:"note,omitempty"` // optional reviewer note } func (h *Handler) postDecision(w http.ResponseWriter, r *http.Request) { slateID, err := uuid.Parse(chi.URLParam(r, "slateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid slate id") return } candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid candidate id") return } var req decisionRequest if !decodeJSON(w, r, &req) { return } if err := h.svc.DecideSlateMember(r.Context(), slateID, candidateID, req.Decision, req.Note); err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]string{"status": req.Decision}) } func (h *Handler) getTrace(w http.ResponseWriter, r *http.Request) { slateID, err := uuid.Parse(chi.URLParam(r, "slateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid slate id") return } candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid candidate id") return } m, err := h.svc.SlateMemberTrace(r.Context(), slateID, candidateID) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, slateMemberTraceView(m)) } func (h *Handler) getSlates(w http.ResponseWriter, r *http.Request) { reqID, err := uuid.Parse(chi.URLParam(r, "requirementID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid requirement id") return } out, err := h.svc.ListSlates(r.Context(), reqID) if err != nil { writeServiceError(w, err) return } if out == nil { out = []SlateSummaryView{} } writeJSON(w, http.StatusOK, out) } func (h *Handler) getSlateDetail(w http.ResponseWriter, r *http.Request) { slateID, err := uuid.Parse(chi.URLParam(r, "slateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid slate id") return } out, err := h.svc.SlateDetail(r.Context(), slateID) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, out) } func (h *Handler) getProposalReadiness(w http.ResponseWriter, r *http.Request) { slateID, err := uuid.Parse(chi.URLParam(r, "slateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid slate id") return } out, err := h.svc.ProposalReadiness(r.Context(), slateID) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, out) } func (h *Handler) exportSlate(w http.ResponseWriter, r *http.Request) { slateID, err := uuid.Parse(chi.URLParam(r, "slateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid slate id") return } csvText, err := h.svc.ExportSlate(r.Context(), slateID) if err != nil { writeServiceError(w, err) return } w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", "attachment; filename=\"slate-"+slateID.String()+".csv\"") // This file carries candidate PII. no-store keeps it out of shared caches and browser history, and // nosniff stops a browser reinterpreting it as something executable. The recruiting exporter already // sets both; this one did not. w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(csvText)) } func (h *Handler) getWhoCanSupport(w http.ResponseWriter, r *http.Request) { reqID, err := uuid.Parse(chi.URLParam(r, "requirementID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid requirement id") return } out, err := h.svc.WhoCanSupport(r.Context(), reqID) if err != nil { writeServiceError(w, err) return } if out == nil { out = []SupportView{} } writeJSON(w, http.StatusOK, out) } // principalID returns the validated session user id for the actor on a decision or generation. func principalID(r *http.Request) *uuid.UUID { if p, ok := tenant.UserFromContext(r.Context()); ok { if id, err := uuid.Parse(p.UserID); err == nil { return &id } } return nil } func (h *Handler) getCandidateIntelligence(w http.ResponseWriter, r *http.Request) { limit := 0 if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil { limit = v } out, err := h.svc.CandidateIntelligence(r.Context(), limit) if err != nil { writeServiceError(w, err) return } if out == nil { out = []CandidateIntelligenceView{} } writeJSON(w, http.StatusOK, out) } func (h *Handler) getCandidates(w http.ResponseWriter, r *http.Request) { limit, offset := pageParams(r) rows, err := h.svc.ListCandidates(r.Context(), limit, offset) if err != nil { writeServiceError(w, err) return } out := make([]CandidateListView, len(rows)) for i, c := range rows { out[i] = candidateListView(c) } writeJSON(w, http.StatusOK, out) } func (h *Handler) getRequirements(w http.ResponseWriter, r *http.Request) { limit, offset := pageParams(r) rows, err := h.svc.ListRequirements(r.Context(), limit, offset) if err != nil { writeServiceError(w, err) return } out := make([]RequirementListView, len(rows)) for i, rq := range rows { out[i] = requirementListView(rq) } writeJSON(w, http.StatusOK, out) } func (h *Handler) getCandidate(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid candidate id") return } c, err := h.svc.GetCandidate(r.Context(), id) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, candidateListView(c)) } func (h *Handler) getRequirement(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "requirementID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid requirement id") return } rq, err := h.svc.GetRequirement(r.Context(), id) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, requirementListView(rq)) } // ---------- Mission Check (W1) ---------- type missionCheckOpportunitiesRequest struct { ResumeText string `json:"resume_text"` Limit int `json:"limit"` } func (h *Handler) postMissionCheckOpportunities(w http.ResponseWriter, r *http.Request) { var req missionCheckOpportunitiesRequest if !decodeJSON(w, r, &req) { return } if strings.TrimSpace(req.ResumeText) == "" { writeError(w, http.StatusBadRequest, "resume_text is required") return } out, err := h.svc.MissionCheckOpportunities(r.Context(), req.ResumeText, req.Limit) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, out) } type missionCheckJDRequest struct { ResumeText string `json:"resume_text"` JobDescriptionText string `json:"job_description_text"` } func (h *Handler) postMissionCheckJD(w http.ResponseWriter, r *http.Request) { var req missionCheckJDRequest if !decodeJSON(w, r, &req) { return } if strings.TrimSpace(req.ResumeText) == "" || strings.TrimSpace(req.JobDescriptionText) == "" { writeError(w, http.StatusBadRequest, "resume_text and job_description_text are required") return } out, err := h.svc.MissionCheckJD(r.Context(), req.ResumeText, req.JobDescriptionText) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, out) } type createCandidateRequest struct { FullName string `json:"full_name"` // CONTACT DETAILS. The candidates table has carried email and headline since migration 0002 and the // insert has always written them, but this request struct never exposed either, so a candidate created // through the API could not be emailed. That went unnoticed until outbound correspondence was built and // tested against a real record. Email string `json:"email"` Headline string `json:"headline"` ClearanceLevel string `json:"clearance_level"` ClearancePoly string `json:"clearance_poly"` ClearanceStatus string `json:"clearance_status"` YearsExperience int `json:"years_experience"` Location string `json:"location"` Remote bool `json:"remote"` Relocatable bool `json:"relocatable"` Available bool `json:"available"` Skills []string `json:"skills"` Certifications []string `json:"certifications"` CustomerExperience []string `json:"customer_experience"` } func (h *Handler) postCreateCandidate(w http.ResponseWriter, r *http.Request) { var req createCandidateRequest if !decodeJSON(w, r, &req) { return } if strings.TrimSpace(req.FullName) == "" { writeError(w, http.StatusBadRequest, "full_name is required") return } id, err := h.svc.SaveCandidate(r.Context(), SaveCandidateInput(req)) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{"id": id.String()}) } type setProvenanceRequest struct { Provenance string `json:"provenance"` // self_reported | sponsor_verified | document | continuous_vetting VerifiedAt *string `json:"verified_at,omitempty"` // optional YYYY-MM-DD, the FSO verification date } func (h *Handler) patchClearanceProvenance(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeError(w, http.StatusBadRequest, "invalid candidate id") return } var req setProvenanceRequest if !decodeJSON(w, r, &req) { return } var va *time.Time if req.VerifiedAt != nil && *req.VerifiedAt != "" { t, perr := time.Parse("2006-01-02", *req.VerifiedAt) if perr != nil { writeError(w, http.StatusBadRequest, "verified_at must be YYYY-MM-DD") return } va = &t } if err := h.svc.SetClearanceProvenance(r.Context(), id, req.Provenance, va); err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } // ---------- helpers ---------- func pageParams(r *http.Request) (limit, offset int32) { limit, offset = 50, 0 if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 500 { limit = int32(v) } if v, err := strconv.Atoi(r.URL.Query().Get("offset")); err == nil && v >= 0 { offset = int32(v) } return limit, offset } func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool { dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) dec.DisallowUnknownFields() if err := dec.Decode(dst); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body: "+err.Error()) return false } return true } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } // writeServiceError maps service/tenant errors to HTTP status without leaking internals. func writeServiceError(w http.ResponseWriter, err error) { switch { 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, ErrBadRequest): writeError(w, http.StatusBadRequest, "bad request") case errors.Is(err, ErrNoDatabase): writeError(w, http.StatusServiceUnavailable, "database not configured") case errors.Is(err, ErrNoExtractor): writeError(w, http.StatusServiceUnavailable, "mission check extraction not configured") case errors.Is(err, ErrNoticeRequired): // 422 and the sentence, not 500 and "internal error". A recruiter meeting this refusal needs to know // it is a rule their organization switched on and exactly what closes it, or the product looks broken // at the precise moment it is behaving correctly. writeError(w, http.StatusUnprocessableEntity, ErrNoticeRequired.Error()) default: writeError(w, http.StatusInternalServerError, "internal error") } } // SlateMemberTraceView is the audit and candidate facing explanation of a frozen decision: the per // component trace, the human rationale, the gap summary, the decision, and the model version that // produced it. The trace and gap summary are passed through as the stored JSON. type SlateMemberTraceView struct { CandidateID string `json:"candidate_id"` TotalScore string `json:"total_score"` Band string `json:"band"` Explanation string `json:"explanation"` Trace json.RawMessage `json:"trace"` GapSummary json.RawMessage `json:"gap_summary"` Decision string `json:"decision"` ModelVersion string `json:"model_version"` } func slateMemberTraceView(m dbgen.SlateMember) SlateMemberTraceView { v := SlateMemberTraceView{ CandidateID: m.CandidateID.String(), TotalScore: m.TotalScore.StringFixed(2), Band: m.Band, Explanation: m.Explanation, Trace: json.RawMessage(m.Trace), GapSummary: json.RawMessage(m.GapSummary), Decision: m.Decision, } if m.ModelVersion != nil { v.ModelVersion = *m.ModelVersion } return v } // putCandidateOwner assigns a candidate to a person's desk. A null or absent owner returns them to the shared // pool, which is deliberate rather than an error: an unowned candidate is visible to everybody by default, so // releasing one is how a departing recruiter's bench stays workable instead of becoming invisible. func (h *Handler) putCandidateOwner(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a candidate identifier"}) return } var in struct { OwnerUserID *string `json:"owner_user_id"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<10)).Decode(&in); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid body"}) return } var owner *uuid.UUID if in.OwnerUserID != nil && strings.TrimSpace(*in.OwnerUserID) != "" { parsed, perr := uuid.Parse(strings.TrimSpace(*in.OwnerUserID)) if perr != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "that is not a user identifier", "field": "owner_user_id"}) return } owner = &parsed } if err := h.svc.SetCandidateOwner(r.Context(), id, owner, r.Header.Get("X-Forwarded-For"), r.UserAgent()); err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"candidate_id": id.String(), "owner_user_id": in.OwnerUserID}) } // CandidateDocumentRoutes serves the file on a person. // // UPLOAD IS candidate.write and READ IS candidate.read, the same tiers as the record the file belongs to. // A separate capability would have meant a recruiter who can edit a candidate but cannot attach their resume, // which is not a distinction anybody in this industry makes. func (h *Handler) CandidateDocumentRoutes(r chi.Router) { read := security.RequireCapability(security.CapCandidateRead) write := security.RequireCapability(security.CapCandidateWrite) r.With(read).Get("/candidates/{candidateID}/documents", h.listCandidateDocuments) r.With(write).Post("/candidates/{candidateID}/documents", h.uploadCandidateDocument) r.With(read).Get("/candidate-documents/{documentID}/content", h.downloadCandidateDocument) // There is no DELETE. A document is SUPERSEDED, because what was sent to a client six months ago has to // stay recoverable, and a record that can be deleted is a record that cannot be audited. r.With(write).Post("/candidate-documents/{documentID}/supersede", h.supersedeCandidateDocument) } func (h *Handler) listCandidateDocuments(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a candidate identifier"}) return } docs, err := h.svc.ListCandidateDocuments(r.Context(), id) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"documents": docs, "count": len(docs)}) } // uploadCandidateDocument accepts a multipart form. The FILE is the payload; document_type is a form field. // Multipart rather than JSON with base64, because a resume is a file and making a recruiter's browser encode // one costs a third of the bytes for nothing. func (h *Handler) uploadCandidateDocument(w http.ResponseWriter, r *http.Request) { candID := chi.URLParam(r, "candidateID") if _, err := uuid.Parse(candID); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a candidate identifier"}) return } // The body is bounded BEFORE it is parsed, so an oversized upload is refused at the reader rather than // after it has been buffered. r.Body = http.MaxBytesReader(w, r.Body, MaxCandidateDocBytes+(1<<20)) if err := r.ParseMultipartForm(8 << 20); err != nil { writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "the upload is too large or is not a valid form"}) return } file, hdr, err := r.FormFile("file") if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "attach the file as the form field named file", "field": "file"}) return } defer file.Close() body, err := io.ReadAll(io.LimitReader(file, MaxCandidateDocBytes+1)) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not read the upload"}) return } doc, err := h.svc.UploadCandidateDocument(r.Context(), CandidateDocInput{ CandidateID: candID, DocumentType: r.FormValue("document_type"), FileName: hdr.Filename, MediaType: hdr.Header.Get("Content-Type"), Bytes: body, }, r.Header.Get("X-Forwarded-For"), r.UserAgent()) if err != nil { writeDocError(w, err) return } writeJSON(w, http.StatusCreated, doc) } // downloadCandidateDocument serves the bytes. EVERY HEADER HERE IS LOAD BEARING. // // The Content-Type is the type the SERVER recorded at upload after checking it against a closed allowlist, // never anything the request supplied. nosniff stops a browser second guessing that. The Content-Disposition // is ATTACHMENT, so a document is downloaded rather than rendered in the origin, which is what keeps a // malicious file from executing as the application. The restrictive policy means that even if a format // slipped through, the response could not load a script or a remote resource. Cache-Control is private // because this is a named person's file and must never land in a shared cache. func (h *Handler) downloadCandidateDocument(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "documentID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a document identifier"}) return } b, meta, err := h.svc.DownloadCandidateDocument(r.Context(), id) if err != nil { writeDocError(w, err) return } w.Header().Set("Content-Type", meta.MediaType) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Security-Policy", "default-src 'none'; sandbox") w.Header().Set("Cache-Control", "private, no-store") w.Header().Set("Content-Disposition", `attachment; filename="`+meta.FileName+`"`) w.WriteHeader(http.StatusOK) _, _ = w.Write(b) } func (h *Handler) supersedeCandidateDocument(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "documentID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a document identifier"}) return } if err := h.svc.SupersedeCandidateDocument(r.Context(), id, r.Header.Get("X-Forwarded-For"), r.UserAgent()); err != nil { writeDocError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"superseded": true}) } func writeDocError(w http.ResponseWriter, err error) { switch { case errors.Is(err, ErrNoStorage): writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "document storage is not configured on this node"}) case errors.Is(err, ErrCandidateUnknown): writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such candidate"}) case errors.Is(err, ErrDocNotFound): writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such document"}) case errors.Is(err, ErrDocDuplicate): writeJSON(w, http.StatusConflict, map[string]string{"error": "that exact file is already on this candidate"}) case errors.Is(err, ErrDocEmpty): writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "the file is empty", "field": "file"}) case errors.Is(err, ErrDocTooLarge): writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": err.Error()}) case errors.Is(err, ErrDocMediaType): writeJSON(w, http.StatusUnsupportedMediaType, map[string]string{"error": "that file type is not accepted, send a PDF, Word document, RTF, text or image"}) case errors.Is(err, ErrDocTypeInvalid): writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error(), "field": "document_type"}) default: writeServiceError(w, err) } } // SearchAndListRoutes serves the recruiter's working surface: search, saved searches, lists, and the bulk // actions that move people between them. // // SEARCH IS search.run, the capability that already gated match and screen, because a filtered search over // the bench is the same class of act. LISTS AND BULK ARE candidate.write, because putting somebody on a // shortlist or on a desk changes the record. func (h *Handler) SearchAndListRoutes(r chi.Router) { read := security.RequireCapability(security.CapCandidateRead) write := security.RequireCapability(security.CapCandidateWrite) search := security.RequireCapability(security.CapSearchRun) r.With(search).Post("/candidates/search", h.postCandidateSearch) r.With(search).Get("/saved-searches", h.getSavedSearches) r.With(search).Post("/saved-searches", h.postSavedSearch) r.With(search).Post("/saved-searches/{searchID}/run", h.runSavedSearch) r.With(search).Delete("/saved-searches/{searchID}", h.deleteSavedSearch) r.With(read).Get("/candidate-lists", h.getLists) r.With(read).Get("/candidates/{candidateID}/lists", h.getListsForCandidate) r.With(write).Post("/candidate-lists", h.postList) r.With(write).Post("/candidate-lists/{listID}/members", h.postListMembers) r.With(write).Delete("/candidate-lists/{listID}/members", h.deleteListMembers) r.With(write).Post("/candidates/bulk/owner", h.postBulkOwner) } func (h *Handler) postCandidateSearch(w http.ResponseWriter, r *http.Request) { var q CandidateQuery if !decodeJSON(w, r, &q) { return } res, err := h.svc.SearchCandidates(r.Context(), q) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, res) } func (h *Handler) getSavedSearches(w http.ResponseWriter, r *http.Request) { out, err := h.svc.ListSavedSearches(r.Context()) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"saved_searches": out, "count": len(out)}) } func (h *Handler) postSavedSearch(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Visibility string `json:"visibility"` Pinned bool `json:"pinned"` Criteria CandidateQuery `json:"criteria"` } if !decodeJSON(w, r, &req) { return } if strings.TrimSpace(req.Name) == "" || len(req.Name) > 120 { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "a saved search needs a name of up to 120 characters", "field": "name"}) return } if req.Visibility == "" { req.Visibility = "private" } if req.Visibility != "private" && req.Visibility != "shared" { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "a saved search is private or shared", "field": "visibility"}) return } id, err := h.svc.SaveSearch(r.Context(), req.Name, req.Visibility, req.Criteria, req.Pinned) if err != nil { if strings.Contains(err.Error(), "duplicate key") { writeJSON(w, http.StatusConflict, map[string]string{"error": "you already have a saved search with that name"}) return } writeServiceError(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{"id": id}) } func (h *Handler) runSavedSearch(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "searchID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a search identifier"}) return } limit, offset := pageParams(r) res, err := h.svc.RunSavedSearch(r.Context(), id, int(limit), int(offset)) if err != nil { if errors.Is(err, ErrSearchUnknown) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such saved search"}) return } writeServiceError(w, err) return } writeJSON(w, http.StatusOK, res) } func (h *Handler) deleteSavedSearch(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "searchID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a search identifier"}) return } if err := h.svc.DeleteSavedSearch(r.Context(), id); err != nil { if errors.Is(err, ErrSearchUnknown) { // 404 whether it does not exist or belongs to somebody else. A distinguishable answer would let // one person enumerate another's private searches by name. writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such saved search"}) return } writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"deleted": true}) } func (h *Handler) getLists(w http.ResponseWriter, r *http.Request) { out, err := h.svc.ListLists(r.Context(), r.URL.Query().Get("include_archived") == "true") if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"lists": out, "count": len(out)}) } func (h *Handler) getListsForCandidate(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "candidateID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a candidate identifier"}) return } out, err := h.svc.ListsForCandidate(r.Context(), id) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"lists": out, "count": len(out)}) } func (h *Handler) postList(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Description string `json:"description"` Purpose string `json:"purpose"` Visibility string `json:"visibility"` } if !decodeJSON(w, r, &req) { return } if strings.TrimSpace(req.Name) == "" || len(req.Name) > 120 { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "a list needs a name of up to 120 characters", "field": "name"}) return } if req.Purpose == "" { req.Purpose = "shortlist" } if !validListPurpose[req.Purpose] { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "unknown list purpose", "field": "purpose"}) return } if req.Visibility == "" { req.Visibility = "shared" } if req.Visibility != "private" && req.Visibility != "shared" { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "a list is private or shared", "field": "visibility"}) return } id, err := h.svc.CreateList(r.Context(), req.Name, req.Description, req.Purpose, req.Visibility) if err != nil { if strings.Contains(err.Error(), "duplicate key") { writeJSON(w, http.StatusConflict, map[string]string{"error": "a list with that name already exists"}) return } writeServiceError(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{"id": id}) } var validListPurpose = map[string]bool{ "shortlist": true, "bench": true, "pipeline": true, "do_not_contact": true, "other": true, } func (h *Handler) postListMembers(w http.ResponseWriter, r *http.Request) { listID, ids, note, ok := h.bulkParams(w, r) if !ok { return } res, err := h.svc.AddToList(r.Context(), listID, ids, note, r.Header.Get("X-Forwarded-For"), r.UserAgent()) if err != nil { writeBulkError(w, err) return } writeJSON(w, http.StatusOK, res) } func (h *Handler) deleteListMembers(w http.ResponseWriter, r *http.Request) { listID, ids, _, ok := h.bulkParams(w, r) if !ok { return } res, err := h.svc.RemoveFromList(r.Context(), listID, ids, r.Header.Get("X-Forwarded-For"), r.UserAgent()) if err != nil { writeBulkError(w, err) return } writeJSON(w, http.StatusOK, res) } func (h *Handler) postBulkOwner(w http.ResponseWriter, r *http.Request) { var req struct { CandidateIDs []string `json:"candidate_ids"` OwnerUserID *string `json:"owner_user_id"` } if !decodeJSON(w, r, &req) { return } ids, ok := parseIDList(w, req.CandidateIDs) if !ok { return } var owner *uuid.UUID if req.OwnerUserID != nil && strings.TrimSpace(*req.OwnerUserID) != "" { parsed, perr := uuid.Parse(strings.TrimSpace(*req.OwnerUserID)) if perr != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "that is not a user identifier", "field": "owner_user_id"}) return } owner = &parsed } res, err := h.svc.BulkAssignOwner(r.Context(), ids, owner, r.Header.Get("X-Forwarded-For"), r.UserAgent()) if err != nil { writeBulkError(w, err) return } writeJSON(w, http.StatusOK, res) } func (h *Handler) bulkParams(w http.ResponseWriter, r *http.Request) (uuid.UUID, []uuid.UUID, string, bool) { listID, err := uuid.Parse(chi.URLParam(r, "listID")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "that is not a list identifier"}) return uuid.Nil, nil, "", false } var req struct { CandidateIDs []string `json:"candidate_ids"` Note string `json:"note"` } if !decodeJSON(w, r, &req) { return uuid.Nil, nil, "", false } ids, ok := parseIDList(w, req.CandidateIDs) if !ok { return uuid.Nil, nil, "", false } return listID, ids, req.Note, true } // parseIDList refuses the WHOLE batch if any identifier is malformed, rather than silently dropping the bad // one. A bulk action that quietly did thirty nine of forty is the kind of thing nobody notices until the // fortieth person never gets called. func parseIDList(w http.ResponseWriter, in []string) ([]uuid.UUID, bool) { if len(in) == 0 { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "name at least one candidate", "field": "candidate_ids"}) return nil, false } if len(in) > MaxBulkIDs { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{ "error": fmt.Sprintf("at most %d records in one action, this asked for %d", MaxBulkIDs, len(in)), "field": "candidate_ids"}) return nil, false } out := make([]uuid.UUID, 0, len(in)) for _, s := range in { id, err := uuid.Parse(strings.TrimSpace(s)) if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "one of the identifiers is not valid, nothing was changed", "field": "candidate_ids"}) return nil, false } out = append(out, id) } return out, true } func writeBulkError(w http.ResponseWriter, err error) { switch { case errors.Is(err, ErrListUnknown): writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()}) case errors.Is(err, ErrTooManyIDs): writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) default: writeServiceError(w, err) } } // MergeRoutes serve duplicate detection and merging. // // FINDING duplicates is candidate.read: it is a report. MERGING is org.settings.write, the owner tier, // deliberately higher than ordinary candidate.write. A merge is not reversible by the person who did it, it // rewrites who owns whose history, and on a shared bench a recruiter merging two people they believe are the // same is exactly the mistake nobody notices until a client asks why a submission disappeared. func (h *Handler) MergeRoutes(r chi.Router) { read := security.RequireCapability(security.CapCandidateRead) merge := security.RequireCapability(security.CapOrgSettings) r.With(read).Get("/candidates/duplicates", h.findDuplicates) r.With(merge).Post("/candidates/merge", h.mergeCandidates) } func (h *Handler) findDuplicates(w http.ResponseWriter, r *http.Request) { limit, _ := pageParams(r) out, err := h.svc.FindDuplicates(r.Context(), int(limit)) if err != nil { writeServiceError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{ "duplicates": out, "count": len(out), "note": "these are SUGGESTIONS. A strong match is the same email address; a weak one is the same " + "name and location, which two different people genuinely share. A human confirms every merge.", }) } func (h *Handler) mergeCandidates(w http.ResponseWriter, r *http.Request) { var req struct { KeepID string `json:"keep_id"` MergeID string `json:"merge_id"` Reason string `json:"reason"` } if !decodeJSON(w, r, &req) { return } keep, err := uuid.Parse(strings.TrimSpace(req.KeepID)) if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "keep_id must be a candidate identifier", "field": "keep_id"}) return } merge, err := uuid.Parse(strings.TrimSpace(req.MergeID)) if err != nil { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "merge_id must be a candidate identifier", "field": "merge_id"}) return } res, err := h.svc.MergeCandidates(r.Context(), keep, merge, req.Reason, r.Header.Get("X-Forwarded-For"), r.UserAgent()) if err != nil { switch { case errors.Is(err, ErrMergeSame): writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) case errors.Is(err, ErrMergeUnknown): writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such candidate"}) case errors.Is(err, ErrAlreadyMerged): writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()}) default: writeServiceError(w, err) } return } writeJSON(w, http.StatusOK, res) }

Search equations, builds, manuscript text, and recovered sources.