123 lines
3.5 KiB
Go
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)
|
|
}
|