licence-server/internal/repository/license_repo.go
Nenad Djukic 1abe7176ec Preimenovanje: dal-license-server -> licence-server (+ debrendiranje)
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>
2026-07-25 07:55:26 +02:00

222 lines
8.7 KiB
Go

package repository
import (
"licence-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
}