[INVITE] Sistema inviti/licenze B2B + provisioning con invite_token
- InviteController: CRUD inviti (gen, list, show, revoke, rigenera, validate) - Token inv_* sha256-hashed, one-shot o multi-use, canali, scadenza - ServicesController::provision() accetta invite_token al posto di X-Provision-Secret Piano e durata forzati dall'invito, markUsed() chiaamto dopo provisioning riuscito - index.php: routing /api/invites/* aggiunto (controller + action map) - integrazioniext.html: nuovo tab "Inviti & Licenze" con flow completo, endpoints, esempi curl/php, guida lg231 aggiornata con sezione provisioning automatico Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6933e1d3fb
commit
612befd66d
+180
-3
@@ -85,6 +85,13 @@
|
||||
padding: .15rem .4rem; border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.method-delete {
|
||||
display: inline-block;
|
||||
background: rgba(239,68,68,.15); color: #ef4444;
|
||||
font-size: .65rem; font-weight: 700;
|
||||
padding: .15rem .4rem; border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #0f172a;
|
||||
@@ -247,6 +254,7 @@
|
||||
<!-- Tabs -->
|
||||
<div class="tab-nav">
|
||||
<button class="tab-btn active" onclick="showTab('api')">Services API</button>
|
||||
<button class="tab-btn" onclick="showTab('invites')">Inviti & Licenze</button>
|
||||
<button class="tab-btn" onclick="showTab('lg231')">Guida lg231</button>
|
||||
<button class="tab-btn" onclick="showTab('webhook')">Webhook</button>
|
||||
<button class="tab-btn" onclick="showTab('quickstart')">Quick Start</button>
|
||||
@@ -310,6 +318,153 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Inviti & Licenze -->
|
||||
<div id="tab-invites" class="tab-pane">
|
||||
<div class="callout">
|
||||
<strong>Sistema Inviti / Licenze B2B:</strong> NIS2 Agile genera token di invito che abilitano
|
||||
il provisioning automatico di organizzazioni e utenti. L'e-commerce o il partner riceve l'invito,
|
||||
lo consegna al cliente finale (es. lg231), che lo usa per attivarsi automaticamente.
|
||||
L'accesso si interrompe alla scadenza dell'invito.
|
||||
</div>
|
||||
|
||||
<div class="section-title">Flusso completo</div>
|
||||
<div class="code-block" style="font-family:monospace;line-height:1.8;font-size:.82rem">
|
||||
<span style="color:#22c55e">1. GENERAZIONE</span> NIS2 Admin (super_admin)
|
||||
POST /api/invites → { token: "inv_abc123...", expires_at, plan, max_uses }
|
||||
<span class="cmt">Il token è visibile UNA SOLA VOLTA. Solo hash SHA-256 in DB.</span>
|
||||
|
||||
<span style="color:#06b6d4">2. DISTRIBUZIONE</span> NIS2 → E-commerce → lg231
|
||||
NIS2 Admin passa inv_abc123... all'e-commerce
|
||||
E-commerce lo consegna al cliente con l'ordine
|
||||
lg231 lo riceve (es. in metadata ordine)
|
||||
|
||||
<span style="color:#a78bfa">3. VALIDAZIONE</span> lg231 prima del provisioning
|
||||
GET /api/invites/validate?token=inv_abc123...
|
||||
→ { valid: true, plan: "professional", expires_at, remaining_uses, plan_features }
|
||||
|
||||
<span style="color:#f59e0b">4. PROVISIONING</span> lg231 attiva automaticamente
|
||||
POST /api/services/provision (body include "invite_token": "inv_abc123...")
|
||||
→ { org_id, api_key, access_token, license_expires_at, temp_password }
|
||||
NIS2 marca l'invito come usato (used_count++)
|
||||
|
||||
<span style="color:#ef4444">5. SCADENZA</span> Accesso automaticamente revocato
|
||||
Quando expires_at raggiunto:
|
||||
- API key rimane ma licenza_expires_at è nel passato
|
||||
- GET /api/invites/validate → { valid: false, code: "EXPIRED" }
|
||||
- Rinnovo: nuovo invito dal NIS2 Admin → nuovo provisioning
|
||||
</div>
|
||||
|
||||
<div class="section-title">API Inviti (solo super_admin)</div>
|
||||
<table class="api-table">
|
||||
<thead><tr><th>Metodo</th><th>Endpoint</th><th>Auth</th><th>Descrizione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span class="method-post">POST</span></td><td><code>/api/invites/create</code></td><td>JWT super_admin</td><td>Genera 1..50 inviti in batch</td></tr>
|
||||
<tr><td><span class="method-get">GET</span></td><td><code>/api/invites/list</code></td><td>JWT super_admin</td><td>Lista con filtri status/channel, auto-scade pending</td></tr>
|
||||
<tr><td><span class="method-get">GET</span></td><td><code>/api/invites/{id}</code></td><td>JWT super_admin</td><td>Dettaglio singolo + org usante</td></tr>
|
||||
<tr><td><span class="method-delete">DELETE</span></td><td><code>/api/invites/{id}</code></td><td>JWT super_admin</td><td>Revoca (non cancella — solo status=revoked)</td></tr>
|
||||
<tr><td><span class="method-post">POST</span></td><td><code>/api/invites/{id}/regenerate</code></td><td>JWT super_admin</td><td>Nuovo token, stessa configurazione</td></tr>
|
||||
<tr><td><span class="method-get">GET</span></td><td><code>/api/invites/validate?token=inv_...</code></td><td>—</td><td>Anteprima pubblica: piano, scadenza, usi rimasti</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Generare un invito (NIS2 Admin)</div>
|
||||
<div class="code-block"><span class="method-post">POST</span> https://nis2.agile.software/api/invites/create
|
||||
Authorization: Bearer {jwt_super_admin}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
<span class="key">"plan"</span>: <span class="str">"professional"</span>, <span class="cmt">// essentials | professional | enterprise</span>
|
||||
<span class="key">"duration_months"</span>: 12, <span class="cmt">// durata licenza dopo attivazione</span>
|
||||
<span class="key">"invite_expires_days"</span>: 30, <span class="cmt">// giorni di validità dell'invito stesso</span>
|
||||
<span class="key">"max_uses"</span>: 1, <span class="cmt">// 1=one-shot, N=batch (es: 10 per reseller)</span>
|
||||
<span class="key">"label"</span>: <span class="str">"lg231 Partner Q1 2026"</span>,
|
||||
<span class="key">"channel"</span>: <span class="str">"lg231"</span>, <span class="cmt">// lg231 | ecommerce | direct | manual</span>
|
||||
<span class="key">"issued_to"</span>: <span class="str">"partner@lg231.it"</span>,
|
||||
<span class="key">"notes"</span>: <span class="str">"Ordine OPP-2026-042"</span>
|
||||
}
|
||||
|
||||
<span class="cmt">// Risposta — token visibile UNA SOLA VOLTA:</span>
|
||||
{
|
||||
<span class="key">"invites"</span>: [{
|
||||
<span class="key">"id"</span>: 7,
|
||||
<span class="key">"token"</span>: <span class="str">"inv_a1b2c3d4e5f6..."</span>, <span class="cmt">← SALVARE SUBITO, non recuperabile</span>
|
||||
<span class="key">"plan"</span>: <span class="str">"professional"</span>,
|
||||
<span class="key">"expires_at"</span>: <span class="str">"2026-04-06 12:00:00"</span>,
|
||||
<span class="key">"invite_url"</span>: <span class="str">"https://nis2.agile.software/onboarding.html?invite=inv_..."</span>
|
||||
}],
|
||||
<span class="key">"warning"</span>: <span class="str">"Salva i token subito — non saranno più visibili in chiaro."</span>
|
||||
}</div>
|
||||
|
||||
<div class="section-title">Provisioning con invito (lg231 → NIS2)</div>
|
||||
<div class="code-block"><span class="cmt">// lg231 usa l'invite_token ricevuto per attivare automaticamente il cliente:</span>
|
||||
<span class="method-post">POST</span> https://nis2.agile.software/api/services/provision
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
<span class="key">"invite_token"</span>: <span class="str">"inv_a1b2c3d4..."</span>, <span class="cmt">← sostituisce X-Provision-Secret</span>
|
||||
<span class="key">"company"</span>: {
|
||||
<span class="key">"ragione_sociale"</span>: <span class="str">"Acme S.r.l."</span>,
|
||||
<span class="key">"partita_iva"</span>: <span class="str">"02345678901"</span>,
|
||||
<span class="key">"ateco_code"</span>: <span class="str">"62.01.00"</span>,
|
||||
<span class="key">"sector"</span>: <span class="str">"ict"</span>
|
||||
},
|
||||
<span class="key">"admin"</span>: {
|
||||
<span class="key">"email"</span>: <span class="str">"ciso@acme.it"</span>,
|
||||
<span class="key">"first_name"</span>: <span class="str">"Marco"</span>,
|
||||
<span class="key">"last_name"</span>: <span class="str">"Rossi"</span>
|
||||
},
|
||||
<span class="key">"caller"</span>: {
|
||||
<span class="key">"system"</span>: <span class="str">"lg231"</span>,
|
||||
<span class="key">"company_id"</span>: 142,
|
||||
<span class="key">"callback_url"</span>: <span class="str">"https://lg231.agile.software/api/integrations/nis2-callback"</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="cmt">// Risposta (il piano e la durata vengono dall'invito, non dal body):</span>
|
||||
{
|
||||
<span class="key">"provisioned"</span>: <span class="kw">true</span>,
|
||||
<span class="key">"org_id"</span>: 8,
|
||||
<span class="key">"api_key"</span>: <span class="str">"nis2_abcdef..."</span>, <span class="cmt">← salvare in lg231 metadata</span>
|
||||
<span class="key">"access_token"</span>: <span class="str">"eyJ..."</span>, <span class="cmt">← JWT 2h per apertura immediata</span>
|
||||
<span class="key">"temp_password"</span>: <span class="str">"NIS2_xxxxxx"</span>, <span class="cmt">← cambio obbligatorio al primo login</span>
|
||||
<span class="key">"invite_plan"</span>: <span class="str">"professional"</span>,
|
||||
<span class="key">"license_expires_at"</span>: <span class="str">"2027-03-07 12:00:00"</span>,
|
||||
<span class="key">"dashboard_url"</span>: <span class="str">"https://nis2.agile.software/dashboard.html"</span>
|
||||
}</div>
|
||||
|
||||
<div class="section-title">Validazione preventiva (opzionale)</div>
|
||||
<div class="code-block"><span class="cmt"># lg231 può validare l'invito prima del provisioning:</span>
|
||||
curl "https://nis2.agile.software/api/invites/validate?token=inv_a1b2c3..."
|
||||
|
||||
<span class="cmt">// Se valido:</span>
|
||||
{ <span class="key">"valid"</span>: <span class="kw">true</span>, <span class="key">"plan"</span>: <span class="str">"professional"</span>, <span class="key">"duration_months"</span>: 12,
|
||||
<span class="key">"expires_at"</span>: <span class="str">"2026-04-06T12:00:00"</span>, <span class="key">"remaining_uses"</span>: 1,
|
||||
<span class="key">"plan_features"</span>: [<span class="str">"Policy Management + AI"</span>, <span class="str">"Supply Chain Assessment"</span>, ...] }
|
||||
|
||||
<span class="cmt">// Se scaduto:</span>
|
||||
{ <span class="key">"valid"</span>: <span class="kw">false</span>, <span class="key">"error"</span>: <span class="str">"Invito scaduto il 2026-03-01 00:00:00"</span>, <span class="key">"code"</span>: <span class="str">"EXPIRED"</span> }
|
||||
|
||||
<span class="cmt">// Se già usato:</span>
|
||||
{ <span class="key">"valid"</span>: <span class="kw">false</span>, <span class="key">"error"</span>: <span class="str">"Invito già utilizzato"</span>, <span class="key">"code"</span>: <span class="str">"ALREADY_USED"</span> }</div>
|
||||
|
||||
<div class="section-title">Gestione ciclo di vita</div>
|
||||
<table class="api-table">
|
||||
<thead><tr><th>Stato invito</th><th>Significato</th><th>Azione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>pending</code></td><td>Valido, non ancora usato</td><td>Usabile per provision</td></tr>
|
||||
<tr><td><code>used</code></td><td>Raggiunto max_uses</td><td>Provisioning bloccato</td></tr>
|
||||
<tr><td><code>expired</code></td><td>expires_at superato</td><td>Provisioning bloccato, rinnovare</td></tr>
|
||||
<tr><td><code>revoked</code></td><td>Revocato da admin</td><td>Provisioning bloccato permanente</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout">
|
||||
<strong>Sicurezza token:</strong> il token <code>inv_</code> viene restituito in chiaro UNA SOLA VOLTA
|
||||
alla creazione. Nel DB è conservato solo il hash SHA-256. Il <code>token_prefix</code> (es: <code>inv_a1b2c3...</code>)
|
||||
è visibile nell'admin panel per identificazione visiva.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Guida lg231 -->
|
||||
<div id="tab-lg231" class="tab-pane">
|
||||
<div class="callout">
|
||||
@@ -427,10 +582,32 @@ $ssoResp = Nis2Client::post(<span class="str">'/sso'</span>, $apiKey, [
|
||||
<code>if (ssoToken) { localStorage.setItem('nis2_token', ssoToken); location.hash = ''; }</code>
|
||||
</div>
|
||||
|
||||
<div class="section-title">5. Provisioning automatico dal portale lg231</div>
|
||||
<div class="callout">
|
||||
Quando un cliente lg231 acquista una licenza NIS2 Agile, lg231 riceve un <strong>invite_token</strong>
|
||||
dall'e-commerce. Con questo token lg231 chiama <code>POST /api/services/provision</code> passando
|
||||
i dati aziendali del cliente. NIS2 crea automaticamente org, admin e API key. Il piano e la
|
||||
durata sono definiti dall'invito (non modificabili dal chiamante).
|
||||
</div>
|
||||
<div class="code-block"><span class="cmt">// Salva in lg231 companies.metadata dopo provisioning:</span>
|
||||
$meta[<span class="str">'nis2_api_key'</span>] = $response[<span class="str">'api_key'</span>];
|
||||
$meta[<span class="str">'nis2_org_id'</span>] = $response[<span class="str">'org_id'</span>];
|
||||
$meta[<span class="str">'nis2_invite_id'</span>] = $response[<span class="str">'invite_id'</span>];
|
||||
$meta[<span class="str">'nis2_license_plan'</span>] = $response[<span class="str">'invite_plan'</span>];
|
||||
$meta[<span class="str">'nis2_expires_at'</span>] = $response[<span class="str">'license_expires_at'</span>];
|
||||
$meta[<span class="str">'nis2_enabled'</span>] = <span class="kw">true</span>;
|
||||
|
||||
<span class="cmt">// Controlla scadenza prima di ogni chiamata:</span>
|
||||
<span class="kw">if</span> (strtotime($meta[<span class="str">'nis2_expires_at'</span>] ?? <span class="str">'1970-01-01'</span>) < time()) {
|
||||
<span class="cmt">// licenza scaduta → mostrare banner rinnovo, non chiamare NIS2</span>
|
||||
<span class="kw">return</span> [<span class="str">'success'</span> => <span class="kw">false</span>, <span class="str">'reason'</span> => <span class="str">'license_expired'</span>];
|
||||
}</div>
|
||||
|
||||
<div class="section-title">Checklist implementazione lg231</div>
|
||||
<ul class="checklist">
|
||||
<li>company-ms: aggiungere <code>nis2_api_key</code>, <code>nis2_org_id</code>, <code>nis2_enabled</code> a provider-config</li>
|
||||
<li>shared: creare <code>Nis2Client</code> (curl wrapper leggero)</li>
|
||||
<li>company-ms: aggiungere <code>nis2_api_key</code>, <code>nis2_org_id</code>, <code>nis2_enabled</code>, <code>nis2_invite_id</code>, <code>nis2_license_plan</code>, <code>nis2_expires_at</code> a provider-config</li>
|
||||
<li>shared: creare <code>Nis2Client</code> (curl wrapper leggero) con check scadenza licenza</li>
|
||||
<li>company-ms: endpoint provisioning — ricevi invite_token, chiama <code>POST /api/services/provision</code>, salva risposta in metadata</li>
|
||||
<li>UI: widget NIS2 score nella company detail (gauge + badge + rischi high)</li>
|
||||
<li>risk-ms: import rischi cyber durante assessment (categoria art.24-bis)</li>
|
||||
<li>monitoring-ms: escalation incidenti significativi → odv_activity</li>
|
||||
@@ -571,7 +748,7 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
||||
<script>
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||
const ids = ['api', 'lg231', 'webhook', 'quickstart'];
|
||||
const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart'];
|
||||
b.classList.toggle('active', ids[i] === id);
|
||||
});
|
||||
document.querySelectorAll('.tab-pane').forEach(p => {
|
||||
|
||||
Reference in New Issue
Block a user