feat(key): extend key metadata and validation

This commit is contained in:
zenfun
2025-12-19 21:24:24 +08:00
parent 5e98368428
commit 524f8c5a4e
6 changed files with 351 additions and 71 deletions

View File

@@ -425,13 +425,31 @@ func (h *AdminHandler) IssueChildKeyForMaster(c *gin.Context) {
return
}
key, rawChildKey, err := h.masterService.IssueChildKeyAsAdmin(masterID, req.Group, req.Scopes)
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):

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/ez-api/ez-api/internal/model"
"github.com/ez-api/ez-api/internal/service"
@@ -23,8 +24,13 @@ func NewMasterHandler(db *gorm.DB, masterService *service.MasterService, syncSer
}
type IssueChildKeyRequest struct {
Group string `json:"group"`
Scopes string `json:"scopes"`
Group string `json:"group"`
Scopes string `json:"scopes"`
ModelLimits string `json:"model_limits,omitempty"`
ModelLimitsEnabled *bool `json:"model_limits_enabled,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AllowIPs string `json:"allow_ips,omitempty"`
DenyIPs string `json:"deny_ips,omitempty"`
}
// IssueChildKey godoc
@@ -68,13 +74,31 @@ func (h *MasterHandler) IssueChildKey(c *gin.Context) {
return
}
key, rawChildKey, err := h.masterService.IssueChildKey(masterModel.ID, group, req.Scopes)
modelLimits := strings.TrimSpace(req.ModelLimits)
modelLimitsEnabled := false
if req.ModelLimitsEnabled != nil {
modelLimitsEnabled = *req.ModelLimitsEnabled
} else if modelLimits != "" {
modelLimitsEnabled = true
}
key, rawChildKey, err := h.masterService.IssueChildKey(masterModel.ID, service.IssueKeyOptions{
Group: 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):
@@ -118,30 +142,54 @@ func (h *MasterHandler) GetSelf(c *gin.Context) {
}
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"`
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"`
ModelLimits string `json:"model_limits,omitempty"`
ModelLimitsEnabled bool `json:"model_limits_enabled"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AllowIPs string `json:"allow_ips,omitempty"`
DenyIPs string `json:"deny_ips,omitempty"`
LastAccessedAt *time.Time `json:"last_accessed_at,omitempty"`
RequestCount int64 `json:"request_count"`
UsedTokens int64 `json:"used_tokens"`
QuotaLimit int64 `json:"quota_limit"`
QuotaUsed int64 `json:"quota_used"`
QuotaResetAt *time.Time `json:"quota_reset_at,omitempty"`
QuotaResetType string `json:"quota_reset_type,omitempty"`
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(),
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),
ModelLimits: strings.TrimSpace(k.ModelLimits),
ModelLimitsEnabled: k.ModelLimitsEnabled,
ExpiresAt: k.ExpiresAt,
AllowIPs: strings.TrimSpace(k.AllowIPs),
DenyIPs: strings.TrimSpace(k.DenyIPs),
LastAccessedAt: k.LastAccessedAt,
RequestCount: k.RequestCount,
UsedTokens: k.UsedTokens,
QuotaLimit: k.QuotaLimit,
QuotaUsed: k.QuotaUsed,
QuotaResetAt: k.QuotaResetAt,
QuotaResetType: strings.TrimSpace(k.QuotaResetType),
CreatedAt: k.CreatedAt.UTC().Unix(),
UpdatedAt: k.UpdatedAt.UTC().Unix(),
}
}
@@ -212,8 +260,13 @@ func (h *MasterHandler) GetToken(c *gin.Context) {
}
type UpdateTokenRequest struct {
Scopes *string `json:"scopes,omitempty"`
Status *string `json:"status,omitempty"` // active/suspended
Scopes *string `json:"scopes,omitempty"`
Status *string `json:"status,omitempty"` // active/suspended
ModelLimits *string `json:"model_limits,omitempty"`
ModelLimitsEnabled *bool `json:"model_limits_enabled,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AllowIPs *string `json:"allow_ips,omitempty"`
DenyIPs *string `json:"deny_ips,omitempty"`
}
// UpdateToken godoc
@@ -256,6 +309,31 @@ func (h *MasterHandler) UpdateToken(c *gin.Context) {
if req.Scopes != nil {
update["scopes"] = strings.TrimSpace(*req.Scopes)
}
if req.ModelLimits != nil {
modelLimits := strings.TrimSpace(*req.ModelLimits)
if modelLimits != "" {
if err := h.masterService.ValidateModelLimits(m, modelLimits); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
update["model_limits"] = modelLimits
if req.ModelLimitsEnabled == nil {
update["model_limits_enabled"] = modelLimits != ""
}
}
if req.ModelLimitsEnabled != nil {
update["model_limits_enabled"] = *req.ModelLimitsEnabled
}
if req.ExpiresAt != nil {
update["expires_at"] = req.ExpiresAt
}
if req.AllowIPs != nil {
update["allow_ips"] = strings.TrimSpace(*req.AllowIPs)
}
if req.DenyIPs != nil {
update["deny_ips"] = strings.TrimSpace(*req.DenyIPs)
}
if req.Status != nil {
st := strings.ToLower(strings.TrimSpace(*req.Status))
if st != "active" && st != "suspended" {