OAuth slug/id field is now visible (must be unique, errors if it’s not)

This commit is contained in:
Matt Aitken
2023-06-28 22:43:13 +01:00
parent d8d156adac
commit 4d85bcd60e
3 changed files with 99 additions and 72 deletions
@@ -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,