From 4d85bcd60ed2f3455fb94f3e3f19883d63f1065b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 28 Jun 2023 22:43:13 +0100 Subject: [PATCH] =?UTF-8?q?OAuth=20slug/id=20field=20is=20now=20visible=20?= =?UTF-8?q?(must=20be=20unique,=20errors=20if=20it=E2=80=99s=20not)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integrations/ConnectToOAuthForm.tsx | 25 +++- ...urces.connection.$organizationId.oauth2.ts | 37 ++++++ .../integrationAuthRepository.server.ts | 109 ++++++------------ 3 files changed, 99 insertions(+), 72 deletions(-) diff --git a/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx b/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx index d07ef09bf..558a12f8a 100644 --- a/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx +++ b/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx @@ -25,6 +25,8 @@ import { NamedIcon } from "../primitives/NamedIcon"; import { Paragraph } from "../primitives/Paragraph"; import type { ConnectionType } from "@trigger.dev/database"; import { useFeatures } from "~/hooks/useFeatures"; +import { Hint } from "../primitives/Hint"; +import { InlineCode } from "../code/InlineCode"; export type Status = "loading" | "idle"; @@ -50,7 +52,14 @@ export function ConnectToOAuthForm({ const [ form, - { title, scopes, hasCustomClient, customClientId, customClientSecret }, + { + title, + slug, + scopes, + hasCustomClient, + customClientId, + customClientSecret, + }, ] = useForm({ lastSubmission: fetcher.data, onValidate({ formData }) { @@ -110,6 +119,20 @@ export function ConnectToOAuthForm({ {form.error} + + + + + This is used in your code to reference this connection. It must be + unique for this project. + + {slug.error} + Promise; + isSlugUnique?: (slug: string) => Promise; } = {} ) { return z .object({ id: z.string(), + slug: z + .string() + .min(2, "The id must be at least 2 characters long") + .superRefine((title, ctx) => { + if (constraints.isSlugUnique === undefined) { + //client-side validation skips this + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: conform.VALIDATION_UNDEFINED, + }); + } else { + // Tell zod this is an async validation by returning the promise + return constraints.isSlugUnique(title).then((isUnique) => { + if (isUnique) { + return; + } + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "The id must be unique in your organization", + }); + }); + } + }), integrationIdentifier: z.string(), integrationAuthMethod: z.string(), title: z @@ -105,6 +130,16 @@ export async function action({ request, params }: ActionArgs) { }, }); + return !existingIntegration; + }, + isSlugUnique: async (slug) => { + const existingIntegration = await prisma.integration.findFirst({ + where: { + organizationId, + slug, + }, + }); + return !existingIntegration; }, }); @@ -117,6 +152,7 @@ export async function action({ request, params }: ActionArgs) { const { id, + slug, hasCustomClient, customClientId, customClientSecret, @@ -143,6 +179,7 @@ export async function action({ request, params }: ActionArgs) { const url = new URL(request.url); const redirectUrl = await integrationAuthRepository.createConnectionClient({ id, + slug, customClient: hasCustomClient ? { id: customClientId!, secret: customClientSecret! } : undefined, diff --git a/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts b/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts index bc2a388d0..3704582dc 100644 --- a/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts +++ b/apps/webapp/app/services/externalApis/integrationAuthRepository.server.ts @@ -82,6 +82,7 @@ export class IntegrationAuthRepository { async createConnectionClient({ id, + slug, customClient, organizationId, integrationIdentifier, @@ -94,6 +95,7 @@ export class IntegrationAuthRepository { redirectTo, }: { id: string; + slug: string; customClient?: OAuthClient; organizationId: string; integrationIdentifier: string; @@ -105,76 +107,6 @@ export class IntegrationAuthRepository { redirectTo: string; url: URL; }): Promise { - //creates a client and retries if it fails - const createClientWithSlug = async ( - tx: PrismaTransactionClient, - customClientReference: SecretReference | undefined, - appendRandom = false, - attemptCount = 0 - ): Promise => { - let slug = createSlug(title); - - if (appendRandom) { - slug = `${slug}-${randomGenerator()}`; - } - - try { - return await tx.integration.create({ - data: { - id, - connectionType: clientType, - scopes, - title, - slug, - authSource: "HOSTED", - description, - customClientReference: customClientReference - ? { - connect: { - id: customClientReference.id, - }, - } - : undefined, - organization: { - connect: { - id: organizationId, - }, - }, - authMethod: { - connect: { - definitionId_key: { - definitionId: integrationIdentifier, - key: integrationAuthMethod, - }, - }, - }, - definition: { - connect: { - id: integrationIdentifier, - }, - }, - }, - }); - } catch (error) { - if ( - error && - typeof error === "object" && - "code" in error && - error.code === "P2002" && - attemptCount < 24 - ) { - return await createClientWithSlug( - tx, - customClientReference, - true, - attemptCount + 1 - ); - } - - throw error; - } - }; - return this.#prismaClient.$transaction(async (tx) => { let customClientReference: SecretReference | undefined = undefined; //if there's a custom client, we need to save the details to the secret store @@ -195,7 +127,42 @@ export class IntegrationAuthRepository { }); } - const client = await createClientWithSlug(tx, customClientReference); + const client = await tx.integration.create({ + data: { + id, + connectionType: clientType, + scopes, + title, + slug, + authSource: "HOSTED", + description, + customClientReference: customClientReference + ? { + connect: { + id: customClientReference.id, + }, + } + : undefined, + organization: { + connect: { + id: organizationId, + }, + }, + authMethod: { + connect: { + definitionId_key: { + definitionId: integrationIdentifier, + key: integrationAuthMethod, + }, + }, + }, + definition: { + connect: { + id: integrationIdentifier, + }, + }, + }, + }); return await this.createConnectionAttempt({ tx,