# Incorporare una dashboard

Incorporare è il momento in cui una dashboard smette di essere qualcosa che il tuo team apre e diventa parte di un prodotto che i tuoi clienti usano. Vibedasher ha due modi per farlo, e la differenza non è cosmetica — decide chi può leggere i dati.

|                                                | Corsia pubblica      | Corsia a token firmato                                             |
| ---------------------------------------------- | -------------------- | ------------------------------------------------------------------ |
| `view_mode` della dashboard                    | `public`             | `protected`                                                        |
| Chi può visualizzare                           | chiunque abbia l'URL | solo un visualizzatore per cui il tuo backend ha generato un token |
| Filtraggio per riga per singolo visualizzatore | no                   | **sì**, applicato lato server                                      |
| Cosa incorpori                                 | un URL               | un URL più un token di breve durata                                |

Una dashboard `private` non è incorporabile da nessuna delle due corsie. Entrambe le corsie richiedono che la dashboard sia prima **pubblicata** — vedi [Pubblica e condividi](/it/guides/publish-and-share.md).

Incorporare è una delle due risposte

Incorporare mette la *nostra* dashboard renderizzata dentro un rettangolo della tua pagina. L'altra risposta è [espellere](/it/guides/eject-your-dashboard.md) — prendere i sorgenti React e SQL e renderizzarli tu, in modo nativo, con noi fuori dal percorso di rendering e solo il motore dati sottostante. Quale delle due vuoi lo decidono soprattutto due domande: visualizzatori diversi devono vedere righe diverse (incorporare è la sola corsia che lo fa), e la dashboard deve avere l'aspetto e il comportamento del resto della tua app (solo l'espulsione te lo dà). I compromessi sono messi a confronto in **[Incorporare o espellere](/it/guides/embed-or-eject.md)**.

## La corsia pubblica[​](#la-corsia-pubblica "Link diretto a La corsia pubblica")

Se la dashboard è pubblicata con `view_mode: public`, un iframe è l'intera integrazione:

```
<iframe

  src="https://cloud.vibedasher.com/v/<slug>"

  width="100%"

  height="800"

  style="border: 0;"

  title="Vibedasher dashboard"

  sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"

  allowfullscreen

  loading="lazy"

></iframe>
```

`allow-same-origin` sembra allarmante accanto ad `allow-scripts` ed è sicuro qui specificamente perché il frame è **cross-origin rispetto alla tua pagina**: concede al renderer la propria origine, non la tua. `allow-top-navigation` è deliberatamente assente, quindi il frame non può far navigare via la tua pagina. `allow-downloads` è ciò che fa funzionare l'esportazione PNG dei grafici.

Lo snippet funziona da qualunque pagina tu controlli — dalla nostra parte non c'è nulla da configurare prima. L'audience sotto cui pubblichi è il controllo che conta qui, e [Il confine del frame](#the-boundary-of-the-embed) è dove quella scelta viene esposta.

La finestra di dialogo Condividi nell'app ti fornisce questo snippet già precompilato. Prendilo da lì invece di assemblare l'URL a mano.

## La corsia a token firmato[​](#la-corsia-a-token-firmato "Link diretto a La corsia a token firmato")

Questa è quella che vale la pena capire, perché è l'unico posto in cui Vibedasher applica la **sicurezza per riga**.

### Il tuo backend genera il token — tu non firmi nulla[​](#il-tuo-backend-genera-il-token--tu-non-firmi-nulla "Link diretto a Il tuo backend genera il token — tu non firmi nulla")

Vibedasher firma il token stesso, ES256, con una propria chiave rotante. Il tuo backend ne richiede uno con la sua chiave API:

```
from vibedasher.models import EmbedTokenRequest, EmbedTokenRequestFilters



resp = client.embed_tokens.create_embed(

    EmbedTokenRequest(

        viz_id=1234,

        filters=EmbedTokenRequestFilters.from_dict({"region": "EMEA"}),

        ttl_seconds=600,

    )

)

print(resp.token, resp.expires_in)
```

```
const resp = await client.embedTokens.createEmbed({

  vizId: 1234,

  filters: { region: 'EMEA' },

  ttlSeconds: 600,

});
```

Non esiste alcun "embed secret"

Non possiedi alcun materiale di firma e non ne hai bisogno — genera i token come mostrato qui.

Oppure in HTTP semplice, da qualsiasi linguaggio:

```
curl -X POST https://api.vibedasher.com/api/v1/embed/token \

  -H "X-Api-Key: $VIBEDASHER_API_KEY" \

  -H "Content-Type: application/json" \

  -d '{"vizId": 1234, "ttlSeconds": 600, "filters": {"region": "EMEA"}}'

# → {"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600}
```

Il corpo della richiesta accetta:

| Campo        | Obbligatorio | Note                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vizId`      | **sì**       | Esattamente una dashboard. Un token non può coprire una cartella o un account.                                                                                                                                                                                                                                                                                                                                                                        |
| `filters`    | no           | I filtri per riga. Vedi sotto.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `viewerId`   | no           | Un id di posto opaco. **Solo per audit e metering — non è un input di controllo degli accessi.** Non farci affidamento per delimitare nulla.                                                                                                                                                                                                                                                                                                          |
| `ttlSeconds` | no           | Predefinito **600**. Minimo 60, massimo rigido 86 400 (24 h). I valori fuori intervallo vengono limitati alla generazione *e* riverificati alla verifica. La verifica ammette un piccolo margine di scarto dell'orologio oltre `exp`, limitato a 120 s **e** alla metà della durata di vita del token stesso: la durata effettivamente applicata non supera mai 1,5× `expires_in` e, da un TTL di 240 s in su, il margine è il valore fisso di 120 s. |
| `apiKeyId`   | no           | Necessario solo quando si genera da una sessione browser, per vincolare il token alla lista consentita di domini di una chiave e alla sua revoca.                                                                                                                                                                                                                                                                                                     |

L'`account_id` viene impresso dalla tua credenziale e non viene mai letto dal corpo.

La generazione fallisce con un messaggio che nomina il controllo fallito — stai generando un token, non presentandolo, quindi l'errore ti dice cosa cambiare nella dashboard o nella chiave:

| Stato | Dettaglio                                                                                      | Rimedio                                                                        |
| ----- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `403` | la visualizzazione appartiene a un altro account                                               | genera con una chiave API dell'account proprietario                            |
| `403` | la visualizzazione non è incorporabile con un token: la sua visibilità deve essere `protected` | imposta la visibilità della dashboard su **Protetta** nella finestra Condividi |
| `404` | la visualizzazione non è pubblicata                                                            | pubblicala prima                                                               |
| `403` | nessuna chiave API utilizzabile a cui legare il token                                          | la chiave manca, è inattiva, scaduta o appartiene a un altro account           |
| `404` | Viz non trovata                                                                                | controlla `vizId`                                                              |

**Una dashboard `public` viene rifiutata da questa corsia** — ha già la corsia anonima, e mescolare le due sarebbe un modo per credere di avere filtri per riga quando non è così.

### Filtri per riga[​](#filtri-per-riga "Link diretto a Filtri per riga")

`filters` è una mappa da nome di colonna a uno scalare o una lista. Uno scalare diventa `=`, una lista diventa `IN`, e più chiavi sono combinate con AND. Le chiavi sono il `sqlName` della colonna così come lo riporta `GET /v1/datasets/{id}` — il nome che la vista SQL espone, non il nome visualizzato.

La piattaforma non aggiunge una clausola `WHERE` alla tua query; **riscrive ogni riferimento a un dataset in una relazione filtrata**, mantenendo l'alias originale:

```
FROM orders

  ->  FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders
```

È questo che rende l'ambito **non allargabile**, e non semplicemente presente. Ciò che la pagina chiede si applica *all'esterno* della relazione filtrata, quindi può solo restringerla ulteriormente — `WHERE tenant = 'globex' OR 1=1` vede comunque un solo tenant, una sottoquery avvolgente avvolge la relazione filtrata, entrambi i rami di una `UNION` vengono riscritti, e una CTE che maschera l'alias, un riferimento qualificato da schema e qualsiasi funzione che legga file sono rifiutati senza appello.

Tre proprietà lo rendono verificabile:

* **I filtri provengono solo dal token verificato.** Non vengono mai letti dal corpo della richiesta, quindi uno spettatore che curiosa nella scheda di rete non può allargare il proprio ambito.
* **Una chiave di filtro che non nomina alcuna colonna dei dataset della dashboard viene rifiutata all'emissione** — `403`, nominando la chiave ed elencando i nomi di colonna che esistono davvero. Nessun token viene firmato. Lo stesso vale per un valore che non può essere convertito nel tipo della sua colonna. Un filtro che corrispondesse silenziosamente a tutto sarebbe una fuga di dati travestita da controllo di sicurezza: viene quindi rifiutato prima di poter diventare un link che consegni a un cliente.
* **L'emissione ti dice cosa permette il token**, nel campo `scope` della sua risposta. Leggilo; non darlo per scontato.

#### Leggi `scope.fullyScoped` — non significa «i filtri sono stati accettati»[​](#read-fullyscoped "Link diretto a read-fullyscoped")

Una dashboard può leggere più di un dataset, e un filtro può applicarsi solo a un dataset che **possiede quella colonna** — non si può filtrare su una colonna che una tabella non ha. Così una dashboard su `orders` e `price_list`, con ambito `{"tenant": "acme"}`, filtra `orders` e legge `price_list` **per intero**. La pagina si carica, ogni riquadro si disegna, ogni numero è plausibile, e nella richiesta non c'è nulla che appaia sbagliato.

È corretto e voluto quando il secondo dataset è una tabella di lookup condivisa — un listino prezzi, un catalogo di piani, un insieme di etichette. **Non** è ciò che vuoi quando il secondo dataset contiene righe per cliente sotto una colonna con un nome diverso (`tenant_id` invece di `tenant`, `customer`, `org`). La risposta dell'emissione è dove scopri in quale dei due casi ti trovi:

```
{

  "token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600,

  "scope": {

    "vizId": 1234,

    "filtered":   [{"datasetId": 7, "datasetName": "orders",

                    "predicate": "\"tenant\" = 'acme'", "columns": ["tenant"]}],

    "unfiltered": [{"datasetId": 8, "datasetName": "price_list",

                    "predicate": null, "columns": []}],

    "unresolvedDatasetIds": [],

    "fullyScoped": false,

    "meaning": "PARTIALLY SCOPED — read this before handing the link out. ..."

  }

}
```

| Campo                  | Cosa farne                                                                                                                                                                             |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fullyScoped`          | **Quello su cui asserire.** `true` solo quando ogni dataset che la dashboard legge è delimitato da questi filtri. Tutto il resto è `false`.                                            |
| `filtered[].predicate` | L'SQL esatto applicato a quel dataset. È la frase da mettere davanti a un revisore di sicurezza.                                                                                       |
| `unfiltered[]`         | Dataset che questo token legge **per intero**. Decidi, per ciascuno, se sia una tabella condivisa o una falla.                                                                         |
| `unresolvedDatasetIds` | Dataset il cui schema non è stato leggibile. Non è la stessa cosa di «non filtrato»: «legge tutto» e «non sappiamo cosa legga» sono risposte diverse, e nessuna viene fusa nell'altra. |

Un controllo di produzione ragionevole nella tua route di emissione è rifiutare di restituire il token quando `scope.fullyScoped` è `false` e ti aspettavi `true` — tu sai quali delle tue dashboard debbano essere per cliente, la piattaforma no.

### Presenta il token[​](#presenta-il-token "Link diretto a Presenta il token")

Il frame non può impostare un header, quindi il token viaggia sull'URL:

```
<iframe

  src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"

  sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"

  allowfullscreen

></iframe>
```

Le risposte di embed vengono inviate con `Referrer-Policy: no-referrer` in modo che il token non trapeli in un header `Referer` sulle sotto-richieste. Resta comunque una credenziale bearer in un URL, che è la vera ragione per cui il TTL predefinito è di dieci minuti: genera un token per ogni caricamento di pagina, tienilo breve, e rinnovalo invece di alzare il TTL.

### Configura la tua lista consentita di domini[​](#configura-la-tua-lista-consentita-di-domini "Link diretto a Configura la tua lista consentita di domini")

Una chiave API può portare con sé un elenco di domini da cui i suoi token di embed saranno onorati. **Un elenco vuoto significa consenti-tutto.** È una decisione deliberata affinché la funzionalità funzioni nel momento in cui la provi, e significa che una chiave non configurata non protegge nulla a livello di origine. Imposta `allowed_domains` sulla chiave prima di andare in produzione.

## Il ciclo di rinnovo del token[​](#il-ciclo-di-rinnovo-del-token "Link diretto a Il ciclo di rinnovo del token")

Un token di dieci minuti su una dashboard che qualcuno lascia aperta tutto il pomeriggio deve essere rinnovato, e il renderer ti dice quando:

1. Il frame invia **`EMBED_TOKEN_EXPIRED`** (o `EMBED_TOKEN_INVALID`) alla tua pagina.
2. La tua pagina chiede al tuo backend un nuovo token.
3. La tua pagina invia **`UPDATE_TOKEN`** nel frame: `{ type: 'UPDATE_TOKEN', token: '<new>' }`.

```
window.addEventListener('message', async (e) => {

  if (e.origin !== 'https://cloud.vibedasher.com') return;

  if (e.data?.type === 'EMBED_TOKEN_EXPIRED' || e.data?.type === 'EMBED_TOKEN_INVALID') {

    const token = await fetch('/api/vibedasher-embed-token').then((r) => r.text());

    frame.contentWindow.postMessage({ type: 'UPDATE_TOKEN', token }, 'https://cloud.vibedasher.com');

  }

});
```

Controlla sempre `e.origin`. `UPDATE_TOKEN` è l'**unico** messaggio che un host di terze parti può inviare verso l'interno — tutto il resto viene ignorato.

## Cosa riceve realmente la tua pagina[​](#cosa-riceve-realmente-la-tua-pagina "Link diretto a Cosa riceve realmente la tua pagina")

I messaggi che un host di terze parti riceve sono un elenco fisso e consentito di tredici. Questo è l'elenco completo:

| Messaggio                                                                | Quando                                                                |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `READY`                                                                  | Il renderer si è avviato.                                             |
| `LOADED`                                                                 | La dashboard ha finito di caricarsi.                                  |
| `RESIZE`                                                                 | L'altezza del contenuto è cambiata — usalo per dimensionare l'iframe. |
| `BUNDLE_STARTED` / `BUNDLE_COMPLETED` / `BUNDLE_FAILED` / `BUNDLE_RETRY` | Ciclo di vita della build del codice della dashboard.                 |
| `RUNTIME_ERROR`                                                          | Il codice della dashboard ha generato un'eccezione.                   |
| `EMPTY_DATA`                                                             | Una query non ha restituito righe.                                    |
| `RENDER_HEALTH`                                                          | Stato di salute del rendering per singolo tile.                       |
| `EMBED_TOKEN_EXPIRED` / `EMBED_TOKEN_INVALID` / `EMBED_UNAVAILABLE`      | Ciclo di vita del token — vedi sopra.                                 |

Qualsiasi altra cosa tu possa vedere menzionata è interna. In particolare un insieme di messaggi non viene esplicitamente **mai** inoltrato a un host di terze parti — `VIZ_DATA`, `FILES`, `UPDATE_TOKEN` in uscita, `SQL_ERROR`, `CONSOLE_ERROR`, `REQUEST_NEW_AUTH_TOKEN`, `ELEMENT_SELECTED`, `FILTER_STATE_CHANGED` — perché portano o il sorgente della tua dashboard o il suo stato grezzo di query.

## Il confine del frame[​](#the-boundary-of-the-embed "Link diretto a Il confine del frame")

Due proprietà decidono su cosa può essere costruita un'integrazione, ed entrambe vale la pena leggerle prima di progettare attorno al frame, non dopo.

**Il frame possiede il proprio stato di filtraggio.** I messaggi che un host terzo riceve sono quelli elencati sopra; lo stato di filtraggio della dashboard non è fra loro, in nessuna delle due direzioni. Il frame è una vista autonoma: guidalo con l'URL e il token che emetti, e lascia che possieda ciò che accade all'interno. Se la tua interfaccia deve condividere una selezione con la dashboard, metti quella selezione nel predicato di filtraggio del token, che la piattaforma aggiunge a ogni query che autorizza — lato server, e con la stessa risposta per ogni tile.

**Scegli l'audience pensando al framing.** Una dashboard pubblicata come `public` è un URL pubblico, con tutto quello che ne segue; la corsia a token firmato è quella da usare quando conta chi guarda e da dove, e l'elenco dei domini consentiti di una chiave API è ciò che lega le sue risposte alla tua origine.

**La corsia di query a token firmato gira in `eu-central-1`.** È lì che vive il runtime di query per gli embed protetti. Se i dati del tuo account sono in un'altra regione, parlane con noi prima di costruire su questa corsia.

## Passi successivi[​](#passi-successivi "Link diretto a Passi successivi")

* **[Chi può vedere cosa](/it/guides/who-can-see-what.md)** — dove si inserisce il filtraggio per riga nel modello di permessi più ampio, e perché esiste solo qui.
* **[Pubblica e condividi](/it/guides/publish-and-share.md)** — portare una dashboard a `published` e scegliere il suo `view_mode`.
* **[Riferimento API](/it/reference/vibedasher-sdk-api.md)** — l'operazione `embed-tokens` per intero.
