veylant/internal/admin/flags.go
2026-02-23 13:35:04 +01:00

123 lines
3.5 KiB
Go

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)
}