mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-25 10:20:03 -06:00
chore: adds webhook types (#4606)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
export const getWebhookCountBySource = (environmentId: string, source?: Webhook["source"]): Promise<number> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [source, z.string().optional()]);
|
||||
|
||||
try {
|
||||
const count = await prisma.webhook.count({
|
||||
where: {
|
||||
environmentId,
|
||||
source,
|
||||
},
|
||||
});
|
||||
return count;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhookCountBySource-${environmentId}-${source}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)],
|
||||
}
|
||||
)();
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import GoogleSheetsLogo from "@/images/googleSheetsLogo.png";
|
||||
import JsLogo from "@/images/jslogo.png";
|
||||
@@ -22,7 +23,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getWebhookCountBySource } from "@formbricks/lib/webhook/service";
|
||||
import { TIntegrationType } from "@formbricks/types/integration";
|
||||
|
||||
const Page = async (props) => {
|
||||
|
||||
@@ -1,76 +1,3 @@
|
||||
import { AddWebhookButton } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton";
|
||||
import { WebhookRowData } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData";
|
||||
import { WebhookTable } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable";
|
||||
import { WebhookTableHeading } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { getWebhooks } from "@formbricks/lib/webhook/service";
|
||||
import { WebhooksPage } from "@/modules/integrations/webhooks/page";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const [session, organization, webhooksUnsorted, surveys, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getWebhooks(params.environmentId),
|
||||
getSurveys(params.environmentId, 200), // HOTFIX: not getting all surveys for now since it's maxing out the prisma accelerate limit
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
const webhooks = webhooksUnsorted.sort((a, b) => {
|
||||
if (a.createdAt > b.createdAt) return -1;
|
||||
if (a.createdAt < b.createdAt) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const renderAddWebhookButton = () => <AddWebhookButton environment={environment} surveys={surveys} />;
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton />
|
||||
<PageHeader pageTitle={t("common.webhooks")} cta={!isReadOnly ? renderAddWebhookButton() : <></>} />
|
||||
<WebhookTable environment={environment} webhooks={webhooks} surveys={surveys} isReadOnly={isReadOnly}>
|
||||
<WebhookTableHeading />
|
||||
{webhooks.map((webhook) => (
|
||||
<WebhookRowData key={webhook.id} webhook={webhook} surveys={surveys} locale={locale} />
|
||||
))}
|
||||
</WebhookTable>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default WebhooksPage;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
||||
import { writeData as airtableWriteData } from "@formbricks/lib/airtable/service";
|
||||
import { writeData } from "@formbricks/lib/googleSheet/service";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
@@ -14,7 +15,6 @@ import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||
import { TPipelineInput } from "@formbricks/types/pipelines";
|
||||
import { TResponseMeta } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createDocumentAndAssignInsight } from "@/app/api/(internal)/pipeline/lib/documents";
|
||||
import { sendSurveyFollowUps } from "@/app/api/(internal)/pipeline/lib/survey-follow-up";
|
||||
import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { sendResponseFinishedEmail } from "@/modules/email";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
|
||||
import { PipelineTriggers, Webhook } from "@prisma/client";
|
||||
import { headers } from "next/headers";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
@@ -16,9 +19,6 @@ import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { convertDatesInObject } from "@formbricks/lib/time";
|
||||
import { getPromptText } from "@formbricks/lib/utils/ai";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { webhookCache } from "@formbricks/lib/webhook/cache";
|
||||
import { TPipelineTrigger, ZPipelineInput } from "@formbricks/types/pipelines";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { getContactAttributes } from "./lib/contact-attribute";
|
||||
import { handleIntegrations } from "./lib/handleIntegrations";
|
||||
|
||||
@@ -53,7 +53,7 @@ export const POST = async (request: Request) => {
|
||||
|
||||
// Fetch webhooks
|
||||
const getWebhooksForPipeline = cache(
|
||||
async (environmentId: string, event: TPipelineTrigger, surveyId: string) => {
|
||||
async (environmentId: string, event: PipelineTriggers, surveyId: string) => {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
@@ -68,7 +68,7 @@ export const POST = async (request: Request) => {
|
||||
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
);
|
||||
const webhooks: TWebhook[] = await getWebhooksForPipeline(environmentId, event, surveyId);
|
||||
const webhooks: Webhook[] = await getWebhooksForPipeline(environmentId, event, surveyId);
|
||||
// Prepare webhook and email promises
|
||||
|
||||
// Fetch with timeout of 5 seconds to prevent hanging
|
||||
|
||||
12
apps/web/app/api/(internal)/pipeline/types/pipelines.ts
Normal file
12
apps/web/app/api/(internal)/pipeline/types/pipelines.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
import { ZResponse } from "@formbricks/types/responses";
|
||||
|
||||
export const ZPipelineInput = z.object({
|
||||
event: ZWebhook.shape.triggers.element,
|
||||
response: ZResponse,
|
||||
environmentId: z.string(),
|
||||
surveyId: z.string(),
|
||||
});
|
||||
|
||||
export type TPipelineInput = z.infer<typeof ZPipelineInput>;
|
||||
59
apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts
Normal file
59
apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const deleteWebhook = async (id: string): Promise<Webhook> => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
try {
|
||||
let deletedWebhook = await prisma.webhook.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
webhookCache.revalidate({
|
||||
id: deletedWebhook.id,
|
||||
environmentId: deletedWebhook.environmentId,
|
||||
source: deletedWebhook.source,
|
||||
});
|
||||
|
||||
return deletedWebhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
||||
throw new ResourceNotFoundError("Webhook", id);
|
||||
}
|
||||
throw new DatabaseError(`Database error when deleting webhook with ID ${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getWebhook = async (id: string): Promise<Webhook | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
try {
|
||||
const webhook = await prisma.webhook.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return webhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhook-${id}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byId(id)],
|
||||
}
|
||||
)();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key";
|
||||
import { deleteWebhook, getWebhook } from "@/app/api/v1/webhooks/[webhookId]/lib/webhook";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { headers } from "next/headers";
|
||||
import { deleteWebhook, getWebhook } from "@formbricks/lib/webhook/service";
|
||||
|
||||
export const GET = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => {
|
||||
const params = await props.params;
|
||||
|
||||
73
apps/web/app/api/v1/webhooks/lib/webhook.ts
Normal file
73
apps/web/app/api/v1/webhooks/lib/webhook.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { TWebhookInput, ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId, ZOptionalNumber } from "@formbricks/types/common";
|
||||
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
|
||||
export const createWebhook = async (environmentId: string, webhookInput: TWebhookInput): Promise<Webhook> => {
|
||||
validateInputs([environmentId, ZId], [webhookInput, ZWebhookInput]);
|
||||
|
||||
try {
|
||||
const createdWebhook = await prisma.webhook.create({
|
||||
data: {
|
||||
...webhookInput,
|
||||
surveyIds: webhookInput.surveyIds || [],
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
webhookCache.revalidate({
|
||||
id: createdWebhook.id,
|
||||
environmentId: createdWebhook.environmentId,
|
||||
source: createdWebhook.source,
|
||||
});
|
||||
|
||||
return createdWebhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
if (!(error instanceof InvalidInputError)) {
|
||||
throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWebhooks = (environmentId: string, page?: number): Promise<Webhook[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
|
||||
|
||||
try {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
return webhooks;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhooks-${environmentId}-${page}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)();
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key";
|
||||
import { createWebhook, getWebhooks } from "@/app/api/v1/webhooks/lib/webhook";
|
||||
import { ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { headers } from "next/headers";
|
||||
import { createWebhook, getWebhooks } from "@formbricks/lib/webhook/service";
|
||||
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
import { ZWebhookInput } from "@formbricks/types/webhooks";
|
||||
|
||||
export const GET = async () => {
|
||||
const headersList = await headers();
|
||||
|
||||
16
apps/web/app/api/v1/webhooks/types/webhooks.ts
Normal file
16
apps/web/app/api/v1/webhooks/types/webhooks.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
|
||||
export const ZWebhookInput = ZWebhook.partial({
|
||||
name: true,
|
||||
source: true,
|
||||
surveyIds: true,
|
||||
}).pick({
|
||||
name: true,
|
||||
source: true,
|
||||
surveyIds: true,
|
||||
triggers: true,
|
||||
url: true,
|
||||
});
|
||||
|
||||
export type TWebhookInput = z.infer<typeof ZWebhookInput>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TPipelineInput } from "@/app/lib/types/pipelines";
|
||||
import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { TPipelineInput } from "@formbricks/types/pipelines";
|
||||
|
||||
export const sendToPipeline = async ({ event, surveyId, environmentId, response }: TPipelineInput) => {
|
||||
return fetch(`${WEBAPP_URL}/api/pipeline`, {
|
||||
|
||||
9
apps/web/app/lib/types/pipelines.ts
Normal file
9
apps/web/app/lib/types/pipelines.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PipelineTriggers } from "@prisma/client";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
|
||||
export interface TPipelineInput {
|
||||
event: PipelineTriggers;
|
||||
response: TResponse;
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { TWebhookInput } from "@formbricks/types/webhooks";
|
||||
|
||||
interface RevalidateProps {
|
||||
id?: string;
|
||||
environmentId?: string;
|
||||
source?: TWebhookInput["source"];
|
||||
source?: Webhook["source"];
|
||||
}
|
||||
|
||||
export const webhookCache = {
|
||||
@@ -15,7 +15,7 @@ export const webhookCache = {
|
||||
byEnvironmentId(environmentId: string) {
|
||||
return `environments-${environmentId}-webhooks`;
|
||||
},
|
||||
byEnvironmentIdAndSource(environmentId: string, source: TWebhookInput["source"]) {
|
||||
byEnvironmentIdAndSource(environmentId: string, source?: Webhook["source"]) {
|
||||
return `environments-${environmentId}-sources-${source}-webhooks`;
|
||||
},
|
||||
},
|
||||
@@ -3,6 +3,7 @@
|
||||
import { apiKeyCache } from "@/lib/cache/api-key";
|
||||
import { contactCache } from "@/lib/cache/contact";
|
||||
import { teamCache } from "@/lib/cache/team";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
@@ -18,7 +19,6 @@ import { responseNoteCache } from "@formbricks/lib/responseNote/cache";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { tagCache } from "@formbricks/lib/tag/cache";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { webhookCache } from "@formbricks/lib/webhook/cache";
|
||||
import { ZId, ZString } from "@formbricks/types/common";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
|
||||
@@ -8,11 +8,15 @@ import {
|
||||
getProjectIdFromEnvironmentId,
|
||||
getProjectIdFromWebhookId,
|
||||
} from "@/lib/utils/helper";
|
||||
import {
|
||||
createWebhook,
|
||||
deleteWebhook,
|
||||
testEndpoint,
|
||||
updateWebhook,
|
||||
} from "@/modules/integrations/webhooks/lib/webhook";
|
||||
import { ZWebhookInput } from "@/modules/integrations/webhooks/types/webhooks";
|
||||
import { z } from "zod";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { testEndpoint } from "@formbricks/lib/webhook/utils";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZWebhookInput } from "@formbricks/types/webhooks";
|
||||
|
||||
const ZCreateWebhookAction = z.object({
|
||||
environmentId: ZId,
|
||||
@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { AddWebhookModal } from "./AddWebhookModal";
|
||||
import { AddWebhookModal } from "./add-webhook-modal";
|
||||
|
||||
interface AddWebhookButtonProps {
|
||||
environment: TEnvironment;
|
||||
@@ -1,11 +1,12 @@
|
||||
import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup";
|
||||
import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup";
|
||||
import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group";
|
||||
import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group";
|
||||
import { validWebHookURL } from "@/modules/integrations/webhooks/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Modal } from "@/modules/ui/components/modal";
|
||||
import { PipelineTriggers } from "@prisma/client";
|
||||
import clsx from "clsx";
|
||||
import { Webhook } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -13,10 +14,9 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { TPipelineTrigger } from "@formbricks/types/pipelines";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TWebhookInput } from "@formbricks/types/webhooks";
|
||||
import { createWebhookAction, testEndpointAction } from "../actions";
|
||||
import { TWebhookInput } from "../types/webhooks";
|
||||
|
||||
interface AddWebhookModalProps {
|
||||
environmentId: string;
|
||||
@@ -37,7 +37,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
const [testEndpointInput, setTestEndpointInput] = useState("");
|
||||
const [hittingEndpoint, setHittingEndpoint] = useState<boolean>(false);
|
||||
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
|
||||
const [selectedTriggers, setSelectedTriggers] = useState<TPipelineTrigger[]>([]);
|
||||
const [selectedTriggers, setSelectedTriggers] = useState<PipelineTriggers[]>([]);
|
||||
const [selectedSurveys, setSelectedSurveys] = useState<string[]>([]);
|
||||
const [selectedAllSurveys, setSelectedAllSurveys] = useState(false);
|
||||
const [creatingWebhook, setCreatingWebhook] = useState(false);
|
||||
@@ -88,7 +88,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (selectedValue: TPipelineTrigger) => {
|
||||
const handleCheckboxChange = (selectedValue: PipelineTriggers) => {
|
||||
setSelectedTriggers((prevValues) =>
|
||||
prevValues.includes(selectedValue)
|
||||
? prevValues.filter((value) => value !== selectedValue)
|
||||
@@ -1,26 +1,29 @@
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
import { PipelineTriggers } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React from "react";
|
||||
import { TPipelineTrigger } from "@formbricks/types/pipelines";
|
||||
|
||||
interface TriggerCheckboxGroupProps {
|
||||
selectedTriggers: TPipelineTrigger[];
|
||||
onCheckboxChange: (selectedValue: TPipelineTrigger) => void;
|
||||
selectedTriggers: PipelineTriggers[];
|
||||
onCheckboxChange: (selectedValue: PipelineTriggers) => void;
|
||||
allowChanges: boolean;
|
||||
}
|
||||
|
||||
const triggers = [
|
||||
const triggers: {
|
||||
title: string;
|
||||
value: PipelineTriggers;
|
||||
}[] = [
|
||||
{
|
||||
title: "environments.integrations.webhooks.response_created",
|
||||
value: "responseCreated" as TPipelineTrigger,
|
||||
value: "responseCreated",
|
||||
},
|
||||
{
|
||||
title: "environments.integrations.webhooks.response_updated",
|
||||
value: "responseUpdated" as TPipelineTrigger,
|
||||
value: "responseUpdated",
|
||||
},
|
||||
{
|
||||
title: "environments.integrations.webhooks.response_finished",
|
||||
value: "responseFinished" as TPipelineTrigger,
|
||||
value: "responseFinished",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { WebhookOverviewTab } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab";
|
||||
import { WebhookSettingsTab } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookSettingsTab";
|
||||
import { WebhookOverviewTab } from "@/modules/integrations/webhooks/components/webhook-overview-tab";
|
||||
import { WebhookSettingsTab } from "@/modules/integrations/webhooks/components/webhook-settings-tab";
|
||||
import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs";
|
||||
import { Webhook } from "lucide-react";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { WebhookIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
|
||||
interface WebhookModalProps {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
webhook: TWebhook;
|
||||
webhook: Webhook;
|
||||
surveys: TSurvey[];
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export const WebhookModal = ({ open, setOpen, webhook, surveys, isReadOnly }: We
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
icon={<Webhook />}
|
||||
icon={<WebhookIcon />}
|
||||
label={webhook.name ? webhook.name : webhook.url}
|
||||
description={""}
|
||||
/>
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
|
||||
interface ActivityTabProps {
|
||||
webhook: TWebhook;
|
||||
webhook: Webhook;
|
||||
surveys: TSurvey[];
|
||||
}
|
||||
|
||||
const getSurveyNamesForWebhook = (webhook: TWebhook, allSurveys: TSurvey[]): string[] => {
|
||||
const getSurveyNamesForWebhook = (webhook: Webhook, allSurveys: TSurvey[]): string[] => {
|
||||
if (webhook.surveyIds.length === 0) {
|
||||
return allSurveys.map((survey) => survey.name);
|
||||
} else {
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
|
||||
const renderSelectedSurveysText = (webhook: TWebhook, allSurveys: TSurvey[]) => {
|
||||
const renderSelectedSurveysText = (webhook: Webhook, allSurveys: TSurvey[]) => {
|
||||
if (webhook.surveyIds.length === 0) {
|
||||
const allSurveyNames = allSurveys.map((survey) => survey.name);
|
||||
return <p className="text-slate-400">{allSurveyNames.join(", ")}</p>;
|
||||
@@ -19,7 +19,7 @@ const renderSelectedSurveysText = (webhook: TWebhook, allSurveys: TSurvey[]) =>
|
||||
}
|
||||
};
|
||||
|
||||
const renderSelectedTriggersText = (webhook: TWebhook, t: (key: string) => string) => {
|
||||
const renderSelectedTriggersText = (webhook: Webhook, t: (key: string) => string) => {
|
||||
if (webhook.triggers.length === 0) {
|
||||
return <p className="text-slate-400">No Triggers</p>;
|
||||
} else {
|
||||
@@ -58,7 +58,7 @@ export const WebhookRowData = ({
|
||||
surveys,
|
||||
locale,
|
||||
}: {
|
||||
webhook: TWebhook;
|
||||
webhook: Webhook;
|
||||
surveys: TSurvey[];
|
||||
locale: TUserLocale;
|
||||
}) => {
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup";
|
||||
import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup";
|
||||
import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group";
|
||||
import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group";
|
||||
import { validWebHookURL } from "@/modules/integrations/webhooks/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { PipelineTriggers, Webhook } from "@prisma/client";
|
||||
import clsx from "clsx";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -16,13 +17,12 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { TPipelineTrigger } from "@formbricks/types/pipelines";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/webhooks";
|
||||
import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions";
|
||||
import { TWebhookInput } from "../types/webhooks";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
webhook: TWebhook;
|
||||
webhook: Webhook;
|
||||
surveys: TSurvey[];
|
||||
setOpen: (v: boolean) => void;
|
||||
isReadOnly: boolean;
|
||||
@@ -42,7 +42,7 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: Ac
|
||||
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [isUpdatingWebhook, setIsUpdatingWebhook] = useState(false);
|
||||
const [selectedTriggers, setSelectedTriggers] = useState<TPipelineTrigger[]>(webhook.triggers);
|
||||
const [selectedTriggers, setSelectedTriggers] = useState<PipelineTriggers[]>(webhook.triggers);
|
||||
const [selectedSurveys, setSelectedSurveys] = useState<string[]>(webhook.surveyIds);
|
||||
const [testEndpointInput, setTestEndpointInput] = useState(webhook.url);
|
||||
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { WebhookModal } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal";
|
||||
import { WebhookModal } from "@/modules/integrations/webhooks/components/webhook-detail-modal";
|
||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||
import { Webhook } from "@prisma/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
|
||||
interface WebhookTableProps {
|
||||
environment: TEnvironment;
|
||||
webhooks: TWebhook[];
|
||||
webhooks: Webhook[];
|
||||
surveys: TSurvey[];
|
||||
children: [JSX.Element, JSX.Element[]];
|
||||
isReadOnly: boolean;
|
||||
@@ -25,7 +25,7 @@ export const WebhookTable = ({
|
||||
}: WebhookTableProps) => {
|
||||
const [isWebhookDetailModalOpen, setWebhookDetailModalOpen] = useState(false);
|
||||
const t = useTranslations();
|
||||
const [activeWebhook, setActiveWebhook] = useState<TWebhook>({
|
||||
const [activeWebhook, setActiveWebhook] = useState<Webhook>({
|
||||
environmentId: environment.id,
|
||||
id: "",
|
||||
name: "",
|
||||
@@ -37,7 +37,7 @@ export const WebhookTable = ({
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const handleOpenWebhookDetailModalClick = (e, webhook: TWebhook) => {
|
||||
const handleOpenWebhookDetailModalClick = (e, webhook: Webhook) => {
|
||||
e.preventDefault();
|
||||
setActiveWebhook(webhook);
|
||||
setWebhookDetailModalOpen(true);
|
||||
@@ -1,143 +1,21 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZOptionalNumber } from "@formbricks/types/common";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TWebhook, TWebhookInput, ZWebhookInput } from "@formbricks/types/webhooks";
|
||||
import { cache } from "../cache";
|
||||
import { ITEMS_PER_PAGE } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { webhookCache } from "./cache";
|
||||
|
||||
export const getWebhooks = (environmentId: string, page?: number): Promise<TWebhook[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
|
||||
|
||||
try {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
return webhooks;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhooks-${environmentId}-${page}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)();
|
||||
|
||||
export const getWebhookCountBySource = (
|
||||
environmentId: string,
|
||||
source: TWebhookInput["source"]
|
||||
): Promise<number> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [source, ZId]);
|
||||
|
||||
try {
|
||||
const count = await prisma.webhook.count({
|
||||
where: {
|
||||
environmentId,
|
||||
source,
|
||||
},
|
||||
});
|
||||
return count;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhookCountBySource-${environmentId}-${source}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)],
|
||||
}
|
||||
)();
|
||||
|
||||
export const getWebhook = async (id: string): Promise<TWebhook | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
try {
|
||||
const webhook = await prisma.webhook.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return webhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhook-${id}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byId(id)],
|
||||
}
|
||||
)();
|
||||
|
||||
export const createWebhook = async (
|
||||
environmentId: string,
|
||||
webhookInput: TWebhookInput
|
||||
): Promise<TWebhook> => {
|
||||
validateInputs([environmentId, ZId], [webhookInput, ZWebhookInput]);
|
||||
|
||||
try {
|
||||
const createdWebhook = await prisma.webhook.create({
|
||||
data: {
|
||||
...webhookInput,
|
||||
surveyIds: webhookInput.surveyIds || [],
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
webhookCache.revalidate({
|
||||
id: createdWebhook.id,
|
||||
environmentId: createdWebhook.environmentId,
|
||||
source: createdWebhook.source,
|
||||
});
|
||||
|
||||
return createdWebhook;
|
||||
} catch (error) {
|
||||
if (!(error instanceof InvalidInputError)) {
|
||||
throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
|
||||
}
|
||||
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
import {
|
||||
DatabaseError,
|
||||
InvalidInputError,
|
||||
ResourceNotFoundError,
|
||||
UnknownError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TWebhookInput } from "../types/webhooks";
|
||||
|
||||
export const updateWebhook = async (
|
||||
webhookId: string,
|
||||
webhookInput: Partial<TWebhookInput>
|
||||
): Promise<TWebhook> => {
|
||||
validateInputs([webhookId, ZId], [webhookInput, ZWebhookInput]);
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const updatedWebhook = await prisma.webhook.update({
|
||||
where: {
|
||||
@@ -157,7 +35,7 @@ export const updateWebhook = async (
|
||||
source: updatedWebhook.source,
|
||||
});
|
||||
|
||||
return updatedWebhook;
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
@@ -167,9 +45,7 @@ export const updateWebhook = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWebhook = async (id: string): Promise<TWebhook> => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
export const deleteWebhook = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
let deletedWebhook = await prisma.webhook.delete({
|
||||
where: {
|
||||
@@ -183,7 +59,7 @@ export const deleteWebhook = async (id: string): Promise<TWebhook> => {
|
||||
source: deletedWebhook.source,
|
||||
});
|
||||
|
||||
return deletedWebhook;
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
||||
throw new ResourceNotFoundError("Webhook", id);
|
||||
@@ -191,3 +67,100 @@ export const deleteWebhook = async (id: string): Promise<TWebhook> => {
|
||||
throw new DatabaseError(`Database error when deleting webhook with ID ${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const createWebhook = async (environmentId: string, webhookInput: TWebhookInput): Promise<boolean> => {
|
||||
try {
|
||||
const createdWebhook = await prisma.webhook.create({
|
||||
data: {
|
||||
...webhookInput,
|
||||
surveyIds: webhookInput.surveyIds || [],
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
webhookCache.revalidate({
|
||||
id: createdWebhook.id,
|
||||
environmentId: createdWebhook.environmentId,
|
||||
source: createdWebhook.source,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
if (!(error instanceof InvalidInputError)) {
|
||||
throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWebhooks = (environmentId: string): Promise<Webhook[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
try {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
return webhooks;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhooks-${environmentId}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)();
|
||||
|
||||
export const testEndpoint = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
event: "testEndpoint",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
const statusCode = response.status;
|
||||
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return true;
|
||||
} else {
|
||||
const errorMessage = await response.text().then(
|
||||
(text) => text.substring(0, 1000) // Limit error message size
|
||||
);
|
||||
throw new UnknownError(`Request failed with status code ${statusCode}: ${errorMessage}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw new UnknownError("Request timed out after 5 seconds");
|
||||
}
|
||||
throw new UnknownError(`Error while fetching the URL: ${error.message}`);
|
||||
}
|
||||
};
|
||||
68
apps/web/modules/integrations/webhooks/page.tsx
Normal file
68
apps/web/modules/integrations/webhooks/page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { AddWebhookButton } from "@/modules/integrations/webhooks/components/add-webhook-button";
|
||||
import { WebhookRowData } from "@/modules/integrations/webhooks/components/webhook-row-data";
|
||||
import { WebhookTable } from "@/modules/integrations/webhooks/components/webhook-table";
|
||||
import { WebhookTableHeading } from "@/modules/integrations/webhooks/components/webhook-table-heading";
|
||||
import { getWebhooks } from "@/modules/integrations/webhooks/lib/webhook";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
|
||||
export const WebhooksPage = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const [session, organization, webhooks, surveys, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getWebhooks(params.environmentId),
|
||||
getSurveys(params.environmentId, 200), // HOTFIX: not getting all surveys for now since it's maxing out the prisma accelerate limit
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
const renderAddWebhookButton = () => <AddWebhookButton environment={environment} surveys={surveys} />;
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton />
|
||||
<PageHeader pageTitle={t("common.webhooks")} cta={!isReadOnly ? renderAddWebhookButton() : <></>} />
|
||||
<WebhookTable environment={environment} webhooks={webhooks} surveys={surveys} isReadOnly={isReadOnly}>
|
||||
<WebhookTableHeading />
|
||||
{webhooks.map((webhook) => (
|
||||
<WebhookRowData key={webhook.id} webhook={webhook} surveys={surveys} locale={locale} />
|
||||
))}
|
||||
</WebhookTable>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
16
apps/web/modules/integrations/webhooks/types/webhooks.ts
Normal file
16
apps/web/modules/integrations/webhooks/types/webhooks.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
|
||||
export const ZWebhookInput = ZWebhook.partial({
|
||||
name: true,
|
||||
source: true,
|
||||
surveyIds: true,
|
||||
}).pick({
|
||||
name: true,
|
||||
source: true,
|
||||
surveyIds: true,
|
||||
triggers: true,
|
||||
url: true,
|
||||
});
|
||||
|
||||
export type TWebhookInput = z.infer<typeof ZWebhookInput>;
|
||||
14
packages/database/zod/webhooks.ts
Normal file
14
packages/database/zod/webhooks.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZWebhook = z.object({
|
||||
id: z.string().cuid2(),
|
||||
name: z.string().nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
url: z.string().url(),
|
||||
source: z.enum(["user", "zapier", "make", "n8n"]),
|
||||
environmentId: z.string().cuid2(),
|
||||
triggers: z.array(z.enum(["responseFinished", "responseCreated", "responseUpdated"])),
|
||||
surveyIds: z.array(z.string().cuid2()),
|
||||
}) satisfies z.ZodType<Webhook>;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { cache } from "../cache";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { webhookCache } from "./cache";
|
||||
import { getWebhook } from "./service";
|
||||
|
||||
export const canUserAccessWebhook = async (userId: string, webhookId: string): Promise<boolean> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [webhookId, ZId]);
|
||||
|
||||
try {
|
||||
const webhook = await getWebhook(webhookId);
|
||||
if (!webhook) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, webhook.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`canUserAccessWebhook-${userId}-${webhookId}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byId(webhookId)],
|
||||
}
|
||||
)();
|
||||
@@ -1,26 +0,0 @@
|
||||
import "server-only";
|
||||
import { UnknownError } from "@formbricks/types/errors";
|
||||
|
||||
export const testEndpoint = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
event: "testEndpoint",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const statusCode = response.status;
|
||||
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return true;
|
||||
} else {
|
||||
const errorMessage = await response.text();
|
||||
throw new UnknownError(`Request failed with status code ${statusCode}: ${errorMessage}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new UnknownError(`Error while fetching the URL: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ZResponse } from "./responses";
|
||||
|
||||
export const ZPipelineTrigger = z.enum(["responseFinished", "responseCreated", "responseUpdated"]);
|
||||
|
||||
export type TPipelineTrigger = z.infer<typeof ZPipelineTrigger>;
|
||||
|
||||
export const ZPipelineInput = z.object({
|
||||
event: ZPipelineTrigger,
|
||||
response: ZResponse,
|
||||
environmentId: z.string(),
|
||||
surveyId: z.string(),
|
||||
});
|
||||
|
||||
export type TPipelineInput = z.infer<typeof ZPipelineInput>;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ZPipelineTrigger } from "./pipelines";
|
||||
|
||||
export const ZWebhookSource = z.enum(["user", "zapier", "make", "n8n"]);
|
||||
|
||||
export const ZWebhook = z.object({
|
||||
id: z.string().cuid2(),
|
||||
name: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
url: z.string().url(),
|
||||
source: ZWebhookSource,
|
||||
environmentId: z.string().cuid2(),
|
||||
triggers: z.array(ZPipelineTrigger),
|
||||
surveyIds: z.array(z.string().cuid2()),
|
||||
});
|
||||
|
||||
export type TWebhook = z.infer<typeof ZWebhook>;
|
||||
|
||||
export const ZWebhookInput = z.object({
|
||||
url: z.string().url(),
|
||||
name: z.string().nullish(),
|
||||
triggers: z.array(ZPipelineTrigger),
|
||||
source: ZWebhookSource.optional(),
|
||||
surveyIds: z.array(z.string().cuid2()).optional(),
|
||||
});
|
||||
|
||||
export type TWebhookInput = z.infer<typeof ZWebhookInput>;
|
||||
Reference in New Issue
Block a user