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.
80 lines
3.0 KiB
Go
80 lines
3.0 KiB
Go
package models
|
||
|
||
import "time"
|
||
|
||
// License Pro 授权。
|
||
// Source: hmac(旧本地签发,迁移窗口内仍接受)| opsx(opsx-licence LIC-/LICOFF-)。
|
||
// Plan: free | pro; Edition: personal | team(opsx 侧用 plan_type 映射)。
|
||
type License struct {
|
||
Plan string `json:"plan"` // free | pro
|
||
Edition string `json:"edition"` // personal | team
|
||
Seats int `json:"seats"` // 历史登录席位字段;产品侧按实例计,勿再展示
|
||
Instances int `json:"instances"` // 部署实例上限(0 = 不限)
|
||
Customer string `json:"customer"` // 客户标识
|
||
// ExpiresAt 0 = 永久;否则 Unix 秒
|
||
ExpiresAt int64 `json:"expiresAt"`
|
||
IssuedAt time.Time `json:"issuedAt"`
|
||
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"`
|
||
// InvalidReason 非空且非有效 Pro 时给前端红条:expired | over_instance | revoked
|
||
// 服务端明确拒绝才写;禁止静默清空 LicenseKey。
|
||
InvalidReason string `json:"invalidReason,omitempty"`
|
||
}
|
||
|
||
func (l *License) Sanitized() map[string]interface{} {
|
||
if l == nil {
|
||
return map[string]interface{}{"plan": "free"}
|
||
}
|
||
expired := l.ExpiresAt > 0 && l.ExpiresAt < time.Now().Unix()
|
||
plan := l.Plan
|
||
if expired && !l.InGrace {
|
||
plan = "free"
|
||
}
|
||
out := map[string]interface{}{
|
||
"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
|
||
}
|
||
if l.PlanType != "" {
|
||
out["planType"] = l.PlanType
|
||
}
|
||
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
|
||
}
|
||
if l.InGrace {
|
||
out["inGrace"] = true
|
||
out["graceDaysLeft"] = l.GraceDaysLeft
|
||
}
|
||
if l.LastVerifiedAt > 0 {
|
||
out["lastVerifiedAt"] = l.LastVerifiedAt
|
||
}
|
||
if l.InstanceID != "" {
|
||
out["instanceId"] = l.InstanceID
|
||
}
|
||
if l.InvalidReason != "" {
|
||
out["invalidReason"] = l.InvalidReason
|
||
}
|
||
return out
|
||
}
|