feat(api): add namespaces, batch ops, and admin logs

This commit is contained in:
zenfun
2025-12-21 23:16:27 +08:00
parent 73147fc55a
commit c2ed2f3f9e
12 changed files with 824 additions and 42 deletions

View File

@@ -0,0 +1,219 @@
package api
import (
"net/http"
"strings"
"github.com/ez-api/ez-api/internal/model"
"github.com/gin-gonic/gin"
)
type NamespaceRequest struct {
Name string `json:"name"`
Status string `json:"status"`
Description string `json:"description"`
}
// CreateNamespace godoc
// @Summary Create namespace
// @Description Create a namespace for bindings
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param namespace body NamespaceRequest true "Namespace payload"
// @Success 201 {object} model.Namespace
// @Failure 400 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /admin/namespaces [post]
func (h *Handler) CreateNamespace(c *gin.Context) {
var req NamespaceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
name := strings.TrimSpace(req.Name)
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name required"})
return
}
status := strings.TrimSpace(req.Status)
if status == "" {
status = "active"
}
ns := model.Namespace{
Name: name,
Status: status,
Description: strings.TrimSpace(req.Description),
}
if err := h.db.Create(&ns).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create namespace", "details": err.Error()})
return
}
c.JSON(http.StatusCreated, ns)
}
// ListNamespaces godoc
// @Summary List namespaces
// @Description List all namespaces
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param page query int false "page (1-based)"
// @Param limit query int false "limit (default 50, max 200)"
// @Param search query string false "search by name/description"
// @Success 200 {array} model.Namespace
// @Failure 500 {object} gin.H
// @Router /admin/namespaces [get]
func (h *Handler) ListNamespaces(c *gin.Context) {
var out []model.Namespace
q := h.db.Model(&model.Namespace{}).Order("id desc")
query := parseListQuery(c)
q = applyListSearch(q, query.Search, "name", "description")
q = applyListPagination(q, query)
if err := q.Find(&out).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list namespaces", "details": err.Error()})
return
}
c.JSON(http.StatusOK, out)
}
// GetNamespace godoc
// @Summary Get namespace
// @Description Get a namespace by id
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param id path int true "Namespace ID"
// @Success 200 {object} model.Namespace
// @Failure 400 {object} gin.H
// @Failure 404 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /admin/namespaces/{id} [get]
func (h *Handler) GetNamespace(c *gin.Context) {
id, ok := parseUintParam(c, "id")
if !ok {
return
}
var ns model.Namespace
if err := h.db.First(&ns, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
return
}
c.JSON(http.StatusOK, ns)
}
type UpdateNamespaceRequest struct {
Name *string `json:"name,omitempty"`
Status *string `json:"status,omitempty"`
Description *string `json:"description,omitempty"`
}
// UpdateNamespace godoc
// @Summary Update namespace
// @Description Update a namespace
// @Tags admin
// @Accept json
// @Produce json
// @Security AdminAuth
// @Param id path int true "Namespace ID"
// @Param namespace body UpdateNamespaceRequest true "Update payload"
// @Success 200 {object} model.Namespace
// @Failure 400 {object} gin.H
// @Failure 404 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /admin/namespaces/{id} [put]
func (h *Handler) UpdateNamespace(c *gin.Context) {
id, ok := parseUintParam(c, "id")
if !ok {
return
}
var req UpdateNamespaceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var ns model.Namespace
if err := h.db.First(&ns, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
return
}
update := map[string]any{}
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name required"})
return
}
update["name"] = name
}
if req.Status != nil {
status := strings.TrimSpace(*req.Status)
if status == "" {
status = "active"
}
update["status"] = status
}
if req.Description != nil {
update["description"] = strings.TrimSpace(*req.Description)
}
if len(update) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
if err := h.db.Model(&ns).Updates(update).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update namespace", "details": err.Error()})
return
}
if err := h.db.First(&ns, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload namespace", "details": err.Error()})
return
}
c.JSON(http.StatusOK, ns)
}
// DeleteNamespace godoc
// @Summary Delete namespace
// @Description Delete a namespace and its bindings
// @Tags admin
// @Produce json
// @Security AdminAuth
// @Param id path int true "Namespace ID"
// @Success 200 {object} gin.H
// @Failure 400 {object} gin.H
// @Failure 404 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /admin/namespaces/{id} [delete]
func (h *Handler) DeleteNamespace(c *gin.Context) {
id, ok := parseUintParam(c, "id")
if !ok {
return
}
var ns model.Namespace
if err := h.db.First(&ns, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "namespace not found"})
return
}
if err := h.db.Delete(&ns).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete namespace", "details": err.Error()})
return
}
if err := h.db.Where("namespace = ?", ns.Name).Delete(&model.Binding{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete namespace bindings", "details": err.Error()})
return
}
if err := h.sync.SyncBindings(h.db); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}