mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Add `POST /admin/masters/{id}/keys` allowing admins to issue child keys
on behalf of a master. Introduce an `issued_by` field in the Key model
to audit whether a key was issued by the master or an admin.
Refactor master service to use typed errors for consistent HTTP status
mapping and ensure validation logic (active status, group check) is
shared.
141 lines
4.5 KiB
Go
141 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type AdminHandler struct {
|
|
masterService *service.MasterService
|
|
syncService *service.SyncService
|
|
}
|
|
|
|
func NewAdminHandler(masterService *service.MasterService, syncService *service.SyncService) *AdminHandler {
|
|
return &AdminHandler{masterService: masterService, syncService: syncService}
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
key, rawChildKey, err := h.masterService.IssueChildKeyAsAdmin(masterID, req.Group, req.Scopes)
|
|
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.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,
|
|
})
|
|
}
|