dal-license-server/internal/service/license_service.go
djuka dc0114e4b7 Inicijalni commit: kompletna implementacija + dokumentacija + testovi
- 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>
2026-03-04 07:42:25 +00:00

196 lines
4.9 KiB
Go

package service
import (
"dal-license-server/internal/model"
"dal-license-server/internal/repository"
"database/sql"
"encoding/json"
"fmt"
"time"
)
type LicenseService struct {
repo *repository.LicenseRepo
audit *repository.AuditRepo
}
func NewLicenseService(repo *repository.LicenseRepo, audit *repository.AuditRepo) *LicenseService {
return &LicenseService{repo: repo, audit: audit}
}
func (s *LicenseService) Create(req *model.CreateLicenseRequest, ip string) (*model.License, error) {
product, err := s.repo.GetProductByID(req.ProductID)
if err != nil {
return nil, fmt.Errorf("product not found: %w", err)
}
key, err := GenerateKey(product.KeyPrefix)
if err != nil {
return nil, fmt.Errorf("generate key: %w", err)
}
limits := req.Limits
if len(limits) == 0 || string(limits) == "" || string(limits) == "null" {
limits = product.DefaultLimits
}
features := req.Features
if len(features) == 0 || string(features) == "" || string(features) == "null" {
features = product.AvailableFeatures
}
graceDays := req.GraceDays
if graceDays == 0 {
graceDays = 30
}
l := &model.License{
ProductID: req.ProductID,
LicenseKey: key,
LicenseType: req.LicenseType,
CustomerName: req.CustomerName,
CustomerPIB: req.CustomerPIB,
CustomerEmail: req.CustomerEmail,
Limits: limits,
Features: features,
GraceDays: graceDays,
Notes: req.Notes,
}
// Set expiry
switch req.LicenseType {
case "TRIAL":
exp := time.Now().AddDate(0, 0, 30)
l.ExpiresAt = sql.NullTime{Time: exp, Valid: true}
case "MONTHLY":
exp := time.Now().AddDate(0, 1, 0)
l.ExpiresAt = sql.NullTime{Time: exp, Valid: true}
case "ANNUAL":
exp := time.Now().AddDate(1, 0, 0)
l.ExpiresAt = sql.NullTime{Time: exp, Valid: true}
case "PERPETUAL":
l.ExpiresAt = sql.NullTime{Valid: false}
}
id, err := s.repo.Create(l)
if err != nil {
return nil, err
}
s.audit.Log(&id, "CREATE", ip, map[string]interface{}{
"product": product.Code,
"type": req.LicenseType,
"customer": req.CustomerName,
})
return s.repo.GetByID(id)
}
func (s *LicenseService) GetByID(id int64) (*model.License, error) {
return s.repo.GetByID(id)
}
func (s *LicenseService) GetByKey(key string) (*model.License, error) {
return s.repo.GetByKey(key)
}
func (s *LicenseService) List(productCode, status, search string) ([]model.LicenseWithActivation, error) {
return s.repo.List(productCode, status, search)
}
func (s *LicenseService) Update(id int64, req *model.UpdateLicenseRequest, ip string) error {
l, err := s.repo.GetByID(id)
if err != nil {
return err
}
l.CustomerName = req.CustomerName
l.CustomerPIB = req.CustomerPIB
l.CustomerEmail = req.CustomerEmail
l.Notes = req.Notes
if len(req.Limits) > 0 {
l.Limits = req.Limits
}
if len(req.Features) > 0 {
l.Features = req.Features
}
if req.GraceDays > 0 {
l.GraceDays = req.GraceDays
}
if req.ExpiresAt != "" {
t, err := time.Parse("2006-01-02", req.ExpiresAt)
if err == nil {
l.ExpiresAt = sql.NullTime{Time: t, Valid: true}
}
}
s.audit.Log(&id, "UPDATE", ip, map[string]interface{}{"customer": req.CustomerName})
return s.repo.Update(l)
}
func (s *LicenseService) Revoke(id int64, reason, ip string) error {
s.audit.Log(&id, "REVOKE", ip, map[string]interface{}{"reason": reason})
return s.repo.Revoke(id, reason)
}
func (s *LicenseService) GetProducts() ([]model.Product, error) {
return s.repo.GetProducts()
}
func (s *LicenseService) GetStats() (*model.StatsResponse, error) {
stats := &model.StatsResponse{}
byProduct, err := s.repo.CountByProduct()
if err != nil {
return nil, err
}
stats.ByProduct = byProduct
for _, p := range byProduct {
stats.TotalLicenses += p.Total
stats.ActiveLicenses += p.Active
stats.ExpiredLicenses += p.Expired
stats.RevokedLicenses += p.Trial // count trials separately
stats.ActiveActivations += p.ActiveActivations
}
// Fix: count revoked properly
var revoked int
for _, p := range byProduct {
_ = p // need a separate query for revoked
}
_ = revoked
licenses, _ := s.repo.List("", "revoked", "")
stats.RevokedLicenses = len(licenses)
return stats, nil
}
func (s *LicenseService) ExpiringIn(days int) ([]model.License, error) {
return s.repo.ExpiringIn(days)
}
func (s *LicenseService) BuildLicenseData(l *model.License, fingerprint string) ([]byte, error) {
expiresAt := ""
if l.ExpiresAt.Valid {
expiresAt = l.ExpiresAt.Time.Format(time.RFC3339)
}
ld := model.LicenseData{
LicenseKey: l.LicenseKey,
Product: l.ProductCode,
LicenseType: l.LicenseType,
IssuedAt: l.IssuedAt.Format(time.RFC3339),
ExpiresAt: expiresAt,
ActivatedAt: time.Now().Format(time.RFC3339),
MachineFingerprint: fingerprint,
GraceDays: l.GraceDays,
Limits: l.Limits,
Features: l.Features,
Customer: model.CustomerData{
Name: l.CustomerName,
Email: l.CustomerEmail,
},
}
return json.Marshal(ld)
}