dal-license-server/internal/repository/license_repo.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

222 lines
8.7 KiB
Go

package repository
import (
"dal-license-server/internal/model"
"database/sql"
"fmt"
)
type LicenseRepo struct {
db *sql.DB
}
func NewLicenseRepo(db *sql.DB) *LicenseRepo {
return &LicenseRepo{db: db}
}
func (r *LicenseRepo) Create(l *model.License) (int64, error) {
res, err := r.db.Exec(`INSERT INTO licenses (product_id, license_key, license_type, customer_name, customer_pib, customer_email, limits_json, features, expires_at, grace_days, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
l.ProductID, l.LicenseKey, l.LicenseType, l.CustomerName, l.CustomerPIB, l.CustomerEmail,
string(l.Limits), string(l.Features), l.ExpiresAt, l.GraceDays, l.Notes)
if err != nil {
return 0, fmt.Errorf("create license: %w", err)
}
return res.LastInsertId()
}
func (r *LicenseRepo) GetByID(id int64) (*model.License, error) {
l := &model.License{}
var limits, features string
var revokedReason, notes sql.NullString
err := r.db.QueryRow(`SELECT l.id, l.product_id, l.license_key, l.license_type, l.customer_name, l.customer_pib, l.customer_email,
l.limits_json, l.features, l.issued_at, l.expires_at, l.grace_days, l.active, l.revoked, l.revoked_at, l.revoked_reason, l.notes, l.created_at, l.updated_at,
p.code, p.name, p.key_prefix
FROM licenses l JOIN products p ON l.product_id = p.id WHERE l.id = ?`, id).
Scan(&l.ID, &l.ProductID, &l.LicenseKey, &l.LicenseType, &l.CustomerName, &l.CustomerPIB, &l.CustomerEmail,
&limits, &features, &l.IssuedAt, &l.ExpiresAt, &l.GraceDays, &l.Active, &l.Revoked, &l.RevokedAt, &revokedReason, &notes, &l.CreatedAt, &l.UpdatedAt,
&l.ProductCode, &l.ProductName, &l.ProductPrefix)
if err != nil {
return nil, fmt.Errorf("get license %d: %w", id, err)
}
l.Limits = []byte(limits)
l.Features = []byte(features)
if revokedReason.Valid {
l.RevokedReason = revokedReason.String
}
if notes.Valid {
l.Notes = notes.String
}
return l, nil
}
func (r *LicenseRepo) GetByKey(key string) (*model.License, error) {
l := &model.License{}
var limits, features string
var revokedReason, notes sql.NullString
err := r.db.QueryRow(`SELECT l.id, l.product_id, l.license_key, l.license_type, l.customer_name, l.customer_pib, l.customer_email,
l.limits_json, l.features, l.issued_at, l.expires_at, l.grace_days, l.active, l.revoked, l.revoked_at, l.revoked_reason, l.notes, l.created_at, l.updated_at,
p.code, p.name, p.key_prefix
FROM licenses l JOIN products p ON l.product_id = p.id WHERE l.license_key = ?`, key).
Scan(&l.ID, &l.ProductID, &l.LicenseKey, &l.LicenseType, &l.CustomerName, &l.CustomerPIB, &l.CustomerEmail,
&limits, &features, &l.IssuedAt, &l.ExpiresAt, &l.GraceDays, &l.Active, &l.Revoked, &l.RevokedAt, &revokedReason, &notes, &l.CreatedAt, &l.UpdatedAt,
&l.ProductCode, &l.ProductName, &l.ProductPrefix)
if err != nil {
return nil, fmt.Errorf("get license by key: %w", err)
}
l.Limits = []byte(limits)
l.Features = []byte(features)
if revokedReason.Valid {
l.RevokedReason = revokedReason.String
}
if notes.Valid {
l.Notes = notes.String
}
return l, nil
}
func (r *LicenseRepo) List(productCode, status, search string) ([]model.LicenseWithActivation, error) {
query := `SELECT l.id, l.product_id, l.license_key, l.license_type, l.customer_name, l.customer_pib, l.customer_email,
l.limits_json, l.features, l.issued_at, l.expires_at, l.grace_days, l.active, l.revoked, l.revoked_at, l.revoked_reason, l.notes, l.created_at, l.updated_at,
p.code, p.name, p.key_prefix,
(SELECT COUNT(*) FROM activations a WHERE a.license_id = l.id AND a.is_active = TRUE) as active_activations
FROM licenses l JOIN products p ON l.product_id = p.id WHERE 1=1`
var args []interface{}
if productCode != "" {
query += " AND p.code = ?"
args = append(args, productCode)
}
if search != "" {
query += " AND (l.customer_name LIKE ? OR l.license_key LIKE ?)"
args = append(args, "%"+search+"%", "%"+search+"%")
}
switch status {
case "active":
query += " AND l.active = TRUE AND l.revoked = FALSE AND (l.expires_at IS NULL OR l.expires_at > NOW())"
case "expired":
query += " AND l.expires_at IS NOT NULL AND l.expires_at <= NOW() AND l.revoked = FALSE"
case "revoked":
query += " AND l.revoked = TRUE"
case "trial":
query += " AND l.license_type = 'TRIAL'"
}
query += " ORDER BY l.created_at DESC LIMIT 200"
rows, err := r.db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("list licenses: %w", err)
}
defer rows.Close()
var licenses []model.LicenseWithActivation
for rows.Next() {
var lwa model.LicenseWithActivation
var limits, features string
var revokedReason, notes sql.NullString
err := rows.Scan(&lwa.ID, &lwa.ProductID, &lwa.LicenseKey, &lwa.LicenseType, &lwa.CustomerName, &lwa.CustomerPIB, &lwa.CustomerEmail,
&limits, &features, &lwa.IssuedAt, &lwa.ExpiresAt, &lwa.GraceDays, &lwa.Active, &lwa.Revoked, &lwa.RevokedAt, &revokedReason, &notes, &lwa.CreatedAt, &lwa.UpdatedAt,
&lwa.ProductCode, &lwa.ProductName, &lwa.ProductPrefix,
&lwa.ActiveActivations)
if err != nil {
return nil, fmt.Errorf("scan license: %w", err)
}
lwa.Limits = []byte(limits)
lwa.Features = []byte(features)
if revokedReason.Valid {
lwa.RevokedReason = revokedReason.String
}
if notes.Valid {
lwa.Notes = notes.String
}
licenses = append(licenses, lwa)
}
return licenses, nil
}
func (r *LicenseRepo) Update(l *model.License) error {
_, err := r.db.Exec(`UPDATE licenses SET customer_name=?, customer_pib=?, customer_email=?, limits_json=?, features=?, grace_days=?, expires_at=?, notes=? WHERE id=?`,
l.CustomerName, l.CustomerPIB, l.CustomerEmail, string(l.Limits), string(l.Features), l.GraceDays, l.ExpiresAt, l.Notes, l.ID)
return err
}
func (r *LicenseRepo) Revoke(id int64, reason string) error {
_, err := r.db.Exec(`UPDATE licenses SET revoked=TRUE, revoked_at=NOW(), revoked_reason=?, active=FALSE WHERE id=?`, reason, id)
return err
}
func (r *LicenseRepo) CountByProduct() ([]model.ProductStats, error) {
rows, err := r.db.Query(`SELECT p.code, p.name,
COUNT(*) as total,
SUM(CASE WHEN l.active AND NOT l.revoked AND (l.expires_at IS NULL OR l.expires_at > NOW()) THEN 1 ELSE 0 END) as act,
SUM(CASE WHEN l.expires_at IS NOT NULL AND l.expires_at <= NOW() AND NOT l.revoked THEN 1 ELSE 0 END) as exp,
SUM(CASE WHEN l.expires_at IS NOT NULL AND l.expires_at <= NOW() AND DATE_ADD(l.expires_at, INTERVAL l.grace_days DAY) > NOW() AND NOT l.revoked THEN 1 ELSE 0 END) as grace,
SUM(CASE WHEN l.license_type = 'TRIAL' THEN 1 ELSE 0 END) as trial,
(SELECT COUNT(*) FROM activations a JOIN licenses l2 ON a.license_id=l2.id WHERE l2.product_id=p.id AND a.is_active=TRUE) as active_acts
FROM licenses l JOIN products p ON l.product_id=p.id GROUP BY p.id, p.code, p.name`)
if err != nil {
return nil, err
}
defer rows.Close()
var stats []model.ProductStats
for rows.Next() {
var s model.ProductStats
rows.Scan(&s.ProductCode, &s.ProductName, &s.Total, &s.Active, &s.Expired, &s.InGrace, &s.Trial, &s.ActiveActivations)
stats = append(stats, s)
}
return stats, nil
}
func (r *LicenseRepo) GetProducts() ([]model.Product, error) {
rows, err := r.db.Query("SELECT id, code, name, key_prefix, default_limits, available_features, active, created_at FROM products WHERE active = TRUE")
if err != nil {
return nil, err
}
defer rows.Close()
var products []model.Product
for rows.Next() {
var p model.Product
var limits, features string
rows.Scan(&p.ID, &p.Code, &p.Name, &p.KeyPrefix, &limits, &features, &p.Active, &p.CreatedAt)
p.DefaultLimits = []byte(limits)
p.AvailableFeatures = []byte(features)
products = append(products, p)
}
return products, nil
}
func (r *LicenseRepo) GetProductByID(id int64) (*model.Product, error) {
var p model.Product
var limits, features string
err := r.db.QueryRow("SELECT id, code, name, key_prefix, default_limits, available_features, active, created_at FROM products WHERE id = ?", id).
Scan(&p.ID, &p.Code, &p.Name, &p.KeyPrefix, &limits, &features, &p.Active, &p.CreatedAt)
if err != nil {
return nil, err
}
p.DefaultLimits = []byte(limits)
p.AvailableFeatures = []byte(features)
return &p, nil
}
func (r *LicenseRepo) ExpiringIn(days int) ([]model.License, error) {
rows, err := r.db.Query(`SELECT l.id, l.license_key, l.license_type, l.customer_name, l.expires_at, p.code, p.name, p.key_prefix
FROM licenses l JOIN products p ON l.product_id=p.id
WHERE l.active AND NOT l.revoked AND l.expires_at IS NOT NULL AND l.expires_at BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL ? DAY)
ORDER BY l.expires_at`, days)
if err != nil {
return nil, err
}
defer rows.Close()
var licenses []model.License
for rows.Next() {
var l model.License
rows.Scan(&l.ID, &l.LicenseKey, &l.LicenseType, &l.CustomerName, &l.ExpiresAt, &l.ProductCode, &l.ProductName, &l.ProductPrefix)
licenses = append(licenses, l)
}
return licenses, nil
}