e623d6fd88
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
269 lines
14 KiB
Plaintext
269 lines
14 KiB
Plaintext
---
|
||
title: Verbindungen
|
||
description: Ermöglichen Sie Ihrer App, im Namen eines Benutzers über OAuth in Diensten von Drittanbietern zu handeln.
|
||
icon: plug
|
||
---
|
||
|
||
Verbindungen sind Anmeldedaten, die ein Benutzer für einen externen Dienst besitzt (Linear, GitHub, Slack, ...). Ihre App legt fest, **wie** diese Anmeldedaten bezogen werden — ein **Verbindungsanbieter** — und verwendet sie zur Laufzeit, um authentifizierte Aufrufe an die Drittanbieter-API zu tätigen.
|
||
|
||
Derzeit wird nur OAuth 2.0 unterstützt. Zukünftige Anmeldedatentypen (Personal Access Tokens, API-Schlüssel, Basic Auth) werden in dieselbe Oberfläche integriert — Apps, die bereits `defineConnectionProvider({ type: 'oauth', ... })` müssen nicht migriert werden.
|
||
|
||
<AccordionGroup>
|
||
|
||
<Accordion title="defineConnectionProvider" description="Legen Sie fest, wie die Verbindungen Ihrer App bezogen werden">
|
||
|
||
Ein Verbindungsanbieter beschreibt den OAuth-Handshake, den Ihre App benötigt. Der Benutzer klickt in den Einstellungen Ihrer App auf "Verbindung hinzufügen", schließt den Zustimmungsbildschirm des Anbieters ab, und in seinem Arbeitsbereich wird eine `ConnectedAccount`-Zeile erstellt.
|
||
|
||
Eine funktionierende Einrichtung benötigt **zwei Dateien** — den Verbindungsanbieter und eine passende `serverVariables`-Deklaration in `defineApplication`, die die OAuth-Client-Anmeldedaten enthält.
|
||
|
||
```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,
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
Hauptpunkte:
|
||
|
||
* `name` ist die eindeutige Bezeichner-Zeichenfolge, die in `listConnections({ providerName })` verwendet wird (kebab-case, muss `^[a-z][a-z0-9-]*$` entsprechen).
|
||
* `displayName` wird im Einstellungs-Tab der jeweiligen App und in der KI-Toolliste angezeigt.
|
||
* `clientIdVariable` / `clientSecretVariable` sind **Namen**, keine Werte — sie müssen den in `defineApplication.serverVariables` deklarierten Schlüsseln entsprechen. Die tatsächlichen `client_id` und `client_secret` werden vom Serveradministrator über die App-Registrierungsoberfläche eingegeben und niemals in Ihr Repository eingecheckt.
|
||
* Verwenden Sie `serverVariables` (nicht `applicationVariables`) — OAuth-Anmeldedaten gelten serverweit und es gibt eine OAuth-App pro Twenty-Server.
|
||
* Solange beide `serverVariables` nicht ausgefüllt sind, zeigt der Einstellungs-Tab pro App den Hinweis "Benötigt Server-Admin" an und der Button "Verbindung hinzufügen" ist deaktiviert.
|
||
* `type: 'oauth'` ist derzeit der einzige unterstützte Wert. Der Diskriminator ist vorwärtskompatibel: zukünftige Typen (`'pat'`, `'api-key'`, ...) werden neue Unterkonfigurationsblöcke neben `oauth` hinzufügen.
|
||
|
||
Die OAuth-Callback-URL, die Ihr Anbieter auf die Whitelist setzen muss, lautet:
|
||
|
||
```
|
||
https://<your-twenty-server>/auth/apps/callback
|
||
```
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Beim Verbindungsaufbau eine Logikfunktion ausführen" description="Reagiere in dem Moment, in dem eine Verbindung hergestellt wird">
|
||
|
||
Einige Anbieter liefern dir beim Verbindungsaufbau Daten, die du persistieren musst, bevor die Verbindung nutzbar ist – das klassische Beispiel ist Slack, bei dem die OAuth-Antwort die `team_id` des Workspaces angibt, anhand derer eingehende Ereignisse zugeordnet werden. Setze `onConnectLogicFunction` so, dass auf eine Logikfunktion in derselben App (über ihren `universalIdentifier`) verwiesen wird; sie wird direkt nach dem Erstellen des `ConnectedAccount` ausgeführt.
|
||
|
||
```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',
|
||
},
|
||
});
|
||
```
|
||
|
||
Der Hook wird **asynchron im verbindenden Workspace** ausgeführt (er wird in eine Warteschlange gestellt, nicht abgewartet), sodass ein langsamer oder fehlerhafter Hook niemals den OAuth-Callback blockiert oder unterbricht – mache ihn idempotent und lass ihn seine eigenen Wiederholungen handhaben. Der Handler erhält:
|
||
|
||
```ts
|
||
type OnConnectPayload = {
|
||
connectionProviderId: string;
|
||
connectionProviderName: string; // e.g. 'slack'
|
||
connectedAccountId: string;
|
||
};
|
||
```
|
||
|
||
Verwenden Sie von dort `getConnection(connectedAccountId)`, um das aktuelle Zugriffstoken auszulesen und die API des Anbieters aufzurufen (z. B. Slack `auth.test`) oder eine Zuordnung im [Key-Value Store](/l/de/developers/extend/apps/logic/key-value-store) zu persistieren.
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Beim Trennen der Verbindung eine Logikfunktion ausführen" description="Bereinigen, wenn eine Verbindung entfernt wird">
|
||
|
||
Alles, was eine App zum Zeitpunkt des Verbindungsaufbaus beansprucht, muss freigegeben werden, wenn die Verbindung endet. Eine Slack-Integration, die zum Beispiel beim Verbindungsaufbau eine `team_id` für sich beansprucht, muss diesen Anspruch wieder freigeben, damit ein anderer Arbeitsbereich dasselbe Slack-Team verbinden kann. Setze `onDisconnectLogicFunction` so, dass auf eine Logikfunktion in derselben App verwiesen wird; sie wird direkt nach dem Löschen des `ConnectedAccount` ausgeführt.
|
||
|
||
```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',
|
||
},
|
||
});
|
||
```
|
||
|
||
Wie der On-Connect-Hook läuft er **asynchron im Workspace, in dem die Trennung erfolgt**, und blockiert die Trennung nie. Der Handler erhält dieselbe Payload-Struktur:
|
||
|
||
```ts
|
||
type OnDisconnectPayload = {
|
||
connectionProviderId: string;
|
||
connectionProviderName: string; // e.g. 'slack'
|
||
connectedAccountId: string;
|
||
};
|
||
```
|
||
|
||
Der `ConnectedAccount` ist bereits entfernt, wenn der Hook ausgeführt wird, daher wird `getConnection(connectedAccountId)` nicht mehr aufgelöst. Alles, was die Bereinigung benötigt (eine `team_id`, eine externe Abonnement-ID), muss zum Zeitpunkt des Verbindungsaufbaus im [Key-Value-Store](/l/de/developers/extend/apps/logic/key-value-store) gespeichert worden sein, indiziert nach `connectedAccountId`.
|
||
|
||
Der Hook wird ausgelöst, wenn eine Verbindung eigenständig entfernt wird. Die Deinstallation der App entfernt ihre Verbindungen hingegen über eine Datenbankkaskade, sodass der Hook dort nicht ausgeführt wird. Deklariere eine `uninstallLogicFunction` in `defineApplication` für diesen Pfad: Sie wird ausgeführt, bevor die Metadaten der App gelöscht werden, sodass sie weiterhin `listConnections` aufrufen und den verbleibenden Rest bereinigen kann.
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="listConnections / getConnection" description="Verbindungen aus einer Logikfunktion verwenden">
|
||
|
||
Innerhalb eines Logikfunktions-Handlers gibt `listConnections({ providerName })` die `ConnectedAccount`-Zeilen dieser App für den angegebenen Anbieter zurück, mit aktualisierten Zugriffstoken.
|
||
|
||
```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 };
|
||
};
|
||
```
|
||
|
||
Jede Verbindung hat:
|
||
|
||
| Feld | Beschreibung |
|
||
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `id` | Eindeutige Zeilen-ID; an `getConnection(id)` übergeben, um eine einzelne Verbindung erneut abzurufen |
|
||
| `visibility` | `'user'` (privat für ein Mitglied des Arbeitsbereichs) oder `'workspace'` (mit allen Mitgliedern geteilt) |
|
||
| `scopes` | Vom Upstream-Anbieter gewährte OAuth-Berechtigungen (unabhängig von `visibility` — diese sind nicht miteinander verknüpft) |
|
||
| `userWorkspaceId` | Die userWorkspace-ID des Eigentümers — nützlich, um "die Verbindung des anfragenden Benutzers" in HTTP-Routen-Triggern auszuwählen |
|
||
| `accessToken` | Frisches OAuth-Zugriffstoken (wird bei Ablauf automatisch erneuert) |
|
||
| `name` / `handle` | Anzeigename der Verbindung (automatisch beim OAuth-Callback abgeleitet, vom Benutzer umbenennbar) |
|
||
| `authFailedAt` | Gesetzt, wenn die jüngste Aktualisierung fehlgeschlagen ist; der Benutzer muss die Verbindung erneut herstellen |
|
||
|
||
Hauptpunkte:
|
||
|
||
* Übergeben Sie `{ providerName }`, um nach Anbieter zu filtern; lassen Sie es weg, um alle Verbindungen dieser App über alle Anbieter hinweg zu erhalten.
|
||
* Der Server aktualisiert das Zugriffstoken vor der Rückgabe transparent. Ihr Handler sieht stets ein verwendbares Token (oder `authFailedAt` ist gesetzt).
|
||
* `getConnection(id)` ist das Pendant für eine einzelne Zeile.
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Sichtbarkeit: pro Benutzer vs. im Arbeitsbereich geteilt" description="Wie Benutzer zwischen privaten und geteilten Anmeldedaten wählen">
|
||
|
||
Wenn ein Benutzer auf "Verbindung hinzufügen" klickt, wird er aufgefordert, eine Sichtbarkeit auszuwählen:
|
||
|
||
* **Nur für mich** — die Anmeldedaten sind für den sich verbindenden Benutzer privat. Jede Logikfunktion, die in seinem/ihrem Auftrag aufgerufen wird (HTTP-Routen-Trigger mit `isAuthRequired: true`), sieht sie; Cron-Trigger und Datenbankereignisse nicht.
|
||
* **Im Arbeitsbereich geteilt** — jedes Arbeitsbereichsmitglied kann die Anmeldedaten verwenden. Cron-/Datenbank-Trigger sehen sie ebenfalls, da sie keinen anfragenden Benutzer haben.
|
||
|
||
Verwenden Sie für jeden Handler die richtige Option:
|
||
|
||
```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');
|
||
```
|
||
|
||
Mehrere Verbindungen pro (Benutzer, Anbieter) sind erlaubt, sodass derselbe Benutzer "Persönliches Linear" und "Arbeits-Linear" nebeneinander haben kann.
|
||
|
||
</Accordion>
|
||
|
||
<Accordion title="Einmalige Anbietereinrichtung" description="Registrieren Sie Ihre OAuth-App beim Drittanbieterdienst">
|
||
|
||
Für jeden Verbindungsanbieter muss der Serveradministrator zunächst eine OAuth-App beim Drittanbieter registrieren.
|
||
|
||
1. Gehen Sie zu den Entwickler-Einstellungen des Anbieters (z. B. https://linear.app/settings/api/applications/new).
|
||
2. Setzen Sie die **Redirect-URI** auf `\<SERVER_URL>/auth/apps/callback`.
|
||
3. Kopieren Sie die generierte **Client ID** und das **Client Secret**.
|
||
4. Öffnen Sie die installierte App in Twenty als Serveradministrator → setzen Sie die Werte in den entsprechenden `serverVariables`.
|
||
5. Mitglieder des Arbeitsbereichs können dann Verbindungen im **Verbindungen**-Abschnitt der jeweiligen App hinzufügen.
|
||
|
||
</Accordion>
|
||
|
||
</AccordionGroup>
|