mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
492 lines
16 KiB
Go
492 lines
16 KiB
Go
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
|
|
// @Success 200 {array} MasterView
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/masters [get]
|
|
func (h *AdminHandler) ListMasters(c *gin.Context) {
|
|
var masters []model.Master
|
|
if err := h.db.Order("id desc").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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
nextEpoch := m.Epoch + 1
|
|
if nextEpoch <= 0 {
|
|
nextEpoch = 1
|
|
}
|
|
if err := h.db.Model(&m).Updates(map[string]any{
|
|
"status": "suspended",
|
|
"epoch": nextEpoch,
|
|
}).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke 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
|
|
}
|
|
|
|
// Revoke all child keys too (defense-in-depth; master status already blocks in DP).
|
|
if err := h.db.Model(&model.Key{}).Where("master_id = ?", m.ID).Update("status", "suspended").Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke child keys", "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
|
|
}
|
|
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, 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,
|
|
})
|
|
}
|