- Kompletna Go implementacija licencnog servera (19 Go fajlova) - Klijentski API: activate, deactivate, validate - Admin API: CRUD licence, stats, audit log - Admin dashboard: htmx + Go templates - RSA-2048 potpisivanje licencnih podataka - Rate limiting i API key autentifikacija - MySQL migracije i seed podaci (ESIR, ARV, LIGHT_TICKET) - Unit testovi: keygen, crypto, model, middleware (24 testa) - Dokumentacija: SPEC.md, ARCHITECTURE.md, SETUP.md, API.md, TESTING.md, README.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
3.2 KiB
Go
130 lines
3.2 KiB
Go
package model
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
type Product struct {
|
|
ID int64 `json:"id"`
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
KeyPrefix string `json:"key_prefix"`
|
|
DefaultLimits json.RawMessage `json:"default_limits"`
|
|
AvailableFeatures json.RawMessage `json:"available_features"`
|
|
Active bool `json:"active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type License struct {
|
|
ID int64 `json:"id"`
|
|
ProductID int64 `json:"product_id"`
|
|
LicenseKey string `json:"license_key"`
|
|
LicenseType string `json:"license_type"`
|
|
CustomerName string `json:"customer_name"`
|
|
CustomerPIB string `json:"customer_pib"`
|
|
CustomerEmail string `json:"customer_email"`
|
|
Limits json.RawMessage `json:"limits"`
|
|
Features json.RawMessage `json:"features"`
|
|
IssuedAt time.Time `json:"issued_at"`
|
|
ExpiresAt sql.NullTime `json:"expires_at"`
|
|
GraceDays int `json:"grace_days"`
|
|
Active bool `json:"active"`
|
|
Revoked bool `json:"revoked"`
|
|
RevokedAt sql.NullTime `json:"revoked_at"`
|
|
RevokedReason string `json:"revoked_reason"`
|
|
Notes string `json:"notes"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// Joined fields
|
|
ProductCode string `json:"product_code,omitempty"`
|
|
ProductName string `json:"product_name,omitempty"`
|
|
ProductPrefix string `json:"-"`
|
|
}
|
|
|
|
func (l *License) IsExpired() bool {
|
|
if !l.ExpiresAt.Valid {
|
|
return false // PERPETUAL
|
|
}
|
|
return time.Now().After(l.ExpiresAt.Time)
|
|
}
|
|
|
|
func (l *License) IsInGrace() bool {
|
|
if !l.ExpiresAt.Valid {
|
|
return false
|
|
}
|
|
if !l.IsExpired() {
|
|
return false
|
|
}
|
|
graceEnd := l.ExpiresAt.Time.AddDate(0, 0, l.GraceDays)
|
|
return time.Now().Before(graceEnd)
|
|
}
|
|
|
|
func (l *License) IsGraceExpired() bool {
|
|
if !l.ExpiresAt.Valid {
|
|
return false
|
|
}
|
|
graceEnd := l.ExpiresAt.Time.AddDate(0, 0, l.GraceDays)
|
|
return time.Now().After(graceEnd)
|
|
}
|
|
|
|
func (l *License) MaskedKey() string {
|
|
if len(l.LicenseKey) < 10 {
|
|
return l.LicenseKey
|
|
}
|
|
return l.LicenseKey[:len(l.ProductPrefix)+4] + "-****-****-" + l.LicenseKey[len(l.LicenseKey)-4:]
|
|
}
|
|
|
|
func (l *License) StatusText() string {
|
|
if l.Revoked {
|
|
return "Opozvana"
|
|
}
|
|
if !l.Active {
|
|
return "Neaktivna"
|
|
}
|
|
if l.IsGraceExpired() {
|
|
return "Istekla (grace)"
|
|
}
|
|
if l.IsInGrace() {
|
|
return "Grace period"
|
|
}
|
|
if l.IsExpired() {
|
|
return "Istekla"
|
|
}
|
|
if l.LicenseType == "TRIAL" {
|
|
return "Trial"
|
|
}
|
|
return "Aktivna"
|
|
}
|
|
|
|
func (l *License) StatusClass() string {
|
|
switch l.StatusText() {
|
|
case "Aktivna":
|
|
return "status-active"
|
|
case "Trial":
|
|
return "status-trial"
|
|
case "Grace period":
|
|
return "status-grace"
|
|
case "Istekla", "Istekla (grace)":
|
|
return "status-expired"
|
|
case "Opozvana":
|
|
return "status-revoked"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (l *License) ExpiresAtFormatted() string {
|
|
if !l.ExpiresAt.Valid {
|
|
return "Neograniceno"
|
|
}
|
|
return l.ExpiresAt.Time.Format("02.01.2006")
|
|
}
|
|
|
|
type LicenseWithActivation struct {
|
|
License
|
|
ActiveActivations int `json:"active_activations"`
|
|
}
|