Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136daecbf5 | ||
|
|
e25ea6069d | ||
|
|
f7ef41959b | ||
|
|
ba0108944f | ||
|
|
03c94727be | ||
|
|
bf22373315 | ||
|
|
e1374e34a7 | ||
|
|
523289efad | ||
|
|
e6bb39deea | ||
|
|
790a58945b | ||
|
|
3295f5ee07 | ||
|
|
2bfd2f6b85 | ||
|
|
5de269a64e | ||
|
|
cc7131b0b5 |
+2
-1
@@ -211,7 +211,8 @@
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
"packages/twenty-oxlint-rules"
|
||||
"packages/twenty-oxlint-rules",
|
||||
"packages/twenty-companion"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
-12998
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"description": "Twenty meeting recorder",
|
||||
"main": ".webpack/main",
|
||||
"scripts": {
|
||||
"start": "concurrently \"npm run start:server\" \"npm run start:electron\"",
|
||||
"start": "concurrently \"yarn start:server\" \"yarn start:electron\"",
|
||||
"start:electron": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
@@ -37,10 +37,8 @@
|
||||
"node-loader": "^2.1.0",
|
||||
"style-loader": "^3.3.4"
|
||||
},
|
||||
"overrides": {
|
||||
"@electron/packager": {
|
||||
"@electron/osx-sign": "github:recallai/osx-sign"
|
||||
}
|
||||
"resolutions": {
|
||||
"@electron/packager/@electron/osx-sign": "github:recallai/osx-sign"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.40.1",
|
||||
|
||||
@@ -574,7 +574,7 @@ export default defineLogicFunction({
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
triggers: [
|
||||
// Public HTTP route trigger '/s/post-card/create'
|
||||
// Öffentlicher HTTP-Routen-Trigger '/s/post-card/create'
|
||||
{
|
||||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||||
type: 'route',
|
||||
@@ -582,13 +582,13 @@ export default defineLogicFunction({
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// Cron-Trigger (CRON-Muster)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
// Database event trigger
|
||||
// Datenbank-Ereignis-Trigger
|
||||
// {
|
||||
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
// type: 'databaseEvent',
|
||||
@@ -804,8 +804,8 @@ const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
title: `Daten für ${params.companyName} anreichern`,
|
||||
body: `Domain: ${params.domain ?? 'unbekannt'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
@@ -818,7 +818,7 @@ const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
description: 'Einen Unternehmensdatensatz mit externen Daten anreichern',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
@@ -827,11 +827,11 @@ export default defineLogicFunction({
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
description: 'Name des Unternehmens, das angereichert werden soll',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
description: 'Website-Domain des Unternehmens (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
@@ -1221,7 +1221,7 @@ const client = new CoreApiClient();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });},{
|
||||
```
|
||||
|
||||
`CoreApiClient` wird von `yarn twenty dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern. `MetadataApiClient` ist im SDK bereits enthalten.
|
||||
@@ -1252,10 +1252,10 @@ const metadataClient = new MetadataApiClient();
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type (defaults to 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
|
||||
fileBuffer, // Dateiinhalte als Buffer
|
||||
'invoice.pdf', // Dateiname
|
||||
'application/pdf', // MIME-Typ (Standard: 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // Universeller Feldbezeichner
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
|
||||
@@ -574,7 +574,7 @@ export default defineLogicFunction({
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
triggers: [
|
||||
// Public HTTP route trigger '/s/post-card/create'
|
||||
// Trigger di route HTTP pubblica '/s/post-card/create'
|
||||
{
|
||||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||||
type: 'route',
|
||||
@@ -582,13 +582,13 @@ export default defineLogicFunction({
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// Trigger Cron (pattern CRON)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
// Database event trigger
|
||||
// Trigger di evento del database
|
||||
// {
|
||||
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
// type: 'databaseEvent',
|
||||
@@ -804,8 +804,8 @@ const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
title: `Arricchisci i dati per ${params.companyName}`,
|
||||
body: `Dominio: ${params.domain ?? 'sconosciuto'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
@@ -818,7 +818,7 @@ const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
description: 'Arricchisci un record aziendale con dati esterni',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
@@ -827,11 +827,11 @@ export default defineLogicFunction({
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
description: 'Il nome dell\'azienda da arricchire',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
description: 'Il dominio del sito web dell\'azienda (facoltativo)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
@@ -1221,7 +1221,7 @@ const client = new CoreApiClient();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });},{
|
||||
```
|
||||
|
||||
`CoreApiClient` viene rigenerato automaticamente da `yarn twenty dev` ogni volta che i tuoi oggetti o campi cambiano. `MetadataApiClient` è fornito pronto all'uso con l'SDK.
|
||||
@@ -1252,10 +1252,10 @@ const metadataClient = new MetadataApiClient();
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type (defaults to 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
|
||||
fileBuffer, // contenuto del file come Buffer
|
||||
'invoice.pdf', // nome del file
|
||||
'application/pdf', // Tipo MIME (predefinito: 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // identificatore universale del campo
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
|
||||
@@ -596,7 +596,7 @@ export default defineLogicFunction({
|
||||
// updatedFields: ['name'],
|
||||
// },
|
||||
],
|
||||
});
|
||||
});},{
|
||||
```
|
||||
|
||||
Tipuri comune de declanșatoare:
|
||||
|
||||
@@ -596,7 +596,7 @@ export default defineLogicFunction({
|
||||
// updatedFields: ['name'],
|
||||
// },
|
||||
],
|
||||
});
|
||||
});},{
|
||||
```
|
||||
|
||||
Распространённые типы триггеров:
|
||||
|
||||
@@ -555,8 +555,8 @@ import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Merhaba dünya'
|
||||
: 'Merhaba dünya';
|
||||
|
||||
const result = await client.mutation({
|
||||
createPostCard: {
|
||||
@@ -574,7 +574,7 @@ export default defineLogicFunction({
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
triggers: [
|
||||
// Public HTTP route trigger '/s/post-card/create'
|
||||
// Herkese açık HTTP rota tetikleyicisi '/s/post-card/create'
|
||||
{
|
||||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||||
type: 'route',
|
||||
@@ -582,13 +582,13 @@ export default defineLogicFunction({
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// Cron tetikleyicisi (CRON deseni)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
// Database event trigger
|
||||
// Veritabanı olay tetikleyicisi
|
||||
// {
|
||||
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
// type: 'databaseEvent',
|
||||
@@ -804,8 +804,8 @@ const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
title: `${params.companyName} için verileri zenginleştir`,
|
||||
body: `Alan adı: ${params.domain ?? 'bilinmiyor'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
@@ -818,7 +818,7 @@ const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
description: 'Bir şirket kaydını harici verilerle zenginleştir',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
@@ -827,11 +827,11 @@ export default defineLogicFunction({
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
description: 'Zenginleştirilecek şirketin adı',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
description: 'Şirket web sitesi alan adı (isteğe bağlı)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
@@ -1221,7 +1221,7 @@ const client = new CoreApiClient();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
|
||||
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });},{
|
||||
```
|
||||
|
||||
`CoreApiClient`, nesneleriniz veya alanlarınız değiştiğinde `yarn twenty dev` tarafından otomatik olarak yeniden oluşturulur. `MetadataApiClient`, SDK ile birlikte önceden hazırlanmış olarak gelir.
|
||||
@@ -1252,10 +1252,10 @@ const metadataClient = new MetadataApiClient();
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await metadataClient.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type (defaults to 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
|
||||
fileBuffer, // dosya içeriği (Buffer olarak)
|
||||
'invoice.pdf', // dosya adı
|
||||
'application/pdf', // MIME türü (varsayılan: 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // alanın evrensel tanımlayıcısı
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
|
||||
@@ -91,7 +91,7 @@ msgstr "Einladung akzeptieren"
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
msgid "Confirm your new email address"
|
||||
msgstr ""
|
||||
msgstr "Bestätige deine neue E-Mail-Adresse"
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
@@ -101,7 +101,7 @@ msgstr "Bestätigen Sie Ihre E-Mail-Adresse"
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
msgid "Confirm new email"
|
||||
msgstr ""
|
||||
msgstr "Neue E-Mail-Adresse bestätigen"
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
|
||||
@@ -1 +1 @@
|
||||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"Suspended Workspace\":[\"Ausgesetzter Arbeitsbereich\"],\"Dear {userName},\":[\"Sehr geehrte/r \",[\"userName\"],\",\"],\"Hello,\":[\"Hallo,\"],\"It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days.\":[\"Es scheint, dass Ihr Workspace <0>\",[\"workspaceDisplayName\"],\"</0> seit \",[\"daysSinceInactive\"],\" Tagen gesperrt ist.\"],\"The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted.\":[\"Der Workspace wird in \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\" deaktiviert, und alle Daten werden gelöscht.\"],\"If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}.\":[\"Wenn Sie Twenty weiterhin nutzen möchten, aktualisieren Sie bitte Ihr Abonnement innerhalb der nächsten \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\".\"],\"Update your subscription\":[\"Aktualisieren Sie Ihr Abonnement\"],\"Validate domain\":[\"Domain validieren\"],\"{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>): Bitte validieren Sie diese Domain, damit Benutzer mit <1>@\",[\"domain\"],\"</1> E-Mail-Adressen Ihrem Arbeitsbereich beitreten können, ohne eine Einladung zu benötigen.\"],\"Test email\":[\"Test E-Mail\"],\"Join your team on Twenty\":[\"Treten Sie Ihrem Team auf Twenty bei\"],\"{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>) hat Sie eingeladen, einem Arbeitsbereich namens <1>\",[\"workspaceName\"],\"</1> beizutreten.\"],\"Accept invite\":[\"Einladung akzeptieren\"],\"Confirm your new email address\":[\"Confirm your new email address\"],\"Confirm your email address\":[\"Bestätigen Sie Ihre E-Mail-Adresse\"],\"Confirm new email\":[\"Confirm new email\"],\"Verify Email\":[\"E-Mail verifizieren\"],\"Password updated\":[\"Passwort wurde aktualisiert\"],\"This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.\":[\"Dies ist eine Bestätigung, dass das Passwort für Ihr Konto (\",[\"email\"],\") am \",[\"formattedDate\"],\" erfolgreich geändert wurde.\"],\"If you did not initiate this change, please contact your workspace owner immediately.\":[\"Wenn Sie diese Änderung nicht veranlasst haben, kontaktieren Sie bitte umgehend den Eigentümer Ihres Workspaces.\"],\"Connect to Twenty\":[\"Mit Twenty verbinden\"],\"Reset your password 🗝\":[\"Setzen Sie Ihr Passwort zurück 🗝\"],\"Set your password 🗝\":[\"Setze dein Passwort 🗝\"],\"Reset\":[\"Zurücksetzen\"],\"Set\":[\"Setzen\"],\"This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:\":[\"Dieser Link ist nur für die nächsten \",[\"duration\"],\" gültig. Wenn der Link nicht funktioniert, können Sie den Anmeldebestätigungslink direkt verwenden:\"],\"Deleted Workspace\":[\"Gelöschter Arbeitsbereich\"],\"Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago.\":[\"Ihr Workspace <0>\",[\"workspaceDisplayName\"],\"</0> wurde gelöscht, da Ihr Abonnement vor \",[\"daysSinceInactive\"],\" Tagen abgelaufen ist.\"],\"All data in this workspace has been permanently deleted.\":[\"Alle Daten in diesem Workspace wurden dauerhaft gelöscht.\"],\"If you wish to use Twenty again, you can create a new workspace.\":[\"Wenn Sie Twenty erneut nutzen möchten, können Sie einen neuen Workspace erstellen.\"],\"Create a new workspace\":[\"Erstellen Sie einen neuen Arbeitsbereich\"],\"What is Twenty?\":[\"Was ist Twenty?\"],\"It's a CRM, a software to help businesses manage their customer data and relationships efficiently.\":[\"Es ist ein CRM, eine Software, die Unternehmen hilft, ihre Kundendaten und -beziehungen effizient zu verwalten.\"],\"Website\":[\"Website\"],\"Visit Twenty's website\":[\"Besuchen Sie die Twenty-Website\"],\"Github\":[\"GitHub\"],\"Visit Twenty's GitHub repository\":[\"Besuchen Sie das GitHub-Repository von Twenty\"],\"User guide\":[\"Benutzerhandbuch\"],\"Read Twenty's user guide\":[\"Lesen Sie das Benutzerhandbuch von Twenty\"],\"Developers\":[\"Entwickler\"],\"Visit Twenty's developer documentation\":[\"Besuchen Sie die Entwicklerdokumentation von Twenty\"],\"Twenty.com, Public Benefit Corporation\":[\"Twenty.com, Gemeinnützige Aktiengesellschaft\"],\"San Francisco / Paris\":[\"San Francisco / Paris\"]}")as Messages;
|
||||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"Suspended Workspace\":[\"Ausgesetzter Arbeitsbereich\"],\"Dear {userName},\":[\"Sehr geehrte/r \",[\"userName\"],\",\"],\"Hello,\":[\"Hallo,\"],\"It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days.\":[\"Es scheint, dass Ihr Workspace <0>\",[\"workspaceDisplayName\"],\"</0> seit \",[\"daysSinceInactive\"],\" Tagen gesperrt ist.\"],\"The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted.\":[\"Der Workspace wird in \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\" deaktiviert, und alle Daten werden gelöscht.\"],\"If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}.\":[\"Wenn Sie Twenty weiterhin nutzen möchten, aktualisieren Sie bitte Ihr Abonnement innerhalb der nächsten \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\".\"],\"Update your subscription\":[\"Aktualisieren Sie Ihr Abonnement\"],\"Validate domain\":[\"Domain validieren\"],\"{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>): Bitte validieren Sie diese Domain, damit Benutzer mit <1>@\",[\"domain\"],\"</1> E-Mail-Adressen Ihrem Arbeitsbereich beitreten können, ohne eine Einladung zu benötigen.\"],\"Test email\":[\"Test E-Mail\"],\"Join your team on Twenty\":[\"Treten Sie Ihrem Team auf Twenty bei\"],\"{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>) hat Sie eingeladen, einem Arbeitsbereich namens <1>\",[\"workspaceName\"],\"</1> beizutreten.\"],\"Accept invite\":[\"Einladung akzeptieren\"],\"Confirm your new email address\":[\"Bestätige deine neue E-Mail-Adresse\"],\"Confirm your email address\":[\"Bestätigen Sie Ihre E-Mail-Adresse\"],\"Confirm new email\":[\"Neue E-Mail-Adresse bestätigen\"],\"Verify Email\":[\"E-Mail verifizieren\"],\"Password updated\":[\"Passwort wurde aktualisiert\"],\"This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.\":[\"Dies ist eine Bestätigung, dass das Passwort für Ihr Konto (\",[\"email\"],\") am \",[\"formattedDate\"],\" erfolgreich geändert wurde.\"],\"If you did not initiate this change, please contact your workspace owner immediately.\":[\"Wenn Sie diese Änderung nicht veranlasst haben, kontaktieren Sie bitte umgehend den Eigentümer Ihres Workspaces.\"],\"Connect to Twenty\":[\"Mit Twenty verbinden\"],\"Reset your password 🗝\":[\"Setzen Sie Ihr Passwort zurück 🗝\"],\"Set your password 🗝\":[\"Setze dein Passwort 🗝\"],\"Reset\":[\"Zurücksetzen\"],\"Set\":[\"Setzen\"],\"This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:\":[\"Dieser Link ist nur für die nächsten \",[\"duration\"],\" gültig. Wenn der Link nicht funktioniert, können Sie den Anmeldebestätigungslink direkt verwenden:\"],\"Deleted Workspace\":[\"Gelöschter Arbeitsbereich\"],\"Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago.\":[\"Ihr Workspace <0>\",[\"workspaceDisplayName\"],\"</0> wurde gelöscht, da Ihr Abonnement vor \",[\"daysSinceInactive\"],\" Tagen abgelaufen ist.\"],\"All data in this workspace has been permanently deleted.\":[\"Alle Daten in diesem Workspace wurden dauerhaft gelöscht.\"],\"If you wish to use Twenty again, you can create a new workspace.\":[\"Wenn Sie Twenty erneut nutzen möchten, können Sie einen neuen Workspace erstellen.\"],\"Create a new workspace\":[\"Erstellen Sie einen neuen Arbeitsbereich\"],\"What is Twenty?\":[\"Was ist Twenty?\"],\"It's a CRM, a software to help businesses manage their customer data and relationships efficiently.\":[\"Es ist ein CRM, eine Software, die Unternehmen hilft, ihre Kundendaten und -beziehungen effizient zu verwalten.\"],\"Website\":[\"Website\"],\"Visit Twenty's website\":[\"Besuchen Sie die Twenty-Website\"],\"Github\":[\"GitHub\"],\"Visit Twenty's GitHub repository\":[\"Besuchen Sie das GitHub-Repository von Twenty\"],\"User guide\":[\"Benutzerhandbuch\"],\"Read Twenty's user guide\":[\"Lesen Sie das Benutzerhandbuch von Twenty\"],\"Developers\":[\"Entwickler\"],\"Visit Twenty's developer documentation\":[\"Besuchen Sie die Entwicklerdokumentation von Twenty\"],\"Twenty.com, Public Benefit Corporation\":[\"Twenty.com, Gemeinnützige Aktiengesellschaft\"],\"San Francisco / Paris\":[\"San Francisco / Paris\"]}")as Messages;
|
||||
@@ -1 +1 @@
|
||||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"Suspended Workspace\":[\"Felfüggesztett munkaterület\"],\"Dear {userName},\":[\"Kedves \",[\"userName\"],\",\"],\"Hello,\":[\"Üdvözlöm,\"],\"It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days.\":[\"Úgy tűnik, hogy a munkaterülete <0>\",[\"workspaceDisplayName\"],\"</0> felfüggesztésre került \",[\"daysSinceInactive\"],\" napja.\"],\"The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted.\":[\"A munkaterület \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\" múlva lesz deaktiválva, és minden adat törlésre kerül.\"],\"If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}.\":[\"Amennyiben szeretné folytatni a Twenty használatát, kérjük frissítse előfizetését a következő \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\" időszakban.\"],\"Update your subscription\":[\"Előfizetés frissítése\"],\"Validate domain\":[\"Domain érvényesítése\"],\"{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>): Kérjük, érvényesítse ezt a domaint annak érdekében, hogy az <1>@\",[\"domain\"],\"</1> e-mail címmel rendelkező felhasználók meghívó nélkül csatlakozhassanak a munkaterületéhez.\"],\"Test email\":[\"Teszt e-mail\"],\"Join your team on Twenty\":[\"Csatlakozzon a csapatához a Twentyn\"],\"{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>) meghívta önt, hogy csatlakozzon egy <1>\",[\"workspaceName\"],\"</1> nevű munkaterülethez.\"],\"Accept invite\":[\"Meghívó elfogadása\"],\"Confirm your new email address\":[\"Confirm your new email address\"],\"Confirm your email address\":[\"Erősítse meg email címét\"],\"Confirm new email\":[\"Confirm new email\"],\"Verify Email\":[\"Email ellenőrzése\"],\"Password updated\":[\"Jelszó frissítve\"],\"This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.\":[\"Ez egy megerősítés arról, hogy fiókja jelszavát (\",[\"email\"],\") sikeresen megváltoztatták \",[\"formattedDate\"],\" napján.\"],\"If you did not initiate this change, please contact your workspace owner immediately.\":[\"Amennyiben Ön nem kezdeményezte ezt a változtatást, kérjük azonnal lépjen kapcsolatba a munkaterület tulajdonosával.\"],\"Connect to Twenty\":[\"Csatlakozás a Twentyhez\"],\"Reset your password 🗝\":[\"Jelszó visszaállítása 🗝\"],\"Set your password 🗝\":[\"Adja meg a jelszavát 🗝\"],\"Reset\":[\"Visszaállítás\"],\"Set\":[\"Beállítás\"],\"This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:\":[\"Ez a hivatkozás csak a következő \",[\"duration\"],\" ideig érvényes. Ha a hivatkozás nem működik, használhatja közvetlenül a belépés ellenőrző hivatkozását:\"],\"Deleted Workspace\":[\"Törölt munkaterület\"],\"Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago.\":[\"Munkaterülete <0>\",[\"workspaceDisplayName\"],\"</0> törlésre került, mivel előfizetése \",[\"daysSinceInactive\"],\" nappal ezelőtt lejárt.\"],\"All data in this workspace has been permanently deleted.\":[\"A munkaterület minden adata végérvényesen törlésre került.\"],\"If you wish to use Twenty again, you can create a new workspace.\":[\"Ha újra szeretné használni a Twenty-t, létrehozhat egy új munkaterületet.\"],\"Create a new workspace\":[\"Hozzon létre új munkaterületet\"],\"What is Twenty?\":[\"Mi az a Twenty?\"],\"It's a CRM, a software to help businesses manage their customer data and relationships efficiently.\":[\"Ez egy CRM, egy szoftver, amely segíti a vállalkozásokat az ügyféladataik és kapcsolataik hatékony kezelésében.\"],\"Website\":[\"Weboldal\"],\"Visit Twenty's website\":[\"Látogasson el a Twenty weboldalára\"],\"Github\":[\"Github\"],\"Visit Twenty's GitHub repository\":[\"Látogasson el a Twenty GitHub tárházához\"],\"User guide\":[\"Felhasználói útmutató\"],\"Read Twenty's user guide\":[\"Olvassa el a Twenty felhasználói útmutatóját\"],\"Developers\":[\"Fejlesztők\"],\"Visit Twenty's developer documentation\":[\"Látogasson el a Twenty fejlesztői dokumentációjához\"],\"Twenty.com, Public Benefit Corporation\":[\"Twenty.com, Közhasznú Vállalat\"],\"San Francisco / Paris\":[\"San Francisco / Párizs\"]}")as Messages;
|
||||
/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{\"Suspended Workspace\":[\"Felfüggesztett munkaterület\"],\"Dear {userName},\":[\"Kedves \",[\"userName\"],\",\"],\"Hello,\":[\"Üdvözlöm,\"],\"It appears that your workspace <0>{workspaceDisplayName}</0> has been suspended for {daysSinceInactive} days.\":[\"Úgy tűnik, hogy a munkaterülete <0>\",[\"workspaceDisplayName\"],\"</0> felfüggesztésre került \",[\"daysSinceInactive\"],\" napja.\"],\"The workspace will be deactivated in {remainingDays} {dayOrDays}, and all its data will be deleted.\":[\"A munkaterület \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\" múlva lesz deaktiválva, és minden adat törlésre kerül.\"],\"If you wish to continue using Twenty, please update your subscription within the next {remainingDays} {dayOrDays}.\":[\"Amennyiben szeretné folytatni a Twenty használatát, kérjük frissítse előfizetését a következő \",[\"remainingDays\"],\" \",[\"dayOrDays\"],\" időszakban.\"],\"Update your subscription\":[\"Előfizetés frissítése\"],\"Validate domain\":[\"Domain érvényesítése\"],\"{senderName} (<0>{senderEmail}</0>): Please validate this domain to allow users with <1>@{domain}</1> email addresses to join your workspace without requiring an invitation.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>): Kérjük, érvényesítse ezt a domaint annak érdekében, hogy az <1>@\",[\"domain\"],\"</1> e-mail címmel rendelkező felhasználók meghívó nélkül csatlakozhassanak a munkaterületéhez.\"],\"Test email\":[\"Teszt e-mail\"],\"Join your team on Twenty\":[\"Csatlakozzon a csapatához a Twentyn\"],\"{senderName} (<0>{senderEmail}</0>) has invited you to join a workspace called <1>{workspaceName}</1>.\":[[\"senderName\"],\" (<0>\",[\"senderEmail\"],\"</0>) meghívta önt, hogy csatlakozzon egy <1>\",[\"workspaceName\"],\"</1> nevű munkaterülethez.\"],\"Accept invite\":[\"Meghívó elfogadása\"],\"Confirm your new email address\":[\"Erősítsd meg az új e-mail címed\"],\"Confirm your email address\":[\"Erősítse meg email címét\"],\"Confirm new email\":[\"Új e-mail megerősítése\"],\"Verify Email\":[\"Email ellenőrzése\"],\"Password updated\":[\"Jelszó frissítve\"],\"This is a confirmation that password for your account ({email}) was successfully changed on {formattedDate}.\":[\"Ez egy megerősítés arról, hogy fiókja jelszavát (\",[\"email\"],\") sikeresen megváltoztatták \",[\"formattedDate\"],\" napján.\"],\"If you did not initiate this change, please contact your workspace owner immediately.\":[\"Amennyiben Ön nem kezdeményezte ezt a változtatást, kérjük azonnal lépjen kapcsolatba a munkaterület tulajdonosával.\"],\"Connect to Twenty\":[\"Csatlakozás a Twentyhez\"],\"Reset your password 🗝\":[\"Jelszó visszaállítása 🗝\"],\"Set your password 🗝\":[\"Adja meg a jelszavát 🗝\"],\"Reset\":[\"Visszaállítás\"],\"Set\":[\"Beállítás\"],\"This link is only valid for the next {duration}. If the link does not work, you can use the login verification link directly:\":[\"Ez a hivatkozás csak a következő \",[\"duration\"],\" ideig érvényes. Ha a hivatkozás nem működik, használhatja közvetlenül a belépés ellenőrző hivatkozását:\"],\"Deleted Workspace\":[\"Törölt munkaterület\"],\"Your workspace <0>{workspaceDisplayName}</0> has been deleted as your subscription expired {daysSinceInactive} days ago.\":[\"Munkaterülete <0>\",[\"workspaceDisplayName\"],\"</0> törlésre került, mivel előfizetése \",[\"daysSinceInactive\"],\" nappal ezelőtt lejárt.\"],\"All data in this workspace has been permanently deleted.\":[\"A munkaterület minden adata végérvényesen törlésre került.\"],\"If you wish to use Twenty again, you can create a new workspace.\":[\"Ha újra szeretné használni a Twenty-t, létrehozhat egy új munkaterületet.\"],\"Create a new workspace\":[\"Hozzon létre új munkaterületet\"],\"What is Twenty?\":[\"Mi az a Twenty?\"],\"It's a CRM, a software to help businesses manage their customer data and relationships efficiently.\":[\"Ez egy CRM, egy szoftver, amely segíti a vállalkozásokat az ügyféladataik és kapcsolataik hatékony kezelésében.\"],\"Website\":[\"Weboldal\"],\"Visit Twenty's website\":[\"Látogasson el a Twenty weboldalára\"],\"Github\":[\"Github\"],\"Visit Twenty's GitHub repository\":[\"Látogasson el a Twenty GitHub tárházához\"],\"User guide\":[\"Felhasználói útmutató\"],\"Read Twenty's user guide\":[\"Olvassa el a Twenty felhasználói útmutatóját\"],\"Developers\":[\"Fejlesztők\"],\"Visit Twenty's developer documentation\":[\"Látogasson el a Twenty fejlesztői dokumentációjához\"],\"Twenty.com, Public Benefit Corporation\":[\"Twenty.com, Közhasznú Vállalat\"],\"San Francisco / Paris\":[\"San Francisco / Párizs\"]}")as Messages;
|
||||
@@ -91,7 +91,7 @@ msgstr "Meghívó elfogadása"
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
msgid "Confirm your new email address"
|
||||
msgstr ""
|
||||
msgstr "Erősítsd meg az új e-mail címed"
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
@@ -101,7 +101,7 @@ msgstr "Erősítse meg email címét"
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
msgid "Confirm new email"
|
||||
msgstr ""
|
||||
msgstr "Új e-mail megerősítése"
|
||||
|
||||
#. js-lingui-explicit-id
|
||||
#: src/emails/send-email-verification-link.email.tsx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-16
@@ -535,20 +535,4 @@ describe('isRecordMatchingFilter', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Missing field metadata', () => {
|
||||
it('should return true when filter references a field not in object metadata', () => {
|
||||
const filter: RecordGqlOperationFilter = {
|
||||
nonExistentFieldId: { eq: 'some-value' },
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: companiesMock[0],
|
||||
filter,
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+6
-4
@@ -215,10 +215,12 @@ export const isRecordMatchingFilter = ({
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataField)) {
|
||||
// Stale cached queries can reference fields that no longer exist
|
||||
// in the metadata (e.g. deleted or deactivated custom fields).
|
||||
// Skip the filter condition to avoid crashing optimistic updates.
|
||||
return true;
|
||||
throw new Error(
|
||||
'Field metadata item "' +
|
||||
filterKey +
|
||||
'" not found for object metadata item ' +
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
}
|
||||
|
||||
switch (objectMetadataField.type) {
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export const useUpsertObjectPermission = ({ roleId }: { roleId: string }) => {
|
||||
newPermissions.canReadObjectRecords = value;
|
||||
}
|
||||
|
||||
if (permissionKey === 'canReadObjectRecords' && !value) {
|
||||
if (permissionKey === 'canReadObjectRecords' && value === false) {
|
||||
newPermissions.canUpdateObjectRecords = false;
|
||||
newPermissions.canSoftDeleteObjectRecords = false;
|
||||
newPermissions.canDestroyObjectRecords = false;
|
||||
|
||||
+1
@@ -111,6 +111,7 @@ const StyledItem = styled.button<StyledItemProps>`
|
||||
height: ${themeCssVariables.spacing[7]};
|
||||
margin-top: ${({ indentationLevel }) =>
|
||||
indentationLevel === 2 ? '2px' : '0'};
|
||||
min-width: 0;
|
||||
padding-bottom: ${themeCssVariables.spacing[1]};
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
padding-right: ${({ hasRightOptions }) =>
|
||||
|
||||
+3
-3
@@ -233,9 +233,9 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{apiKey?.name && (
|
||||
{isDefined(apiKey) && (
|
||||
<SubMenuTopBarContainer
|
||||
title={apiKey?.name}
|
||||
title={apiKey.name || t`Unnamed API Key`}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
@@ -245,7 +245,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
children: t`APIs & Webhooks`,
|
||||
href: getSettingsPath(SettingsPath.ApiWebhooks),
|
||||
},
|
||||
{ children: apiKey?.name },
|
||||
{ children: apiKey.name || t`Unnamed API Key` },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
|
||||
+5
-1
@@ -75,6 +75,10 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
formValues.expirationDate ?? 30,
|
||||
).toISOString();
|
||||
|
||||
if (!formValues.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleIdToUse = formValues.roleId;
|
||||
|
||||
if (!roleIdToUse) {
|
||||
@@ -137,7 +141,7 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
]}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!isDefined(canSave)}
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => {
|
||||
navigateSettings(SettingsPath.ApiWebhooks);
|
||||
}}
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
import { type NextFunction, type Request, type Response } from 'express';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { workspaceAuthContextStorage } from 'src/engine/core-modules/auth/storage/workspace-auth-context.storage';
|
||||
|
||||
import { WorkspaceAuthContextMiddleware } from '../workspace-auth-context.middleware';
|
||||
|
||||
const mockWorkspace = {
|
||||
id: 'workspace-id',
|
||||
displayName: 'Test Workspace',
|
||||
} as Request['workspace'];
|
||||
|
||||
const mockUser = {
|
||||
id: 'user-id',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
} as Request['user'];
|
||||
|
||||
const mockApplication = {
|
||||
id: 'application-id',
|
||||
name: 'Test App',
|
||||
defaultRoleId: 'app-role-id',
|
||||
} as Request['application'];
|
||||
|
||||
const mockApiKey = {
|
||||
id: 'api-key-id',
|
||||
name: 'Test API Key',
|
||||
} as Request['apiKey'];
|
||||
|
||||
const mockWorkspaceMember = {
|
||||
id: 'workspace-member-id',
|
||||
name: { firstName: 'Test', lastName: 'User' },
|
||||
} as Request['workspaceMember'];
|
||||
|
||||
describe('WorkspaceAuthContextMiddleware', () => {
|
||||
let middleware: WorkspaceAuthContextMiddleware;
|
||||
let mockResponse: Response;
|
||||
let mockNext: NextFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
middleware = new WorkspaceAuthContextMiddleware();
|
||||
mockResponse = {} as Response;
|
||||
mockNext = jest.fn();
|
||||
});
|
||||
|
||||
const buildRequest = (overrides: Partial<Request> = {}): Request =>
|
||||
({
|
||||
workspace: mockWorkspace,
|
||||
...overrides,
|
||||
}) as unknown as Request;
|
||||
|
||||
it('should call next without auth context when workspace is not defined', () => {
|
||||
const req = buildRequest({ workspace: undefined });
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(workspaceAuthContextStorage.getStore()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create an apiKey auth context when apiKey is present', () => {
|
||||
const req = buildRequest({ apiKey: mockApiKey });
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({ type: 'apiKey', apiKey: mockApiKey }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a user auth context when both application and user are present', () => {
|
||||
const req = buildRequest({
|
||||
application: mockApplication,
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceMember: mockWorkspaceMember,
|
||||
});
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'user',
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceMember: mockWorkspaceMember,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create an application auth context when application is present without user', () => {
|
||||
const req = buildRequest({ application: mockApplication });
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'application',
|
||||
application: mockApplication,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to application auth context when application and user are present but workspaceMember is missing', () => {
|
||||
const req = buildRequest({
|
||||
application: mockApplication,
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
});
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'application',
|
||||
application: mockApplication,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a user auth context when user is present without application', () => {
|
||||
const req = buildRequest({
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceMember: mockWorkspaceMember,
|
||||
});
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'user',
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a pendingActivationUser auth context when user and userWorkspaceId are present without workspaceMember', () => {
|
||||
const req = buildRequest({
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
});
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'pendingActivationUser',
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw AuthException when workspace is present but no auth mechanism is found', () => {
|
||||
const req = buildRequest();
|
||||
|
||||
expect(() => middleware.use(req, mockResponse, mockNext)).toThrow(
|
||||
new AuthException(
|
||||
'No authentication context found',
|
||||
AuthExceptionCode.UNAUTHENTICATED,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should prioritize apiKey over application and user', () => {
|
||||
const req = buildRequest({
|
||||
apiKey: mockApiKey,
|
||||
application: mockApplication,
|
||||
user: mockUser,
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceMember: mockWorkspaceMember,
|
||||
});
|
||||
let capturedContext: unknown;
|
||||
|
||||
(mockNext as jest.Mock).mockImplementation(() => {
|
||||
capturedContext = workspaceAuthContextStorage.getStore();
|
||||
});
|
||||
|
||||
middleware.use(req, mockResponse, mockNext);
|
||||
|
||||
expect(capturedContext).toEqual(
|
||||
expect.objectContaining({ type: 'apiKey' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
+7
-7
@@ -38,13 +38,6 @@ export class WorkspaceAuthContextMiddleware implements NestMiddleware {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(req.application)) {
|
||||
return buildApplicationAuthContext({
|
||||
workspace: req.workspace!,
|
||||
application: req.application,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(req.userWorkspaceId) &&
|
||||
isDefined(req.workspaceMemberId) &&
|
||||
@@ -60,6 +53,13 @@ export class WorkspaceAuthContextMiddleware implements NestMiddleware {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(req.application)) {
|
||||
return buildApplicationAuthContext({
|
||||
workspace: req.workspace!,
|
||||
application: req.application,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(req.userWorkspaceId) && isDefined(req.user)) {
|
||||
return buildPendingActivationUserAuthContext({
|
||||
workspace: req.workspace!,
|
||||
|
||||
@@ -401,6 +401,20 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
context.user = userContext.user;
|
||||
context.userWorkspace = userContext.userWorkspace;
|
||||
context.userWorkspaceId = userContext.userWorkspace.id;
|
||||
|
||||
const { flatWorkspaceMemberMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspace.id, [
|
||||
'flatWorkspaceMemberMaps',
|
||||
]);
|
||||
|
||||
const workspaceMemberId =
|
||||
flatWorkspaceMemberMaps.idByUserId[userContext.user.id];
|
||||
|
||||
if (isDefined(workspaceMemberId)) {
|
||||
context.workspaceMemberId = workspaceMemberId;
|
||||
context.workspaceMember =
|
||||
flatWorkspaceMemberMaps.byId[workspaceMemberId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -1,8 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileCorePictureResolver } from 'src/engine/core-modules/file/file-core-picture/resolvers/file-core-picture.resolver';
|
||||
@@ -16,10 +14,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule,
|
||||
TypeOrmModule.forFeature([FileEntity, WorkspaceEntity, ApplicationEntity]),
|
||||
TypeOrmModule.forFeature([FileEntity, WorkspaceEntity]),
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
ApplicationModule,
|
||||
FileUrlModule,
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
|
||||
+30
-22
@@ -11,7 +11,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { Like, type QueryRunner, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
@@ -29,7 +32,6 @@ export class FileCorePictureService {
|
||||
|
||||
constructor(
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(FileEntity)
|
||||
@@ -38,6 +40,25 @@ export class FileCorePictureService {
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
) {}
|
||||
|
||||
private async findCustomApplicationUniversalIdentifier(
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: ['workspaceCustomApplicationId'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (!isDefined(workspace)) {
|
||||
throw new ApplicationException(
|
||||
`Could not find workspace ${workspaceId}`,
|
||||
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return workspace.workspaceCustomApplicationId;
|
||||
}
|
||||
|
||||
private async uploadCorePicture({
|
||||
file,
|
||||
filename,
|
||||
@@ -59,11 +80,7 @@ export class FileCorePictureService {
|
||||
|
||||
const universalIdentifier =
|
||||
applicationUniversalIdentifier ??
|
||||
(
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
)
|
||||
).workspaceCustomFlatApplication.universalIdentifier;
|
||||
(await this.findCustomApplicationUniversalIdentifier(workspaceId));
|
||||
|
||||
const savedFile = await this.fileStorageService.writeFile({
|
||||
sourceFile: sanitizedFile,
|
||||
@@ -169,17 +186,12 @@ export class FileCorePictureService {
|
||||
},
|
||||
});
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
const customApplicationUniversalIdentifier =
|
||||
await this.findCustomApplicationUniversalIdentifier(workspaceId);
|
||||
|
||||
await this.fileStorageService.delete({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
applicationUniversalIdentifier: customApplicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
resourcePath: removeFileFolderFromFileEntityPath(file.path),
|
||||
});
|
||||
@@ -286,16 +298,12 @@ export class FileCorePictureService {
|
||||
},
|
||||
});
|
||||
|
||||
const { workspaceCustomFlatApplication: sourceApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId: sourceWorkspaceId,
|
||||
},
|
||||
);
|
||||
const sourceApplicationUniversalIdentifier =
|
||||
await this.findCustomApplicationUniversalIdentifier(sourceWorkspaceId);
|
||||
|
||||
const fileStream = await this.fileStorageService.readFile({
|
||||
workspaceId: sourceWorkspaceId,
|
||||
applicationUniversalIdentifier: sourceApplication.universalIdentifier,
|
||||
applicationUniversalIdentifier: sourceApplicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
resourcePath: removeFileFolderFromFileEntityPath(sourceFile.path),
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user