mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(admin/master): provider+master CRUD, token mgmt, logs APIs
This commit is contained in:
@@ -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"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user