package admin import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/veylant/ia-gateway/internal/apierror" "github.com/veylant/ia-gateway/internal/flags" ) // ─── Feature flags admin API (E11-07) ──────────────────────────────────────── // // Routes (mounted under /v1/admin): // GET /flags → list all flags for the tenant + global defaults // PUT /flags/{name} → upsert a flag (create or update) // DELETE /flags/{name} → delete a tenant-scoped flag // upsertFlagRequest is the JSON body for PUT /flags/{name}. type upsertFlagRequest struct { Enabled bool `json:"enabled"` } // flagNotEnabled writes a 501 if the flag store is not configured. func (h *Handler) flagNotEnabled(w http.ResponseWriter) bool { if h.flagStore == nil { apierror.WriteError(w, &apierror.APIError{ Type: "not_implemented", Message: "feature flag store not enabled", HTTPStatus: http.StatusNotImplemented, }) return true } return false } // listFlags handles GET /v1/admin/flags. // Returns all flags scoped to the caller's tenant plus global (tenant_id="") flags. func (h *Handler) listFlags(w http.ResponseWriter, r *http.Request) { if h.flagNotEnabled(w) { return } tenantID, ok := tenantFromCtx(w, r) if !ok { return } list, err := h.flagStore.List(r.Context(), tenantID) if err != nil { apierror.WriteError(w, apierror.NewUpstreamError("failed to list flags: "+err.Error())) return } if list == nil { list = []flags.FeatureFlag{} } writeJSON(w, http.StatusOK, map[string]interface{}{"data": list}) } // upsertFlag handles PUT /v1/admin/flags/{name}. // Creates or updates the flag for the caller's tenant. The flag name is taken // from the URL; global flags (tenant_id="") cannot be modified via this endpoint. func (h *Handler) upsertFlag(w http.ResponseWriter, r *http.Request) { if h.flagNotEnabled(w) { return } tenantID, ok := tenantFromCtx(w, r) if !ok { return } name := chi.URLParam(r, "name") if name == "" { apierror.WriteError(w, apierror.NewBadRequestError("flag name is required")) return } var req upsertFlagRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { apierror.WriteError(w, apierror.NewBadRequestError("invalid JSON: "+err.Error())) return } f, err := h.flagStore.Set(r.Context(), tenantID, name, req.Enabled) if err != nil { apierror.WriteError(w, apierror.NewUpstreamError("failed to set flag: "+err.Error())) return } writeJSON(w, http.StatusOK, f) } // deleteFlag handles DELETE /v1/admin/flags/{name}. // Removes the tenant-scoped flag. Returns 404 if the flag does not exist. // Global flags (tenant_id="") are not deleted by this endpoint. func (h *Handler) deleteFlag(w http.ResponseWriter, r *http.Request) { if h.flagNotEnabled(w) { return } tenantID, ok := tenantFromCtx(w, r) if !ok { return } name := chi.URLParam(r, "name") if name == "" { apierror.WriteError(w, apierror.NewBadRequestError("flag name is required")) return } err := h.flagStore.Delete(r.Context(), tenantID, name) if err == flags.ErrNotFound { apierror.WriteError(w, &apierror.APIError{ Type: "not_found_error", Message: "feature flag not found", HTTPStatus: http.StatusNotFound, }) return } if err != nil { apierror.WriteError(w, apierror.NewUpstreamError("failed to delete flag: "+err.Error())) return } w.WriteHeader(http.StatusNoContent) }