From d45bd472cd7e8fdf379d8d975d2f0e0fbc506a35 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Tue, 23 Jun 2026 19:50:50 +0200 Subject: [PATCH] [FIX] Vocea scaffold: rotta tenantInfo non versionata + portalUrl reale + namespace vault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correzioni dallo smoke live su api.vocea.cloud (23/6, VIGILE/Vocea): - tenantInfo(): /tenants/{slug}/info è sotto /api/wb (NON /api/wb/v1) → request() ora accetta baseOverride; aggiunto $apiBase (base senza /vN). I 3 /integrations/* restano /v1. - portalUrl(): path reale /{locale}/segnala/{slug} (era /wb/{slug}); aggiunto portalStatusUrl() /{locale}/stato/{slug}; channelStatus espone portal_status_url. - Vault namespace corretto: tier1__nis2-app__vocea (l'app reale è nis2-app, non nis2-agile). Scaffold resta DORMIENTE (VOCEA_ENABLED=false). NB: VIGILE segnala che le route /api/wb/v1/integrations sono ancora 404 (deploy MS incompleto) → non attivare finché Vocea non completa il deploy e l'API key non è nel vault. Co-Authored-By: Claude Opus 4.8 (1M context) --- application/config/config.php | 2 +- .../controllers/WhistleblowingController.php | 1 + application/services/VoceaService.php | 31 +++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/application/config/config.php b/application/config/config.php index b25a380..9cf661b 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -108,7 +108,7 @@ define('ANTHROPIC_MAX_TOKENS', Env::int('ANTHROPIC_MAX_TOKENS', 4096)); // ═══════════════════════════════════════════════════════════════════════════ // SCAFFOLD DORMIENTE: con VOCEA_ENABLED=false (default) VoceaService non effettua // nessuna chiamata di rete. Attivazione a MS live: applicare migrate_063 + -// VOCEA_ENABLED=true + VOCEA_API_KEY (vault tier1__nis2-agile__vocea__*) + +// VOCEA_ENABLED=true + VOCEA_API_KEY (vault tier1__nis2-app__vocea__*) + // slug del canale su organizations.vocea_tenant_slug (status='active'). define('VOCEA_ENABLED', filter_var(Env::get('VOCEA_ENABLED', 'false'), FILTER_VALIDATE_BOOLEAN)); define('VOCEA_BASE_URL', Env::get('VOCEA_BASE_URL', 'https://api.vocea.cloud/api/wb/v1')); // API integratori (X-API-Key) diff --git a/application/controllers/WhistleblowingController.php b/application/controllers/WhistleblowingController.php index 2b011c6..8d2711d 100644 --- a/application/controllers/WhistleblowingController.php +++ b/application/controllers/WhistleblowingController.php @@ -419,6 +419,7 @@ class WhistleblowingController extends BaseController 'channel_status' => $status, 'provisioned' => $enabled && !empty($slug) && $status === 'active', 'portal_url' => !empty($slug) ? VoceaService::portalUrl($slug) : null, + 'portal_status_url' => !empty($slug) ? VoceaService::portalStatusUrl($slug) : null, ]); } diff --git a/application/services/VoceaService.php b/application/services/VoceaService.php index 43ec4b8..2f4c129 100644 --- a/application/services/VoceaService.php +++ b/application/services/VoceaService.php @@ -9,7 +9,7 @@ * SCAFFOLD DORMIENTE: finché VOCEA_ENABLED=false (default) ogni metodo che richiede * rete ritorna ['ok'=>false,'error'=>'DISABLED']. Si "accende" così: * 1. applicare migrate_063_vocea_channel.php (colonne organizations.vocea_*) - * 2. VOCEA_ENABLED=true + VOCEA_API_KEY (vault tier1__nis2-agile__vocea__*) + * 2. VOCEA_ENABLED=true + VOCEA_API_KEY (vault tier1__nis2-app__vocea__*) * 3. impostare organizations.vocea_tenant_slug + vocea_channel_status='active' sul canale * * CONFINE ZERO-KNOWLEDGE: l'API NON ritorna MAI contenuti in chiaro. listReports @@ -32,11 +32,14 @@ class VoceaService private const CONNECT_TIMEOUT_SEC = 6; private string $baseUrl; + private string $apiBase; // base NON versionata (endpoint pubblici tipo /tenants/{slug}/info) private string $apiKey; public function __construct(?string $apiKey = null) { $this->baseUrl = rtrim(defined('VOCEA_BASE_URL') ? VOCEA_BASE_URL : 'https://api.vocea.cloud/api/wb/v1', '/'); + // gli endpoint /integrations/* sono versionati (/v1); gli endpoint pubblici (/tenants/{slug}/info) NO. + $this->apiBase = preg_replace('#/v\d+$#', '', $this->baseUrl); $this->apiKey = $apiKey ?? (defined('VOCEA_API_KEY') ? VOCEA_API_KEY : ''); } @@ -56,22 +59,31 @@ class VoceaService } /** - * URL del portale segnalante da embeddare via wb-link.js. + * URL del portale segnalante (per embed via wb-link.js). * NB: iframe diretto è bloccato dalla CSP `frame-ancestors 'self'` di Vocea → * usare il widget wb-link.js oppure far allargare la CSP al dominio NIS2. + * Path reale (confermato smoke 23/6): /{locale}/segnala/{slug}. */ - public static function portalUrl(string $slug): string + public static function portalUrl(string $slug, string $locale = 'it'): string { $base = rtrim(defined('VOCEA_PORTAL_URL') ? VOCEA_PORTAL_URL : 'https://app.vocea.cloud', '/'); - return "{$base}/wb/" . rawurlencode($slug); + return "{$base}/" . rawurlencode($locale) . '/segnala/' . rawurlencode($slug); + } + + /** URL portale per il tracciamento stato di una segnalazione (/{locale}/stato/{slug}). */ + public static function portalStatusUrl(string $slug, string $locale = 'it'): string + { + $base = rtrim(defined('VOCEA_PORTAL_URL') ? VOCEA_PORTAL_URL : 'https://app.vocea.cloud', '/'); + return "{$base}/" . rawurlencode($locale) . '/stato/' . rawurlencode($slug); } // ── Read / management (server-to-server, API key) ──────────────────────── - /** Public key del canale (endpoint pubblico, no API key). */ + /** Public key del canale (endpoint pubblico NON versionato, no API key). */ public function tenantInfo(string $slug): array { - return $this->request('GET', '/tenants/' . rawurlencode($slug) . '/info', null, false); + // /tenants/{slug}/info vive sotto /api/wb (NON /api/wb/v1) — confermato smoke 23/6. + return $this->request('GET', '/tenants/' . rawurlencode($slug) . '/info', null, false, $this->apiBase); } /** Lista metadati segnalazioni del canale (paginata, SENZA plaintext). */ @@ -107,21 +119,22 @@ class VoceaService /** * @param bool $auth Se true invia X-API-Key (default). Endpoint pubblici (tenantInfo) → false. + * @param ?string $baseOverride Base URL alternativa (es. NON versionata per /tenants/{slug}/info). * @return array ['ok'=>bool,'status'=>int,'data'=>mixed,'error'=>?string] */ - private function request(string $method, string $path, ?array $body = null, bool $auth = true): array + private function request(string $method, string $path, ?array $body = null, bool $auth = true, ?string $baseOverride = null): array { if (!self::enabled()) { return ['ok' => false, 'error' => 'DISABLED', 'message' => 'Integrazione Vocea non attiva (VOCEA_ENABLED=false).']; } if ($auth && $this->apiKey === '') { - return ['ok' => false, 'error' => 'NOT_PROVISIONED', 'message' => 'VOCEA_API_KEY non configurata (vault tier1__nis2-agile__vocea__*).']; + return ['ok' => false, 'error' => 'NOT_PROVISIONED', 'message' => 'VOCEA_API_KEY non configurata (vault tier1__nis2-app__vocea__*).']; } $headers = ['Accept: application/json']; if ($auth) { $headers[] = 'X-API-Key: ' . $this->apiKey; } - $ch = curl_init($this->baseUrl . $path); + $ch = curl_init(($baseOverride ?? $this->baseUrl) . $path); $opts = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method,