package middleware import ( "net/http" "time" "github.com/gin-gonic/gin" ) const ( PhaseNormal = "normal" PhaseGrace = "grace" // expired 0–7 days: writable, show banner PhaseReadOnly = "readonly" // expired 7–15 days: read-only PhaseLocked = "locked" // expired 15+ days: no login ) var ( graceWindow = 7 * 24 * time.Hour readOnlyWindow = 15 * 24 * time.Hour ) // CalcLicensePhase computes the degradation phase based on expires_at. // nil expiresAt = perpetual license = normal. func CalcLicensePhase(expiresAt *time.Time) string { if expiresAt == nil { return PhaseNormal } elapsed := time.Since(*expiresAt) if elapsed <= 0 { return PhaseNormal } if elapsed <= graceWindow { return PhaseGrace } if elapsed <= readOnlyWindow { return PhaseReadOnly } return PhaseLocked } // GetLicensePhase returns the current phase for the authenticated request. func GetLicensePhase(c *gin.Context) string { v, _ := c.Get(CtxLicenseExpiresAt) ptr, _ := v.(*int64) if ptr == nil { return PhaseNormal } t := time.Unix(*ptr, 0) return CalcLicensePhase(&t) } // LicenseGuard blocks write operations when the shop's license is expired (readonly/locked). // License routes (/license/*) must be mounted outside this middleware so users can // view status and activate a new key even when locked. func LicenseGuard() gin.HandlerFunc { return func(c *gin.Context) { phase := GetLicensePhase(c) switch phase { case PhaseLocked: c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "授权已锁定,请续费或激活新授权码", "phase": PhaseLocked, }) case PhaseReadOnly: if c.Request.Method != http.MethodGet { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "授权已过期,当前为只读模式", "phase": PhaseReadOnly, }) return } c.Next() default: c.Next() } } }