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>
222 lines
8.7 KiB
Go
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, ¬es, &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, ¬es, &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, ¬es, &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
|
|
}
|