package api import ( "errors" "net/http" "strconv" "strings" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type AdminHandler struct { db *gorm.DB logDB *gorm.DB masterService *service.MasterService syncService *service.SyncService logPartitioner *service.LogPartitioner } func NewAdminHandler(db *gorm.DB, logDB *gorm.DB, masterService *service.MasterService, syncService *service.SyncService, partitioner *service.LogPartitioner) *AdminHandler { if logDB == nil { logDB = db } return &AdminHandler{db: db, logDB: logDB, masterService: masterService, syncService: syncService, logPartitioner: partitioner} } func (h *AdminHandler) logDBConn() *gorm.DB { if h == nil || h.logDB == nil { return h.db } return h.logDB } func (h *AdminHandler) logBaseQuery() *gorm.DB { return logBaseQuery(h.logDBConn(), h.logPartitioner) } type CreateMasterRequest struct { Name string `json:"name" binding:"required"` Group string `json:"group" binding:"required"` MaxChildKeys int `json:"max_child_keys"` GlobalQPS int `json:"global_qps"` } // CreateMaster godoc // @Summary Create a new master tenant // @Description Create a new master account (tenant) // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param master body CreateMasterRequest true "Master Info" // @Success 201 {object} gin.H // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/masters [post] func (h *AdminHandler) CreateMaster(c *gin.Context) { var req CreateMasterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Use defaults if not provided if req.MaxChildKeys == 0 { req.MaxChildKeys = 5 } if req.GlobalQPS == 0 { req.GlobalQPS = 3 } master, rawMasterKey, err := h.masterService.CreateMaster(req.Name, req.Group, req.MaxChildKeys, req.GlobalQPS) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create master key", "details": err.Error()}) return } if err := h.syncService.SyncMaster(master); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync master key", "details": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "id": master.ID, "name": master.Name, "group": master.Group, "master_key": rawMasterKey, // Only show this on creation "max_child_keys": master.MaxChildKeys, "global_qps": master.GlobalQPS, }) } type MasterView struct { ID uint `json:"id"` Name string `json:"name"` Group string `json:"group"` DefaultNamespace string `json:"default_namespace"` Namespaces string `json:"namespaces"` Epoch int64 `json:"epoch"` Status string `json:"status"` MaxChildKeys int `json:"max_child_keys"` GlobalQPS int `json:"global_qps"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } func toMasterView(m model.Master) MasterView { return MasterView{ ID: m.ID, Name: m.Name, Group: m.Group, DefaultNamespace: strings.TrimSpace(m.DefaultNamespace), Namespaces: strings.TrimSpace(m.Namespaces), Epoch: m.Epoch, Status: m.Status, MaxChildKeys: m.MaxChildKeys, GlobalQPS: m.GlobalQPS, CreatedAt: m.CreatedAt.UTC().Unix(), UpdatedAt: m.UpdatedAt.UTC().Unix(), } } // ListMasters godoc // @Summary List masters // @Description List all master tenants // @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/group" // @Success 200 {array} MasterView // @Failure 500 {object} gin.H // @Router /admin/masters [get] func (h *AdminHandler) ListMasters(c *gin.Context) { var masters []model.Master q := h.db.Model(&model.Master{}).Order("id desc") query := parseListQuery(c) q = applyListSearch(q, query.Search, "name", `"group"`) q = applyListPagination(q, query) if err := q.Find(&masters).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list masters", "details": err.Error()}) return } out := make([]MasterView, 0, len(masters)) for _, m := range masters { out = append(out, toMasterView(m)) } c.JSON(http.StatusOK, out) } // GetMaster godoc // @Summary Get master // @Description Get a master tenant by id // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Success 200 {object} MasterView // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/masters/{id} [get] func (h *AdminHandler) GetMaster(c *gin.Context) { idRaw := strings.TrimSpace(c.Param("id")) idU64, err := strconv.ParseUint(idRaw, 10, 64) if err != nil || idU64 == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid master id"}) return } var m model.Master if err := h.db.First(&m, uint(idU64)).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "master not found"}) return } c.JSON(http.StatusOK, toMasterView(m)) } type UpdateMasterRequest struct { Name *string `json:"name,omitempty"` Group *string `json:"group,omitempty"` MaxChildKeys *int `json:"max_child_keys,omitempty"` GlobalQPS *int `json:"global_qps,omitempty"` PropagateToKeys bool `json:"propagate_to_keys,omitempty"` } // UpdateMaster godoc // @Summary Update master // @Description Update master fields; optionally propagate group to existing keys // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Param request body UpdateMasterRequest true "Update payload" // @Success 200 {object} MasterView // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/masters/{id} [put] func (h *AdminHandler) UpdateMaster(c *gin.Context) { idRaw := strings.TrimSpace(c.Param("id")) idU64, err := strconv.ParseUint(idRaw, 10, 64) if err != nil || idU64 == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid master id"}) return } var req UpdateMasterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var m model.Master if err := h.db.First(&m, uint(idU64)).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "master not found"}) return } update := make(map[string]any) if req.Name != nil { update["name"] = strings.TrimSpace(*req.Name) } if req.Group != nil { g := strings.TrimSpace(*req.Group) if g != "" { update["group"] = g } } if req.MaxChildKeys != nil && *req.MaxChildKeys > 0 { update["max_child_keys"] = *req.MaxChildKeys } if req.GlobalQPS != nil && *req.GlobalQPS > 0 { update["global_qps"] = *req.GlobalQPS } if len(update) == 0 && !req.PropagateToKeys { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } if len(update) > 0 { if err := h.db.Model(&m).Updates(update).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update master", "details": err.Error()}) return } } if err := h.db.First(&m, uint(idU64)).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload master", "details": err.Error()}) return } if req.PropagateToKeys && req.Group != nil { g := strings.TrimSpace(*req.Group) if g != "" { if err := h.db.Model(&model.Key{}).Where("master_id = ?", m.ID).Updates(map[string]any{"group": g}).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to propagate group to keys", "details": err.Error()}) return } } } // Sync master metadata and (if propagated) keys. if err := h.syncService.SyncMaster(&m); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync master", "details": err.Error()}) return } if req.PropagateToKeys { var keys []model.Key if err := h.db.Where("master_id = ?", m.ID).Find(&keys).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload keys for sync", "details": err.Error()}) return } for i := range keys { if err := h.syncService.SyncKey(&keys[i]); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync key", "details": err.Error()}) return } } } c.JSON(http.StatusOK, toMasterView(m)) } type ManageMasterRequest struct { Action string `json:"action" binding:"required"` // freeze/unfreeze } // ManageMaster godoc // @Summary Manage master status // @Description Freeze or unfreeze a master tenant // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Param request body ManageMasterRequest true "Action" // @Success 200 {object} MasterView // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/masters/{id}/manage [post] func (h *AdminHandler) ManageMaster(c *gin.Context) { idRaw := strings.TrimSpace(c.Param("id")) idU64, err := strconv.ParseUint(idRaw, 10, 64) if err != nil || idU64 == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid master id"}) return } var req ManageMasterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } action := strings.ToLower(strings.TrimSpace(req.Action)) var status string switch action { case "freeze": status = "suspended" case "unfreeze": status = "active" default: c.JSON(http.StatusBadRequest, gin.H{"error": "invalid action"}) return } var m model.Master if err := h.db.First(&m, uint(idU64)).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "master not found"}) return } if err := h.db.Model(&m).Update("status", status).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update master status", "details": err.Error()}) return } if err := h.db.First(&m, uint(idU64)).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload master", "details": err.Error()}) return } if err := h.syncService.SyncMaster(&m); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync master", "details": err.Error()}) return } c.JSON(http.StatusOK, toMasterView(m)) } // DeleteMaster godoc // @Summary Delete (revoke) master // @Description Suspends a master and revokes all existing keys by bumping epoch and syncing to Redis // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Success 200 {object} gin.H // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/masters/{id} [delete] func (h *AdminHandler) DeleteMaster(c *gin.Context) { idRaw := strings.TrimSpace(c.Param("id")) idU64, err := strconv.ParseUint(idRaw, 10, 64) if err != nil || idU64 == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid master id"}) return } if err := h.revokeMasterByID(uint(idU64)); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "master not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke master", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "revoked"}) } // IssueChildKeyForMaster godoc // @Summary Issue a child key on behalf of a master // @Description Issue a new access token (child key) for a specified master. The key still belongs to the master; issuer is recorded as admin for audit. // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Param request body IssueChildKeyRequest true "Key Request" // @Success 201 {object} gin.H // @Failure 400 {object} gin.H // @Failure 403 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/masters/{id}/keys [post] func (h *AdminHandler) IssueChildKeyForMaster(c *gin.Context) { idRaw := strings.TrimSpace(c.Param("id")) if idRaw == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "master id required"}) return } idU64, err := strconv.ParseUint(idRaw, 10, 64) if err != nil || idU64 == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid master id"}) return } masterID := uint(idU64) var req IssueChildKeyRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } modelLimits := strings.TrimSpace(req.ModelLimits) modelLimitsEnabled := false if req.ModelLimitsEnabled != nil { modelLimitsEnabled = *req.ModelLimitsEnabled } else if modelLimits != "" { modelLimitsEnabled = true } key, rawChildKey, err := h.masterService.IssueChildKeyAsAdmin(masterID, service.IssueKeyOptions{ Group: strings.TrimSpace(req.Group), Scopes: strings.TrimSpace(req.Scopes), ModelLimits: modelLimits, ModelLimitsEnabled: modelLimitsEnabled, ExpiresAt: req.ExpiresAt, AllowIPs: strings.TrimSpace(req.AllowIPs), DenyIPs: strings.TrimSpace(req.DenyIPs), }) if err != nil { switch { case errors.Is(err, service.ErrMasterNotFound): c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) case errors.Is(err, service.ErrMasterNotActive): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) case errors.Is(err, service.ErrModelLimitForbidden): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) case errors.Is(err, service.ErrChildKeyGroupForbidden): c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) case errors.Is(err, service.ErrChildKeyLimitReached): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to issue child key", "details": err.Error()}) } return } if err := h.syncService.SyncKey(key); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync child key", "details": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "id": key.ID, "key_secret": rawChildKey, "group": key.Group, "scopes": key.Scopes, "issued_by": key.IssuedBy, }) }