Vlasnik izlazi iz DAL price za ovaj projekat - postaje samostalan proizvod. Preimenovanje je mehanicko (import putanje, imena fajlova/baze/UI naslova), NIJEDNA linija poslovne logike u internal/handler ili internal/service nije dirana - go build, go vet i go test ./... prolaze cisto pod novim imenom. - go.mod: module dal-license-server -> licence-server; 28 import linija u 13 .go fajlova usaglaseno - internal/config/config.go (+test): default DB_NAME licence_db - HTML sabloni (7 stranica) + 7 Playwright e2e spec fajlova: "DAL License Server" -> "Licence Server" u title/nav-brand, testovi usaglaseni u istom prolazu (nista ne moze da se razdvoji) - package.json/package-lock.json/.gitignore/.claude/project.json: ime projekta/binarnog fajla/work_dir - Usput: package.json je u repository.url nosio OTVORENU Gitea lozinku u cistom tekstu - uklonjena (URL sad bez kredencijala). Lozinka je i dalje u staroj git istoriji - preporuka: rotirati je posebno. - CLAUDE.md/README/API/TESTING/docs/SPEC/ARCHITECTURE/SETUP: naslovi, ASCII dijagrami, git clone URL, mysqldump primer, systemd predlozak u SETUP.md zamenjen stvarnim (obrazac terminia.service - journal log + graceful shutdown, ne stari minimalni predlozak) - Debrendiranje: "Univerzalni licencni server za sve DAL proizvode" -> "za vise proizvoda i klijenata"; potpis "Nenad Djukic / DAL d.o.o." -> "Nenad Djukic". NE dirano: nazivi ESIR/ARV/LIGHT_TICKET (tudji proizvodi, ne DAL brend), "DAL" kao stvaran naziv org-a u RBAC modelu (docs/loggerservice/README.md) - to je podatak, ne branding ovog servera. - docs/asp-terminia/, docs/loggerservice/: prozni pomeni imena servera Baza (dal_license_db -> licence_db) i deploy na 151 idu u posebnom koraku, posle preimenovanja Gitea repoa - vidi CLAUDE.md istorijsku belesku na dnu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
196 lines
4.9 KiB
Go
196 lines
4.9 KiB
Go
package service
|
|
|
|
import (
|
|
"licence-server/internal/model"
|
|
"licence-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)
|
|
}
|