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>
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"licence-server/internal/model"
|
|
"licence-server/internal/repository"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type ActivationService struct {
|
|
activations *repository.ActivationRepo
|
|
licenses *repository.LicenseRepo
|
|
audit *repository.AuditRepo
|
|
crypto *CryptoService
|
|
licSvc *LicenseService
|
|
}
|
|
|
|
func NewActivationService(activations *repository.ActivationRepo, licenses *repository.LicenseRepo, audit *repository.AuditRepo, crypto *CryptoService, licSvc *LicenseService) *ActivationService {
|
|
return &ActivationService{activations: activations, licenses: licenses, audit: audit, crypto: crypto, licSvc: licSvc}
|
|
}
|
|
|
|
func (s *ActivationService) Activate(req *model.ActivateRequest, ip string) (*model.ActivateResponse, error) {
|
|
license, err := s.licenses.GetByKey(req.LicenseKey)
|
|
if err != nil {
|
|
return nil, &LicenseError{Code: "INVALID_KEY", Message: "Licencni kljuc nije pronadjen"}
|
|
}
|
|
|
|
if license.Revoked {
|
|
return nil, &LicenseError{Code: "KEY_REVOKED", Message: "Licenca je opozvana"}
|
|
}
|
|
|
|
if !license.Active {
|
|
return nil, &LicenseError{Code: "KEY_REVOKED", Message: "Licenca nije aktivna"}
|
|
}
|
|
|
|
if license.IsExpired() && !license.IsInGrace() {
|
|
return nil, &LicenseError{Code: "KEY_EXPIRED", Message: "Licenca je istekla"}
|
|
}
|
|
|
|
// Check existing activation
|
|
existing, err := s.activations.GetActiveByLicense(license.ID)
|
|
if err == nil && existing != nil {
|
|
if existing.MachineFingerprint == req.MachineFingerprint {
|
|
// Same machine — refresh
|
|
s.activations.UpdateLastSeen(existing.ID)
|
|
} else {
|
|
return nil, &LicenseError{
|
|
Code: "ALREADY_ACTIVATED",
|
|
Message: "Licenca je vec aktivirana na drugom racunaru",
|
|
Details: map[string]interface{}{
|
|
"activated_on": existing.Hostname,
|
|
"activated_at": existing.ActivatedAt.Format(time.RFC3339),
|
|
},
|
|
}
|
|
}
|
|
} else {
|
|
// New activation
|
|
_, err = s.activations.Create(&model.Activation{
|
|
LicenseID: license.ID,
|
|
MachineFingerprint: req.MachineFingerprint,
|
|
Hostname: req.Hostname,
|
|
OSInfo: req.OS,
|
|
AppVersion: req.AppVersion,
|
|
IPAddress: ip,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("activate: %w", err)
|
|
}
|
|
}
|
|
|
|
s.audit.Log(&license.ID, "ACTIVATE", ip, map[string]interface{}{
|
|
"fingerprint": req.MachineFingerprint,
|
|
"hostname": req.Hostname,
|
|
"os": req.OS,
|
|
"app_version": req.AppVersion,
|
|
})
|
|
|
|
// Build signed license data
|
|
licenseJSON, err := s.licSvc.BuildLicenseData(license, req.MachineFingerprint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build license data: %w", err)
|
|
}
|
|
|
|
signature, err := s.crypto.Sign(licenseJSON)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign license: %w", err)
|
|
}
|
|
|
|
var ld model.LicenseData
|
|
json.Unmarshal(licenseJSON, &ld)
|
|
|
|
return &model.ActivateResponse{
|
|
License: ld,
|
|
Signature: signature,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ActivationService) Deactivate(req *model.DeactivateRequest, ip string) (*model.DeactivateResponse, error) {
|
|
license, err := s.licenses.GetByKey(req.LicenseKey)
|
|
if err != nil {
|
|
return nil, &LicenseError{Code: "INVALID_KEY", Message: "Licencni kljuc nije pronadjen"}
|
|
}
|
|
|
|
act, err := s.activations.GetByLicenseAndFingerprint(license.ID, req.MachineFingerprint)
|
|
if err != nil || act == nil {
|
|
return nil, &LicenseError{Code: "NOT_ACTIVATED", Message: "Licenca nije aktivirana na ovom racunaru"}
|
|
}
|
|
|
|
s.activations.Deactivate(license.ID, req.MachineFingerprint)
|
|
|
|
s.audit.Log(&license.ID, "DEACTIVATE", ip, map[string]interface{}{
|
|
"fingerprint": req.MachineFingerprint,
|
|
})
|
|
|
|
return &model.DeactivateResponse{
|
|
Message: "Licenca uspesno deaktivirana",
|
|
CanReactivate: true,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ActivationService) Validate(req *model.ValidateRequest, ip string) (*model.ValidateResponse, error) {
|
|
license, err := s.licenses.GetByKey(req.LicenseKey)
|
|
if err != nil {
|
|
return &model.ValidateResponse{Valid: false}, nil
|
|
}
|
|
|
|
// Update last_seen
|
|
act, err := s.activations.GetByLicenseAndFingerprint(license.ID, req.MachineFingerprint)
|
|
if err == nil && act != nil {
|
|
s.activations.UpdateLastSeen(act.ID)
|
|
}
|
|
|
|
s.audit.Log(&license.ID, "VALIDATE", ip, map[string]interface{}{
|
|
"fingerprint": req.MachineFingerprint,
|
|
})
|
|
|
|
expiresAt := ""
|
|
if license.ExpiresAt.Valid {
|
|
expiresAt = license.ExpiresAt.Time.Format(time.RFC3339)
|
|
}
|
|
|
|
valid := license.Active && !license.Revoked
|
|
if license.IsExpired() && !license.IsInGrace() {
|
|
valid = false
|
|
}
|
|
|
|
return &model.ValidateResponse{
|
|
Valid: valid,
|
|
ExpiresAt: expiresAt,
|
|
Revoked: license.Revoked,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ActivationService) ForceRelease(licenseID int64, ip string) error {
|
|
s.activations.ForceRelease(licenseID)
|
|
s.audit.Log(&licenseID, "FORCE_RELEASE", ip, nil)
|
|
return nil
|
|
}
|
|
|
|
func (s *ActivationService) ListByLicense(licenseID int64) ([]model.Activation, error) {
|
|
return s.activations.ListByLicense(licenseID)
|
|
}
|
|
|
|
// LicenseError is a typed error for client API
|
|
type LicenseError struct {
|
|
Code string
|
|
Message string
|
|
Details interface{}
|
|
}
|
|
|
|
func (e *LicenseError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// Needed to satisfy import
|
|
var _ = sql.NullTime{}
|