OAuth slug/id field is now visible (must be unique, errors if it’s not)
This commit is contained in:
@@ -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({
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">ID</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(slug)}
|
||||
placeholder={`e.g. ${integration.identifier}`}
|
||||
/>
|
||||
<Hint>
|
||||
This is used in your code to reference this connection. It must be
|
||||
unique for this project.
|
||||
</Hint>
|
||||
<FormError>{slug.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">Name</Label>
|
||||
<Input
|
||||
|
||||
@@ -10,11 +10,36 @@ import { requireUserId } from "~/services/session.server";
|
||||
export function createSchema(
|
||||
constraints: {
|
||||
isTitleUnique?: (title: string) => Promise<boolean>;
|
||||
isSlugUnique?: (slug: string) => Promise<boolean>;
|
||||
} = {}
|
||||
) {
|
||||
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,
|
||||
|
||||
@@ -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<string> {
|
||||
//creates a client and retries if it fails
|
||||
const createClientWithSlug = async (
|
||||
tx: PrismaTransactionClient,
|
||||
customClientReference: SecretReference | undefined,
|
||||
appendRandom = false,
|
||||
attemptCount = 0
|
||||
): Promise<Integration> => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user