dal-license-server/internal/service/activation_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

179 lines
5.0 KiB
Go

package service
import (
"dal-license-server/internal/model"
"dal-license-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{}