fix(license): shared invalid/grace banners + instance copy

Surface expired/over_instance/revoked red banners with CTAs via a
shared LicenseStatusBanners used by settings and ProGate. Hide seats
in product UI; keep HMAC dual-path and never silent-clear keys.
This commit is contained in:
opsx committed 2026-11-01 03:08:37 +08:00
1 parent af0298e994
commit 62d9b81a70
10 files changed
+265 -45

No files matched your search

+22 -13
View File
@@ -8,7 +8,7 @@ import "time"
type License struct {
Plan string `json:"plan"` // free | pro
Edition string `json:"edition"` // personal | team
Seats int `json:"seats"` // 登录席位(历史字段; opsx 按实例计,映射到 Instances)
Seats int `json:"seats"` // 历史登录席位字段;产品侧按实例计,勿再展示
Instances int `json:"instances"` // 部署实例上限(0 = 不限)
Customer string `json:"customer"` // 客户标识
// ExpiresAt 0 = 永久;否则 Unix 秒
@@ -17,17 +17,20 @@ type License struct {
ActivatedAt time.Time `json:"activatedAt"`
// --- opsx-licence 对接字段(JSON 向后兼容,缺省为零值) ---
Source string `json:"source,omitempty"` // hmac | opsx
LicenseKey string `json:"licenseKey,omitempty"` // 完整密钥(周检/心跳需要;Sanitized 不外泄)
InstanceID string `json:"instanceId,omitempty"` // 机器指纹,无 PII
PlanType string `json:"planType,omitempty"` // trial | yearly | perpetual
MaxDevices int `json:"maxDevices,omitempty"` // 服务端 max_devices(实例数)
LastVerifiedAt int64 `json:"lastVerifiedAt,omitempty"` // unix 秒
LastServerTime int64 `json:"lastServerTime,omitempty"` // 防时钟回拨
ActivationToken string `json:"activationToken,omitempty"` // 解绑令牌
DeprecationNotice string `json:"deprecationNotice,omitempty"` // 旧 HMAC 迁移提示
InGrace bool `json:"inGrace,omitempty"`
GraceDaysLeft int `json:"graceDaysLeft,omitempty"`
Source string `json:"source,omitempty"` // hmac | opsx
LicenseKey string `json:"licenseKey,omitempty"` // 完整密钥(周检/心跳需要;Sanitized 不外泄)
InstanceID string `json:"instanceId,omitempty"` // 机器指纹,无 PII
PlanType string `json:"planType,omitempty"` // trial | yearly | perpetual
MaxDevices int `json:"maxDevices,omitempty"` // 服务端 max_devices(实例数)
LastVerifiedAt int64 `json:"lastVerifiedAt,omitempty"` // unix 秒
LastServerTime int64 `json:"lastServerTime,omitempty"` // 防时钟回拨
ActivationToken string `json:"activationToken,omitempty"` // 解绑令牌
DeprecationNotice string `json:"deprecationNotice,omitempty"` // 旧 HMAC 迁移提示
InGrace bool `json:"inGrace,omitempty"`
GraceDaysLeft int `json:"graceDaysLeft,omitempty"`
// InvalidReason 非空且非有效 Pro 时给前端红条:expired | over_instance | revoked
// 服务端明确拒绝才写;禁止静默清空 LicenseKey。
InvalidReason string `json:"invalidReason,omitempty"`
}
func (l *License) Sanitized() map[string]interface{} {
@@ -40,9 +43,10 @@ func (l *License) Sanitized() map[string]interface{} {
plan = "free"
}
out := map[string]interface{}{
"plan": plan, "edition": l.Edition, "seats": l.Seats, "instances": l.Instances,
"plan": plan, "edition": l.Edition, "instances": l.Instances,
"customer": l.Customer, "expiresAt": l.ExpiresAt, "expired": expired,
}
// seats 故意不外抛给产品 UI(按实例计)
if l.Source != "" {
out["source"] = l.Source
}
@@ -52,6 +56,8 @@ func (l *License) Sanitized() map[string]interface{} {
if l.MaxDevices > 0 {
out["maxDevices"] = l.MaxDevices
out["instances"] = l.MaxDevices
} else if l.Instances > 0 {
out["instances"] = l.Instances
}
if l.DeprecationNotice != "" {
out["deprecationNotice"] = l.DeprecationNotice
@@ -66,5 +72,8 @@ func (l *License) Sanitized() map[string]interface{} {
if l.InstanceID != "" {
out["instanceId"] = l.InstanceID
}
if l.InvalidReason != "" {
out["invalidReason"] = l.InvalidReason
}
return out
}