Files
aura-crm/production/backend/internal/license/status.go
T
root 56ffca767d feat: offline license/trial system
7-day full-access trial, then gated behind an Ed25519-signed license key
(internal/license) verified entirely offline — no phone-home dependency.
Middleware 402s non-exempt API routes once the trial lapses with no valid
key; frontend shows a countdown banner in the last 3 days and a blocking
overlay once actually gated, both pointing at the new Settings -> Лицензия
tab. tools/gen-license mints keys for the vendor side (never ships the
private key).
2026-08-21 18:44:54 +00:00

56 lines
1.8 KiB
Go

package license
import "time"
// TrialDuration is deliberately not configurable — an owner who could
// extend their own trial from Settings would defeat the point of having
// one. Change this constant and rebuild if the vendor ever wants a
// different trial length for a new release.
const TrialDuration = 7 * 24 * time.Hour
// Status is the outcome of checking a license key + trial window together —
// exactly what a request-gating middleware and the Settings UI both need,
// computed once so they can't disagree with each other.
type Status struct {
Licensed bool
TrialActive bool
TrialDaysLeft int
Payload *Payload // nil unless Licensed
}
// Allowed is the actual gate condition: a valid license OR still inside the
// trial window. Either one is sufficient.
func (s Status) Allowed() bool {
return s.Licensed || s.TrialActive
}
// Check combines a (possibly empty/invalid) stored license key with the
// trial start timestamp into one Status. Never returns an error — an
// invalid/expired/malformed key just means Licensed=false, falling back to
// whatever the trial window says, same as no key at all.
func Check(licenseKey string, trialStartedAt time.Time) Status {
var s Status
if licenseKey != "" {
if p, err := Parse(licenseKey); err == nil && p.Valid() {
s.Licensed = true
s.Payload = p
}
}
trialEnd := trialStartedAt.Add(TrialDuration)
if remaining := time.Until(trialEnd); remaining > 0 {
s.TrialActive = true
// Ceiling division so "23 hours left" still reads as 1 day, not 0 —
// an owner watching the countdown hit zero without a key entered
// should never see it jump straight from 1 to a hard lock with no
// "0 days left, expires today" warning in between.
s.TrialDaysLeft = int(remaining / (24 * time.Hour))
if remaining%(24*time.Hour) > 0 {
s.TrialDaysLeft++
}
}
return s
}