From d407fd0510d3dc727ec9e91698aca0fe7d6ea8c5 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sat, 7 Mar 2026 16:05:18 +0100 Subject: [PATCH] [MKTG-API] Documentazione + auth API Key per licenze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InviteController: requireLicenseAuth() accetta X-API-Key (scope admin:licenses) oppure JWT super_admin — tutti i metodi admin aggiornati - mktg-api-doc.html: risponde alle 6 domande del marketing con esempi curl, tabelle risposta, riepilogo endpoint, link Postman collection - nis2-license-api.postman.json: collection completa (login, create, list, revoke, regenerate, validate, provision) con pre-script salva JWT/invite_id Co-Authored-By: Claude Sonnet 4.6 --- application/controllers/InviteController.php | 88 ++++- .../integration/nis2-license-api.postman.json | 259 +++++++++++++ public/mktg-api-doc.html | 365 ++++++++++++++++++ 3 files changed, 696 insertions(+), 16 deletions(-) create mode 100644 docs/integration/nis2-license-api.postman.json create mode 100644 public/mktg-api-doc.html diff --git a/application/controllers/InviteController.php b/application/controllers/InviteController.php index 80869b4..074c2a6 100644 --- a/application/controllers/InviteController.php +++ b/application/controllers/InviteController.php @@ -6,15 +6,20 @@ * Gli inviti vengono generati da NIS2 Admin (o via API admin), * distribuiti dall'e-commerce, usati da lg231 e altri sistemi Agile. * - * Endpoints admin (richiedono JWT super_admin): - * POST /api/invites → genera invito - * GET /api/invites → lista inviti - * GET /api/invites/{id} → dettaglio invito - * DELETE /api/invites/{id} → revoca invito + * Endpoints admin (richiedono JWT super_admin OPPURE API Key con scope admin:licenses): + * POST /api/invites/create → genera invito + * GET /api/invites/list → lista inviti + * GET /api/invites/{id} → dettaglio invito + * DELETE /api/invites/{id} → revoca invito * POST /api/invites/{id}/regenerate → rigenera token mantenendo config * * Endpoints pubblici (nessuna auth): - * GET /api/invites/validate/{token} → valida invito (preview piano, scadenza) + * GET /api/invites/validate?token= → valida invito (preview piano, scadenza) + * + * Auth da sistemi esterni (es. mktg-agile): + * Header: X-API-Key: nis2_xxxx... + * Scope richiesto: admin:licenses (oppure read:all per sola lettura) + * Genera la chiave in: NIS2 Agile → Settings → API Keys */ require_once __DIR__ . '/BaseController.php'; @@ -53,8 +58,7 @@ class InviteController extends BaseController */ public function create(): void { - $this->requireAuth(); - $this->requireRole(['super_admin']); + $this->requireLicenseAuth(writeRequired: true); $body = $this->getBody(); $plan = in_array($body['plan'] ?? '', ['essentials','professional','enterprise']) @@ -135,8 +139,7 @@ class InviteController extends BaseController */ public function index(): void { - $this->requireAuth(); - $this->requireRole(['super_admin']); + $this->requireLicenseAuth(writeRequired: false); $status = $_GET['status'] ?? null; // pending/used/expired/revoked $channel = $_GET['channel'] ?? null; @@ -184,8 +187,7 @@ class InviteController extends BaseController */ public function show(int $id = 0): void { - $this->requireAuth(); - $this->requireRole(['super_admin']); + $this->requireLicenseAuth(writeRequired: false); $row = Database::fetchOne('SELECT * FROM invites WHERE id=? LIMIT 1', [$id]); if (!$row) $this->jsonError('Invito non trovato', 404, 'NOT_FOUND'); @@ -206,8 +208,7 @@ class InviteController extends BaseController */ public function revoke(int $id = 0): void { - $this->requireAuth(); - $this->requireRole(['super_admin']); + $this->requireLicenseAuth(writeRequired: true); $row = Database::fetchOne('SELECT id, status FROM invites WHERE id=? LIMIT 1', [$id]); if (!$row) $this->jsonError('Invito non trovato', 404, 'NOT_FOUND'); if ($row['status'] === 'used') $this->jsonError('Invito già usato — non revocabile', 422, 'ALREADY_USED'); @@ -224,8 +225,7 @@ class InviteController extends BaseController */ public function regenerate(int $id = 0): void { - $this->requireAuth(); - $this->requireRole(['super_admin']); + $this->requireLicenseAuth(writeRequired: true); $row = Database::fetchOne('SELECT * FROM invites WHERE id=? LIMIT 1', [$id]); if (!$row) $this->jsonError('Invito non trovato', 404, 'NOT_FOUND'); if ($row['status'] === 'used') $this->jsonError('Invito già usato — impossibile rigenerare', 422, 'ALREADY_USED'); @@ -376,6 +376,62 @@ class InviteController extends BaseController return $features[$plan] ?? $features['professional']; } + // ══════════════════════════════════════════════════════════════════════ + // AUTH HELPER — JWT super_admin OR API Key con scope admin:licenses + // ══════════════════════════════════════════════════════════════════════ + + /** + * Accetta: + * a) JWT super_admin (header Authorization: Bearer eyJ...) + * b) API Key con scope admin:licenses o read:all (header X-API-Key: nis2_...) + * + * Se nessuno dei due è valido → 401. + * Se $writeRequired=true e la chiave ha solo read:all → 403. + */ + private function requireLicenseAuth(bool $writeRequired = false): void + { + // ── tenta API Key ──────────────────────────────────────────────── + $rawKey = $_SERVER['HTTP_X_API_KEY'] + ?? (isset($_SERVER['HTTP_AUTHORIZATION']) && str_starts_with($_SERVER['HTTP_AUTHORIZATION'], 'ApiKey ') + ? substr($_SERVER['HTTP_AUTHORIZATION'], 7) : null); + + if ($rawKey) { + if (!str_starts_with($rawKey, 'nis2_')) { + $this->jsonError('Formato API Key non valido', 401, 'INVALID_API_KEY_FORMAT'); + } + $hash = hash('sha256', $rawKey); + $key = Database::fetchOne( + 'SELECT id, organization_id, scopes, is_active, expires_at + FROM api_keys WHERE key_hash=? LIMIT 1', + [$hash] + ); + if (!$key || !$key['is_active']) { + $this->jsonError('API Key non valida o disattivata', 401, 'INVALID_API_KEY'); + } + if ($key['expires_at'] && strtotime($key['expires_at']) < time()) { + $this->jsonError('API Key scaduta', 401, 'EXPIRED_API_KEY'); + } + $scopes = json_decode($key['scopes'] ?? '[]', true); + $hasAdmin = in_array('admin:licenses', $scopes, true); + $hasRead = in_array('read:all', $scopes, true) || in_array('admin:licenses', $scopes, true); + + if (!$hasRead) { + $this->jsonError('Scope insufficiente (richiesto: admin:licenses o read:all)', 403, 'INSUFFICIENT_SCOPE'); + } + if ($writeRequired && !$hasAdmin) { + $this->jsonError('Scope insufficiente per scrittura (richiesto: admin:licenses)', 403, 'WRITE_SCOPE_REQUIRED'); + } + + // Aggiorna last_used_at + Database::execute('UPDATE api_keys SET last_used_at=NOW() WHERE id=?', [$key['id']]); + return; + } + + // ── fallback: JWT super_admin ──────────────────────────────────── + $this->requireAuth(); + $this->requireRole(['super_admin']); + } + // ── helper ─────────────────────────────────────────────────────────── private function getBody(): array { diff --git a/docs/integration/nis2-license-api.postman.json b/docs/integration/nis2-license-api.postman.json new file mode 100644 index 0000000..27b9eea --- /dev/null +++ b/docs/integration/nis2-license-api.postman.json @@ -0,0 +1,259 @@ +{ + "info": { + "name": "NIS2 Agile — License API", + "description": "API per la gestione licenze NIS2 Agile da sistemi esterni (mktg-agile, e-commerce, partner).\n\nAutenticazione: API Key con scope `admin:licenses`\nHeader: `X-API-Key: nis2_xxxx`\n\nPer ottenere una chiave: login su https://nis2.agile.software → licenseExt.html → login → Settings → API Keys → scope: admin:licenses", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_postman_id": "nis2-license-api-v1" + }, + "variable": [ + { "key": "base_url", "value": "https://nis2.agile.software/api", "type": "string" }, + { "key": "api_key", "value": "nis2_LA_TUA_CHIAVE_QUI", "type": "string" }, + { "key": "jwt", "value": "", "type": "string" }, + { "key": "invite_id","value": "1", "type": "string" } + ], + "item": [ + { + "name": "0. Auth — Ottieni JWT (alternativa all'API Key)", + "item": [ + { + "name": "POST /auth/login", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const d = pm.response.json();", + "if (d.success) {", + " pm.collectionVariables.set('jwt', d.data.access_token);", + " console.log('JWT salvato:', d.data.access_token.substring(0,30) + '...');", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"cristiano.benassati@gmail.com\",\n \"password\": \"Silvia1978!@\"\n}" + }, + "url": { "raw": "{{base_url}}/auth/login", "host": ["{{base_url}}"], "path": ["auth","login"] }, + "description": "Ottieni JWT super_admin (valido 2h). Alternativa all'API Key per test manuali.\nIl JWT viene salvato automaticamente nella variabile {{jwt}}." + } + } + ] + }, + { + "name": "1. Crea Licenza", + "item": [ + { + "name": "POST /invites/create — Licenza singola professional 12m", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const d = pm.response.json();", + "pm.test('Status 201', () => pm.response.to.have.status(201));", + "pm.test('Contiene token', () => pm.expect(d.data.invites[0].token).to.match(/^inv_/));", + "if (d.success && d.data.invites.length) {", + " pm.collectionVariables.set('invite_id', d.data.invites[0].id);", + " console.log('TOKEN (salva subito!):', d.data.invites[0].token);", + " console.log('URL onboarding:', d.data.invites[0].invite_url);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" }, + { "key": "X-API-Key", "value": "{{api_key}}" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"plan\": \"professional\",\n \"duration_months\": 12,\n \"invite_expires_days\": 30,\n \"max_uses\": 1,\n \"max_users_per_org\": 10,\n \"label\": \"NIS2 Professional — Ordine MKT-2026-001\",\n \"channel\": \"ecommerce\",\n \"issued_to\": \"cliente@azienda.it\",\n \"reseller_name\": \"mktg-agile S.r.l.\",\n \"price_eur\": 990.00,\n \"notes\": \"Campagna Q1 2026\"\n}" + }, + "url": { "raw": "{{base_url}}/invites/create", "host": ["{{base_url}}"], "path": ["invites","create"] }, + "description": "Crea una licenza singola Professional 12 mesi.\n\nRESPONSE (201):\n- `invites[].token` — inv_xxx... → SALVARE SUBITO, non recuperabile\n- `invites[].invite_url` — URL per attivare da browser\n- `invites[].expires_at` — scadenza invito (entro cui attivare)\n- `warning` — promemoria sicurezza token" + } + }, + { + "name": "POST /invites/create — Batch 5 licenze Essentials", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" }, + { "key": "X-API-Key", "value": "{{api_key}}" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"plan\": \"essentials\",\n \"duration_months\": 6,\n \"invite_expires_days\": 60,\n \"max_uses\": 1,\n \"max_users_per_org\": 3,\n \"quantity\": 5,\n \"label\": \"Essentials 6m — Bundle Reseller Q1\",\n \"channel\": \"reseller\",\n \"reseller_name\": \"Partner XYZ\",\n \"price_eur\": 490.00,\n \"notes\": \"Batch 5 licenze per partner XYZ\"\n}" + }, + "url": { "raw": "{{base_url}}/invites/create", "host": ["{{base_url}}"], "path": ["invites","create"] }, + "description": "Genera 5 token in un'unica chiamata (quantity=5).\nRisposta contiene array `invites` con 5 elementi, ciascuno con token unico." + } + }, + { + "name": "POST /invites/create — Enterprise con restrizione P.IVA", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" }, + { "key": "X-API-Key", "value": "{{api_key}}" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"plan\": \"enterprise\",\n \"duration_months\": 24,\n \"invite_expires_days\": 14,\n \"max_uses\": 1,\n \"label\": \"Enterprise 24m — Cliente Acme S.r.l.\",\n \"channel\": \"direct\",\n \"issued_to\": \"ciso@acme.it\",\n \"restrict_vat\": \"02345678901\",\n \"restrict_email\": \"ciso@acme.it\",\n \"price_eur\": 4800.00,\n \"notes\": \"Contratto enterprise diretto — approvato da DG\"\n}" + }, + "url": { "raw": "{{base_url}}/invites/create", "host": ["{{base_url}}"], "path": ["invites","create"] }, + "description": "Licenza Enterprise riservata a una specifica P.IVA e email admin.\nSe usata da P.IVA diversa → errore 403 INVITE_VAT_MISMATCH." + } + } + ] + }, + { + "name": "2. Lista Licenze", + "item": [ + { + "name": "GET /invites/list — tutte", + "request": { + "method": "GET", + "header": [{ "key": "X-API-Key", "value": "{{api_key}}" }], + "url": { + "raw": "{{base_url}}/invites/list?limit=50", + "host": ["{{base_url}}"], + "path": ["invites","list"], + "query": [ + { "key": "limit", "value": "50" }, + { "key": "offset", "value": "0", "disabled": true } + ] + }, + "description": "Lista tutte le licenze. Filtrabile per status e channel.\nCampi risposta: id, token_prefix, plan, status, used_count, max_uses, max_users_per_org, price_eur, reseller_name, expires_at, channel, label, issued_to, used_by_org_id." + } + }, + { + "name": "GET /invites/list — solo pending (attive)", + "request": { + "method": "GET", + "header": [{ "key": "X-API-Key", "value": "{{api_key}}" }], + "url": { + "raw": "{{base_url}}/invites/list?status=pending&limit=100", + "host": ["{{base_url}}"], + "path": ["invites","list"], + "query": [ + { "key": "status", "value": "pending" }, + { "key": "limit", "value": "100" } + ] + } + } + }, + { + "name": "GET /invites/list — per canale ecommerce", + "request": { + "method": "GET", + "header": [{ "key": "X-API-Key", "value": "{{api_key}}" }], + "url": { + "raw": "{{base_url}}/invites/list?channel=ecommerce&status=pending", + "host": ["{{base_url}}"], + "path": ["invites","list"], + "query": [ + { "key": "channel", "value": "ecommerce" }, + { "key": "status", "value": "pending" } + ] + } + } + }, + { + "name": "GET /invites/{id} — dettaglio singolo", + "request": { + "method": "GET", + "header": [{ "key": "X-API-Key", "value": "{{api_key}}" }], + "url": { + "raw": "{{base_url}}/invites/{{invite_id}}", + "host": ["{{base_url}}"], + "path": ["invites","{{invite_id}}"] + }, + "description": "Dettaglio completo. Se usata, include used_by_org con name/sector/nis2_entity_type." + } + } + ] + }, + { + "name": "3. Revoca / Rigenera", + "item": [ + { + "name": "DELETE /invites/{id} — revoca", + "request": { + "method": "DELETE", + "header": [{ "key": "X-API-Key", "value": "{{api_key}}" }], + "url": { + "raw": "{{base_url}}/invites/{{invite_id}}", + "host": ["{{base_url}}"], + "path": ["invites","{{invite_id}}"] + }, + "description": "Revoca la licenza. Operazione non reversibile.\nLa licenza passa a status=revoked. Eventuali aziende già provisionate rimangono attive (il provisioning è già avvenuto).\nNon si può revocare una licenza già usata (status=used)." + } + }, + { + "name": "POST /invites/{id}/regenerate — nuovo token", + "request": { + "method": "POST", + "header": [{ "key": "X-API-Key", "value": "{{api_key}}" }], + "url": { + "raw": "{{base_url}}/invites/{{invite_id}}/regenerate", + "host": ["{{base_url}}"], + "path": ["invites","{{invite_id}}","regenerate"] + }, + "description": "Genera un nuovo token invalidando il vecchio. Utile se il token è stato inviato per errore.\nRisposta: { id, token, token_prefix, warning }. Il nuovo token è visibile una sola volta." + } + } + ] + }, + { + "name": "4. Validazione pubblica (no auth)", + "item": [ + { + "name": "GET /invites/validate?token= — anteprima invito", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/invites/validate?token=inv_INSERISCI_TOKEN_QUI", + "host": ["{{base_url}}"], + "path": ["invites","validate"], + "query": [{ "key": "token", "value": "inv_INSERISCI_TOKEN_QUI" }] + }, + "description": "Endpoint pubblico — nessuna auth richiesta.\nUsato da lg231 prima del provisioning e dalla pagina onboarding per mostrare l'anteprima piano.\nRisposta: { valid, plan, duration_months, expires_at, remaining_uses, max_users_per_org, plan_features[] }" + } + } + ] + }, + { + "name": "5. Provisioning (per lg231 / e-commerce)", + "item": [ + { + "name": "POST /services/provision — attivazione con invite_token", + "request": { + "method": "POST", + "header": [{ "key": "Content-Type", "value": "application/json" }], + "body": { + "mode": "raw", + "raw": "{\n \"invite_token\": \"inv_INSERISCI_TOKEN_RICEVUTO\",\n \"company\": {\n \"ragione_sociale\": \"Acme S.r.l.\",\n \"partita_iva\": \"02345678901\",\n \"ateco_code\": \"62.01.00\",\n \"sector\": \"ict\"\n },\n \"admin\": {\n \"email\": \"ciso@acme.it\",\n \"first_name\": \"Marco\",\n \"last_name\": \"Rossi\"\n },\n \"caller\": {\n \"system\": \"mktg-agile\",\n \"callback_url\": \"https://mktg.agile.software/api/webhooks/nis2-provisioned\"\n }\n}" + }, + "url": { + "raw": "{{base_url}}/services/provision", + "host": ["{{base_url}}"], + "path": ["services","provision"] + }, + "description": "Attiva automaticamente una licenza usando il token ricevuto.\nNON richiede X-API-Key — l'invite_token è l'auth.\nRisposta: org_id, api_key, access_token (JWT 2h), temp_password, license_expires_at, dashboard_url." + } + } + ] + } + ] +} diff --git a/public/mktg-api-doc.html b/public/mktg-api-doc.html new file mode 100644 index 0000000..8dc30b1 --- /dev/null +++ b/public/mktg-api-doc.html @@ -0,0 +1,365 @@ + + + + + +NIS2 Agile — License API · Documentazione Marketing + + + + +
+ +
+

NIS2 Agile — License API

+

Documentazione completa per l'integrazione da mktg-agile e sistemi e-commerce.
+ Risponde alle domande del team marketing su endpoint, autenticazione e risposta API.

+
+ REST / JSON + HTTPS + API Key + v1.0 + + ⬇ Postman Collection (JSON) + + + 🖥 Pannello Licenze (web) + +
+
+ + +

1 API di creazione licenza

+
+
+ POST + https://nis2.agile.software/api/invites/create +
+ +
// Header richiesti:
+Content-Type: application/json
+X-API-Key: nis2_LA_TUA_CHIAVE  ← vedi sezione 2
+
+// Body (tutti i campi disponibili):
+{
+  "plan":               "professional",  // essentials | professional | enterprise
+  "duration_months":    12,               // durata licenza dopo attivazione
+  "invite_expires_days": 30,               // giorni per attivare l'invito
+  "max_uses":           1,                // n. aziende che possono usarlo (1=singolo)
+  "max_users_per_org":  10,               // max utenti per azienda (ometti = illimitato)
+  "label":             "NIS2 Pro — Ordine MKT-001",  // etichetta interna
+  "channel":           "ecommerce",      // ecommerce|lg231|direct|reseller|manual
+  "issued_to":         "cliente@co.it",  // email o nome destinatario
+  "reseller_name":     "Partner XYZ",    // riferimento reseller (solo vostro)
+  "price_eur":         990.00,           // prezzo applicato (solo riferimento)
+  "notes":             "Campagna Q1 2026", // note interne marketing
+  "quantity":          1,                // genera N token in batch (default 1, max 50)
+  // OPZIONALE — restrizioni di sicurezza:
+  "restrict_vat":      "02345678901",    // solo questa P.IVA può usarlo
+  "restrict_email":    "ciso@co.it"      // solo questo admin può usarlo
+}
+ +
+ Campi obbligatori: plan, label. Tutti gli altri sono opzionali con valori di default ragionevoli. +
+
+ + +

2 Autenticazione

+
+

+ Ogni chiamata da mktg-agile deve includere l'header X-API-Key con una chiave dedicata. +

+ +

Come ottenere la chiave API

+
    +
  1. Vai su nis2.agile.software/licenseExt.html → login con account super_admin
  2. +
  3. Naviga su Settings → API Keys → Nuova API Key
  4. +
  5. Nome: mktg-agile integration → Scope: admin:licenses
  6. +
  7. Copia la chiave — formato nis2_xxxx... — visibile una sola volta
  8. +
  9. Salvala in mktg-agile come variabile d'ambiente: NIS2_API_KEY=nis2_xxxx...
  10. +
+ +
# Chiamata da mktg-agile (curl):
+curl -X POST https://nis2.agile.software/api/invites/create \
+  -H "X-API-Key: nis2_LA_TUA_CHIAVE" \
+  -H "Content-Type: application/json" \
+  -d '{"plan":"professional","duration_months":12,"label":"Test"}'
+ + + + + + + +
ScopeTipoPermessi
admin:licenses CONSIGLIATOLettura + ScritturaCrea, lista, revoca, rigenera licenze
read:all SOLA LETTURASolo letturaLista e dettaglio licenze, nessuna modifica
+ +
+ Alternativa JWT: se preferisci usare un JWT (es. per test manuali), fai prima + POST /api/auth/login con le credenziali super_admin e usa il token restituito + come Authorization: Bearer eyJ.... Valido 2 ore. +
+
+ + +

3 Response alla creazione

+
+

HTTP 201. JSON. Array invites — uno per token generato.

+ +
{
+  "success": true,
+  "data": {
+    "invites": [
+      {
+        "id":              42,
+        "token":          "inv_a1b2c3d4e5f6...",   ← SALVA SUBITO — non recuperabile
+        "token_prefix":  "inv_a1b2c3...",          ← riferimento visivo (non il token completo)
+        "plan":          "professional",
+        "duration_months": 12,
+        "max_uses":        1,
+        "max_users_per_org": 10,
+        "price_eur":      990.00,
+        "expires_at":     "2026-04-07 14:00:00",   ← entro cui attivare
+        "channel":        "ecommerce",
+        "label":          "NIS2 Pro — Ordine MKT-001",
+        "issued_to":      "cliente@co.it",
+        "invite_url":     "https://nis2.agile.software/onboarding.html?invite=inv_a1b2...",
+        "provision_hint": "POST /api/services/provision con invite_token: ..."
+      }
+    ],
+    "count":   1,
+    "warning": "Salva i token subito — non saranno più visibili in chiaro."
+  }
+}
+ + + + + + + + + +
CampoUso
tokenIl token completo inv_xxx — consegnarlo al cliente / lg231. Non viene mai restituito di nuovo.
invite_urlURL diretto per onboarding browser — il cliente apre questo link e si registra
expires_atEntro quando il cliente deve attivare. Dopo questa data il token non funziona.
idID numerico per revoca / dettaglio successivo
+ +
+ Export CSV +

+ Il pannello licenseExt.html ha un bottone + Esporta CSV che scarica tutte le licenze generate nella sessione. + In alternativa, esporta programmaticamente da GET /api/invites/list e converti. +

+
+
+ + +

4 Lista licenze

+
+
+ GET + https://nis2.agile.software/api/invites/list +
+ +
# Parametri query (tutti opzionali):
+?status=pending       // pending | used | expired | revoked
+&channel=ecommerce    // ecommerce | lg231 | direct | reseller | manual
+&limit=50             // default 50, max 100
+&offset=0             // paginazione
+
+# Esempio — tutte le licenze ecommerce attive:
+GET /api/invites/list?channel=ecommerce&status=pending
+X-API-Key: nis2_LA_TUA_CHIAVE
+ +

+ Risposta: { invites: [...], total: N, limit: 50, offset: 0 }
+ Ogni licenza include tutti i campi tranne il token in chiaro (non recuperabile dopo creazione). +

+ +
+ GET + https://nis2.agile.software/api/invites/{id} +
+

Dettaglio singola licenza. Se è stata usata, include used_by_org con nome azienda e settore.

+
+ + +

5 Revoca / modifica

+
+
+ DELETE + https://nis2.agile.software/api/invites/{id} +
+

+ Revoca la licenza (status → revoked). Il record rimane in DB per audit. +

+ +
+ POST + https://nis2.agile.software/api/invites/{id}/regenerate +
+

+ Genera un nuovo token invalidando il precedente. Stessa configurazione (piano, durata, scadenza). Usare se il token è stato inviato per errore o compromesso prima dell'uso. +

+ + + + + + + + + +
Stato licenzaRevocabileRigenerabileNote
pending✓ Sì✓ SìNormale
used✗ No✗ NoGià attivata — azienda rimane attiva
expired✓ Sì✓ SìScaduta ma non usata
revoked✓ SìGià revocata
+ +
+ Importante: revocare una licenza blocca solo future attivazioni. + Le aziende già provisionate con quella licenza rimangono operative fino a license_expires_at. + La revoca non disattiva retroattivamente nessun cliente. +
+
+ + +

6 La seconda pagina (licenseExt.html)

+
+

+ È il pannello admin lato marketing — non il cliente. +

+ + + + + + + + + + + + + + + + + + + + +
PaginaAudienceFunzione
licenseExt.htmlMarketing / Admin NIS2Genera licenze, vede stats (aziende provisionate, utenti coinvolti), revoca, esporta CSV
onboarding.html?invite=inv_xxxCliente finaleUsa il token ricevuto per attivare la propria azienda in NIS2 (wizard 5 step)
integrazioniext.htmlPartner tecnici (lg231, e-commerce)Documentazione flow completo, provisioning automatico B2B, webhook
+ +
+ licenseExt.html richiede login con credenziali super_admin. + Non è pubblica — accedervi comunicando l'URL ai soli membri del team autorizzati. +
+
+ + +

Riepilogo rapido

+
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ BASE URL  https://nis2.agile.software/api
+ AUTH       X-API-Key: nis2_xxxx  (scope: admin:licenses)
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+POST   /invites/create              Crea licenza/e
+GET    /invites/list                Lista licenze (filtri: status, channel)
+GET    /invites/{id}                Dettaglio singola
+DELETE /invites/{id}                Revoca
+POST   /invites/{id}/regenerate    Nuovo token (stesso config)
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ PUBBLICO (no auth):
+GET    /invites/validate?token=inv_ Anteprima invito (piano, scadenza)
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ COSA CONTROLLA NIS2 (non il mktg):
+ ✓ N. aziende attivabili  → max_uses
+ ✓ Tempo validità invito  → invite_expires_days → expires_at
+ ✓ Durata licenza         → duration_months → license_expires_at
+ ✓ N. utenti per azienda  → max_users_per_org → license_max_users
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ PROVA IN 30 SECONDI:
+curl https://nis2.agile.software/api/services/status
+
+ +
+ NIS2 Agile v1.0 · Pannello Licenze · + Guida Partner · + Postman Collection +
+ +
+ + +