Files
license/internal/httpapi/issuance_handler.go

290 lines
7.9 KiB
Go
Raw Permalink Normal View History

2026-07-31 16:59:55 +08:00
package httpapi
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"git.apinb.com/ops/license/internal/issuance"
"git.apinb.com/ops/license/internal/subject"
"git.apinb.com/ops/license/sdk/licence"
)
const timeLayout = time.RFC3339Nano
type issuanceHandler struct {
service *issuance.Service
subjectService *subject.Service
}
type issueRequest struct {
ValidFrom string `json:"valid_from"`
ExpiresOn string `json:"expires_on"`
Quotas quotaRequest `json:"quotas"`
}
type quotaRequest struct {
MaxDatabase *int64 `json:"max_database"`
MaxMiddleware *int64 `json:"max_middleware"`
MaxNetworkDevice *int64 `json:"max_network_device"`
MaxSecurity *int64 `json:"max_security"`
MaxStorage *int64 `json:"max_storage"`
MaxPC *int64 `json:"max_pc"`
MaxServer *int64 `json:"max_server"`
MaxUser *int64 `json:"max_user"`
MaxRole *int64 `json:"max_role"`
MaxPermission *int64 `json:"max_permission"`
MaxMenu *int64 `json:"max_menu"`
}
type issuanceSummaryView struct {
ID uuid.UUID `json:"id"`
SubjectID uuid.UUID `json:"subject_id"`
PlatformName string `json:"platform_name"`
Workspace string `json:"workspace"`
IssuanceType string `json:"issuance_type"`
PreviousLicenceID *uuid.UUID `json:"previous_licence_id"`
ValidFrom string `json:"valid_from"`
ExpiresOn string `json:"expires_on"`
IssuedAt string `json:"issued_at"`
}
type issuanceDetailView struct {
issuanceSummaryView
Quotas licence.Quotas `json:"quotas"`
SigningKeyID uuid.UUID `json:"signing_key_id"`
OperatorName string `json:"operator_name"`
}
func (h issuanceHandler) issue(c *gin.Context) {
subjectID, ok := parseID(c)
if !ok {
return
}
input, ok := readIssueInput(c)
if !ok {
return
}
record, err := h.service.Issue(c.Request.Context(), subjectID, input, "system")
if err != nil {
writeError(c, err)
return
}
writeLicence(c, record.FileContent, record.ID)
}
func (h issuanceHandler) renew(c *gin.Context) {
previousID, ok := parseID(c)
if !ok {
return
}
input, ok := readIssueInput(c)
if !ok {
return
}
record, err := h.service.Renew(c.Request.Context(), previousID, input, "system")
if err != nil {
writeError(c, err)
return
}
writeLicence(c, record.FileContent, record.ID)
}
func (h issuanceHandler) list(c *gin.Context) {
page, pageSize, ok := parsePagination(c)
if !ok {
writeBadRequest(c)
return
}
filter, ok := parseIssuanceFilter(c)
if !ok {
return
}
var items []issuance.RecordSummary
var total int64
var err error
if rawSubjectID, exists := c.GetQuery("subject_id"); exists {
if rawSubjectID == "" || hasGeneralIssuanceFilter(c) {
writeBadRequest(c)
return
}
subjectID, parseErr := uuid.Parse(rawSubjectID)
if parseErr != nil || subjectID == uuid.Nil {
writeBadRequest(c)
return
}
items, total, err = h.service.ListBySubject(c.Request.Context(), subjectID, page, pageSize)
} else {
items, total, err = h.service.List(c.Request.Context(), filter, page, pageSize)
}
if err != nil {
writeError(c, err)
return
}
views := make([]issuanceSummaryView, len(items))
for index := range items {
views[index] = newIssuanceSummaryView(items[index])
}
writePage(c, views, page, pageSize, total)
}
func (h issuanceHandler) get(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
record, err := h.service.Get(c.Request.Context(), id)
if err != nil {
writeError(c, err)
return
}
item, err := h.subjectService.Get(c.Request.Context(), record.SubjectID)
if err != nil {
writeError(c, err)
return
}
summary := issuance.RecordSummary{
ID: record.ID,
SubjectID: record.SubjectID,
PlatformName: item.PlatformName,
Workspace: item.Workspace,
IssuanceType: record.IssuanceType,
PreviousLicenceID: record.PreviousLicenceID,
ValidFrom: record.ValidFrom,
ExpiresOn: record.ExpiresOn,
IssuedAt: record.IssuedAt,
}
writeData(c, http.StatusOK, issuanceDetailView{
issuanceSummaryView: newIssuanceSummaryView(summary),
Quotas: record.Quotas,
SigningKeyID: record.SigningKeyID,
OperatorName: "system",
})
}
func (h issuanceHandler) download(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
content, metadata, err := h.service.Download(c.Request.Context(), id)
if err != nil {
writeError(c, err)
return
}
writeLicence(c, content, metadata.ID)
}
func readIssueInput(c *gin.Context) (issuance.IssueInput, bool) {
var request issueRequest
if err := decodeJSON(c, &request); err != nil {
writeBadRequest(c)
return issuance.IssueInput{}, false
}
validFrom, err := licence.ParseDate(request.ValidFrom)
if err != nil {
writeError(c, issuance.ErrInvalidDates)
return issuance.IssueInput{}, false
}
expiresOn, err := licence.ParseDate(request.ExpiresOn)
if err != nil {
writeError(c, issuance.ErrInvalidDates)
return issuance.IssueInput{}, false
}
validFromTime, _ := validFrom.Time()
expiresOnTime, _ := expiresOn.Time()
quotas, ok := request.Quotas.value()
if !ok {
writeError(c, issuance.ErrInvalidInput)
return issuance.IssueInput{}, false
}
return issuance.IssueInput{ValidFrom: validFromTime, ExpiresOn: expiresOnTime, Quotas: quotas}, true
}
func (q quotaRequest) value() (licence.Quotas, bool) {
if q.MaxDatabase == nil || q.MaxMiddleware == nil || q.MaxNetworkDevice == nil || q.MaxSecurity == nil ||
q.MaxStorage == nil || q.MaxPC == nil || q.MaxServer == nil || q.MaxUser == nil || q.MaxRole == nil ||
q.MaxPermission == nil || q.MaxMenu == nil {
return licence.Quotas{}, false
}
return licence.Quotas{
MaxDatabase: *q.MaxDatabase,
MaxMiddleware: *q.MaxMiddleware,
MaxNetworkDevice: *q.MaxNetworkDevice,
MaxSecurity: *q.MaxSecurity,
MaxStorage: *q.MaxStorage,
MaxPC: *q.MaxPC,
MaxServer: *q.MaxServer,
MaxUser: *q.MaxUser,
MaxRole: *q.MaxRole,
MaxPermission: *q.MaxPermission,
MaxMenu: *q.MaxMenu,
}, true
}
func parseIssuanceFilter(c *gin.Context) (issuance.Filter, bool) {
filter := issuance.Filter{
PlatformName: c.Query("platform_name"),
Workspace: c.Query("workspace"),
}
var ok bool
if filter.ValidFrom, ok = parseOptionalDate(c, "valid_from"); !ok {
return issuance.Filter{}, false
}
if filter.ExpiresOn, ok = parseOptionalDate(c, "expires_on"); !ok {
return issuance.Filter{}, false
}
if filter.IssuedOn, ok = parseOptionalDate(c, "issued_on"); !ok {
return issuance.Filter{}, false
}
return filter, true
}
func parseOptionalDate(c *gin.Context, name string) (*time.Time, bool) {
raw, exists := c.GetQuery(name)
if !exists {
return nil, true
}
date, err := licence.ParseDate(raw)
if err != nil {
writeBadRequest(c)
return nil, false
}
value, _ := date.Time()
return &value, true
}
func hasGeneralIssuanceFilter(c *gin.Context) bool {
for _, name := range []string{"platform_name", "workspace", "valid_from", "expires_on", "issued_on"} {
if _, exists := c.GetQuery(name); exists {
return true
}
}
return false
}
func newIssuanceSummaryView(item issuance.RecordSummary) issuanceSummaryView {
return issuanceSummaryView{
ID: item.ID,
SubjectID: item.SubjectID,
PlatformName: item.PlatformName,
Workspace: item.Workspace,
IssuanceType: item.IssuanceType,
PreviousLicenceID: item.PreviousLicenceID,
ValidFrom: item.ValidFrom.Format("2006-01-02"),
ExpiresOn: item.ExpiresOn.Format("2006-01-02"),
IssuedAt: item.IssuedAt.UTC().Format(timeLayout),
}
}
func writeLicence(c *gin.Context, content []byte, id uuid.UUID) {
filename := fmt.Sprintf("%s.licence.key", id)
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
c.Header("X-Licence-ID", id.String())
c.Data(http.StatusOK, "application/json", content)
}