dal-license-server/CLAUDE.md
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

745 lines
26 KiB
Markdown

# DAL License Server — CLAUDE.md
## Pregled
Univerzalni licencni server za **sve DAL proizvode**: ESIR, ARV, Light-Ticket, i buduće aplikacije.
Zamenjuje stari `esir-license-server` (koji niko ne koristi u produkciji). Arhitektura prema ARV licencnoj dokumentaciji (`/root/projects/arv/TASKS/Licenciranje-Starter.md`), ali univerzalna — podržava N proizvoda.
**Princip:** License Server je jedini koji ima RSA private key. Klijentske aplikacije imaju samo public key. Server potpisuje licence, klijenti verifikuju potpis. Niko osim servera ne može da kreira validnu licencu.
**KRITIČNO — Testiranje:** Ova aplikacija je osnova budućeg poslovanja DAL-a. Mora biti **rigorozno testirana** svim poznatim tipovima testova:
- **Unit testovi** — svaki servis, model, helper, middleware
- **Integration testovi** — kompletni API flow-ovi (activate → validate → deactivate → re-activate)
- **Security testovi** — SQL injection, brute force, API key bypass, tampered signatures, invalid inputs
- **Edge case testovi** — expired licence, grace period granice, perpetual licence, race conditions
- **Regression testovi** — svaki bug fix mora imati test koji potvrdjuje ispravku
- **Load testovi** — rate limiting pod pritiskom, konkurentni zahtevi
- Nikad ne smanjivati pokrivenost testovima. Svaka nova funkcionalnost MORA imati test pre merge-a.
---
## Kako radi
```
┌─────────────────────────────────────────────────────┐
│ DAL LICENSE SERVER │
│ port: 8090 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Admin API │ │ Klijent │ │ Admin Dashboard │ │
│ │ (CRUD) │ │ API │ │ (htmx) │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │ │ │ │
│ └──────────────┼───────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ MySQL baza │ │
│ │ license_db │ │
│ └─────────────────┘ │
│ │
│ RSA-2048 Private Key (samo ovde, nikad ne izlazi) │
└─────────────────────────────────────────────────────┘
▲ ▲ ▲
│ HTTPS │ HTTPS │ HTTPS
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────────┐
│ ESIR │ │ ARV │ │ Light-Ticket │
│ klijent │ │ klijent │ │ klijent │
└─────────┘ └─────────┘ └──────────────┘
Svaki ima RSA public key ugrađen u binary
Svaki čuva license.enc lokalno
```
---
## Tok — od kupovine do rada
### 1. Admin kreira licencu
```
Admin → Dashboard → Nova licenca
→ Bira proizvod (ESIR / ARV / LIGHT_TICKET)
→ Unosi: firma, email, tip (MONTHLY/ANNUAL/PERPETUAL), limiti
→ Server generiše ključ: {PREFIX}-XXXX-XXXX-XXXX-XXXX
→ Ključ se šalje klijentu (email ili ručno)
```
### 2. Klijent aktivira
```
Klijent instalira aplikaciju
→ Unese licencni ključ u Settings
→ Aplikacija šalje serveru: ključ + machine_fingerprint + app_version + OS
→ Server proverava: ključ validan? nije aktiviran drugde? nije istekao?
→ Server potpisuje licencne podatke RSA private key-em
→ Vraća: licencni JSON + RSA potpis
→ Klijent kreira license.enc (AES-256-GCM + RSA potpis)
→ Aplikacija radi OFFLINE dok licenca važi
```
### 3. Svakodnevni rad (offline)
```
Aplikacija se pokrene
→ Čita license.enc sa diska
→ Dekriptuje (AES sa machine fingerprint)
→ Proverava RSA potpis (public key ugrađen u binary)
→ Proverava fingerprint (isti računar?)
→ Proverava rok (nije istekao?)
→ SVE OK → normalan rad
→ Internet NIJE potreban za svakodnevni rad
```
### 4. Opciona online provera
```
Jednom dnevno (ako ima internet):
→ Aplikacija šalje: ključ + fingerprint
→ Server proverava: nije revocirana?
→ Ako je revocirana na serveru → klijent invalidira lokalnu licencu
→ Ako nema interneta → preskače, radi sa lokalnim fajlom
```
---
## Tech stack
| Komponenta | Tehnologija |
|------------|-------------|
| Backend | Go + net/http (Go 1.22+ routing) |
| Admin UI | htmx + Go html/template |
| Baza | MySQL 8.0 |
| Kripto | RSA-2048 (potpis), AES-256-GCM (enkripcija na klijentima) |
| Auth | API key za admin API, session za dashboard |
---
## Go struktura projekta
```
dal-license-server/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go # .env konfiguracija
│ ├── model/
│ │ ├── license.go # License, Product, LicenseType
│ │ ├── activation.go # Activation model
│ │ ├── audit.go # Audit log model
│ │ └── request.go # Request/Response structs
│ ├── repository/
│ │ ├── license_repo.go # License CRUD
│ │ ├── activation_repo.go # Activation CRUD
│ │ └── audit_repo.go # Audit log
│ ├── service/
│ │ ├── license_service.go # Poslovna logika
│ │ ├── activation_service.go # Aktivacija/deaktivacija
│ │ ├── crypto_service.go # RSA potpisivanje
│ │ └── keygen.go # Generisanje ključeva
│ ├── handler/
│ │ ├── client_handler.go # API za klijentske app-e (activate, deactivate, validate)
│ │ ├── admin_handler.go # Admin CRUD API
│ │ ├── dashboard_handler.go # Admin dashboard (htmx)
│ │ └── helpers.go # JSON/error helpers
│ ├── middleware/
│ │ ├── auth.go # API key + session auth
│ │ └── ratelimit.go # Rate limiting
│ └── router/
│ └── router.go
├── templates/
│ ├── layout/
│ │ └── base.html
│ ├── pages/
│ │ ├── login.html
│ │ ├── dashboard.html # Statistike po proizvodu
│ │ ├── licenses.html # Lista licenci + CRUD
│ │ ├── license-detail.html # Detalji licence + aktivacije
│ │ └── audit.html # Audit log pregled
│ └── partials/
│ ├── license-row.html
│ └── stats.html
├── crypto/
│ ├── private.pem # RSA private key (NIKAD u git-u!)
│ └── public.pem # RSA public key (deli se sa klijentima)
├── migrations/
│ ├── 001_create_tables.sql
│ └── 002_seed_products.sql
├── .env.example
├── .gitignore
├── go.mod
├── README.md
├── TESTING.md
└── CLAUDE.md
```
---
## Baza podataka (MySQL)
### Tabele
#### `products`
| Kolona | Tip | Opis |
|--------|-----|------|
| id | BIGINT PK AUTO_INCREMENT | |
| code | VARCHAR(20) UNIQUE | ESIR, ARV, LIGHT_TICKET |
| name | VARCHAR(100) | Puno ime proizvoda |
| key_prefix | VARCHAR(10) | ESIR-, ARV-, LT- |
| default_limits | JSON | Default limiti za taj proizvod |
| available_features | JSON | Sve moguće features za taj proizvod |
| active | BOOLEAN DEFAULT TRUE | |
| created_at | TIMESTAMP | |
**Seed podaci:**
```sql
INSERT INTO products (code, name, key_prefix, default_limits, available_features) VALUES
('ESIR', 'ESIR Fiskalizacija', 'ESIR-',
'{"max_installations": 1}',
'["FISCALIZATION", "REPORTS"]'),
('ARV', 'ARV Evidencija RV', 'ARV-',
'{"max_employees": 50, "max_readers": 4}',
'["TIME_ATTENDANCE", "BASIC_REPORTS", "EMPLOYEE_MANAGEMENT", "SHIFTS", "HR_MODULE", "ACCESS_CONTROL"]'),
('LIGHT_TICKET', 'Light-Ticket', 'LT-',
'{"max_operators": 3}',
'["TICKET_VALIDATION", "REPORTS", "EXCEL_EXPORT", "LIVE_FEED"]');
```
**Light-Ticket paketi licenci (odlučeno 03.03.2026):**
- Admin ručno upisuje `max_operators` prilikom kreiranja licence
- Nema eksplicitnog `edition` polja — paketi su samo konvencija:
| Paket | max_operators | Tip licence | Features |
|-------|--------------|-------------|----------|
| Starter | 3 | MONTHLY ili PERPETUAL | Sve |
| Pro | 10 | MONTHLY ili PERPETUAL | Sve |
| Enterprise | 0 (neograničeno) | MONTHLY ili PERPETUAL | Sve |
- Default u seed-u je `max_operators: 3` (Starter)
- Sve features su iste za sve pakete — razlikuje se SAMO `max_operators`
- Tipovi licence za LT: MONTHLY (mesečna) i PERPETUAL (trajna). TRIAL i ANNUAL nisu predviđeni za V1.
#### `licenses`
| Kolona | Tip | Opis |
|--------|-----|------|
| id | BIGINT PK AUTO_INCREMENT | |
| product_id | BIGINT FK(products.id) | Koji proizvod |
| license_key | VARCHAR(25) UNIQUE | {PREFIX}-XXXX-XXXX-XXXX-XXXX |
| license_type | VARCHAR(20) NOT NULL | TRIAL / MONTHLY / ANNUAL / PERPETUAL |
| customer_name | VARCHAR(255) NOT NULL | Naziv firme |
| customer_pib | VARCHAR(20) | PIB (opciono) |
| customer_email | VARCHAR(255) | Email |
| limits | JSON NOT NULL | {"max_employees": 50, "max_readers": 4} |
| features | JSON NOT NULL | ["TIME_ATTENDANCE", "BASIC_REPORTS"] |
| issued_at | TIMESTAMP DEFAULT NOW() | |
| expires_at | TIMESTAMP NULL | NULL = neograničena (PERPETUAL) |
| grace_days | INT DEFAULT 30 | Koliko dana grace period |
| active | BOOLEAN DEFAULT TRUE | |
| revoked | BOOLEAN DEFAULT FALSE | |
| revoked_at | TIMESTAMP NULL | |
| revoked_reason | TEXT | |
| notes | TEXT | Interne beleške |
| created_at | TIMESTAMP | |
| updated_at | TIMESTAMP | |
**Indeksi:**
- `idx_licenses_key` na (license_key) — UNIQUE
- `idx_licenses_product` na (product_id)
- `idx_licenses_customer` na (customer_name)
- `idx_licenses_expires` na (expires_at)
#### `activations`
| Kolona | Tip | Opis |
|--------|-----|------|
| id | BIGINT PK AUTO_INCREMENT | |
| license_id | BIGINT FK(licenses.id) | |
| machine_fingerprint | VARCHAR(100) NOT NULL | sha256:... |
| hostname | VARCHAR(100) | Ime računara |
| os_info | VARCHAR(50) | windows / linux |
| app_version | VARCHAR(20) | Verzija aplikacije |
| ip_address | VARCHAR(45) | IP pri aktivaciji |
| activated_at | TIMESTAMP DEFAULT NOW() | |
| deactivated_at | TIMESTAMP NULL | NULL = aktivna |
| is_active | BOOLEAN DEFAULT TRUE | |
| last_seen_at | TIMESTAMP | Poslednja online provera |
**Indeksi:**
- `idx_activations_license` na (license_id)
- `idx_activations_fingerprint` na (machine_fingerprint)
- `idx_activations_active` na (license_id, is_active)
#### `audit_log`
| Kolona | Tip | Opis |
|--------|-----|------|
| id | BIGINT PK AUTO_INCREMENT | |
| license_id | BIGINT FK(licenses.id) NULL | |
| action | VARCHAR(30) NOT NULL | ACTIVATE, DEACTIVATE, VALIDATE, REVOKE, FORCE_RELEASE, CREATE, UPDATE |
| ip_address | VARCHAR(45) | |
| details | JSON | Dodatni podaci (fingerprint, hostname, error, itd.) |
| created_at | TIMESTAMP DEFAULT NOW() | |
**Indeksi:**
- `idx_audit_license` na (license_id)
- `idx_audit_action` na (action)
- `idx_audit_created` na (created_at)
---
## API endpointi
### Klijentski API (za ESIR, ARV, Light-Ticket aplikacije)
| Metoda | Putanja | Auth | Opis |
|--------|---------|------|------|
| POST | `/api/v1/activate` | - | Aktivacija licence |
| POST | `/api/v1/deactivate` | - | Deaktivacija (transfer) |
| POST | `/api/v1/validate` | - | Opciona online provera |
| GET | `/api/v1/check-update` | - | Provera za update aplikacije |
#### POST `/api/v1/activate`
```json
// Request (od klijentske aplikacije)
{
"license_key": "LT-K7M2-9P4N-R3W8-J6T1",
"machine_fingerprint": "sha256:a1b2c3d4e5f6...",
"app_version": "1.0.0",
"os": "windows",
"hostname": "FIRMA-PC"
}
// Response 200
{
"license": {
"license_key": "LT-K7M2-9P4N-R3W8-J6T1",
"product": "LIGHT_TICKET",
"license_type": "MONTHLY",
"issued_at": "2026-03-01T00:00:00Z",
"expires_at": "2026-04-01T00:00:00Z",
"activated_at": "2026-03-03T10:00:00Z",
"machine_fingerprint": "sha256:a1b2c3d4e5f6...",
"grace_days": 30,
"limits": {
"max_operators": 3
},
"features": ["TICKET_VALIDATION", "REPORTS", "EXCEL_EXPORT", "LIVE_FEED"],
"customer": {
"name": "Firma DOO",
"email": "admin@firma.rs"
}
},
"signature": "RSA-SHA256:base64encodedSignature..."
}
// Response 400
{
"error": {
"code": "ALREADY_ACTIVATED",
"message": "Licenca je već aktivirana na drugom računaru",
"details": {
"activated_on": "DRUGA-PC",
"activated_at": "2026-02-15T08:00:00Z"
}
}
}
```
**Error kodovi:** INVALID_KEY, ALREADY_ACTIVATED, KEY_EXPIRED, KEY_REVOKED, PRODUCT_MISMATCH
#### POST `/api/v1/deactivate`
```json
// Request
{
"license_key": "LT-K7M2-9P4N-R3W8-J6T1",
"machine_fingerprint": "sha256:a1b2c3d4e5f6..."
}
// Response 200
{
"message": "Licenca uspešno deaktivirana",
"can_reactivate": true
}
```
#### POST `/api/v1/validate`
```json
// Request
{
"license_key": "LT-K7M2-9P4N-R3W8-J6T1",
"machine_fingerprint": "sha256:a1b2c3d4e5f6..."
}
// Response 200
{
"valid": true,
"expires_at": "2026-04-01T00:00:00Z",
"revoked": false
}
```
### Admin API
| Metoda | Putanja | Auth | Opis |
|--------|---------|------|------|
| GET | `/api/v1/admin/products` | API key | Lista proizvoda |
| GET | `/api/v1/admin/licenses` | API key | Lista licenci (filter po proizvodu) |
| POST | `/api/v1/admin/licenses` | API key | Kreiraj licencu |
| GET | `/api/v1/admin/licenses/{id}` | API key | Detalji licence |
| PUT | `/api/v1/admin/licenses/{id}` | API key | Izmeni licencu |
| POST | `/api/v1/admin/licenses/{id}/revoke` | API key | Opozovi licencu |
| POST | `/api/v1/admin/licenses/{id}/release` | API key | Force release (računar crkao) |
| GET | `/api/v1/admin/licenses/{id}/activations` | API key | Aktivacije za licencu |
| GET | `/api/v1/admin/audit` | API key | Audit log |
| GET | `/api/v1/admin/stats` | API key | Statistike |
### Admin Dashboard (htmx)
| Metoda | Putanja | Auth | Opis |
|--------|---------|------|------|
| GET | `/login` | - | Login |
| POST | `/login` | - | Login submit |
| GET | `/dashboard` | Session | Početna — statistike po proizvodu |
| GET | `/licenses` | Session | Tabela licenci + filteri |
| GET | `/licenses/new` | Session | Forma za novu licencu |
| POST | `/licenses` | Session | Kreiraj licencu |
| GET | `/licenses/{id}` | Session | Detalji + aktivacije |
| POST | `/licenses/{id}/revoke` | Session | Opozovi |
| POST | `/licenses/{id}/release` | Session | Force release |
| GET | `/audit` | Session | Audit log |
---
## RSA ključevi
### Generisanje (jednom, čuva se zauvek)
```bash
# Generiši private key (ČUVAJ TAJNO)
openssl genrsa -out crypto/private.pem 2048
# Izvuci public key (deli se sa klijentima)
openssl rsa -in crypto/private.pem -pubout -out crypto/public.pem
```
### Korišćenje
- **License Server:** koristi `private.pem` za potpisivanje licenci pri aktivaciji
- **Klijenti (ARV, ESIR, LT):** imaju `public.pem` ugrađen u Go binary (`embed`) za verifikaciju potpisa
- **Isti par ključeva** za sve proizvode — jedan server, jedan ključ
### Šta se potpisuje
```
Server prima activate request
→ Kreira licencni JSON (bez signature polja)
→ Potpisuje JSON sa RSA-SHA256 (private key)
→ Dodaje potpis u response
→ Klijent prima JSON + potpis
→ Klijent verifikuje potpis (public key)
→ Klijent enkriptuje JSON sa AES-256-GCM (ključ = machine_fingerprint + app_secret)
→ Klijent sačuva kao license.enc
```
---
## Licencni ključ — format
```
{PREFIX}-XXXX-XXXX-XXXX-XXXX
Primeri:
ESIR-K7M2-9P4N-R3W8-J6T1
ARV-A3B5-C8D2-E7F4-G9H6
LT-M4N8-P2Q6-R5S3-T7U9
```
- Prefix po proizvodu: čita se iz `products.key_prefix`
- 4 grupe po 4 alfanumerička karaktera
- Karakteri: A-H, J-N, P-Y, 2-9 (bez O/0/I/1 konfuzije)
- Generisanje: crypto/rand
---
## Tipovi licenci
| Tip | Trajanje | Obnova | Primer |
|-----|----------|--------|--------|
| TRIAL | 30 dana | Nema | Besplatno testiranje |
| MONTHLY | 30 dana | Automatski ili ručno | Light-Ticket |
| ANNUAL | 365 dana | Ručno | ARV, ESIR |
| PERPETUAL | Zauvek | Nema (expires_at = NULL) | Kupljena zauvek |
### Grace period
- Default: 30 dana posle isteka
- Konfigurisano per licenca (`grace_days` kolona)
- Tokom grace perioda: pun rad + upozorenje na klijentskoj app
- Posle grace-a: read-only režim (GET dozvoljen, POST/PUT/DELETE blokiran)
---
## Dashboard — šta prikazuje
### Početna strana
- **Po proizvodu:** broj aktivnih / isteklih / u grace-u / trial
- **Ukupno:** sve licence, aktivne aktivacije
- **Alarm:** licence koje ističu u narednih 7 dana
- **Poslednja aktivnost:** zadnjih 10 akcija iz audit loga
### Lista licenci
- **Filter:** proizvod, status (active/expired/revoked/trial), pretraga po firmi
- **Kolone:** ključ, firma, proizvod, tip, ističe, aktivacija, status
- **Sortiranje:** po datumu isteka (najhitnije prvo)
### Detalji licence
- Sve informacije o licenci
- Lista aktivacija (hostname, OS, verzija, IP, poslednji put viđen)
- Audit log za tu licencu
- Akcije: produži, opozovi, force release
---
## Migracija sa ESIR License Server-a
Pošto stari server niko ne koristi u produkciji, migracija je jednostavna:
1. Kreiraj novi `dal-license-server` projekat
2. Kopiraj korisnu logiku iz `esir-license-server` (keygen, verify flow, helpers)
3. Proširi model (product, features, limits, RSA)
4. Zameni port 8090 (isti port, drop-in replacement)
5. Stari `esir-license-server` → arhiviraj
---
## Implementacioni taskovi
### Faza 0: Priprema
- **T0-01:** Go projekat (go mod init, struktura foldera)
- **T0-02:** .env.example, .gitignore, README.md
- **T0-03:** Git repo na Gitea
- **T0-04:** Generisanje RSA ključeva (private.pem, public.pem)
### Faza 1: Baza
- **T1-01:** Config modul (.env)
- **T1-02:** MySQL konekcija
- **T1-03:** Migracija 001_create_tables.sql (products, licenses, activations, audit_log)
- **T1-04:** Migracija 002_seed_products.sql (ESIR, ARV, LIGHT_TICKET)
- **T1-05:** Repository sloj (license_repo, activation_repo, audit_repo)
### Faza 2: Core servis
- **T2-01:** Keygen — generisanje ključa sa prefix-om po proizvodu
- **T2-02:** Crypto service — RSA potpisivanje licencnog JSON-a
- **T2-03:** License service — CRUD, validacija, revoke
- **T2-04:** Activation service — activate, deactivate, force release, validate
- **T2-05:** Audit logging — svaka akcija se loguje
### Faza 3: Klijentski API
- **T3-01:** POST /api/v1/activate
- **T3-02:** POST /api/v1/deactivate
- **T3-03:** POST /api/v1/validate
- **T3-04:** Rate limiting na klijentske endpointe
### Faza 4: Admin API
- **T4-01:** Auth middleware (API key)
- **T4-02:** CRUD endpointi za licence
- **T4-03:** Revoke + Force release endpointi
- **T4-04:** Aktivacije i audit endpointi
- **T4-05:** Statistike endpoint
### Faza 5: Admin Dashboard (htmx)
- **T5-01:** Login stranica + session auth
- **T5-02:** Dashboard — statistike po proizvodu
- **T5-03:** Lista licenci — tabela, filteri, pretraga
- **T5-04:** Nova licenca — forma sa izborom proizvoda
- **T5-05:** Detalji licence — info, aktivacije, audit, akcije
- **T5-06:** Audit log stranica
### Faza 6: Testovi
- **T6-01:** Unit — keygen (format, prefix, uniqueness)
- **T6-02:** Unit — crypto (RSA sign/verify, tampered data)
- **T6-03:** Unit — license service (create, expire, grace, revoke)
- **T6-04:** Unit — activation service (activate, deactivate, already_activated, force_release)
- **T6-05:** Integration — activate flow (full roundtrip)
- **T6-06:** Integration — deactivate flow
- **T6-07:** Integration — expired license
- **T6-08:** Integration — revoked license
- **T6-09:** Security — SQL injection na svim inputima
- **T6-10:** Security — rate limiting (brute force key)
- **T6-11:** Security — API key validation
- **T6-12:** TESTING.md
### Faza 7: DevOps
- **T7-01:** Gitea CI workflow
- **T7-02:** Systemd service fajl
- **T7-03:** Backup skripta za bazu
---
## Konfiguracija (.env)
```env
# Server
APP_PORT=8090
APP_ENV=development
# MySQL
DB_HOST=localhost
DB_PORT=3306
DB_NAME=dal_license_db
DB_USER=license
DB_PASS=OBAVEZNO-PROMENITI
# Auth
ADMIN_API_KEY=OBAVEZNO-GENERISATI
SESSION_SECRET=OBAVEZNO-PROMENITI
# RSA
RSA_PRIVATE_KEY_PATH=./crypto/private.pem
# Rate limiting
RATE_LIMIT_ACTIVATE=10/min
RATE_LIMIT_VALIDATE=60/min
# Logging
LOG_LEVEL=info
LOG_FILE=./log/server.log
```
---
## Konvencije
- Go net/http (bez framework-a, isto kao ESIR)
- Go 1.22+ method routing (`POST /api/v1/activate`)
- database/sql + raw SQL (bez ORM-a)
- RSA-2048 za potpis, SHA-256 za fingerprint
- API key u `X-API-Key` header-u za admin
- Sve akcije se loguju u audit_log
- Error wrapping: `fmt.Errorf("activate: %w", err)`
- Licencni ključ se NIKAD ne loguje ceo — samo prefix + poslednja 4 karaktera
---
## Bezbednost
- `private.pem` NIKAD u git-u (u .gitignore)
- `private.pem` permisije: 600 (samo owner čita)
- Admin API key min 32 karaktera
- Rate limiting na activate/validate (zaštita od brute force)
- Audit log za svaku akciju
- HTTPS u produkciji (TLS termination na reverse proxy)
- Licencni ključ u logovima maskiran: `LT-K7M2-****-****-J6T1`
---
## Status implementacije (mart 2026)
**Kompletno implementiran** — server je funkcionalan, testiran (22/22 testova prošlo).
### Implementirano
- ✅ Go projekat inicijalizovan (go.mod, go.sum)
- ✅ MySQL baza `dal_license_db` kreirana, user `license`
- ✅ Migracije (001_create_tables.sql, 002_seed_products.sql)
- ✅ RSA-2048 ključevi generisani (crypto/private.pem, crypto/public.pem)
- ✅ Seed podaci: 3 proizvoda (ESIR, ARV, LIGHT_TICKET)
- ✅ Client API: activate, deactivate, validate
- ✅ Admin API: CRUD licence, stats, audit log
- ✅ Dashboard: login, pregled licenci, kreiranje, detalji, audit log
- ✅ Rate limiting (in-memory sliding window)
- ✅ API key autentifikacija (X-API-Key header)
- ✅ RSA-SHA256 potpisivanje licencnih podataka
- ✅ Audit log za sve akcije
- ✅ Specifikacija kompletna (ovaj CLAUDE.md)
### Struktura fajlova
```
cmd/server/main.go — Entry point, MySQL konekcija, migracije, wire-up
internal/config/config.go — .env loading, DSN builder (multiStatements=true)
internal/model/
license.go — Product, License, LicenseWithActivation modeli
activation.go — Activation model
audit.go — AuditEntry model
request.go — Request/Response strukture (API)
internal/repository/
license_repo.go — License CRUD, product queries, stats
activation_repo.go — Activation CRUD, deactivate, force release
audit_repo.go — Audit log insert/list
internal/service/
license_service.go — License CRUD biznis logika, expiry kalkulacija
activation_service.go — Activate, Deactivate, Validate, ForceRelease
crypto_service.go — RSA-2048 potpisivanje (SHA-256, PKCS1v15)
keygen.go — Generisanje ključeva: {PREFIX}-XXXX-XXXX-XXXX-XXXX
internal/handler/
client_handler.go — POST /api/v1/activate, deactivate, validate
admin_handler.go — Admin CRUD API endpointi
dashboard_handler.go — Dashboard stranice, in-memory sesije
helpers.go — writeJSON, writeError, clientIP
internal/middleware/
auth.go — API key auth (X-API-Key header)
ratelimit.go — In-memory sliding window rate limiter
internal/router/router.go — Sve rute (client, admin, dashboard)
migrations/
001_create_tables.sql — products, licenses, activations, audit_log
002_seed_products.sql — ESIR, ARV, LIGHT_TICKET
templates/
layout/base.html — Glavni layout sa navbar-om
pages/login.html — Login stranica
pages/dashboard.html — Dashboard sa statistikama
pages/licenses.html — Lista licenci sa filterima
pages/license-new.html — Forma za novu licencu
pages/license-detail.html — Detalji licence, aktivacije, audit
pages/audit.html — Globalni audit log
static/css/style.css — Kompletni CSS
static/js/htmx.min.js — htmx biblioteka
crypto/private.pem — RSA-2048 privatni ključ (chmod 600)
crypto/public.pem — RSA javni ključ
```
### Konfiguracija (.env)
```
APP_PORT=8090
DB_HOST=localhost / DB_PORT=3306
DB_NAME=dal_license_db
DB_USER=license / DB_PASS=license_pass_2026
ADMIN_API_KEY=dal-admin-key-2026-supersecret-change-me
ADMIN_PASSWORD=DalAdmin2026!
RSA_PRIVATE_KEY_PATH=./crypto/private.pem
```
### DSN napomena
DSN sadrži `multiStatements=true` — neophodno za izvršavanje migracija sa više SQL naredbi u jednom `db.Exec()` pozivu.
### Testirano (22/22)
1. Health endpoint
2. Products list
3. Create license (sva 3 proizvoda)
4. List licenses
5. License detail
6. Activate license
7. Already activated (drugi hardver)
8. Deactivate license
9. Re-activate (novi hardver)
10. Validate license
11. Revoke license
12. Validate revoked (odbijeno)
13. Invalid license key
14. API key auth (401 bez ključa)
15. Stats endpoint
16. Audit log
17. Dashboard login
18. Dashboard index
19. Licenses page
20. New license form
21. License detail page
22. Audit page
### Sledeći koraci
- Faza 8 (Light-Ticket): Integracija sa license serverom
- ARV: Integracija sa license serverom
- ESIR: Integracija sa license serverom
---
*Kreirano: mart 2026*
*Ažurirano: 04.03.2026 — Kompletna implementacija, 22/22 testova*
*Autor: Nenad Đukić / DAL d.o.o.*