package oppintel import ( "encoding/json" "errors" "net/http" "strconv" "strings" "github.com/go-chi/chi/v5" "github.com/missionworkforce/missionmatch/internal/platform/security" ) // Handler serves the read API over the unified opportunity records catalog. Every route is gated by a // capability (search.run, held by all roles) so a new endpoint never ships ungated. type Handler struct{ svc *Service } // NewHandler builds the handler. func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // Routes mounts the opportunity intelligence endpoints under the /api group. The caller has already // applied the session and CSRF middleware; this adds the per route capability gate. func (h *Handler) Routes(r chi.Router) { r.With(security.RequireCapability(security.CapSearchRun)).Get("/opportunity-intelligence", h.list) } func (h *Handler) list(w http.ResponseWriter, r *http.Request) { f := Filters{ Kinds: multiVals(r, "kind"), NAICS: multiVals(r, "naics"), Agencies: multiVals(r, "agency"), States: multiVals(r, "state"), Limit: intParam(r, "limit", 0), } items, err := h.svc.List(r.Context(), f) if err != nil { if errors.Is(err, ErrNoDatabase) { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "database not configured"}) return } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"}) return } writeJSON(w, http.StatusOK, items) } // multiVals collects a filter that may be repeated (?kind=a&kind=b) or comma listed (?kind=a,b). func multiVals(r *http.Request, key string) []string { var out []string for _, v := range r.URL.Query()[key] { for _, p := range strings.Split(v, ",") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } } return out } func intParam(r *http.Request, key string, def int) int { if v := strings.TrimSpace(r.URL.Query().Get(key)); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }