Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
270 lines
15 KiB
Plaintext
270 lines
15 KiB
Plaintext
---
|
||
title: Připojení
|
||
description: Umožněte své aplikaci jednat jménem uživatele ve službách třetích stran prostřednictvím OAuth.
|
||
icon: plug
|
||
---
|
||
|
||
Připojení jsou pověření, která uživatel uchovává pro externí službu (Linear, GitHub, Slack, ...). Vaše aplikace deklaruje, **jak** se tato pověření získávají — **poskytovatel připojení** — a za běhu je používá k provádění ověřených volání na rozhraní API třetí strany.
|
||
|
||
V současnosti je podporován pouze OAuth 2.0. Budoucí typy pověření (osobní přístupové tokeny, klíče API, základní autentizace) se připojí ke stejnému rozhraní — aplikace, které již používají `defineConnectionProvider({ type: 'oauth', ... })` nebudou muset migrovat.
|
||
|
||
<AccordionGroup>
|
||
|
||
<Accordion title="defineConnectionProvider" description="Definujte, jak vaše aplikace získává připojení">
|
||
|
||
Poskytovatel připojení popisuje OAuth handshake, který vaše aplikace potřebuje. Uživatel klikne v nastavení vaší aplikace na "Přidat připojení", projde souhlasovou obrazovkou poskytovatele a v jeho pracovním prostoru se vytvoří řádek `ConnectedAccount`.
|
||
|
||
Funkční nastavení vyžaduje **dva soubory** — poskytovatele připojení a odpovídající deklaraci `serverVariables` v `defineApplication`, která obsahuje klientská pověření OAuth.
|
||
|
||
```ts src/connection-providers/linear-connection.ts
|
||
import { defineConnectionProvider } from 'twenty-sdk/define';
|
||
|
||
export default defineConnectionProvider({
|
||
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
|
||
name: 'linear',
|
||
displayName: 'Linear',
|
||
icon: 'IconBrandLinear',
|
||
type: 'oauth',
|
||
oauth: {
|
||
authorizationEndpoint: 'https://linear.app/oauth/authorize',
|
||
tokenEndpoint: 'https://api.linear.app/oauth/token',
|
||
scopes: ['read', 'write'],
|
||
// These must match keys in `defineApplication.serverVariables` below.
|
||
clientIdVariable: 'LINEAR_CLIENT_ID',
|
||
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
|
||
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
|
||
// 'form-urlencoded' for the token request.
|
||
tokenRequestContentType: 'form-urlencoded',
|
||
// Optional: defaults to true. Disable only if the provider rejects PKCE.
|
||
usePkce: false,
|
||
// Optional: extra query params on the authorize URL.
|
||
// authorizationParams: { prompt: 'consent' },
|
||
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
|
||
// revokeEndpoint: 'https://example.com/oauth/revoke',
|
||
},
|
||
// Optional: a logic function in this app to run right after a connection is
|
||
// established. See "Run a logic function on connect".
|
||
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
|
||
// Optional: a logic function in this app to run right after a connection is
|
||
// removed. See "Run a logic function on disconnect".
|
||
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
|
||
});
|
||
```
|
||
|
||
```ts src/application.config.ts
|
||
import { defineApplication } from 'twenty-sdk/define';
|
||
|
||
export default defineApplication({
|
||
universalIdentifier: '...',
|
||
displayName: 'Linear',
|
||
description: 'Connect Linear to Twenty.',
|
||
// OAuth client credentials live on the app registration (one OAuth app per
|
||
// Twenty server, configured by the admin) — not per-workspace. Declare them
|
||
// as serverVariables so the admin can fill them in once for all installs.
|
||
serverVariables: {
|
||
LINEAR_CLIENT_ID: {
|
||
description: 'OAuth client ID from your Linear OAuth application.',
|
||
isSecret: false,
|
||
isRequired: true,
|
||
},
|
||
LINEAR_CLIENT_SECRET: {
|
||
description: 'OAuth client secret from your Linear OAuth application.',
|
||
isSecret: true,
|
||
isRequired: true,
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
Hlavní body:
|
||
|
||
* `name` je jedinečný identifikátor (řetězec) používaný v `listConnections({ providerName })` (kebab-case, musí odpovídat `^[a-z][a-z0-9-]*$`).
|
||
* `displayName` se zobrazuje na kartě nastavení jednotlivé aplikace a v seznamu nástrojů AI.
|
||
* `clientIdVariable` / `clientSecretVariable` jsou názvy, ne hodnoty — musí odpovídat klíčům deklarovaným v `defineApplication.serverVariables`. Skutečné `client_id` a `client_secret` zadává správce serveru prostřednictvím rozhraní pro registraci aplikace, nikdy se necommitují do vašeho repozitáře.
|
||
* Použijte `serverVariables` (nikoli `applicationVariables`) — pověření OAuth jsou celoserverová a na jeden server Twenty je jedna aplikace OAuth.
|
||
* Dokud nejsou vyplněny obě `serverVariables`, karta nastavení aplikace zobrazuje nápovědu "vyžaduje správce serveru" a tlačítko "Přidat připojení" je zakázané.
|
||
* `type: 'oauth'` je dnes jediná podporovaná hodnota. Rozlišovač je kompatibilní do budoucna: budoucí typy (`'pat'`, `'api-key'`, ...) přidají nové podbloky konfigurace vedle `oauth`.
|
||
|
||
URL zpětného volání OAuth, kterou musí váš poskytovatel zařadit na seznam povolených, je:
|
||
|
||
```
|
||
https://<your-twenty-server>/auth/apps/callback
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Spusťte logickou funkci při připojení" description="Reagujte ve chvíli, kdy je připojení navázáno">
|
||
|
||
Někteří poskytovatelé vám při připojení předají data, která je potřeba uložit dříve, než lze připojení používat — klasickým příkladem je Slack, kde odpověď OAuth určuje `team_id` pracovního prostoru, podle kterého budou příchozí události indexovány. Nastavte `onConnectLogicFunction` tak, aby odkazovala na logickou funkci ve stejné aplikaci (podle jejího `universalIdentifier`), a ta se spustí hned poté, co je vytvořen `ConnectedAccount`.
|
||
|
||
```ts src/connection-providers/slack-connection.ts
|
||
export default defineConnectionProvider({
|
||
universalIdentifier: '...',
|
||
name: 'slack',
|
||
displayName: 'Slack',
|
||
type: 'oauth',
|
||
oauth: {
|
||
/* ... */
|
||
},
|
||
// Runs claimSlackTeam after every successful Slack connection.
|
||
onConnectLogicFunction: {
|
||
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
|
||
},
|
||
});
|
||
```
|
||
|
||
Hook běží **asynchronně v připojujícím se pracovním prostoru** (je zařazen do fronty, nečeká se na něj), takže pomalý nebo chybující hook nikdy neblokuje ani nenaruší OAuth callback — udělejte jej idempotentní a zajistěte, aby sám zpracovával opakované pokusy. Obslužná funkce přijímá:
|
||
|
||
```ts
|
||
type OnConnectPayload = {
|
||
connectionProviderId: string;
|
||
connectionProviderName: string; // e.g. 'slack'
|
||
connectedAccountId: string;
|
||
};
|
||
```
|
||
|
||
Odtud použijte `getConnection(connectedAccountId)` ke čtení čerstvého přístupového tokenu a zavolejte API poskytovatele (např. Slack `auth.test`) nebo uložte mapování pomocí [úložiště klíč–hodnota](/l/cs/developers/extend/apps/logic/key-value-store).
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Spusťte logickou funkci při odpojení" description="Proveďte vyčištění při odstranění připojení">
|
||
|
||
Cokoli, co aplikace nárokuje v čase připojení, musí být uvolněno, jakmile připojení zanikne. Integrace Slacku, která si například při připojení nárokuje `team_id`, musí tento nárok uvolnit, aby se mohl jiný pracovní prostor připojit ke stejnému týmu ve Slacku. Nastavte `onDisconnectLogicFunction` tak, aby odkazovala na logickou funkci ve stejné aplikaci, a ta se spustí hned poté, co je `ConnectedAccount` smazán.
|
||
|
||
```ts src/connection-providers/slack-connection.ts
|
||
export default defineConnectionProvider({
|
||
universalIdentifier: '...',
|
||
name: 'slack',
|
||
displayName: 'Slack',
|
||
type: 'oauth',
|
||
oauth: {
|
||
/* ... */
|
||
},
|
||
// Runs releaseSlackTeam after every Slack disconnection.
|
||
onDisconnectLogicFunction: {
|
||
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
|
||
},
|
||
});
|
||
```
|
||
|
||
Stejně jako hook pro připojení běží **asynchronně v odpojovaném pracovním prostoru** a nikdy neblokuje odpojení. Handler dostává stejný tvar payloadu:
|
||
|
||
```ts
|
||
type OnDisconnectPayload = {
|
||
connectionProviderId: string;
|
||
connectionProviderName: string; // e.g. 'slack'
|
||
connectedAccountId: string;
|
||
};
|
||
```
|
||
|
||
`ConnectedAccount` už v době spuštění hooku neexistuje, takže `getConnection(connectedAccountId)` už nic nevrátí. Všechno, co čištění potřebuje (například `team_id`, externí ID předplatného), muselo být při připojení zapsáno do [key-value store](/l/cs/developers/extend/apps/logic/key-value-store) pod klíčem `connectedAccountId`.
|
||
|
||
Hook se spustí, když se připojení odstraní samostatně. Odinstalace aplikace zruší její připojení pomocí kaskádového mazání v databázi, takže se hook v tomto případě nespustí. Pro tento případ deklarujte `uninstallLogicFunction` na `defineApplication`: spustí se před tím, než jsou smazána metadata aplikace, takže může stále volat `listConnections` a dočistit vše, co zůstalo.
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="listConnections / getConnection" description="Použijte připojení z logické funkce">
|
||
|
||
Uvnitř handleru logické funkce vrací `listConnections({ providerName })` řádky `ConnectedAccount` této aplikace pro daného poskytovatele s obnovenými přístupovými tokeny.
|
||
|
||
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
|
||
import { listConnections } from 'twenty-sdk/logic-function';
|
||
|
||
export const createLinearIssueHandler = async (input: {
|
||
teamId?: string;
|
||
title?: string;
|
||
}) => {
|
||
if (!input.teamId || !input.title) {
|
||
return { success: false, error: 'teamId and title are required' };
|
||
}
|
||
|
||
const connections = await listConnections({ providerName: 'linear' });
|
||
|
||
// Workspace-shared credentials win when present; fall back to the first
|
||
// user-visibility one. For HTTP-route triggers you typically pick the
|
||
// request user's connection via event.userWorkspaceId instead.
|
||
const connection =
|
||
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
|
||
|
||
if (!connection) {
|
||
return {
|
||
success: false,
|
||
error:
|
||
'Linear is not connected. Open the app settings and click "Add connection".',
|
||
};
|
||
}
|
||
|
||
// Use connection.accessToken to call the third-party API.
|
||
const response = await fetch('https://api.linear.app/graphql', {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${connection.accessToken}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
|
||
}),
|
||
});
|
||
|
||
return { success: response.ok };
|
||
};
|
||
```
|
||
|
||
Každé připojení má:
|
||
|
||
| Pole | Popis |
|
||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `id` | Jedinečné ID řádku; předejte do `getConnection(id)` pro opětovné načtení jednoho záznamu |
|
||
| `visibility` | `'user'` (soukromé pro jednoho člena pracovního prostoru) nebo `'workspace'` (sdílené se všemi členy) |
|
||
| `scopes` | Oprávnění OAuth udělená poskytovatelem (odlišná od `visibility` — ty spolu nesouvisejí) |
|
||
| `userWorkspaceId` | ID `userWorkspace` vlastníka — užitečné pro výběr "připojení uživatele požadavku" ve spouštěčích tras HTTP |
|
||
| `accessToken` | Aktuální přístupový token OAuth (v případě vypršení je automaticky obnoven) |
|
||
| `name` | Zobrazovaný název připojení (automaticky odvozený při OAuth callbacku, uživatelem přejmenovatelný) |
|
||
| `handle` | e-mail, který je připojen na předcházející účet, aktualizován při každém opětovném propojení; vyřešeno z OIDC `id_token` vráceno při výměně tokenu, spadne se zpět na spojovací e-mail dvaceti uživatelů, pokud jej poskytovatel nevrátí. |
|
||
| `authFailedAt` | Nastaveno, když poslední obnovení selhalo; uživatel se musí znovu připojit |
|
||
|
||
Hlavní body:
|
||
|
||
* Předejte `{ providerName }` pro filtrování podle poskytovatele; vynechejte jej, chcete-li získat všechna připojení, která tato aplikace vlastní napříč všemi poskytovateli.
|
||
* Server před vrácením výsledku transparentně obnoví přístupový token. Váš handler vždy uvidí použitelný token (nebo nastavené `authFailedAt`).
|
||
* `getConnection(id)` je jednořádkový ekvivalent.
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Viditelnost pro jednotlivce vs. sdílená v pracovním prostoru" description="Jak si uživatelé vybírají mezi soukromými a sdílenými pověřeními">
|
||
|
||
Když uživatel klikne na "Přidat připojení", je vyzván k výběru viditelnosti:
|
||
|
||
* **Jen pro mě** — pověření je soukromé pro připojujícího se uživatele. Jakákoli logická funkce volaná jejich jménem (spouštěč HTTP trasy s `isAuthRequired: true`) jej uvidí; spouštěče cron a události databáze nikoli.
|
||
* **Sdíleno v pracovním prostoru** — jakýkoli člen pracovního prostoru může pověření použít. Spouštěče cron/databáze jej také uvidí, protože nemají žádného uživatele požadavku.
|
||
|
||
Pro každý handler použijte tu správnou variantu:
|
||
|
||
```ts
|
||
// HTTP-route trigger — prefer the request user's own connection.
|
||
const conn =
|
||
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
|
||
connections.find((c) => c.visibility === 'workspace');
|
||
|
||
// Cron trigger — no request user; only shared credentials are sensible.
|
||
const conn = connections.find((c) => c.visibility === 'workspace');
|
||
```
|
||
|
||
Více připojení na (uživatele, poskytovatele) je povoleno, takže tentýž uživatel může mít vedle sebe "Personal Linear" a "Work Linear".
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Jednorázové nastavení poskytovatele" description="Zaregistrujte svou aplikaci OAuth u služby třetí strany">
|
||
|
||
Pro každého poskytovatele připojení musí správce serveru nejprve zaregistrovat u třetí strany aplikaci OAuth.
|
||
|
||
1. Přejděte do vývojářského nastavení poskytovatele (např. https://linear.app/settings/api/applications/new).
|
||
2. Nastavte **Redirect URI** na `\<SERVER_URL>/auth/apps/callback`.
|
||
3. Zkopírujte vygenerované **Client ID** a **Client Secret**.
|
||
4. Otevřete nainstalovanou aplikaci v Twenty jako správce serveru → nastavte hodnoty na odpovídajících `serverVariables`.
|
||
5. Členové pracovního prostoru pak mohou přidávat připojení v sekci aplikace **Připojení**.
|
||
|
||
</Accordion>
|
||
|
||
</AccordionGroup>
|