[MKTG-API] Documentazione + auth API Key per licenze
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
cb0988da27
commit
d407fd0510
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user