licence-server/CLAUDE.md
Nenad Djukic 28296b390c Preuzet radni paket LoggerService (RBAC kontrolna ravan) iz firmverskog repoa
Paket je nastao u asp-terminia/serviseri/ ali je sav posao serverski (isti
backend i baza kao ASP-Terminia licenciranje), pa se seli ovde - isto
obrazlozenje i put kao docs/asp-terminia/.

- docs/loggerservice/ - 7 dokumenata: vizija/MVP (merodavan dokument),
  ZAKLJUCEN RBAC model (stablo orgs, 4 role, invarijante, seme tabela),
  heartbeat/discovery formati koje uredjaj vec salje, provisioning i
  katalog firmvera, zadaci S1-S7, pravila rada na produkciji
- reference prepravljene: "ASP repo" = https://git.terminia.net/djuka/asp-terminia,
  link ka docs/asp-terminia/ unutar istog repoa umesto stare "dal-license-server
  repo:" oznake
- CLAUDE.md: sekcija LoggerService + koordinacija sa docs/asp-terminia/
  (S1-S2 idu POSLE Z1 da se migracije ne sudare)

Firmverska strana (svc.cpp, discovery/heartbeat) ostaje u ASP repou i ne dira se.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 07:10:37 +02:00

794 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
- **ASP-Terminia (4. proizvod):** vidi `docs/asp-terminia/` — radni paket Z1Z6
---
## Radni paket: ASP-Terminia (ESP32 terminal)
Četvrti proizvod na ovom serveru — **ASP-Terminia** (Autonomni Sistem Prolaza),
ESP32-S3 terminal za kontrolu prolaza (bazeni/spa). Licenca je vezana za **MAC**
čipa (eFuse) preko `machine_fingerprint`, a ne za računar.
**Klijentska strana je već gotova** — firmware modul `licenca.h/.cpp` (od FW
0.10.0) radi aktivaciju, offline verifikaciju RSA potpisa i UI. Čeka samo da
proizvod i ključ postoje ovde na serveru.
Kompletan paket (postojeće stanje, tačan API ugovor koji firmware očekuje,
zadaci Z1Z6, pravila rada na produkciji, enforcement pitanja za vlasnika):
**[`docs/asp-terminia/README.md`](docs/asp-terminia/README.md)** — čitati prvo.
Razlika u odnosu na ESIR/ARV/LIGHT_TICKET koja se mora poštovati:
- fingerprint je MAC uređaja, ne hash računara;
- uređaj radi **offline** — mrežna greška NIKAD ne sme da ga obori;
- postojeći proizvodi su ŽIVI u produkciji (~140 licenci) — ništa se ne lomi.
---
## Radni paket: LoggerService (kontrolna ravan za flote terminala)
**Proširenje ovog servera**, ne novi projekat — RBAC stablo organizacija
(vendor → kupac → objekat, proizvoljna dubina), uređaji + heartbeat, servisni
grantovi, provisioning i katalog firmvera za ASP liniju terminala. Jedan Go
backend + baza, **dva lica** po hostu: `terminia.net` (vendor pogled) i
`loggerservice.com` (serviserski brend — kasnije samostalna aplikacija za
servisne firme, uređaji su joj samo modul).
RBAC model je **ZAKLJUČEN** sa vlasnikom (2026-07-24) — nije predlog za
raspravu, implementacione odluke (kolone/indeksi/Go slojevi) su slobodne.
Kompletan paket (vizija/MVP, RBAC šema i invarijante, heartbeat/discovery
formati koje uređaj već šalje, provisioning i katalog `.bin`-ova, zadaci
S1S7, pravila rada na produkciji):
**[`docs/loggerservice/README.md`](docs/loggerservice/README.md)** — čitati prvo,
pa **[`docs/loggerservice/00-vizija-i-mvp.md`](docs/loggerservice/00-vizija-i-mvp.md)**
(merodavan nad ostalim fajlovima u paketu gde se razlikuju).
**Koordinacija sa `docs/asp-terminia/`:** isti backend, ista baza — zadaci S1S2
(migracije RBAC šeme) idu POSLE Z1 (ASP proizvod na licenci) da se migracije ne
sudare; `devices.license_id` referencira licencu iz tog paketa.
---
*Kreirano: mart 2026*
*Ažurirano: 04.03.2026 — Kompletna implementacija, 22/22 testova*
*Autor: Nenad Đukić / DAL d.o.o.*