From 842cb349429990dbfa349d2bb52ffd30a22c6cef Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Tue, 13 Jun 2023 11:22:46 +0200 Subject: [PATCH] Add Webhooks (#364) --- .../components/shared/APILayout.tsx | 50 +++++----- apps/formbricks-com/lib/docsNavigation.ts | 15 ++- .../pages/docs/api/create-response/index.mdx | 72 -------------- .../pages/docs/api/update-response/index.mdx | 75 --------------- .../docs/client-api/create-response/index.mdx | 95 +++++++++++++++++++ .../pages/docs/client-api/overview/index.mdx | 12 +++ .../docs/client-api/update-response/index.mdx | 84 ++++++++++++++++ .../docs/webhook-api/create-webhook/index.mdx | 90 ++++++++++++++++++ .../docs/webhook-api/delete-webhook/index.mdx | 67 +++++++++++++ .../docs/webhook-api/get-webhook/index.mdx | 55 +++++++++++ .../docs/webhook-api/list-webhooks/index.mdx | 57 +++++++++++ .../pages/docs/webhook-api/overview/index.mdx | 20 ++++ .../app/api/v1/webhooks/[webhookId]/route.ts | 34 +++---- apps/web/app/api/v1/webhooks/route.ts | 27 ++---- apps/web/lib/api/response.ts | 24 ++++- 15 files changed, 562 insertions(+), 215 deletions(-) delete mode 100644 apps/formbricks-com/pages/docs/api/create-response/index.mdx delete mode 100644 apps/formbricks-com/pages/docs/api/update-response/index.mdx create mode 100644 apps/formbricks-com/pages/docs/client-api/create-response/index.mdx create mode 100644 apps/formbricks-com/pages/docs/client-api/overview/index.mdx create mode 100644 apps/formbricks-com/pages/docs/client-api/update-response/index.mdx create mode 100644 apps/formbricks-com/pages/docs/webhook-api/create-webhook/index.mdx create mode 100644 apps/formbricks-com/pages/docs/webhook-api/delete-webhook/index.mdx create mode 100644 apps/formbricks-com/pages/docs/webhook-api/get-webhook/index.mdx create mode 100644 apps/formbricks-com/pages/docs/webhook-api/list-webhooks/index.mdx create mode 100644 apps/formbricks-com/pages/docs/webhook-api/overview/index.mdx diff --git a/apps/formbricks-com/components/shared/APILayout.tsx b/apps/formbricks-com/components/shared/APILayout.tsx index e188933d2e..b6581a731d 100644 --- a/apps/formbricks-com/components/shared/APILayout.tsx +++ b/apps/formbricks-com/components/shared/APILayout.tsx @@ -56,7 +56,7 @@ export function APILayout({ method, url, description, headers, bodies, responses {method}
- http://localhost:300 + https://app.formbricks.com {url}
{description}
@@ -75,30 +75,34 @@ export function APILayout({ method, url, description, headers, bodies, responses )} -
-

Body

-
- {} - {bodies.map((b) => ( - - ))} - {example && ( -
-

Body Example

+ {bodies && ( +
+

Body

+
+ {} + {bodies?.map((b) => ( + + ))} + {example && (
-
-                        {example}
-                      
+

Body Example

+
+
+                          {example}
+                        
+
-
- )} + )} +
+ )} +

Responses

@@ -194,7 +198,7 @@ function Response({ color, statusCode, description, example }: RespProps) {
{example && toggleExample && ( -
+
{example}
)} diff --git a/apps/formbricks-com/lib/docsNavigation.ts b/apps/formbricks-com/lib/docsNavigation.ts index e94ce244f8..1e1a54755b 100644 --- a/apps/formbricks-com/lib/docsNavigation.ts +++ b/apps/formbricks-com/lib/docsNavigation.ts @@ -53,8 +53,19 @@ const navigation = [ { title: "Client API", links: [ - { title: "Create Response", href: "/docs/api/create-response" }, - { title: "Update Response", href: "/docs/api/update-response" }, + { title: "Overview", href: "/docs/client-api/overview" }, + { title: "Create Response", href: "/docs/client-api/create-response" }, + { title: "Update Response", href: "/docs/client-api/update-response" }, + ], + }, + { + title: "Webhook API", + links: [ + { title: "Overview", href: "/docs/webhook-api/overview" }, + { title: "List Webhooks", href: "/docs/webhook-api/list-webhooks" }, + { title: "Get Webhook", href: "/docs/webhook-api/get-webhook" }, + { title: "Create Webhook", href: "/docs/webhook-api/create-webhook" }, + { title: "Delete Webhook", href: "/docs/webhook-api/delete-webhook" }, ], }, { diff --git a/apps/formbricks-com/pages/docs/api/create-response/index.mdx b/apps/formbricks-com/pages/docs/api/create-response/index.mdx deleted file mode 100644 index 63baf18567..0000000000 --- a/apps/formbricks-com/pages/docs/api/create-response/index.mdx +++ /dev/null @@ -1,72 +0,0 @@ -import { Layout } from "@/components/docs/Layout"; -import { Fence } from "@/components/shared/Fence"; -import { APILayout } from "@/components/shared/APILayout.tsx"; - -export const meta = { - title: "API: Create response", - description: "Learn how to create a new response to a survey via API.", -}; - - - -| field name | required | default | description | -| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| response | yes | - | The response object (answers to the survey). It requires a `data` object. In this object the key is the questionId, the value the answer of the user to this question. | -| personId | yes | - | The person this response is connected to. | -| surveyId | yes | - | The survey this response is connected to. | -| finished | no | false | Mark a response as complete to be able to filter accordingly. | - -export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/api/update-response/index.mdx b/apps/formbricks-com/pages/docs/api/update-response/index.mdx deleted file mode 100644 index 2d9114ceaf..0000000000 --- a/apps/formbricks-com/pages/docs/api/update-response/index.mdx +++ /dev/null @@ -1,75 +0,0 @@ -import { Layout } from "@/components/docs/Layout"; -import { Fence } from "@/components/shared/Fence"; -import { APILayout } from "@/components/shared/APILayout.tsx"; - -export const meta = { - title: "API: Update submission", - description: "Learn how to update a new response to a survey via API.", -}; - - - -| field name | required | default | description | -| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| response | yes | - | The response object (answers to the survey). It requires a `data` object. In this object the key is the questionId, the value the answer of the user to this question. | - -export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/client-api/create-response/index.mdx b/apps/formbricks-com/pages/docs/client-api/create-response/index.mdx new file mode 100644 index 0000000000..c93d80f240 --- /dev/null +++ b/apps/formbricks-com/pages/docs/client-api/create-response/index.mdx @@ -0,0 +1,95 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; +import { APILayout } from "@/components/shared/APILayout.tsx"; + +export const meta = { + title: "API: Create response", + description: "Learn how to create a new response to a survey via API.", +}; + + + +| field name | required | default | description | +| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| data | yes | - | The response data object (answers to the survey). In this object the key is the questionId, the value the answer of the user to this question. | +| personId | no | - | The person this response is connected to. | +| surveyId | yes | - | The survey this response is connected to. | +| finished | yes | false | Mark a response as complete to be able to filter accordingly. | + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/client-api/overview/index.mdx b/apps/formbricks-com/pages/docs/client-api/overview/index.mdx new file mode 100644 index 0000000000..88e68edfba --- /dev/null +++ b/apps/formbricks-com/pages/docs/client-api/overview/index.mdx @@ -0,0 +1,12 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Client API Overview", + description: + "Explore the Formbricks Public Client API for client-side tasks and integration into your website.", +}; + +The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information. + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/client-api/update-response/index.mdx b/apps/formbricks-com/pages/docs/client-api/update-response/index.mdx new file mode 100644 index 0000000000..f1f7914744 --- /dev/null +++ b/apps/formbricks-com/pages/docs/client-api/update-response/index.mdx @@ -0,0 +1,84 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; +import { APILayout } from "@/components/shared/APILayout.tsx"; + +export const meta = { + title: "API: Update submission", + description: "Learn how to update a new response to a survey via API.", +}; + + + +| field name | required | default | description | +| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| data | yes | - | The response data object (answers to the survey). In this object the key is the questionId, the value the answer of the user to this question. | +| finished | yes | false | Mark a response as complete to be able to filter accordingly. | + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/webhook-api/create-webhook/index.mdx b/apps/formbricks-com/pages/docs/webhook-api/create-webhook/index.mdx new file mode 100644 index 0000000000..510b5fa5b9 --- /dev/null +++ b/apps/formbricks-com/pages/docs/webhook-api/create-webhook/index.mdx @@ -0,0 +1,90 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; +import { APILayout } from "@/components/shared/APILayout.tsx"; + +export const meta = { + title: "API: Create webhook", + description: "Learn how to create a new webhook via API.", +}; + + + +| field name | required | default | description | +| ---------- | -------- | ------- | ------------------------------------------------------------------------------------------------------ | +| url | yes | - | The endpoint that the webhook will send data to | +| trigger | yes | - | The event that will trigger the webhook ("responseCreated" or "responseUpdated" or "responseFinished") | + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/webhook-api/delete-webhook/index.mdx b/apps/formbricks-com/pages/docs/webhook-api/delete-webhook/index.mdx new file mode 100644 index 0000000000..d03b5de135 --- /dev/null +++ b/apps/formbricks-com/pages/docs/webhook-api/delete-webhook/index.mdx @@ -0,0 +1,67 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; +import { APILayout } from "@/components/shared/APILayout.tsx"; + +export const meta = { + title: "API: Delete Webhook", + description: "Learn how to delete a specific webhook by its ID via API.", +}; + + + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/webhook-api/get-webhook/index.mdx b/apps/formbricks-com/pages/docs/webhook-api/get-webhook/index.mdx new file mode 100644 index 0000000000..343790105f --- /dev/null +++ b/apps/formbricks-com/pages/docs/webhook-api/get-webhook/index.mdx @@ -0,0 +1,55 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; +import { APILayout } from "@/components/shared/APILayout.tsx"; + +export const meta = { + title: "API: Get Webhook", + description: "Learn how to retrieve a specific webhook by its ID via API.", +}; + + + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/webhook-api/list-webhooks/index.mdx b/apps/formbricks-com/pages/docs/webhook-api/list-webhooks/index.mdx new file mode 100644 index 0000000000..5e5e1c3dc9 --- /dev/null +++ b/apps/formbricks-com/pages/docs/webhook-api/list-webhooks/index.mdx @@ -0,0 +1,57 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; +import { APILayout } from "@/components/shared/APILayout.tsx"; + +export const meta = { + title: "API: List Webhooks", + description: "Learn how to retrieve a list of all webhooks via API.", +}; + + + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/docs/webhook-api/overview/index.mdx b/apps/formbricks-com/pages/docs/webhook-api/overview/index.mdx new file mode 100644 index 0000000000..3c4797e5fc --- /dev/null +++ b/apps/formbricks-com/pages/docs/webhook-api/overview/index.mdx @@ -0,0 +1,20 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Webhook API Overview", + description: "Learn how to use the Formbricks Webhook API.", +}; + +Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks in Formbricks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment. + +Our API has several REST endpoints enabling you to manage these webhooks, providing a great deal of flexibility: + +1. **List Webhooks:** Retrieve a list of all existing webhooks. +2. **Create a New Webhook:** Add a new webhook to your system. +3. **Get a Specific Webhook:** Query the details of a specific webhook using its unique ID. +4. **Delete a Webhook:** Remove an existing webhook. + +These APIs are designed to facilitate seamless integration of Formbricks with third-party systems. By making use of our webhook API, you can automate the process of sending data to these systems whenever significant events occur within your Formbricks environment. + +export default ({ children }) => {children}; diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index 1d1a343a3e..778aa301ca 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -1,14 +1,12 @@ -import { headers } from "next/headers"; -import { prisma } from "@formbricks/database"; -import { NextResponse } from "next/server"; import { hashApiKey } from "@/lib/api/apiHelper"; +import { responses } from "@/lib/api/response"; +import { prisma } from "@formbricks/database"; +import { headers } from "next/headers"; export async function GET(_: Request, { params }: { params: { webhookId: string } }) { const apiKey = headers().get("x-api-key"); if (!apiKey) { - return new Response("Not authenticated. This route is only available via API-Key authorization", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } const apiKeyData = await prisma.apiKey.findUnique({ where: { @@ -19,9 +17,7 @@ export async function GET(_: Request, { params }: { params: { webhookId: string }, }); if (!apiKeyData) { - return new Response("Not authenticated", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } // add webhook to database @@ -31,19 +27,15 @@ export async function GET(_: Request, { params }: { params: { webhookId: string }, }); if (!webhook) { - return new Response("Webhook not found", { - status: 404, - }); + return responses.notFoundResponse("Webhook", params.webhookId); } - return NextResponse.json({ data: webhook }); + return responses.successResponse(webhook); } export async function DELETE(_: Request, { params }: { params: { webhookId: string } }) { const apiKey = headers().get("x-api-key"); if (!apiKey) { - return new Response("Not authenticated. This route is only available via API-Key authorization", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } const apiKeyData = await prisma.apiKey.findUnique({ where: { @@ -54,9 +46,7 @@ export async function DELETE(_: Request, { params }: { params: { webhookId: stri }, }); if (!apiKeyData) { - return new Response("Not authenticated", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } // add webhook to database @@ -66,9 +56,7 @@ export async function DELETE(_: Request, { params }: { params: { webhookId: stri }, }); if (!webhook) { - return new Response("Webhook not found", { - status: 404, - }); + return responses.notFoundResponse("Webhook", params.webhookId); } - return NextResponse.json({ data: webhook }); + return responses.successResponse(webhook); } diff --git a/apps/web/app/api/v1/webhooks/route.ts b/apps/web/app/api/v1/webhooks/route.ts index 45318b4496..2b3c4cc581 100644 --- a/apps/web/app/api/v1/webhooks/route.ts +++ b/apps/web/app/api/v1/webhooks/route.ts @@ -2,13 +2,12 @@ import { headers } from "next/headers"; import { prisma } from "@formbricks/database"; import { NextResponse } from "next/server"; import { hashApiKey } from "@/lib/api/apiHelper"; +import { responses } from "@/lib/api/response"; export async function GET() { const apiKey = headers().get("x-api-key"); if (!apiKey) { - return new Response("Not authenticated. This route is only available via API-Key authorization", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } const apiKeyData = await prisma.apiKey.findUnique({ where: { @@ -19,9 +18,7 @@ export async function GET() { }, }); if (!apiKeyData) { - return new Response("Not authenticated", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } // add webhook to database @@ -36,9 +33,7 @@ export async function GET() { export async function POST(request: Request) { const apiKey = headers().get("x-api-key"); if (!apiKey) { - return new Response("Not authenticated. This route is only available via API-Key authorization", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } const apiKeyData = await prisma.apiKey.findUnique({ where: { @@ -49,21 +44,15 @@ export async function POST(request: Request) { }, }); if (!apiKeyData) { - return new Response("Not authenticated", { - status: 401, - }); + return responses.notAuthenticatedResponse(); } const { url, trigger } = await request.json(); if (!url) { - return new Response("Missing url", { - status: 400, - }); + return responses.missingFieldResponse("url"); } if (!trigger) { - return new Response("Missing trigger", { - status: 400, - }); + return responses.missingFieldResponse("trigger"); } // add webhook to database @@ -78,5 +67,5 @@ export async function POST(request: Request) { }, }, }); - return NextResponse.json({ data: webhook }); + return responses.successResponse(webhook); } diff --git a/apps/web/lib/api/response.ts b/apps/web/lib/api/response.ts index 978092e471..a75928dd2e 100644 --- a/apps/web/lib/api/response.ts +++ b/apps/web/lib/api/response.ts @@ -8,7 +8,13 @@ export interface ApiSuccessResponse { } export interface ApiErrorResponse { - code: "not_found" | "bad_request" | "internal_server_error" | "unauthorized" | "method_not_allowed"; + code: + | "not_found" + | "bad_request" + | "internal_server_error" + | "unauthorized" + | "method_not_allowed" + | "not_authenticated"; message: string; details: { [key: string]: string | string[] | number | number[] | boolean | boolean[]; @@ -80,6 +86,21 @@ const notFoundResponse = (resourceType: string, resourceId: string, cors: boolea } ); +const notAuthenticatedResponse = (cors: boolean = false) => + NextResponse.json( + { + code: "not_authenticated", + message: "Not authenticated", + details: { + "X-Api-Key": "Header not provided or API Key invalid", + }, + } as ApiErrorResponse, + { + status: 401, + ...(cors && { headers: corsHeaders }), + } + ); + const successResponse = (data: Object, cors: boolean = false) => NextResponse.json( { @@ -95,6 +116,7 @@ export const responses = { badRequestResponse, missingFieldResponse, methodNotAllowedResponse, + notAuthenticatedResponse, notFoundResponse, successResponse, };