Files
ai_site/platform/internal/handler/license.go
2026-07-31 10:19:22 +08:00

197 lines
5.8 KiB
Go

package handler
import (
"crypto/subtle"
"encoding/json"
"io"
"net/http"
"strings"
"time"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/license"
"aijianzhan/platform/internal/svc"
"github.com/zeromicro/go-zero/rest/httpx"
)
func requireLicenseSecret(svcCtx *svc.ServiceContext, r *http.Request) error {
want := strings.TrimSpace(svcCtx.Config.License.ControlSecret)
if want == "" {
want = strings.TrimSpace(svcCtx.Config.Auth.IssueSecret)
}
if want == "" {
return errMsg("未配置 License.ControlSecret")
}
got := strings.TrimSpace(r.Header.Get("X-License-Secret"))
if got == "" {
got = strings.TrimSpace(r.URL.Query().Get("secret"))
}
if subtle.ConstantTimeCompare([]byte(got), []byte(want)) != 1 {
return errMsg("invalid license control secret")
}
return nil
}
type errMsg string
func (e errMsg) Error() string { return string(e) }
func licenseStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if svcCtx.License == nil || !svcCtx.License.Enabled() {
httpx.OkJson(w, map[string]any{"enabled": false, "message": "本实例未启用授权租约"})
return
}
httpx.OkJson(w, svcCtx.License.Status(time.Now()))
}
}
func licenseRenewHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := requireLicenseSecret(svcCtx, r); err != nil {
authx.WriteError(w, http.StatusUnauthorized, err.Error())
return
}
if svcCtx.License == nil || !svcCtx.License.Enabled() {
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
return
}
var body struct {
NotAfter string `json:"not_after"`
Note string `json:"note"`
By string `json:"by"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
na, err := license.ParseNotAfter(body.NotAfter)
if err != nil || na.IsZero() {
authx.WriteError(w, http.StatusBadRequest, "not_after 无效")
return
}
by := strings.TrimSpace(body.By)
if by == "" {
by = "remote"
}
lease, err := svcCtx.License.Renew(na, by, body.Note)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
}
}
func licenseExtendHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := requireLicenseSecret(svcCtx, r); err != nil {
authx.WriteError(w, http.StatusUnauthorized, err.Error())
return
}
if svcCtx.License == nil || !svcCtx.License.Enabled() {
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
return
}
var body struct {
Days int `json:"days"`
Note string `json:"note"`
By string `json:"by"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
if body.Days <= 0 {
body.Days = license.DefaultExtensionMaxDays
}
by := strings.TrimSpace(body.By)
if by == "" {
by = "remote"
}
lease, err := svcCtx.License.Extend(body.Days, by, body.Note)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
}
}
func licensePutLeaseHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := requireLicenseSecret(svcCtx, r); err != nil {
authx.WriteError(w, http.StatusUnauthorized, err.Error())
return
}
if svcCtx.License == nil || !svcCtx.License.Enabled() {
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
return
}
var body struct {
Customer string `json:"customer"`
NotAfter string `json:"not_after"`
ExtensionsUsed int `json:"extensions_used"`
ExtensionsMax int `json:"extensions_max"`
ExtensionMaxDays int `json:"extension_max_days"`
Note string `json:"note"`
By string `json:"by"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
na, err := license.ParseNotAfter(body.NotAfter)
if err != nil || na.IsZero() {
authx.WriteError(w, http.StatusBadRequest, "not_after 无效")
return
}
by := strings.TrimSpace(body.By)
if by == "" {
by = "watchdog"
}
lease, err := svcCtx.License.PutLease(&license.Lease{
Customer: body.Customer,
NotAfter: na,
ExtensionsUsed: body.ExtensionsUsed,
ExtensionsMax: body.ExtensionsMax,
ExtensionMaxDays: body.ExtensionMaxDays,
Note: body.Note,
}, by)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
}
}
func licenseImportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := requireLicenseSecret(svcCtx, r); err != nil {
authx.WriteError(w, http.StatusUnauthorized, err.Error())
return
}
if svcCtx.License == nil || !svcCtx.License.Enabled() {
authx.WriteError(w, http.StatusBadRequest, "license 未启用")
return
}
raw, err := io.ReadAll(r.Body)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
by := strings.TrimSpace(r.URL.Query().Get("by"))
if by == "" {
by = "offline"
}
lease, err := svcCtx.License.ImportSigned(raw, by)
if err != nil {
authx.WriteError(w, http.StatusBadRequest, err.Error())
return
}
httpx.OkJson(w, map[string]any{"ok": true, "lease": lease, "status": svcCtx.License.Status(time.Now())})
}
}