feat(admin/master): provider+master CRUD, token mgmt, logs APIs

This commit is contained in:
zenfun
2025-12-18 16:21:46 +08:00
parent b2d2df18c5
commit a61eff27e7
12 changed files with 1374 additions and 8 deletions

View File

@@ -3,20 +3,23 @@ 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 MasterHandler struct {
db *gorm.DB
masterService *service.MasterService
syncService *service.SyncService
}
func NewMasterHandler(masterService *service.MasterService, syncService *service.SyncService) *MasterHandler {
return &MasterHandler{masterService: masterService, syncService: syncService}
func NewMasterHandler(db *gorm.DB, masterService *service.MasterService, syncService *service.SyncService) *MasterHandler {
return &MasterHandler{db: db, masterService: masterService, syncService: syncService}
}
type IssueChildKeyRequest struct {
@@ -94,3 +97,243 @@ func (h *MasterHandler) IssueChildKey(c *gin.Context) {
"scopes": key.Scopes,
})
}
// GetSelf godoc
// @Summary Get current master info
// @Description Returns master metadata for the authenticated master key
// @Tags master
// @Produce json
// @Security MasterAuth
// @Success 200 {object} MasterView
// @Failure 401 {object} gin.H
// @Router /v1/self [get]
func (h *MasterHandler) GetSelf(c *gin.Context) {
master, exists := c.Get("master")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "master key not found in context"})
return
}
m := master.(*model.Master)
c.JSON(http.StatusOK, toMasterView(*m))
}
type TokenView struct {
ID uint `json:"id"`
Group string `json:"group"`
Scopes string `json:"scopes"`
Status string `json:"status"`
IssuedBy string `json:"issued_by"`
IssuedAtEpoch int64 `json:"issued_at_epoch"`
DefaultNamespace string `json:"default_namespace"`
Namespaces string `json:"namespaces"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
func toTokenView(k model.Key) TokenView {
return TokenView{
ID: k.ID,
Group: k.Group,
Scopes: k.Scopes,
Status: k.Status,
IssuedBy: k.IssuedBy,
IssuedAtEpoch: k.IssuedAtEpoch,
DefaultNamespace: strings.TrimSpace(k.DefaultNamespace),
Namespaces: strings.TrimSpace(k.Namespaces),
CreatedAt: k.CreatedAt.UTC().Unix(),
UpdatedAt: k.UpdatedAt.UTC().Unix(),
}
}
// ListTokens godoc
// @Summary List child keys
// @Description List child keys issued under the authenticated master
// @Tags master
// @Produce json
// @Security MasterAuth
// @Success 200 {array} TokenView
// @Failure 401 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /v1/tokens [get]
func (h *MasterHandler) ListTokens(c *gin.Context) {
master, exists := c.Get("master")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "master key not found in context"})
return
}
m := master.(*model.Master)
var keys []model.Key
if err := h.db.Where("master_id = ?", m.ID).Order("id desc").Find(&keys).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list tokens", "details": err.Error()})
return
}
out := make([]TokenView, 0, len(keys))
for _, k := range keys {
out = append(out, toTokenView(k))
}
c.JSON(http.StatusOK, out)
}
// GetToken godoc
// @Summary Get child key
// @Description Get a child key by id under the authenticated master
// @Tags master
// @Produce json
// @Security MasterAuth
// @Param id path int true "Token ID"
// @Success 200 {object} TokenView
// @Failure 400 {object} gin.H
// @Failure 401 {object} gin.H
// @Failure 404 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /v1/tokens/{id} [get]
func (h *MasterHandler) GetToken(c *gin.Context) {
master, exists := c.Get("master")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "master key not found in context"})
return
}
m := master.(*model.Master)
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 token id"})
return
}
var k model.Key
if err := h.db.Where("master_id = ? AND id = ?", m.ID, uint(idU64)).First(&k).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "token not found"})
return
}
c.JSON(http.StatusOK, toTokenView(k))
}
type UpdateTokenRequest struct {
Scopes *string `json:"scopes,omitempty"`
Status *string `json:"status,omitempty"` // active/suspended
}
// UpdateToken godoc
// @Summary Update child key
// @Description Update token scopes/status under the authenticated master
// @Tags master
// @Accept json
// @Produce json
// @Security MasterAuth
// @Param id path int true "Token ID"
// @Param request body UpdateTokenRequest true "Update payload"
// @Success 200 {object} TokenView
// @Failure 400 {object} gin.H
// @Failure 401 {object} gin.H
// @Failure 404 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /v1/tokens/{id} [put]
func (h *MasterHandler) UpdateToken(c *gin.Context) {
master, exists := c.Get("master")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "master key not found in context"})
return
}
m := master.(*model.Master)
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 token id"})
return
}
var req UpdateTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
update := make(map[string]any)
if req.Scopes != nil {
update["scopes"] = strings.TrimSpace(*req.Scopes)
}
if req.Status != nil {
st := strings.ToLower(strings.TrimSpace(*req.Status))
if st != "active" && st != "suspended" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid status"})
return
}
update["status"] = st
}
if len(update) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
var k model.Key
if err := h.db.Where("master_id = ? AND id = ?", m.ID, uint(idU64)).First(&k).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "token not found"})
return
}
if err := h.db.Model(&k).Updates(update).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update token", "details": err.Error()})
return
}
if err := h.db.Where("master_id = ? AND id = ?", m.ID, uint(idU64)).First(&k).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload token", "details": err.Error()})
return
}
if err := h.syncService.SyncKey(&k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync token", "details": err.Error()})
return
}
c.JSON(http.StatusOK, toTokenView(k))
}
// DeleteToken godoc
// @Summary Delete (revoke) child key
// @Description Suspends a child key under the authenticated master
// @Tags master
// @Produce json
// @Security MasterAuth
// @Param id path int true "Token ID"
// @Success 200 {object} gin.H
// @Failure 400 {object} gin.H
// @Failure 401 {object} gin.H
// @Failure 404 {object} gin.H
// @Failure 500 {object} gin.H
// @Router /v1/tokens/{id} [delete]
func (h *MasterHandler) DeleteToken(c *gin.Context) {
master, exists := c.Get("master")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "master key not found in context"})
return
}
m := master.(*model.Master)
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 token id"})
return
}
var k model.Key
if err := h.db.Where("master_id = ? AND id = ?", m.ID, uint(idU64)).First(&k).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "token not found"})
return
}
if err := h.db.Model(&k).Update("status", "suspended").Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to revoke token", "details": err.Error()})
return
}
if err := h.db.Where("master_id = ? AND id = ?", m.ID, uint(idU64)).First(&k).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload token", "details": err.Error()})
return
}
if err := h.syncService.SyncKey(&k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync token", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "revoked"})
}