diff --git a/apps/formbricks-com/app/docs/api/display/page.mdx b/apps/formbricks-com/app/docs/api/client/displays/page.mdx similarity index 96% rename from apps/formbricks-com/app/docs/api/display/page.mdx rename to apps/formbricks-com/app/docs/api/client/displays/page.mdx index 18b746b0aa..fe7c7aa916 100644 --- a/apps/formbricks-com/app/docs/api/display/page.mdx +++ b/apps/formbricks-com/app/docs/api/client/displays/page.mdx @@ -6,12 +6,16 @@ export const meta = { "Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on how to mark surveys as displayed as well as responded for individual persons, ensuring seamless client-side interactions without compromising data security.", }; -#### Management API +#### Client API # Displays API 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. +This set of API can be used to +- [Mark Survey as Displayed](#mark-survey-as-displayed-for-person) +- [Mark Survey as Responded](#mark-survey-as-responded-for-person) + --- ## Mark Survey as Displayed for Person {{ tag: 'POST', label: '/api/v1/client/diplays' }} diff --git a/apps/formbricks-com/app/docs/api/overview/page.mdx b/apps/formbricks-com/app/docs/api/client/overview/page.mdx similarity index 68% rename from apps/formbricks-com/app/docs/api/overview/page.mdx rename to apps/formbricks-com/app/docs/api/client/overview/page.mdx index 58029f9136..eb8de73d1e 100644 --- a/apps/formbricks-com/app/docs/api/overview/page.mdx +++ b/apps/formbricks-com/app/docs/api/client/overview/page.mdx @@ -10,10 +10,15 @@ export const meta = { Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings. +Checkout the [API Key Setup](/docs/api/api-key-setup) - to generate, store, or delete API Keys. + ## Public Client API 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. +- [Displays API](/docs/api/client/displays) - Mark Survey as Displayed or Responded for a Person +- [Responses API](/docs/api/client/responses) - Create & update responses for a survey + ## Management API The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed. @@ -24,6 +29,14 @@ API requests made to the Management API are authorized using a personal API key. To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/api-key-setup). +- [Action Class API](/docs/api/management/action-classes) - Create, Update, and Delete Action Classes +- [Attribute Class API](/docs/api/management/attribute-classes) - Create, Update, and Delete Attribute Classes +- [Me API](/docs/api/management/me) - Retrieve Account Information +- [People API](/docs/api/management/people) - Create, Update, and Delete People +- [Responses API](/docs/api/management/responses) - Create, Update, and Delete Responses +- [Surveys API](/docs/api/management/surveys) - Create, Update, and Delete Surveys +- [Webhook API](/docs/api/management/webhooks) - Create, Update, and Delete Webhooks + By understanding the differences between these two APIs, you can choose the appropriate one for your needs, ensuring a secure and efficient integration with the Formbricks platform. diff --git a/apps/formbricks-com/app/docs/api/responses/page.mdx b/apps/formbricks-com/app/docs/api/client/responses/page.mdx similarity index 73% rename from apps/formbricks-com/app/docs/api/responses/page.mdx rename to apps/formbricks-com/app/docs/api/client/responses/page.mdx index 480d506073..7ff444056a 100644 --- a/apps/formbricks-com/app/docs/api/responses/page.mdx +++ b/apps/formbricks-com/app/docs/api/client/responses/page.mdx @@ -14,104 +14,6 @@ The Public Client API is designed for the JavaScript SDK and does not require au --- -## List all responses {{ tag: 'GET', label: '/api/v1/management/responses' }} - - - - - Retrieve all the responses you have received for all your surveys. - - ### Mandatory Headers - - - - Your Formbricks API key. - - - - ### Optional Query Params - - - SurveyId to filter responses by. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl --location \ - 'https://app.formbricks.com/api/v1/management/responses' \ - --header \ - 'x-api-key: ' - ``` - - - - - - ```json {{title:'200 Success'}} - { - "data":[ - { - "id":"cllnfybxd051rpl0gieavthr9", - "createdAt":"2023-08-23T07:56:33.121Z", - "updatedAt":"2023-08-23T07:56:41.793Z", - "surveyId":"cllnfy2780fromy0hy7uoxvtn", - "finished":true, - "data":{ - "ec7agikkr58j8uonhioinkyk":"Loved it!", - "gml6mgy71efgtq8np3s9je5p":"clicked", - "klvpwd4x08x8quesihvw5l92":"Product Manager", - "kp62fbqe8cfzmvy8qwpr81b2":"Very disappointed", - "lkjaxb73ulydzeumhd51sx9g":"Not interesed.", - "ryo75306flyg72iaeditbv51":"Would love if it had dark theme." - }, - "meta":{ - "url":"https://app.formbricks.com/s/cllnfy2780fromy0hy7uoxvtn", - "userAgent":{ - "os":"Linux", - "browser":"Chrome" - } - }, - "personAttributes":null, - "person":null, - "notes":[], - "tags":[] - } - ] - } - ``` - - ```json {{ title: '404 Not Found' }} - { - "code": "not_found", - "message": "cllnfy2780fromy0hy7uoxvt not found", - "details": { - "resource_id": "survey", - "resource_type": "cllnfy2780fromy0hy7uoxvt" - } - } - ``` - - ```json {{ title: '401 Not Authenticated' }} - { - "code": "not_authenticated", - "message": "Not authenticated", - "details": { - "x-Api-Key": "Header not provided or API Key invalid" - } - } - ``` - - - - - ---- - ## Create a response {{ tag: 'POST', label: '/api/v1/client/responses' }} Add a new response to a survey. @@ -222,7 +124,7 @@ Add a new response to a survey. --- -## Update a response {{ tag: 'POST', label: '/api/v1/client/responses/[responseId]' }} +## Update a response {{ tag: 'POST', label: '/api/v1/client/responses/' }} Update an existing response in a survey. @@ -247,10 +149,10 @@ Update an existing response in a survey. - + ```bash {{ title: 'cURL' }} - curl --location --request POST 'https://app.formbricks.com/api/v1/client/responses/[responseId]' \ + curl --location --request POST 'https://app.formbricks.com/api/v1/client/responses/' \ --data-raw '{ "personId": "clfqjny0v000ayzgsycx54a2c", "surveyId": "clfqz1esd0000yzah51trddn8", diff --git a/apps/formbricks-com/app/docs/api/management/action-classes/page.mdx b/apps/formbricks-com/app/docs/api/management/action-classes/page.mdx new file mode 100644 index 0000000000..85a5505b3f --- /dev/null +++ b/apps/formbricks-com/app/docs/api/management/action-classes/page.mdx @@ -0,0 +1,301 @@ +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Formbricks People API: Fetch or Create Person Overview", + description: + "Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.", +}; + +#### Management API + +# Action Classes API + +This set of API can be used to +- [List Actions](#get-all-action-classes) +- [Get Action](#get-action-class-by-id) +- [Create Actions](#create-action-class) +- [Delete Actions](#delete-action-class) + +You will need an API Key to interact with these APIs. + +--- + +## Get all Action Classes {{ tag: 'GET', label: '/api/v1/management/action-classes' }} + + + + + Get all the existing action classes in your environment. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/action-classes' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": [ + { + "id": "cln8k0t47000gz87nw4ibwv35", + "createdAt": "2023-10-02T07:13:19.207Z", + "updatedAt": "2023-10-02T07:13:19.207Z", + "name": "New Session", + "description": "Gets fired when a new session is created", + "type": "automatic", + "noCodeConfig": null, + "environmentId": "cln8k0t47000fz87njmmu2bck" + }, + { + "id": "cln8k0t55000uz87noerwdooj", + "createdAt": "2023-10-02T07:13:19.241Z", + "updatedAt": "2023-10-02T07:13:19.241Z", + "name": "Invited Team Member", + "description": "Person invited a team member", + "type": "noCode", + "noCodeConfig": { + "type": "innerHtml", + "innerHtml": { + "value": "Add Team Member" + } + }, + "environmentId": "cln8k0t47000fz87njmmu2bck" + }, + ] + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Get Action Class by ID {{ tag: 'GET', label: '/api/v1/management/action-classes/' }} + + + + + Fetch an action class by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/action-classes/' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "cln8k0t55000uz87noerwdooj", + "createdAt": "2023-10-02T07:13:19.241Z", + "updatedAt": "2023-10-02T07:13:19.241Z", + "name": "Invited Team Member", + "description": "Person invited a team member", + "type": "noCode", + "noCodeConfig": { + "type": "innerHtml", + "innerHtml": { + "value": "Add Team Member" + } + }, + "environmentId": "cln8k0t47000fz87njmmu2bck" + } + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Create Action Class {{ tag: 'POST', label: '/api/v1/management/action-classes/' }} + + + + + Create an action class. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + ### Body + + + ```json {{ title: 'cURL' }} + { + "environmentId": "cln8k0t47000fz87njmmu2bck", + "name": "My Action from API", + "type": "code" + } + ``` + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST https://app.formbricks.com/api/v1/management/action-classes/ \ + --header 'Content-Type: application/json' \ + --header 'x-api-key: ' \ + -d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Action from API", "type": "code"}' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "cln9w1cno0008z8zu79nk5w0c", + "createdAt": "2023-10-03T05:37:26.100Z", + "updatedAt": "2023-10-03T05:37:26.100Z", + "name": "My Action from API", + "description": null, + "type": "code", + "noCodeConfig": null, + "environmentId": "cln8k0t47000fz87njmmu2bck" + } + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Delete Action Class {{ tag: 'DELETE', label: '/api/v1/management/action-classes/' }} + + + + + Delete an action class by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://app.formbricks.com/api/v1/management/action-classes/ \ + --header 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "cln9w1cno0008z8zu79nk5w0c", + "createdAt": "2023-10-03T05:37:26.100Z", + "updatedAt": "2023-10-03T05:37:26.100Z", + "name": "My Action from API", + "description": null, + "type": "code", + "noCodeConfig": null, + "environmentId": "cln8k0t47000fz87njmmu2bck" + } + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/add-api-key.webp b/apps/formbricks-com/app/docs/api/management/api-key-setup/add-api-key.webp similarity index 100% rename from apps/formbricks-com/app/docs/api/api-key-setup/add-api-key.webp rename to apps/formbricks-com/app/docs/api/management/api-key-setup/add-api-key.webp diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/api-key-secret.webp b/apps/formbricks-com/app/docs/api/management/api-key-setup/api-key-secret.webp similarity index 100% rename from apps/formbricks-com/app/docs/api/api-key-setup/api-key-secret.webp rename to apps/formbricks-com/app/docs/api/management/api-key-setup/api-key-secret.webp diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx b/apps/formbricks-com/app/docs/api/management/api-key-setup/page.mdx similarity index 100% rename from apps/formbricks-com/app/docs/api/api-key-setup/page.mdx rename to apps/formbricks-com/app/docs/api/management/api-key-setup/page.mdx diff --git a/apps/formbricks-com/app/docs/api/management/attribute-classes/page.mdx b/apps/formbricks-com/app/docs/api/management/attribute-classes/page.mdx new file mode 100644 index 0000000000..c02f5bdabd --- /dev/null +++ b/apps/formbricks-com/app/docs/api/management/attribute-classes/page.mdx @@ -0,0 +1,292 @@ +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Formbricks People API: Fetch or Create Person Overview", + description: + "Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interAttributes while maintaining data privacy.", +}; + +#### Management API + +# Attribute Classes API + +This set of API can be used to +- [List Attributes](#get-all-attribute-classes) +- [Get Attributes](#get-attribute-class-by-id) +- [Create Attributes](#create-attribute-class) +- [Delete Attributes](#delete-attribute-class) + +You will need an API Key to interact with these APIs. + +--- + +## Get all Attribute Classes {{ tag: 'GET', label: '/api/v1/management/attribute-classes' }} + + + + + Get all the existing attribute classes in your environment. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/attribute-classes' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": [ + { + "id": "cln8k0t47000kz87n3lh23zf0", + "createdAt": "2023-10-02T07:13:19.207Z", + "updatedAt": "2023-10-02T07:13:19.207Z", + "name": "email", + "description": "The email of the person", + "archived": false, + "type": "automatic", + "environmentId": "cln8k0t47000fz87njmmu2bck" + }, + { + "id": "cln8k0t55000xz87nrtwbo7sf", + "createdAt": "2023-10-02T07:13:19.241Z", + "updatedAt": "2023-10-02T07:13:19.241Z", + "name": "Name", + "description": "Full Name of the Person", + "archived": false, + "type": "code", + "environmentId": "cln8k0t47000fz87njmmu2bck" + }, + ] + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Get Attribute Class by ID {{ tag: 'GET', label: '/api/v1/management/attribute-classes/' }} + + + + + Fetch an Attribute class by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/attribute-classes/' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "cln8k0t47000jz87nfwcey6mh", + "createdAt": "2023-10-02T07:13:19.207Z", + "updatedAt": "2023-10-02T07:13:19.207Z", + "name": "userId", + "description": "The internal ID of the person", + "archived": false, + "type": "automatic", + "environmentId": "cln8k0t47000fz87njmmu2bck" + } + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Create Attribute Class {{ tag: 'POST', label: '/api/v1/management/attribute-classes/' }} + + + + + Create an Attribute class. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + ### Body + + + ```json {{ title: 'cURL' }} + { + "environmentId": "clmlmwdqq0003196ufewo6ibg", + "name": "My Attribute from API", + "type": "code", + "description": "My description" + } + ``` + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST https://app.formbricks.com/api/v1/management/attribute-classes/ \ + --header 'Content-Type: application/json' \ + --header 'x-api-key: ' \ + -d '{"environmentId": "clmlmwdqq0003196ufewo6ibg", "name": "My Attribute from API", "type": "code", "description":"My description"}' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "clna0hd7z0009z8zue2z3a7wy", + "createdAt": "2023-10-03T07:41:51.792Z", + "updatedAt": "2023-10-03T07:41:51.792Z", + "name": "My Attribute from API", + "description": null, + "archived": false, + "type": "code", + "environmentId": "cln8k0t47000fz87njmmu2bck" + } + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Delete Attribute Class {{ tag: 'DELETE', label: '/api/v1/management/attribute-classes/' }} + + + + + Delete an Attribute class by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://app.formbricks.com/api/v1/management/attribute-classes/ \ + --header 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "clna0hd7z0009z8zue2z3a7wy", + "createdAt": "2023-10-03T07:41:51.792Z", + "updatedAt": "2023-10-03T07:41:51.792Z", + "name": "My Attribute from API", + "description": null, + "archived": false, + "type": "code", + "environmentId": "cln8k0t47000fz87njmmu2bck" + } + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- diff --git a/apps/formbricks-com/app/docs/api/management/me/page.mdx b/apps/formbricks-com/app/docs/api/management/me/page.mdx new file mode 100644 index 0000000000..147f8f58e9 --- /dev/null +++ b/apps/formbricks-com/app/docs/api/management/me/page.mdx @@ -0,0 +1,78 @@ +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Formbricks People API: Fetch or Create Person Overview", + description: + "Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.", +}; + +#### Management API + +# Me API + +This API can be used to get your own current environment details. + +You will need an API Key to interact with these APIs. + +--- + +## Get Environment {{ tag: 'GET', label: '/api/v1/management/me' }} + + + + + Get your current environment details. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/me' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "id": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.207Z", + "updatedAt": "2023-10-02T07:14:14.162Z", + "type": "production", + "product": { + "id": "cln8k0t47000ez87n57aqywvz", + "name": "Demo Product" + }, + "widgetSetupCompleted": true + } + ``` + + ```json {{ title: '401 Unauthorized' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- diff --git a/apps/formbricks-com/app/docs/api/management/people/page.mdx b/apps/formbricks-com/app/docs/api/management/people/page.mdx new file mode 100644 index 0000000000..569a401ebd --- /dev/null +++ b/apps/formbricks-com/app/docs/api/management/people/page.mdx @@ -0,0 +1,234 @@ +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Formbricks People API: Fetch or Create Person Overview", + description: + "Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.", +}; + +#### Management API + +# People API + +This set of API can be used to +- [List People](#list-people) +- [Get Person](#get-person) +- [Delete Person](#delete-person) + +You will need an API Key to interact with these APIs. + +--- + +## List People {{ tag: 'GET', label: '/api/v1/management/people' }} + + + + + List People + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/people' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": [ + { + "id": "b4wgrzl363dn3zb6yy5gf265", + "attributes": { + "userId": "CYO618", + "email": "sophia@amazon.com", + "Name": "Sophia Johnson", + "Role": "Designer", + "Company": "Amazon", + "Experience": "7 years", + "Usage Frequency": "Yearly", + "Company Size": "1628 employees", + "Product Satisfaction Score": "62", + "Recommendation Likelihood": "9" + }, + "environmentId": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.444Z", + "updatedAt": "2023-10-02T07:13:19.444Z" + }, + { + "id": "jrb5iyzqvnkg9322ckhde3j4", + "attributes": { + "userId": "CYO511", + "email": "antonio@ibm.com", + "Name": "Antonio García", + "Role": "Designer", + "Company": "IBM", + "Experience": "1 years", + "Usage Frequency": "Weekly", + "Company Size": "4023 employees", + "Product Satisfaction Score": "77", + "Recommendation Likelihood": "4" + }, + "environmentId": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.444Z", + "updatedAt": "2023-10-02T07:13:19.444Z" + }, + ] + } + ``` + + ```json {{ title: '400 Bad Request' }} + { + "code": "bad_request", + "message": "Fields are missing or incorrectly formatted", + "details": { + "userId": "" + } + } + ``` + + + + + +--- + +## Get Person {{ tag: 'GET', label: '/api/v1/management/people/' }} + + + + + Get Person by ID + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/people/' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "jrb5iyzqvnkg9322ckhde3j4", + "attributes": { + "userId": "CYO511", + "email": "antonio@ibm.com", + "Name": "Antonio García", + "Role": "Designer", + "Company": "IBM", + "Experience": "1 years", + "Usage Frequency": "Weekly", + "Company Size": "4023 employees", + "Product Satisfaction Score": "77", + "Recommendation Likelihood": "4" + }, + "environmentId": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.444Z", + "updatedAt": "2023-10-02T07:13:19.444Z" + } + } + ``` + + ```json {{ title: '404 Not Found' }} + { + "code": "not_found", + "message": "Person not found", + "details": { + "resource_id": "clmlmykc2000019vz5o3jglsa", + "resource_type": "Person" + } + } + ``` + + + + + +--- + +## Delete Person {{ tag: 'DELETE', label: '/api/v1/management/people/' }} + + + + + Delete Person by ID + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://app.formbricks.com/api/v1/management/people/ \ + --header 'x-api-key: ' + ``` + + + + + ```json {{title:'200 Success'}} + { + "data": { + "success": "Person deleted successfully" + } + } + ``` + + ```json {{ title: '404 Not Found' }} + { + "code": "not_found", + "message": "Person not found", + "details": { + "resource_id": "clmlmykc2000019vz5o3jglsa", + "resource_type": "Person" + } + } + ``` + + + + + +--- diff --git a/apps/formbricks-com/app/docs/api/management/responses/page.mdx b/apps/formbricks-com/app/docs/api/management/responses/page.mdx new file mode 100644 index 0000000000..eb564b7c2b --- /dev/null +++ b/apps/formbricks-com/app/docs/api/management/responses/page.mdx @@ -0,0 +1,289 @@ +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly", + description: + "Unlock the full potential of Formbricks' Responses API. From fetching to updating survey responses, our comprehensive guide helps you integrate and manage survey data efficiently without compromising security. Ideal for client-side interactions.", +}; + +#### Management API + +# Responses API + +This set of API can be used to +- [List Responses](#list-all-responses) +- [Get Response](#get-response-by-id) +- [Delete Response](#delete-a-response) + +You will need an API Key to interact with these APIs. + +--- + +## List all Responses {{ tag: 'GET', label: '/api/v1/management/responses' }} + + + + + Retrieve all the responses you have received in your environment. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/responses' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data":[ + { + "id": "cln8k0tqv00pcz87no4qrw333", + "createdAt": "2023-10-02T07:13:20.023Z", + "updatedAt": "2023-10-02T07:13:20.023Z", + "surveyId": "cln8k0tqu00p7z87nqr4thi3k", + "finished": true, + "data": { + "interview-prompt": "clicked" + }, + "meta": { + "userAgent": { + "os": "MacOS", + "browser": "Chrome" + } + }, + "personAttributes": null, + "person": { + "id": "e0x4i5tvsp8puxfztyrwykvn", + "attributes": { + "userId": "CYO675", + "email": "ravi@netflix.com", + "Name": "Ravi Kumar", + "Role": "Manager", + "Company": "Netflix", + "Experience": "6 years", + "Usage Frequency": "Monthly", + "Company Size": "4610 employees", + "Product Satisfaction Score": "43", + "Recommendation Likelihood": "4" + }, + "environmentId": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.444Z", + "updatedAt": "2023-10-02T07:13:19.444Z" + }, + "notes": [], + "tags": [] + }, + ] + } + ``` + + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Get Response by ID {{ tag: 'GET', label: '/api/v1/management/responses/' }} + + + + + Retrieve a response by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/responses/' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": + { + "id": "cln8k0tqv00pbz87nwo5lr72b", + "createdAt": "2023-10-02T07:13:20.023Z", + "updatedAt": "2023-10-02T07:13:20.023Z", + "surveyId": "cln8k0tqu00p7z87nqr4thi3k", + "finished": true, + "data": { + "interview-prompt": "clicked" + }, + "meta": { + "userAgent": { + "os": "Windows", + "browser": "Edge" + } + }, + "personAttributes": null, + "person": { + "id": "hsx38f15v50ua8383uadagq5", + "attributes": { + "userId": "CYO278", + "email": "jorge@facebook.com", + "Name": "Jorge Sanchez", + "Role": "Product Manager", + "Company": "Facebook", + "Experience": "10 years", + "Usage Frequency": "Daily", + "Company Size": "1685 employees", + "Product Satisfaction Score": "84", + "Recommendation Likelihood": "6" + }, + "environmentId": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.444Z", + "updatedAt": "2023-10-02T07:13:19.444Z" + }, + "notes": [], + "tags": [] + } + } + ``` + + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + +## Delete a response {{ tag: 'DELETE', label: '/api/v1/client/responses/' }} + + + + + Delete Response by ID + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE https://app.formbricks.com/api/v1/management/resposnes/ \ + --header 'x-api-key: ' + ``` + + + + + + ```json {{ title: '200 Success' }} + { + "data": { + "id": "cln8k0tqv00pbz87nwo5lr72b", + "createdAt": "2023-10-02T07:13:20.023Z", + "updatedAt": "2023-10-02T07:13:20.023Z", + "surveyId": "cln8k0tqu00p7z87nqr4thi3k", + "finished": true, + "data": { + "interview-prompt": "clicked" + }, + "meta": { + "userAgent": { + "os": "Windows", + "browser": "Edge" + } + }, + "personAttributes": null, + "person": { + "id": "hsx38f15v50ua8383uadagq5", + "attributes": { + "userId": "CYO278", + "email": "jorge@facebook.com", + "Name": "Jorge Sanchez", + "Role": "Product Manager", + "Company": "Facebook", + "Experience": "10 years", + "Usage Frequency": "Daily", + "Company Size": "1685 employees", + "Product Satisfaction Score": "84", + "Recommendation Likelihood": "6" + }, + "environmentId": "cln8k0t47000fz87njmmu2bck", + "createdAt": "2023-10-02T07:13:19.444Z", + "updatedAt": "2023-10-02T07:13:19.444Z" + }, + "notes": [], + "tags": [] + } + } + ``` + + ```json {{ title: '400 Bad Request' }} + { + "code": "bad_request", + "message": "surveyId was not provided.", + "details": { + "surveyId": "This field is required." + } + } + ``` + + + + + + +--- diff --git a/apps/formbricks-com/app/docs/api/management/surveys/page.mdx b/apps/formbricks-com/app/docs/api/management/surveys/page.mdx new file mode 100644 index 0000000000..5e12ae6b15 --- /dev/null +++ b/apps/formbricks-com/app/docs/api/management/surveys/page.mdx @@ -0,0 +1,667 @@ +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Formbricks Surveys API Documentation - How to Retrieve All Surveys", + description: + "Explore the comprehensive guide to the Formbricks Surveys API. Learn how to effectively retrieve all the surveys in your environment with the necessary headers and API key setup. Includes sample request and response formats.", +}; + +#### Management API + +# Surveys API + +This set of API can be used to +- [List All Surveys](#list-all-surveys) +- [Get Survey](#get-survey-by-id) +- [Create Survey](#create-survey) +- [Delete Survey](#delete-survey-by-id) + +You will need an API Key to interact with these APIs. + +--- + +## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }} + + + + + Retrieve all the surveys you have for the environment. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/surveys' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": [ + { + "id": "cllnfy2780fromy0hy7uoxvtn", + "createdAt": "2023-08-23T07:56:20.516Z", + "updatedAt": "2023-08-23T07:56:26.947Z", + "name": "Product Market Fit (Superhuman)", + "type": "link", + "environmentId": "cll2m30r70004mx0huqkitgqv", + "status": "inProgress", + "attributeFilters": [], + "displayOption": "displayOnce", + "autoClose": null, + "triggers": [], + "redirectUrl": null, + "recontactDays": null, + "questions": [ + { + "id": "gml6mgy71efgtq8np3s9je5p", + "type": "cta", + "headline": "You are one of our power users! Do you have 5 minutes?", + "required": false, + "buttonLabel": "Happy to help!", + "logic": [ + { + "condition": "skipped", + "destination": "end" + } + ], + "html": "

We would love to understand your user experience better. Sharing your insight helps a lot!

", + "buttonExternal": false, + "dismissButtonLabel": "No, thanks." + }, + { + "id": "kp62fbqe8cfzmvy8qwpr81b2", + "type": "multipleChoiceSingle", + "headline": "How disappointed would you be if you could no longer use My Product?", + "subheader": "Please select one of the following options:", + "required": true, + "choices": [ + { + "id": "bdgy1hnwd7uwmfxk1ljqp1n5", + "label": "Not at all disappointed" + }, + { + "id": "poabnvgtwenp8rb2v70gj4hj", + "label": "Somewhat disappointed" + }, + { + "id": "opfiqyqz8wrqn0i0f7t24d3n", + "label": "Very disappointed" + } + ], + "shuffleOption": "none" + }, + { + "id": "klvpwd4x08x8quesihvw5l92", + "type": "multipleChoiceSingle", + "headline": "What is your role?", + "subheader": "Please select one of the following options:", + "required": true, + "choices": [ + { + "id": "c8nerw6l9gpsxcmqkn10f9hy", + "label": "Founder" + }, + { + "id": "ebjqezei6a2axtuq86cleetn", + "label": "Executive" + }, + { + "id": "ctiijjblyhlp22snypfamqt1", + "label": "Product Manager" + }, + { + "id": "ibalyr0mhemfkkr82vypmg40", + "label": "Product Owner" + }, + { + "id": "fipk606aegslbd0e7yhc0xjx", + "label": "Software Engineer" + } + ], + "shuffleOption": "none" + }, + { + "id": "ryo75306flyg72iaeditbv51", + "type": "openText", + "headline": "What type of people do you think would most benefit from My Product?", + "required": true + }, + { + "id": "lkjaxb73ulydzeumhd51sx9g", + "type": "openText", + "headline": "What is the main benefit your receive from My Product?", + "required": true + }, + { + "id": "ec7agikkr58j8uonhioinkyk", + "type": "openText", + "headline": "How can we improve My Product for you?", + "subheader": "Please be as specific as possible.", + "required": true + } + ], + "thankYouCard": { + "enabled": true, + "headline": "Thank you!", + "subheader": "We appreciate your feedback." + }, + "delay": 0, + "autoComplete": null, + "closeOnDate": null + } + ] + } + ``` + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` +
+ + +
+ +--- + +## Get Survey by ID {{ tag: 'GET', label: '/api/v1/management/surveys/' }} + + + + + Get a specific survey by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/management/surveys/' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "cln8k0tjz00n5z87nwq527h3z", + "createdAt": "2023-10-02T07:13:19.775Z", + "updatedAt": "2023-10-02T07:13:19.775Z", + "name": "Churn Survey", + "type": "link", + "environmentId": "cln8k0t47000fz87njmmu2bck", + "status": "inProgress", + "attributeFilters": [], + "displayOption": "displayOnce", + "autoClose": null, + "triggers": [], + "redirectUrl": null, + "recontactDays": null, + "questions": [ + { + "id": "churn-reason", + "type": "multipleChoiceSingle", + "headline": "Why did you cancel your subscription?", + "subheader": "We're sorry to see you leave. Help us do better:", + "required": true, + "logic": [ + { + "condition": "equals", + "value": "Difficult to use", + "destination": "easier-to-use" + }, + { + "condition": "equals", + "value": "It's too expensive", + "destination": "30-off" + }, + { + "condition": "equals", + "value": "I am missing features", + "destination": "missing-features" + }, + { + "condition": "equals", + "value": "Poor customer service", + "destination": "poor-service" + }, + { + "condition": "equals", + "value": "I just didn't need it anymore", + "destination": "end" + } + ], + "choices": [ + { + "id": "isud2xethsw63dlwl89kr4kj", + "label": "Difficult to use" + }, + { + "id": "opuu4ba3dlele3n0gjkuh27c", + "label": "It's too expensive" + }, + { + "id": "gnypapo0rhvkt8pwosrphvbl", + "label": "I am missing features" + }, + { + "id": "wkgsrsrazd9kfunqhzjezx6t", + "label": "Poor customer service" + }, + { + "id": "pykmgyyw74vg0gaeryj6bo4c", + "label": "I just didn't need it anymore" + } + ] + }, + { + "id": "easier-to-use", + "type": "openText", + "headline": "What would have made {{productName}} easier to use?", + "subheader": "", + "required": true, + "buttonLabel": "Send", + "logic": [ + { + "condition": "submitted", + "destination": "end" + } + ] + }, + { + "id": "30-off", + "type": "cta", + "headline": "Get 30% off for the next year!", + "required": true, + "buttonLabel": "Get 30% off", + "logic": [ + { + "condition": "clicked", + "destination": "end" + } + ], + "html": "

We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.

", + "buttonUrl": "https://formbricks.com", + "buttonExternal": true, + "dismissButtonLabel": "Skip" + }, + { + "id": "missing-features", + "type": "openText", + "headline": "What features are you missing?", + "subheader": "", + "required": true, + "logic": [ + { + "condition": "submitted", + "destination": "end" + } + ] + }, + { + "id": "poor-service", + "type": "cta", + "headline": "So sorry to hear 😔 Talk to our CEO directly!", + "required": true, + "buttonLabel": "Send email to CEO", + "logic": [ + { + "condition": "clicked", + "destination": "end" + } + ], + "html": "

We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.

", + "buttonUrl": "mailto:ceo@company.com", + "buttonExternal": true, + "dismissButtonLabel": "Skip" + } + ], + "thankYouCard": { + "enabled": false + }, + "delay": 0, + "autoComplete": null, + "closeOnDate": null, + "surveyClosedMessage": null, + "verifyEmail": null + } + } + ``` + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` +
+ + +
+ +--- + +## Create Survey {{ tag: 'POST', label: '/api/v1/management/surveys' }} + + + + + Create a survey + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + ### Body + + + ```json {{ title: 'cURL' }} + { + "environmentId": "clmlmwdqq0003196ufewo6ibg", + "type": "link", + "name": "My new Survey" + } + ``` + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE \ + 'https://app.formbricks.com/api/v1/management/surveys' \ + --header \ + 'x-api-key: ' + ``` + + ```bash {{ title: 'cURL' }} + curl -X POST https://app.formbricks.com/api/v1/management/surveys/ \ + --header 'Content-Type: application/json' \ + --header 'x-api-key: ' \ + -d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Survey from API", "type": "link"}' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "clna6bqnz000az8zubq3e757t", + "createdAt": "2023-10-03T10:25:26.975Z", + "updatedAt": "2023-10-03T10:25:26.975Z", + "name": "My new Survey", + "redirectUrl": null, + "type": "link", + "environmentId": "cln8k0t47000fz87njmmu2bck", + "status": "draft", + "questions": [], + "thankYouCard": { + "enabled": false + }, + "displayOption": "displayOnce", + "recontactDays": null, + "autoClose": null, + "delay": 0, + "autoComplete": null, + "closeOnDate": null, + "surveyClosedMessage": null, + "verifyEmail": null + } + } + ``` + + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + + +--- + + +## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/' }} + + + + + Delete a survey by its ID. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE \ + 'https://app.formbricks.com/api/v1/management/surveys/' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data": { + "id": "cln8k0tjz00n5z87nwq527h3z", + "createdAt": "2023-10-02T07:13:19.775Z", + "updatedAt": "2023-10-02T07:13:19.775Z", + "name": "Churn Survey", + "type": "link", + "environmentId": "cln8k0t47000fz87njmmu2bck", + "status": "inProgress", + "attributeFilters": [], + "displayOption": "displayOnce", + "autoClose": null, + "triggers": [], + "redirectUrl": null, + "recontactDays": null, + "questions": [ + { + "id": "churn-reason", + "type": "multipleChoiceSingle", + "headline": "Why did you cancel your subscription?", + "subheader": "We're sorry to see you leave. Help us do better:", + "required": true, + "logic": [ + { + "condition": "equals", + "value": "Difficult to use", + "destination": "easier-to-use" + }, + { + "condition": "equals", + "value": "It's too expensive", + "destination": "30-off" + }, + { + "condition": "equals", + "value": "I am missing features", + "destination": "missing-features" + }, + { + "condition": "equals", + "value": "Poor customer service", + "destination": "poor-service" + }, + { + "condition": "equals", + "value": "I just didn't need it anymore", + "destination": "end" + } + ], + "choices": [ + { + "id": "isud2xethsw63dlwl89kr4kj", + "label": "Difficult to use" + }, + { + "id": "opuu4ba3dlele3n0gjkuh27c", + "label": "It's too expensive" + }, + { + "id": "gnypapo0rhvkt8pwosrphvbl", + "label": "I am missing features" + }, + { + "id": "wkgsrsrazd9kfunqhzjezx6t", + "label": "Poor customer service" + }, + { + "id": "pykmgyyw74vg0gaeryj6bo4c", + "label": "I just didn't need it anymore" + } + ] + }, + { + "id": "easier-to-use", + "type": "openText", + "headline": "What would have made {{productName}} easier to use?", + "subheader": "", + "required": true, + "buttonLabel": "Send", + "logic": [ + { + "condition": "submitted", + "destination": "end" + } + ] + }, + { + "id": "30-off", + "type": "cta", + "headline": "Get 30% off for the next year!", + "required": true, + "buttonLabel": "Get 30% off", + "logic": [ + { + "condition": "clicked", + "destination": "end" + } + ], + "html": "

We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.

", + "buttonUrl": "https://formbricks.com", + "buttonExternal": true, + "dismissButtonLabel": "Skip" + }, + { + "id": "missing-features", + "type": "openText", + "headline": "What features are you missing?", + "subheader": "", + "required": true, + "logic": [ + { + "condition": "submitted", + "destination": "end" + } + ] + }, + { + "id": "poor-service", + "type": "cta", + "headline": "So sorry to hear 😔 Talk to our CEO directly!", + "required": true, + "buttonLabel": "Send email to CEO", + "logic": [ + { + "condition": "clicked", + "destination": "end" + } + ], + "html": "

We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.

", + "buttonUrl": "mailto:ceo@company.com", + "buttonExternal": true, + "dismissButtonLabel": "Skip" + } + ], + "thankYouCard": { + "enabled": false + }, + "delay": 0, + "autoComplete": null, + "closeOnDate": null, + "surveyClosedMessage": null, + "verifyEmail": null + } + } + ``` + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` +
+ + +
+ +--- diff --git a/apps/formbricks-com/app/docs/api/webhooks/page.mdx b/apps/formbricks-com/app/docs/api/management/webhooks/page.mdx similarity index 90% rename from apps/formbricks-com/app/docs/api/webhooks/page.mdx rename to apps/formbricks-com/app/docs/api/management/webhooks/page.mdx index d0410f78fc..9ace9def1c 100644 --- a/apps/formbricks-com/app/docs/api/webhooks/page.mdx +++ b/apps/formbricks-com/app/docs/api/management/webhooks/page.mdx @@ -1,30 +1,31 @@ export const meta = { title: "Formbricks Webhook API Documentation - List, Retrieve, Create, and Delete Webhooks", - description: "Explore the comprehensive guide to the Formbricks Webhooks API. This is all you need to interact and play with the Formbricks Webhooks and integrate them into any third party app of your choice", + description: + "Explore the comprehensive guide to the Formbricks Webhooks API. This is all you need to interact and play with the Formbricks Webhooks and integrate them into any third party app of your choice", }; -#### Webhook API +#### Management API # 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. - -Before you start managing webhooks, you need to create an API key. This will be used for authorization when making requests to the Webhook API. Please see the [API key setup page](/docs/api/api-key-setup) for more details on how to create and manage your API keys. +Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment. The behavior of the webhooks is determined by their trigger settings. The trigger determines which updates the webhook sends. Current available triggers include "responseCreated", "responseUpdated", and "responseFinished". This allows you to customize your webhooks to only send notifications for the events that are relevant to your application. Webhooks are tied to a specific Formbricks environment. Once set, a webhook will receive updates from all surveys within this environment. This makes it easy to manage your data flow and ensure that all relevant updates are caught by the webhook. -Our API has several REST endpoints enabling you to manage these webhooks, providing a great deal of flexibility: +This set of API can be used to +- [List All Webhooks](#list-webhooks) +- [Get Webhook](#retrieve-webhook-by-id) +- [Create Webhook](#create-webhook) +- [Delete Webhook](#delete-webhook-by-id) -1. **List Webhooks:** Retrieve a list of all existing webhooks. -1. **Retrieve Webhook by ID:** Retrieve a specific webhook by it's ID. -1. **Create a New Webhook:** Add a new webhook to your system. -1. **Get a Specific Webhook:** Query the details of a specific webhook using its unique ID. -1. **Delete a Webhook:** Remove an existing webhook. +And the detailed Webhook Paylod is elaborated [here](#webhook-payload). 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. +You will need an API Key to interact with these APIs. + --- ## List Webhooks {{ tag: 'GET', label: '/api/v1/webhooks' }} @@ -92,7 +93,7 @@ These APIs are designed to facilitate seamless integration of Formbricks with th --- -## Retrieve Webhook by ID {{ tag: 'GET', label: '/api/v1/webhooks/[webhookId]' }} +## Retrieve Webhook by ID {{ tag: 'GET', label: '/api/v1/webhooks/' }} @@ -108,11 +109,11 @@ These APIs are designed to facilitate seamless integration of Formbricks with th - + ```bash {{ title: 'cURL' }} curl --location \ - 'https://app.formbricks.com/api/v1/webhooks/[webhookId]' \ + 'https://app.formbricks.com/api/v1/webhooks/' \ --header \ 'x-api-key: ' ``` @@ -255,7 +256,7 @@ Add a webhook to your product. --- -## Delete Webhook by ID {{ tag: 'DELETE', label: '/api/v1/webhooks/[webhookId]' }} +## Delete Webhook by ID {{ tag: 'DELETE', label: '/api/v1/webhooks/' }} @@ -271,10 +272,10 @@ Add a webhook to your product. - + ```bash {{ title: 'cURL' }} - curl --location --request DELETE 'https://app.formbricks.com/api/v1/webhooks/[webhookId]' \ + curl --location --request DELETE 'https://app.formbricks.com/api/v1/webhooks/' \ --header 'x-api-key: ' ``` diff --git a/apps/formbricks-com/app/docs/api/people/page.mdx b/apps/formbricks-com/app/docs/api/people/page.mdx deleted file mode 100644 index c682733736..0000000000 --- a/apps/formbricks-com/app/docs/api/people/page.mdx +++ /dev/null @@ -1,76 +0,0 @@ -import { Fence } from "@/components/shared/Fence"; - -export const meta = { - title: "Formbricks People API: Fetch or Create Person Overview", - description: - "Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.", -}; - -#### Management API - -# People API - -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. - ---- - -## Get or Create Person {{ tag: 'GET', label: '/api/v1/client/people/getOrCreate' }} - - - - - Fetch a Person or create (if they don't exist) by their userId and the environmentId. - - ### Mandatory Query Parameters - - - User ID to fetch or create (if it does not exist) - - - - - - Formbricks Environment ID - - - - - - - - - ```bash {{ title: 'cURL' }} - curl --location \ - 'https://app.formbricks.com/api/v1/client/people/getOrCreate?userId=&environmentId=' - ``` - - - - - - ```json {{title:'200 Success'}} - { - "data": { - "person": { - "id": "clm4bcxms02hspj0ht05k6q5c", - "environmentId": "cll2m30r70004mx0huqkitgqv" - } - } - } - ``` - - ```json {{ title: '400 Bad Request' }} - { - "code": "bad_request", - "message": "Fields are missing or incorrectly formatted", - "details": { - "userId": "" - } - } - ``` - - - - - ---- diff --git a/apps/formbricks-com/app/docs/api/surveys/page.mdx b/apps/formbricks-com/app/docs/api/surveys/page.mdx deleted file mode 100644 index 68faa22201..0000000000 --- a/apps/formbricks-com/app/docs/api/surveys/page.mdx +++ /dev/null @@ -1,180 +0,0 @@ -import { Fence } from "@/components/shared/Fence"; - -export const meta = { - title: "Formbricks Surveys API Documentation - How to Retrieve All Surveys", - description: - "Explore the comprehensive guide to the Formbricks Surveys API. Learn how to effectively retrieve all the surveys in your environment with the necessary headers and API key setup. Includes sample request and response formats.", -}; - -#### Management API - -# Surveys API - -The Survey API currently has one endpoint that allows you to get all the surveys you have in your Formbricks environment. You will need the [API Key](/docs/api/api-key-setup) to access the same! - ---- - -## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }} - - - - - Retrieve all the surveys you have for the environment. - - ### Mandatory Headers - - - - Your Formbricks API key. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl --location \ - 'https://app.formbricks.com/api/v1/management/surveys' \ - --header \ - 'x-api-key: ' - ``` - - - - - - ```json {{title:'200 Success'}} - { - "data": [ - { - "id": "cllnfy2780fromy0hy7uoxvtn", - "createdAt": "2023-08-23T07:56:20.516Z", - "updatedAt": "2023-08-23T07:56:26.947Z", - "name": "Product Market Fit (Superhuman)", - "type": "link", - "environmentId": "cll2m30r70004mx0huqkitgqv", - "status": "inProgress", - "attributeFilters": [], - "displayOption": "displayOnce", - "autoClose": null, - "triggers": [], - "redirectUrl": null, - "recontactDays": null, - "questions": [ - { - "id": "gml6mgy71efgtq8np3s9je5p", - "type": "cta", - "headline": "You are one of our power users! Do you have 5 minutes?", - "required": false, - "buttonLabel": "Happy to help!", - "logic": [ - { - "condition": "skipped", - "destination": "end" - } - ], - "html": "

We would love to understand your user experience better. Sharing your insight helps a lot!

", - "buttonExternal": false, - "dismissButtonLabel": "No, thanks." - }, - { - "id": "kp62fbqe8cfzmvy8qwpr81b2", - "type": "multipleChoiceSingle", - "headline": "How disappointed would you be if you could no longer use My Product?", - "subheader": "Please select one of the following options:", - "required": true, - "choices": [ - { - "id": "bdgy1hnwd7uwmfxk1ljqp1n5", - "label": "Not at all disappointed" - }, - { - "id": "poabnvgtwenp8rb2v70gj4hj", - "label": "Somewhat disappointed" - }, - { - "id": "opfiqyqz8wrqn0i0f7t24d3n", - "label": "Very disappointed" - } - ], - "shuffleOption": "none" - }, - { - "id": "klvpwd4x08x8quesihvw5l92", - "type": "multipleChoiceSingle", - "headline": "What is your role?", - "subheader": "Please select one of the following options:", - "required": true, - "choices": [ - { - "id": "c8nerw6l9gpsxcmqkn10f9hy", - "label": "Founder" - }, - { - "id": "ebjqezei6a2axtuq86cleetn", - "label": "Executive" - }, - { - "id": "ctiijjblyhlp22snypfamqt1", - "label": "Product Manager" - }, - { - "id": "ibalyr0mhemfkkr82vypmg40", - "label": "Product Owner" - }, - { - "id": "fipk606aegslbd0e7yhc0xjx", - "label": "Software Engineer" - } - ], - "shuffleOption": "none" - }, - { - "id": "ryo75306flyg72iaeditbv51", - "type": "openText", - "headline": "What type of people do you think would most benefit from My Product?", - "required": true - }, - { - "id": "lkjaxb73ulydzeumhd51sx9g", - "type": "openText", - "headline": "What is the main benefit your receive from My Product?", - "required": true - }, - { - "id": "ec7agikkr58j8uonhioinkyk", - "type": "openText", - "headline": "How can we improve My Product for you?", - "subheader": "Please be as specific as possible.", - "required": true - } - ], - "thankYouCard": { - "enabled": true, - "headline": "Thank you!", - "subheader": "We appreciate your feedback." - }, - "delay": 0, - "autoComplete": null, - "closeOnDate": null - } - ] - } - ``` - ```json {{ title: '401 Not Authenticated' }} - { - "code": "not_authenticated", - "message": "Not authenticated", - "details": { - "x-Api-Key": "Header not provided or API Key invalid" - } - } - ``` -
- - -
- ---- diff --git a/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx b/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx index 443ed002d4..3854d3c785 100644 --- a/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx +++ b/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx @@ -14,7 +14,7 @@ export const meta = { # Troubleshooting -Here you'll find help with Frequently Recurring Problems +Here you'll find help with frequently recurring problems ## "The app doesn't work after doing a prisma migration" @@ -24,7 +24,7 @@ This can happen but fear not, the fix is easy: Delete the application storage of src={ClearAppData} alt="Demo App Preview" quality="100" - className="rounded-lg max-w-full sm:max-w-3xl" + className="max-w-full rounded-lg sm:max-w-3xl" /> ## "I ran 'pnpm i' but there seems to be an error with the packages" @@ -74,9 +74,9 @@ However, in our experience it's better to run `pnpm dev` than having two termina src={UncaughtPromise} alt="Uncaught promise" quality="100" - className="rounded-lg max-w-full sm:max-w-3xl" + className="max-w-full rounded-lg sm:max-w-3xl" /> This happens when you're using the Demo App and delete the Person within the Formbricks app which the widget is currently connected with. We're fixing it, but you can also just logout your test person and reload the page to get rid of it. -Logout Person +Logout Person diff --git a/apps/formbricks-com/app/docs/faq/page.mdx b/apps/formbricks-com/app/docs/faq/page.mdx new file mode 100644 index 0000000000..197cb5b704 --- /dev/null +++ b/apps/formbricks-com/app/docs/faq/page.mdx @@ -0,0 +1,14 @@ +import FAQ from "@/components/docs/docsFaq"; + +export const meta = { + title: "FAQ", + description: "Frequently Asked Questions about Formbricks and how to use it.", +}; + +#### FAQ + +# Frequently Asked Questions + +Here you'll find help with frequently recurring problems. If you can't find an answer to your question, please join our [Discord server](https://formbricks.com/discord). + + diff --git a/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx b/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx index 33e0d1462c..ea333e00fd 100644 --- a/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx +++ b/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx @@ -45,5 +45,3 @@ Natively embed qualitative user research into your B2B SaaS. Leverage Best Pract - - diff --git a/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx b/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx index 3e31fb1cd5..8bfcfae9a7 100644 --- a/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx +++ b/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx @@ -42,16 +42,6 @@ The `userId` we are refering to is the `userId` of your own system. For example, This allows you to connect the response to the user profile of this specific in the Formbricks database. You can then use the response data to create segments for further surveying or invite them to an interview, etc. -## getOrCreateUser - how it works exactly - -By default, respondents of link surveys are **not** recorded and displayed in the People view. This would lead to your People view to be spammend with unidentified people. - -If you add the `userId` URL parameter a Person will be created, if there is no existing person in your database with a matching `userId`. - -**Case 1:** User with userId `ABC111` exists, the response from the link survey will be added to this users profile. - -**Case 2:** User with userId `ABC222` does not yet exists, so it is created with the response from the link survey connected to this user. - If users are identified by email, it will show. If not, the internal Id will be set. = [ { title: "Gitpod", href: "/docs/contributing/gitpod" }, { title: "Demo App", href: "/docs/contributing/demo" }, { title: "Troubleshooting", href: "/docs/contributing/troubleshooting" }, + { title: "FAQ", href: "/docs/faq" }, ], }, { - title: "API", + title: "Client API", links: [ - { title: "Overview", href: "/docs/api/overview" }, - { title: "API Key Setup", href: "/docs/api/api-key-setup" }, - { title: "Displays", href: "/docs/api/display" }, - { title: "People", href: "/docs/api/people" }, - { title: "Responses", href: "/docs/api/responses" }, - { title: "Surveys", href: "/docs/api/surveys" }, - { title: "Webhook", href: "/docs/api/webhooks" }, + { title: "Overview", href: "/docs/api/client/overview" }, + { title: "Displays", href: "/docs/api/client/displays" }, + { title: "Responses", href: "/docs/api/client/responses" }, + ], + }, + { + title: "Management API", + links: [ + { title: "API Key Setup", href: "/docs/api/management/api-key-setup" }, + { title: "Action Classes", href: "/docs/api/management/action-classes" }, + { title: "Attribute Classes", href: "/docs/api/management/attribute-classes" }, + { title: "Me", href: "/docs/api/management/me" }, + { title: "People", href: "/docs/api/management/people" }, + { title: "Surveys", href: "/docs/api/management/surveys" }, + { title: "Webhooks", href: "/docs/api/management/webhooks" }, ], }, ]; diff --git a/apps/formbricks-com/components/docs/docsFaq.tsx b/apps/formbricks-com/components/docs/docsFaq.tsx new file mode 100644 index 0000000000..b0da4979fa --- /dev/null +++ b/apps/formbricks-com/components/docs/docsFaq.tsx @@ -0,0 +1,69 @@ +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui"; +import FaqJsonLdComponent from "./faQJsonLD"; + +const FAQ_DATA = [ + { + question: "What is an environment ID?", + answer: () => ( + <> + The environment ID is a unique identifier associated with each Environment in Formbricks, + distinguishing between different setups like production, development, etc. + + ), + }, + { + question: "How can I implement authentication for the Formbricks API?", + answer: () => ( + <> + Formbricks provides 2 types of API keys for each environment ie development and production. You can + generate, view, and manage these keys in the Settings section on the Admin dashboard. Include the API + key in your requests to authenticate and gain access to Formbricks functionalities. + + ), + }, + { + question: "Can I run the deployment shell script on any server?", + answer: () => ( + <> + You can run it on any machine you own as long as its running a Linux Ubuntu distribution. And + to forward the requests, make sure you have an A record setup for your domain pointing to the + server. + + ), + }, + { + question: "Can I self-host Formbricks?", + answer: () => ( + <> + Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more + control over data and compliance. And the best part? Self-hosting is available for free, always. For + documentation on self hosting, click{" "} + + here + + . + + ), + }, +]; + +export const faqJsonLdData = FAQ_DATA.map((faq) => ({ + questionName: faq.question, + acceptedAnswerText: faq.answer(), +})); + +export default function FAQ() { + return ( + <> + + + {FAQ_DATA.map((faq, index) => ( + + {faq.question} + {faq.answer()} + + ))} + + + ); +} diff --git a/apps/formbricks-com/components/docs/faQJsonLD.tsx b/apps/formbricks-com/components/docs/faQJsonLD.tsx new file mode 100644 index 0000000000..930261eaab --- /dev/null +++ b/apps/formbricks-com/components/docs/faQJsonLD.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { FAQPageJsonLd } from "next-seo"; + +export default function FaqJsonLdComponent({ data }) { + const faqEntities = data.map(({ question, answer }) => ({ + questionName: question, + acceptedAnswerText: answer, + })); + + return ; +} diff --git a/apps/formbricks-com/components/home/Faq.tsx b/apps/formbricks-com/components/home/Faq.tsx new file mode 100644 index 0000000000..7ea60cc829 --- /dev/null +++ b/apps/formbricks-com/components/home/Faq.tsx @@ -0,0 +1,87 @@ +import { FAQPageJsonLd } from "next-seo"; + +import HeadingCentered from "@/components/shared/HeadingCentered"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui"; + +const FAQ_DATA = [ + { + question: "What is Formbricks?", + answer: () => ( + <> + Formbricks is an open-source Experience Management tool that helps businesses understand what + customers think and feel about their products. It integrates natively into your platform to conduct + user research with a focus on data privacy and minimal development intervention. + + ), + }, + { + question: "How do I integrate Formbricks into my application?", + answer: () => ( + <> + Integrating Formbricks is a breeze. Simply copy a script tag to your HTML head, or use NPM to install + Formbricks for platforms like React, Vue, Svelte, etc. Once installed, initialize Formbricks with your + environment details. Learn more with our framework guides{" "} + + here + + . + + ), + }, + { + question: "Is Formbricks GDPR compliant?", + answer: () => ( + <> + Yes, Formbricks is fully GDPR compliant. Whether you use our cloud solution or decide to self-host, we + ensure compliance with all data privacy regulations. + + ), + }, + { + question: "Can I self-host Formbricks?", + answer: () => ( + <> + Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more + control over data and compliance. And the best part? Self-hosting is available for free, always. For + documentation on self hosting, click{" "} + + here + + . + + ), + }, + { + question: "How does Formbricks pricing work?", + answer: () => ( + <> + Formbricks offers a Free forever plan on the cloud that includes unlimited surveys, in-product + surveys, and more. We also provide a self-hosting option which includes all free features and more, + available at no cost. If you require additional features or responses, check out our pricing section + above for more details. + + ), + }, +]; + +const faqJsonLdData = FAQ_DATA.map((faq) => ({ + questionName: faq.question, + acceptedAnswerText: faq.answer(), +})); + +export default function FAQ() { + return ( +
+ + + + {FAQ_DATA.map((faq, index) => ( + + {faq.question} + {faq.answer()} + + ))} + +
+ ); +} diff --git a/apps/formbricks-com/pages/formtribe/index.tsx b/apps/formbricks-com/pages/formtribe/index.tsx index 409b1f4c72..9a993efd3f 100644 --- a/apps/formbricks-com/pages/formtribe/index.tsx +++ b/apps/formbricks-com/pages/formtribe/index.tsx @@ -299,8 +299,8 @@ const Leaderboard = [ points: "200", }, { - name: "Arjun", - points: "100", + name: "Naitik Kapadia (Arjun)", + points: "200", }, { name: "Yashhhh", @@ -332,7 +332,7 @@ const Leaderboard = [ }, { name: "Eldemarkki", - points: "100", + points: "500", }, { name: "Suyash", @@ -360,17 +360,57 @@ const Leaderboard = [ }, { name: "Aditya Deshlahre", - points: "450", + points: "550", link: "https://github.com/adityadeshlahre", }, { name: "Rutam", - points: "250", + points: "350", }, { name: "Sagnik Sahoo", points: "100", }, + { + name: "Prasoon Mahawar", + points: "100", + }, + { + name: "Dushmanta", + points: "100", + }, + { + name: "Arjavv", + points: "100", + }, + { + name: "Ashish Khare", + points: "100", + }, + { + name: "Rohit Mondal", + points: "100", + }, + { + name: "noobcoder", + points: "100", + }, + { + name: "Rayyan Alam (Rayy)", + points: "100", + }, + { + name: "Ayush", + points: "100", + }, + { + name: "Zechariah", + points: "100", + }, + { + name: "Rajarshi Misra", + points: "100", + }, ]; export default function FormTribeHackathon() { diff --git a/apps/formbricks-com/pages/index.tsx b/apps/formbricks-com/pages/index.tsx index c4b8bc18dd..5915a86874 100644 --- a/apps/formbricks-com/pages/index.tsx +++ b/apps/formbricks-com/pages/index.tsx @@ -1,11 +1,12 @@ -import Layout from "@/components/shared/Layout"; -import Hero from "@/components/home/Hero"; +import Faq from "@/components/home/Faq"; import Features from "@/components/home/Features"; -import Highlights from "@/components/home/Highlights"; -import BreakerCTA from "@/components/shared/BreakerCTA"; -import Steps from "@/components/home/Steps"; import GitHubSponsorship from "@/components/home/GitHubSponsorship"; +import Hero from "@/components/home/Hero"; +import Highlights from "@/components/home/Highlights"; +import Steps from "@/components/home/Steps"; import BestPractices from "@/components/shared/BestPractices"; +import BreakerCTA from "@/components/shared/BreakerCTA"; +import Layout from "@/components/shared/Layout"; const IndexPage = () => ( ( href="https://app.formbricks.com/auth/signup" inverted /> + + ); diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts index 11dd1b590f..0a82437d09 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts @@ -12,7 +12,7 @@ import { getActionCountInLast7Days, getActionCountInLastHour, } from "@formbricks/lib/services/actions"; -import { getSurveysByActionClassId } from "@formbricks/lib/services/survey"; +import { getSurveysByActionClassId } from "@formbricks/lib/survey/service"; import { AuthorizationError } from "@formbricks/types/v1/errors"; export async function deleteActionClassAction(environmentId, actionClassId: string) { diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts index 55f9b34315..54bdcabb3e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { getSurveysByAttributeClassId } from "@formbricks/lib/services/survey"; +import { getSurveysByAttributeClassId } from "@formbricks/lib/survey/service"; export const GetActiveInactiveSurveysAction = async ( attributeClassId: string diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index dc4caa403c..1be53741c3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -1,133 +1,45 @@ "use server"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { prisma } from "@formbricks/database"; -import { ResourceNotFoundError } from "@formbricks/types/v1/errors"; -import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { createMembership } from "@formbricks/lib/services/membership"; +import { createProduct } from "@formbricks/lib/services/product"; +import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/services/team"; +import { canUserAccessSurvey } from "@formbricks/lib/survey/auth"; +import { deleteSurvey, getSurvey } from "@formbricks/lib/survey/service"; +import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; import { Team } from "@prisma/client"; import { Prisma as prismaClient } from "@prisma/client/"; -import { createProduct } from "@formbricks/lib/services/product"; +import { getServerSession } from "next-auth"; -export async function createTeam(teamName: string, ownerUserId: string): Promise { - const newTeam = await prisma.team.create({ - data: { - name: teamName, - memberships: { - create: { - user: { connect: { id: ownerUserId } }, - role: "owner", - accepted: true, - }, - }, - products: { - create: [ - { - name: "My Product", - environments: { - create: [ - { - type: "production", - eventClasses: { - create: [ - { - name: "New Session", - description: "Gets fired when a new session is created", - type: "automatic", - }, - { - name: "Exit Intent (Desktop)", - description: "A user on Desktop leaves the website with the cursor.", - type: "automatic", - }, - { - name: "50% Scroll", - description: "A user scrolled 50% of the current page", - type: "automatic", - }, - ], - }, - attributeClasses: { - create: [ - { - name: "userId", - description: "The internal ID of the person", - type: "automatic", - }, - { - name: "email", - description: "The email of the person", - type: "automatic", - }, - ], - }, - }, - { - type: "development", - eventClasses: { - create: [ - { - name: "New Session", - description: "Gets fired when a new session is created", - type: "automatic", - }, - { - name: "Exit Intent (Desktop)", - description: "A user on Desktop leaves the website with the cursor.", - type: "automatic", - }, - { - name: "50% Scroll", - description: "A user scrolled 50% of the current page", - type: "automatic", - }, - ], - }, - attributeClasses: { - create: [ - { - name: "userId", - description: "The internal ID of the person", - type: "automatic", - }, - { - name: "email", - description: "The email of the person", - type: "automatic", - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - include: { - memberships: true, - products: { - include: { - environments: true, - }, - }, - }, +export async function createTeamAction(teamName: string): Promise { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const newTeam = await createTeam({ + name: teamName, }); - const teamId = newTeam?.id; + await createMembership(newTeam.id, session.user.id, { + role: "owner", + accepted: true, + }); - if (teamId) { - fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, { - method: "POST", - headers: { - "x-api-key": INTERNAL_SECRET, - }, - }); - } + await createProduct(newTeam.id, { + name: "My Product", + }); return newTeam; } export async function duplicateSurveyAction(environmentId: string, surveyId: string) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + const existingSurvey = await getSurvey(surveyId); if (!existingSurvey) { @@ -180,6 +92,24 @@ export async function copyToOtherEnvironmentAction( surveyId: string, targetEnvironmentId: string ) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorizedToAccessSourceEnvironment = await hasUserEnvironmentAccess( + session.user.id, + environmentId + ); + if (!isAuthorizedToAccessSourceEnvironment) throw new AuthorizationError("Not authorized"); + + const isAuthorizedToAccessTargetEnvironment = await hasUserEnvironmentAccess( + session.user.id, + targetEnvironmentId + ); + if (!isAuthorizedToAccessTargetEnvironment) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + const existingSurvey = await prisma.survey.findFirst({ where: { id: surveyId, @@ -305,12 +235,32 @@ export async function copyToOtherEnvironmentAction( } export const deleteSurveyAction = async (surveyId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + await deleteSurvey(surveyId); }; export const createProductAction = async (environmentId: string, productName: string) => { - const productCreated = await createProduct(environmentId, productName); + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); - const newEnvironment = productCreated.environments[0]; - return newEnvironment; + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + const team = await getTeamByEnvironmentId(environmentId); + if (!team) throw new ResourceNotFoundError("Team from environment", environmentId); + + const product = await createProduct(team.id, { + name: productName, + }); + + // get production environment + const productionEnvironment = product.environments.find((environment) => environment.type === "production"); + if (!productionEnvironment) throw new ResourceNotFoundError("Production environment", environmentId); + + return productionEnvironment; }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx index 6fea3e8ea3..a9bc49678f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx @@ -2,7 +2,7 @@ import GoogleSheetWrapper from "@/app/(app)/environments/[environmentId]/integra import GoBackButton from "@/components/shared/GoBackButton"; import { getSpreadSheets } from "@formbricks/lib/services/googleSheet"; import { getIntegrations } from "@formbricks/lib/services/integrations"; -import { getSurveys } from "@formbricks/lib/services/survey"; +import { getSurveys } from "@formbricks/lib/survey/service"; import { TGoogleSheetIntegration, TGoogleSpreadsheet } from "@formbricks/types/v1/integrations"; import { GOOGLE_SHEETS_CLIENT_ID, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx index 41ed71d2b8..3c4839696d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx @@ -4,7 +4,7 @@ import WebhookRowData from "@/app/(app)/environments/[environmentId]/integration import WebhookTable from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTable"; import WebhookTableHeading from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTableHeading"; import GoBackButton from "@/components/shared/GoBackButton"; -import { getSurveys } from "@formbricks/lib/services/survey"; +import { getSurveys } from "@formbricks/lib/survey/service"; import { getWebhooks } from "@formbricks/lib/services/webhook"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/services/environment"; diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx index 60f73bced7..969d094116 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx @@ -1,6 +1,6 @@ import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline"; +import { getSurveys } from "@formbricks/lib/survey/service"; import { getResponsesByPersonId } from "@formbricks/lib/response/service"; -import { getSurveys } from "@formbricks/lib/services/survey"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; import { TSurvey } from "@formbricks/types/v1/surveys"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditTeamName.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditTeamName.tsx index 87320cb6ab..dad6c6800c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditTeamName.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditTeamName.tsx @@ -1,6 +1,6 @@ "use client"; -import { updateTeamAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions"; +import { updateTeamNameAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions"; import { TTeam } from "@formbricks/types/v1/teams"; import { Button, Input, Label } from "@formbricks/ui"; import { useRouter } from "next/navigation"; @@ -43,7 +43,7 @@ export default function EditTeamName({ team }: TEditTeamNameProps) { const handleUpdateTeamName: SubmitHandler = async (data) => { try { setIsUpdatingTeam(true); - await updateTeamAction(team.id, data); + await updateTeamNameAction(team.id, data.name); setIsUpdatingTeam(false); toast.success("Team name updated successfully."); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts index d101e9fc00..c9277e84a1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts @@ -20,13 +20,22 @@ import { import { deleteTeam, updateTeam } from "@formbricks/lib/services/team"; import { TInviteUpdateInput } from "@formbricks/types/v1/invites"; import { TMembershipRole, TMembershipUpdateInput } from "@formbricks/types/v1/memberships"; -import { TTeamUpdateInput } from "@formbricks/types/v1/teams"; import { getServerSession } from "next-auth"; import { hasTeamAccess, hasTeamAuthority, hasTeamOwnership, isOwner } from "@formbricks/lib/auth"; import { INVITE_DISABLED } from "@formbricks/lib/constants"; -export const updateTeamAction = async (teamId: string, data: TTeamUpdateInput) => { - return await updateTeam(teamId, data); +export const updateTeamNameAction = async (teamId: string, teamName: string) => { + const session = await getServerSession(authOptions); + if (!session) { + throw new AuthenticationError("Not authenticated"); + } + + const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId); + if (!isUserAuthorized) { + throw new AuthenticationError("Not authorized"); + } + + return await updateTeam(teamId, { name: teamName }); }; export const updateMembershipAction = async ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx index bf5fcf60be..c75e81083d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx @@ -5,7 +5,7 @@ import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; import { SURVEY_BASE_URL } from "@formbricks/lib/constants"; import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getSurveys } from "@formbricks/lib/services/survey"; +import { getSurveys } from "@formbricks/lib/survey/service"; import type { TEnvironment } from "@formbricks/types/v1/environment"; import { Badge } from "@formbricks/ui"; import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx index 7878b3b755..2d94b0003a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyStarter.tsx @@ -4,6 +4,7 @@ import TemplateList from "@/app/(app)/environments/[environmentId]/surveys/templ import LoadingSpinner from "@/components/shared/LoadingSpinner"; import type { TEnvironment } from "@formbricks/types/v1/environment"; import type { TProduct } from "@formbricks/types/v1/product"; +import { TSurveyInput } from "@formbricks/types/v1/surveys"; import { TTemplate } from "@formbricks/types/v1/templates"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -28,7 +29,7 @@ export default function SurveyStarter({ ...template.preset, type: surveyType, autoComplete, - }; + } as Partial; try { const survey = await createSurveyAction(environmentId, augmentedTemplate); router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts index bd86c900a1..479538c7a4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts @@ -1,7 +1,7 @@ import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service"; import { getSurveyResponses } from "@formbricks/lib/response/service"; -import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey"; import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; export const getAnalysisData = async (surveyId: string, environmentId: string) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index c5866fb710..c9a4526191 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -30,7 +30,12 @@ export default async function Page({ params }) { getEnvironment(params.environmentId), ]); const isSingleUseSurvey = survey.singleUse?.enabled ?? false; - const singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false); + + let singleUseIds: string[] | undefined = undefined; + if (isSingleUseSurvey) { + singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false); + } + if (!environment) { throw new Error("Environment not found"); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx index 5d6d861b1e..704985c24f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx @@ -23,7 +23,7 @@ import LinkSurveyShareButton from "@/app/(app)/environments/[environmentId]/surv import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TProduct } from "@formbricks/types/v1/product"; -import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions"; +import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions"; interface SummaryHeaderProps { surveyId: string; @@ -114,7 +114,7 @@ const SummaryHeader = ({ value={survey.status} onValueChange={(value) => { const castedValue = value as "draft" | "inProgress" | "paused" | "completed"; - surveyMutateAction({ ...survey, status: castedValue }) + updateSurveyAction({ ...survey, status: castedValue }) .then(() => { toast.success( value === "inProgress" diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx index 7f3e764e16..b0f9bd0378 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx @@ -14,7 +14,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { validateQuestion } from "./Validation"; -import { deleteSurveyAction, surveyMutateAction } from "./actions"; +import { deleteSurveyAction, updateSurveyAction } from "./actions"; interface SurveyMenuBarProps { localSurvey: TSurveyWithAnalytics; @@ -143,7 +143,7 @@ export default function SurveyMenuBar({ } try { - await surveyMutateAction({ ...strippedSurvey }); + await updateSurveyAction({ ...strippedSurvey }); router.refresh(); setIsMutatingSurvey(false); toast.success("Changes saved."); @@ -242,7 +242,7 @@ export default function SurveyMenuBar({ if (!validateSurvey(localSurvey)) { return; } - await surveyMutateAction({ ...localSurvey, status: "inProgress" }); + await updateSurveyAction({ ...localSurvey, status: "inProgress" }); router.refresh(); setIsMutatingSurvey(false); router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts index ebd4ce8307..97a0304f2e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts @@ -1,12 +1,28 @@ "use server"; import { TSurvey } from "@formbricks/types/v1/surveys"; -import { deleteSurvey, updateSurvey } from "@formbricks/lib/services/survey"; +import { deleteSurvey, updateSurvey } from "@formbricks/lib/survey/service"; +import { canUserAccessSurvey } from "@formbricks/lib/survey/auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { getServerSession } from "next-auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; + +export async function updateSurveyAction(survey: TSurvey): Promise { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessSurvey(session.user.id, survey.id); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); -export async function surveyMutateAction(survey: TSurvey): Promise { return await updateSurvey(survey); } -export async function deleteSurveyAction(surveyId: string) { +export const deleteSurveyAction = async (surveyId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + await deleteSurvey(surveyId); -} +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx index 9376c15f6a..fd18a81945 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx @@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL; import React from "react"; import { FORMBRICKS_ENCRYPTION_KEY, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import SurveyEditor from "./SurveyEditor"; -import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey"; +import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; import { getEnvironment } from "@formbricks/lib/services/environment"; import { getActionClasses } from "@formbricks/lib/actionClass/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts index bd3454703c..31240985e5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts @@ -1,7 +1,18 @@ "use server"; -import { createSurvey } from "@formbricks/lib/services/survey"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { getServerSession } from "next-auth"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { createSurvey } from "@formbricks/lib/survey/service"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; +import { TSurvey } from "@formbricks/types/v1/surveys"; + +export async function createSurveyAction(environmentId: string, surveyBody: Partial) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); -export async function createSurveyAction(environmentId: string, surveyBody: any) { return await createSurvey(environmentId, surveyBody); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/TemplateList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/TemplateList.tsx index 96bfdac84d..832287750f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/TemplateList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/TemplateList.tsx @@ -22,6 +22,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { createSurveyAction } from "./actions"; import { customSurvey, templates } from "./templates"; +import { TSurveyInput } from "@formbricks/types/v1/surveys"; type TemplateList = { environmentId: string; @@ -77,7 +78,7 @@ export default function TemplateList({ ...activeTemplate.preset, type: surveyType, autoComplete, - }; + } as Partial; const survey = await createSurveyAction(environmentId, augmentedTemplate); router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts index bd3454703c..33cb980756 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts @@ -1,7 +1,18 @@ "use server"; -import { createSurvey } from "@formbricks/lib/services/survey"; +import { createSurvey } from "@formbricks/lib/survey/service"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { getServerSession } from "next-auth"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; +import { TSurvey } from "@formbricks/types/v1/surveys"; + +export async function createSurveyAction(environmentId: string, surveyBody: Partial) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); -export async function createSurveyAction(environmentId: string, surveyBody: any) { return await createSurvey(environmentId, surveyBody); } diff --git a/apps/web/app/(app)/onboarding/components/Objective.tsx b/apps/web/app/(app)/onboarding/components/Objective.tsx index c69bd36c85..d0b406604e 100644 --- a/apps/web/app/(app)/onboarding/components/Objective.tsx +++ b/apps/web/app/(app)/onboarding/components/Objective.tsx @@ -41,7 +41,11 @@ const Objective: React.FC = ({ next, skip, formbricksResponseId, if (selectedObjective) { try { setIsProfileUpdating(true); - const updatedProfile = { ...profile, objective: selectedObjective.id }; + const updatedProfile = { + ...profile, + objective: selectedObjective.id, + name: profile.name ?? undefined, + }; await updateProfileAction(updatedProfile); setIsProfileUpdating(false); } catch (e) { diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 0997cd6f38..56796e13e4 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -1,23 +1,25 @@ export const revalidate = REVALIDATION_INTERVAL; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getFirstEnvironmentByUserId } from "@formbricks/lib/services/environment"; +import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; +import { getProfile } from "@formbricks/lib/services/profile"; import { getServerSession } from "next-auth"; import Onboarding from "./components/Onboarding"; -import { getEnvironmentByUser } from "@formbricks/lib/services/environment"; -import { getProfile } from "@formbricks/lib/services/profile"; -import { ErrorComponent } from "@formbricks/ui"; -import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; export default async function OnboardingPage() { const session = await getServerSession(authOptions); - const environment = await getEnvironmentByUser(session?.user); + if (!session) { + throw new Error("No session found"); + } + const environment = await getFirstEnvironmentByUserId(session?.user.id); const profile = await getProfile(session?.user.id!); const product = await getProductByEnvironmentId(environment?.id!); if (!environment || !profile || !product) { - return ; + throw new Error("Failed to get environment, profile, or product"); } - return ; + return ; } diff --git a/apps/web/app/api/integration/integrations.ts b/apps/web/app/api/integration/integrations.ts index 3ffbc383d7..c28c768867 100644 --- a/apps/web/app/api/integration/integrations.ts +++ b/apps/web/app/api/integration/integrations.ts @@ -1,5 +1,5 @@ import { writeData } from "@formbricks/lib/services/googleSheet"; -import { getSurvey } from "@formbricks/lib/services/survey"; +import { getSurvey } from "@formbricks/lib/survey/service"; import { TGoogleSheetIntegration, TIntegration } from "@formbricks/types/v1/integrations"; import { TPipelineInput } from "@formbricks/types/v1/pipelines"; diff --git a/apps/web/app/api/v1/client/displays/route.ts b/apps/web/app/api/v1/client/displays/route.ts index e51d6639af..7d46e99a8e 100644 --- a/apps/web/app/api/v1/client/displays/route.ts +++ b/apps/web/app/api/v1/client/displays/route.ts @@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/lib/api/validator"; import { InvalidInputError } from "@formbricks/types/v1/errors"; import { capturePosthogEvent } from "@formbricks/lib/posthogServer"; import { createDisplay } from "@formbricks/lib/services/displays"; -import { getSurvey } from "@formbricks/lib/services/survey"; +import { getSurvey } from "@formbricks/lib/survey/service"; import { getTeamDetails } from "@formbricks/lib/services/teamDetails"; import { TDisplay, ZDisplayInput } from "@formbricks/types/v1/displays"; import { NextResponse } from "next/server"; diff --git a/apps/web/app/api/v1/client/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/responses/[responseId]/route.ts index aba81b2fb4..59edb5b4cb 100644 --- a/apps/web/app/api/v1/client/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/responses/[responseId]/route.ts @@ -2,8 +2,8 @@ import { responses } from "@/lib/api/response"; import { transformErrorToDetails } from "@/lib/api/validator"; import { sendToPipeline } from "@/lib/pipelines"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; +import { getSurvey } from "@formbricks/lib/survey/service"; import { updateResponse } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/services/survey"; import { ZResponseUpdateInput } from "@formbricks/types/v1/responses"; import { NextResponse } from "next/server"; diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts index aac5cbbabc..0def24c540 100644 --- a/apps/web/app/api/v1/client/responses/route.ts +++ b/apps/web/app/api/v1/client/responses/route.ts @@ -3,8 +3,8 @@ import { transformErrorToDetails } from "@/lib/api/validator"; import { sendToPipeline } from "@/lib/pipelines"; import { InvalidInputError } from "@formbricks/types/v1/errors"; import { capturePosthogEvent } from "@formbricks/lib/posthogServer"; +import { getSurvey } from "@formbricks/lib/survey/service"; import { createResponse } from "@formbricks/lib/response/service"; -import { getSurvey } from "@formbricks/lib/services/survey"; import { getTeamDetails } from "@formbricks/lib/services/teamDetails"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses"; import { NextResponse } from "next/server"; diff --git a/apps/web/app/api/v1/js/surveys.ts b/apps/web/app/api/v1/js/surveys.ts index 4b09a5502e..bd0a77f446 100644 --- a/apps/web/app/api/v1/js/surveys.ts +++ b/apps/web/app/api/v1/js/surveys.ts @@ -1,5 +1,5 @@ import { prisma } from "@formbricks/database"; -import { selectSurvey } from "@formbricks/lib/services/survey"; +import { selectSurvey } from "@formbricks/lib/survey/service"; import { TPerson } from "@formbricks/types/v1/people"; import { TSurvey } from "@formbricks/types/v1/surveys"; import { unstable_cache } from "next/cache"; diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 7941a82eea..4158e05bc0 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -4,7 +4,7 @@ import { transformErrorToDetails } from "@/lib/api/validator"; import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service"; import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; -import { getSurvey } from "@formbricks/lib/services/survey"; +import { getSurvey } from "@formbricks/lib/survey/service"; import { authenticateRequest } from "@/app/api/v1/auth"; import { handleErrorResponse } from "@/app/api/v1/auth"; diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts index c25e2ec9a1..cc096f9caa 100644 --- a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts @@ -1,6 +1,6 @@ import { responses } from "@/lib/api/response"; import { NextResponse } from "next/server"; -import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/services/survey"; +import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/survey/service"; import { TSurvey, ZSurveyInput } from "@formbricks/types/v1/surveys"; import { transformErrorToDetails } from "@/lib/api/validator"; import { authenticateRequest } from "@/app/api/v1/auth"; diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts index 1ced6ec5d9..4a7d3eb939 100644 --- a/apps/web/app/api/v1/management/surveys/route.ts +++ b/apps/web/app/api/v1/management/surveys/route.ts @@ -2,7 +2,7 @@ import { responses } from "@/lib/api/response"; import { authenticateRequest } from "@/app/api/v1/auth"; import { NextResponse } from "next/server"; import { transformErrorToDetails } from "@/lib/api/validator"; -import { createSurvey, getSurveys } from "@formbricks/lib/services/survey"; +import { createSurvey, getSurveys } from "@formbricks/lib/survey/service"; import { ZSurveyInput } from "@formbricks/types/v1/surveys"; import { DatabaseError } from "@formbricks/types/v1/errors"; diff --git a/apps/web/app/api/v1/users/route.ts b/apps/web/app/api/v1/users/route.ts index 16fefd3f2c..b4352157eb 100644 --- a/apps/web/app/api/v1/users/route.ts +++ b/apps/web/app/api/v1/users/route.ts @@ -1,16 +1,13 @@ import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/lib/email"; -import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { populateEnvironment } from "@/lib/populate"; import { prisma } from "@formbricks/database"; +import { EMAIL_VERIFICATION_DISABLED, INVITE_DISABLED, SIGNUP_ENABLED } from "@formbricks/lib/constants"; +import { verifyInviteToken } from "@formbricks/lib/jwt"; +import { deleteInvite } from "@formbricks/lib/services/invite"; +import { createMembership } from "@formbricks/lib/services/membership"; +import { createProduct } from "@formbricks/lib/services/product"; +import { createProfile } from "@formbricks/lib/services/profile"; +import { createTeam } from "@formbricks/lib/services/team"; import { NextResponse } from "next/server"; -import { Prisma } from "@prisma/client"; -import { - EMAIL_VERIFICATION_DISABLED, - INTERNAL_SECRET, - INVITE_DISABLED, - SIGNUP_ENABLED, - WEBAPP_URL, -} from "@formbricks/lib/constants"; export async function POST(request: Request) { let { inviteToken, ...user } = await request.json(); @@ -22,7 +19,6 @@ export async function POST(request: Request) { let inviteId; try { - let data: Prisma.UserCreateArgs; let invite; if (inviteToken) { @@ -40,92 +36,35 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Invalid invite ID" }, { status: 400 }); } - data = { - data: { - ...user, - memberships: { - create: { - accepted: true, - role: invite.role, - team: { - connect: { - id: invite.teamId, - }, - }, - }, - }, - }, - }; - } else { - data = { - data: { - ...user, - memberships: { - create: [ - { - accepted: true, - role: "owner", - team: { - create: { - name: `${user.name}'s Team`, - products: { - create: [ - { - name: "My Product", - environments: { - create: [ - { - type: "production", - ...populateEnvironment, - }, - { - type: "development", - ...populateEnvironment, - }, - ], - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }; - } + // create a user and assign him to the team - type UserWithMemberships = Prisma.UserGetPayload<{ include: { memberships: true } }>; - - const userData = (await prisma.user.create({ - ...data, - include: { - memberships: true, - }, - // TODO: This is a hack to get the correct types (casting), we should find a better way to do this - })) as UserWithMemberships; - - const teamId = userData.memberships[0].teamId; - - if (teamId) { - fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, { - method: "POST", - headers: { - "x-api-key": INTERNAL_SECRET, - }, + const profile = await createProfile(user); + await createMembership(invite.teamId, profile.id, { + accepted: true, + role: invite.role, }); - } - if (inviteId) { - sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email); - await prisma.invite.delete({ where: { id: inviteId } }); - } + if (!EMAIL_VERIFICATION_DISABLED) { + await sendVerificationEmail(profile); + } - if (!EMAIL_VERIFICATION_DISABLED) { - await sendVerificationEmail(userData); + await sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email); + await deleteInvite(inviteId); + + return NextResponse.json(profile); + } else { + const team = await createTeam({ + name: `${user.name}'s Team`, + }); + await createProduct(team.id, { name: "My Product" }); + const profile = await createProfile(user); + await createMembership(team.id, profile.id, { role: "owner", accepted: true }); + + if (!EMAIL_VERIFICATION_DISABLED) { + await sendVerificationEmail(profile); + } + return NextResponse.json(profile); } - return NextResponse.json(userData); } catch (e) { if (e.code === "P2002") { return NextResponse.json( diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 30ac126219..e6489cb9d8 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,6 +1,6 @@ import ClientLogout from "@/app/ClientLogout"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; -import { getEnvironmentByUser } from "@formbricks/lib/services/environment"; +import { getFirstEnvironmentByUserId } from "@formbricks/lib/services/environment"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; @@ -18,7 +18,10 @@ export default async function Home() { let environment; try { - environment = await getEnvironmentByUser(session?.user); + environment = await getFirstEnvironmentByUserId(session?.user.id); + if (!environment) { + throw new Error("No environment found"); + } } catch (error) { console.error("error getting environment", error); } diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx index 1b7ff9bb5c..43a4940dc3 100644 --- a/apps/web/app/s/[surveyId]/page.tsx +++ b/apps/web/app/s/[surveyId]/page.tsx @@ -5,7 +5,7 @@ import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive"; import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants"; import { getOrCreatePersonByUserId } from "@formbricks/lib/services/person"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getSurvey } from "@formbricks/lib/services/survey"; +import { getSurvey } from "@formbricks/lib/survey/service"; import { getEmailVerificationStatus } from "./helpers"; import { checkValidity } from "@/app/s/[surveyId]/prefilling"; import { notFound } from "next/navigation"; diff --git a/apps/web/components/environments/SecondNavBar.tsx b/apps/web/components/environments/SecondNavBar.tsx index ec553fcf10..f825f92710 100644 --- a/apps/web/components/environments/SecondNavBar.tsx +++ b/apps/web/components/environments/SecondNavBar.tsx @@ -2,7 +2,7 @@ import { cn } from "@formbricks/lib/cn"; import SurveyNavBarName from "@/components/shared/SurveyNavBarName"; import Link from "next/link"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getSurvey } from "@formbricks/lib/services/survey"; +import { getSurvey } from "@formbricks/lib/survey/service"; interface SecondNavbarProps { tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[]; diff --git a/apps/web/components/shared/SurveyStatusDropdown.tsx b/apps/web/components/shared/SurveyStatusDropdown.tsx index 63e508b6c5..74941fa02a 100644 --- a/apps/web/components/shared/SurveyStatusDropdown.tsx +++ b/apps/web/components/shared/SurveyStatusDropdown.tsx @@ -1,6 +1,6 @@ "use client"; -import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions"; +import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions"; import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TSurvey } from "@formbricks/types/v1/surveys"; @@ -44,7 +44,7 @@ export default function SurveyStatusDropdown({ disabled={isStatusChangeDisabled} onValueChange={(value) => { const castedValue = value as "draft" | "inProgress" | "paused" | "completed"; - surveyMutateAction({ ...survey, status: castedValue }) + updateSurveyAction({ ...survey, status: castedValue }) .then(() => { toast.success( value === "inProgress" diff --git a/apps/web/components/team/CreateTeamModal.tsx b/apps/web/components/team/CreateTeamModal.tsx index 750f5c6eeb..b6d48ddf81 100644 --- a/apps/web/components/team/CreateTeamModal.tsx +++ b/apps/web/components/team/CreateTeamModal.tsx @@ -1,6 +1,5 @@ -import { createTeam } from "@/app/(app)/environments/[environmentId]/actions"; +import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions"; import Modal from "@/components/shared/Modal"; -import { useProfile } from "@/lib/profile"; import { Button, Input, Label } from "@formbricks/ui"; import { PlusCircleIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; @@ -15,13 +14,12 @@ interface CreateTeamModalProps { export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps) { const router = useRouter(); - const { profile } = useProfile(); const [loading, setLoading] = useState(false); const { register, handleSubmit } = useForm(); const submitTeam = async (data) => { setLoading(true); - const newTeam = await createTeam(data.name, (profile as any).id); + const newTeam = await createTeamAction(data.name); toast.success("Team created successfully!"); router.push(`/teams/${newTeam.id}`); diff --git a/apps/web/lib/populate.ts b/apps/web/lib/populate.ts deleted file mode 100644 index e36684173d..0000000000 --- a/apps/web/lib/populate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { EventType } from "@prisma/client"; -export const populateEnvironment = { - eventClasses: { - create: [ - { - name: "New Session", - description: "Gets fired when a new session is created", - type: EventType.automatic, - }, - { - name: "Exit Intent (Desktop)", - description: "A user on Desktop leaves the website with the cursor.", - type: EventType.automatic, - }, - { - name: "50% Scroll", - description: "A user scrolled 50% of the current page", - type: EventType.automatic, - }, - ], - }, - attributeClasses: { - create: [ - { name: "userId", description: "The internal ID of the person", type: EventType.automatic }, - { name: "email", description: "The email of the person", type: EventType.automatic }, - ], - }, -}; diff --git a/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts index d261973c77..1c4043240d 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts @@ -1,7 +1,6 @@ import { getSessionUser, hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; -import { EnvironmentType } from "@prisma/client"; -import { populateEnvironment } from "@/lib/populate"; +import { createProduct } from "@formbricks/lib/services/product"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -93,29 +92,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) } // Create a new product and associate it with the current team - const newProduct = await prisma.product.create({ - data: { - name, - team: { - connect: { id: environment.product.teamId }, - }, - environments: { - create: [ - { - type: EnvironmentType.production, - ...populateEnvironment, - }, - { - type: EnvironmentType.development, - ...populateEnvironment, - }, - ], - }, - }, - select: { - environments: true, - }, - }); + const newProduct = await createProduct(environment.product.teamId, { name }); const firstEnvironment = newProduct.environments[0]; res.json(firstEnvironment); diff --git a/packages/lib/response/auth.ts b/packages/lib/response/auth.ts index f4adfe0858..513354f212 100644 --- a/packages/lib/response/auth.ts +++ b/packages/lib/response/auth.ts @@ -1,12 +1,12 @@ import "server-only"; import { ZId } from "@formbricks/types/v1/environment"; -import { validateInputs } from "../utils/validate"; -import { hasUserEnvironmentAccess } from "../environment/auth"; -import { getResponse, getResponseCacheTag } from "./service"; import { unstable_cache } from "next/cache"; -import { getSurvey } from "../services/survey"; import { SERVICES_REVALIDATION_INTERVAL } from "../constants"; +import { hasUserEnvironmentAccess } from "../environment/auth"; +import { getSurvey } from "../survey/service"; +import { validateInputs } from "../utils/validate"; +import { getResponse, getResponseCacheTag } from "./service"; export const canUserAccessResponse = async (userId: string, responseId: string): Promise => await unstable_cache( diff --git a/packages/lib/services/environment.ts b/packages/lib/services/environment.ts index 87714c5f19..1657d3d6a8 100644 --- a/packages/lib/services/environment.ts +++ b/packages/lib/services/environment.ts @@ -1,15 +1,24 @@ import "server-only"; import { prisma } from "@formbricks/database"; -import { z } from "zod"; -import { Prisma, EnvironmentType } from "@prisma/client"; +import type { + TEnvironment, + TEnvironmentCreateInput, + TEnvironmentUpdateInput, +} from "@formbricks/types/v1/environment"; +import { + ZEnvironment, + ZEnvironmentCreateInput, + ZEnvironmentUpdateInput, + ZId, +} from "@formbricks/types/v1/environment"; import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors"; -import type { TEnvironment, TEnvironmentId, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment"; -import { populateEnvironment } from "../utils/createDemoProductHelpers"; -import { ZEnvironment, ZEnvironmentUpdateInput, ZId } from "@formbricks/types/v1/environment"; -import { validateInputs } from "../utils/validate"; -import { unstable_cache, revalidateTag } from "next/cache"; +import { EventType, Prisma } from "@prisma/client"; +import { revalidateTag, unstable_cache } from "next/cache"; +import "server-only"; +import { z } from "zod"; import { SERVICES_REVALIDATION_INTERVAL } from "../constants"; +import { validateInputs } from "../utils/validate"; export const getEnvironmentCacheTag = (environmentId: string) => `environments-${environmentId}`; export const getEnvironmentsCacheTag = (productId: string) => `products-${productId}-environments`; @@ -125,86 +134,84 @@ export const updateEnvironment = async ( } }; -export const getEnvironmentByUser = async (user: any): Promise => { - const firstMembership = await prisma.membership.findFirst({ - where: { - userId: user.id, - }, - select: { - teamId: true, - }, - }); - - if (!firstMembership) { - // create a new team and return environment - const membership = await prisma.membership.create({ - data: { - accepted: true, - role: "owner", - user: { connect: { id: user.id } }, - team: { - create: { - name: `${user.name}'s Team`, - products: { - create: { - name: "My Product", - environments: { - create: [ - { - type: EnvironmentType.production, - ...populateEnvironment, - }, - { - type: EnvironmentType.development, - ...populateEnvironment, - }, - ], - }, - }, - }, - }, - }, - }, - include: { - team: { - include: { - products: { - include: { - environments: true, +export const getFirstEnvironmentByUserId = async (userId: string): Promise => { + validateInputs([userId, ZId]); + let environmentPrisma; + try { + environmentPrisma = await prisma.environment.findFirst({ + where: { + type: "production", + product: { + team: { + memberships: { + some: { + userId, }, }, }, }, }, }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } - const environment = membership.team.products[0].environments[0]; + throw error; + } + try { + const environment = ZEnvironment.parse(environmentPrisma); return environment; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); + } + throw new ValidationError("Data validation of environment failed"); } - - const firstProduct = await prisma.product.findFirst({ - where: { - teamId: firstMembership.teamId, - }, - select: { - id: true, - }, - }); - if (firstProduct === null) { - return null; - } - const firstEnvironment = await prisma.environment.findFirst({ - where: { - productId: firstProduct.id, - type: "production", - }, - select: { - id: true, - }, - }); - if (firstEnvironment === null) { - return null; - } - return firstEnvironment; +}; + +export const createEnvironment = async ( + productId: string, + environmentInput: Partial +): Promise => { + validateInputs([productId, ZId], [environmentInput, ZEnvironmentCreateInput]); + + return await prisma.environment.create({ + data: { + type: environmentInput.type || "development", + product: { connect: { id: productId } }, + widgetSetupCompleted: environmentInput.widgetSetupCompleted || false, + eventClasses: { + create: populateEnvironment.eventClasses, + }, + attributeClasses: { + create: populateEnvironment.attributeClasses, + }, + }, + }); +}; + +export const populateEnvironment = { + eventClasses: [ + { + name: "New Session", + description: "Gets fired when a new session is created", + type: EventType.automatic, + }, + { + name: "Exit Intent (Desktop)", + description: "A user on Desktop leaves the website with the cursor.", + type: EventType.automatic, + }, + { + name: "50% Scroll", + description: "A user scrolled 50% of the current page", + type: EventType.automatic, + }, + ], + attributeClasses: [ + { name: "userId", description: "The internal ID of the person", type: EventType.automatic }, + { name: "email", description: "The email of the person", type: EventType.automatic }, + ], }; diff --git a/packages/lib/services/membership.ts b/packages/lib/services/membership.ts index c1e2ab07ed..16f0123aaa 100644 --- a/packages/lib/services/membership.ts +++ b/packages/lib/services/membership.ts @@ -62,6 +62,26 @@ export const getMembershipsByUserId = cache(async (userId: string): Promise +): Promise => { + try { + const membership = await prisma.membership.create({ + data: { + userId, + teamId, + accepted: data.accepted, + role: data.role as TMembership["role"], + }, + }); + + return membership; + } catch (error) { + throw error; + } +}; export const updateMembership = async ( userId: string, teamId: string, diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts index 8072f3a88e..54dca17d8f 100644 --- a/packages/lib/services/product.ts +++ b/packages/lib/services/product.ts @@ -9,11 +9,9 @@ import { Prisma } from "@prisma/client"; import { revalidateTag, unstable_cache } from "next/cache"; import { cache } from "react"; import { z } from "zod"; -import { validateInputs } from "../utils/validate"; -import { EnvironmentType } from "@prisma/client"; -import { EventType } from "@prisma/client"; -import { getEnvironmentCacheTag, getEnvironmentsCacheTag } from "./environment"; import { SERVICES_REVALIDATION_INTERVAL } from "../constants"; +import { validateInputs } from "../utils/validate"; +import { createEnvironment, getEnvironmentCacheTag, getEnvironmentsCacheTag } from "./environment"; export const getProductsCacheTag = (teamId: string): string => `teams-${teamId}-products`; const getProductCacheTag = (environmentId: string): string => `environments-${environmentId}-product`; @@ -35,34 +33,6 @@ const selectProduct = { environments: true, }; -const populateEnvironment = { - eventClasses: { - create: [ - { - name: "New Session", - description: "Gets fired when a new session is created", - type: EventType.automatic, - }, - { - name: "Exit Intent (Desktop)", - description: "A user on Desktop leaves the website with the cursor.", - type: EventType.automatic, - }, - { - name: "50% Scroll", - description: "A user scrolled 50% of the current page", - type: EventType.automatic, - }, - ], - }, - attributeClasses: { - create: [ - { name: "userId", description: "The internal ID of the person", type: EventType.automatic }, - { name: "email", description: "The email of the person", type: EventType.automatic }, - ], - }, -}; - export const getProducts = async (teamId: string): Promise => unstable_cache( async () => { @@ -135,6 +105,7 @@ export const updateProduct = async ( inputProduct: Partial ): Promise => { validateInputs([productId, ZId], [inputProduct, ZProductUpdateInput.partial()]); + const { environments, ...data } = inputProduct; let updatedProduct; try { updatedProduct = await prisma.product.update({ @@ -142,7 +113,10 @@ export const updateProduct = async ( id: productId, }, data: { - ...inputProduct, + ...data, + environments: { + connect: environments?.map((environment) => ({ id: environment.id })) ?? [], + }, }, select: selectProduct, }); @@ -210,43 +184,35 @@ export const deleteProduct = cache(async (productId: string): Promise return product; }); -export const createProduct = async (environmentId: string, productName: string): Promise => { - const environment = await prisma.environment.findUnique({ - where: { id: environmentId }, - select: { - product: { - select: { - teamId: true, - }, - }, - }, - }); - - if (!environment) { - throw new Error("Invalid environment"); +export const createProduct = async ( + teamId: string, + productInput: Partial +): Promise => { + if (!productInput.name) { + throw new ValidationError("Product Name is required"); } + const { environments, ...data } = productInput; - const newProduct = await prisma.product.create({ + let product = await prisma.product.create({ data: { - name: productName, - team: { - connect: { id: environment.product.teamId }, - }, - environments: { - create: [ - { - type: EnvironmentType.production, - ...populateEnvironment, - }, - { - type: EnvironmentType.development, - ...populateEnvironment, - }, - ], - }, + ...data, + name: productInput.name, + teamId, }, select: selectProduct, }); - return newProduct; + const devEnvironment = await createEnvironment(product.id, { + type: "development", + }); + + const prodEnvironment = await createEnvironment(product.id, { + type: "production", + }); + + product = await updateProduct(product.id, { + environments: [devEnvironment, prodEnvironment], + }); + + return product; }; diff --git a/packages/lib/services/profile.ts b/packages/lib/services/profile.ts index b176dff86f..74ca017421 100644 --- a/packages/lib/services/profile.ts +++ b/packages/lib/services/profile.ts @@ -4,7 +4,12 @@ import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/v1/environment"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/v1/memberships"; -import { TProfile, TProfileUpdateInput, ZProfileUpdateInput } from "@formbricks/types/v1/profile"; +import { + TProfile, + TProfileCreateInput, + TProfileUpdateInput, + ZProfileUpdateInput, +} from "@formbricks/types/v1/profile"; import { MembershipRole, Prisma } from "@prisma/client"; import { unstable_cache, revalidateTag } from "next/cache"; import { validateInputs } from "../utils/validate"; @@ -149,6 +154,19 @@ const deleteUser = async (userId: string): Promise => { return profile; }; +export const createProfile = async (data: TProfileCreateInput): Promise => { + validateInputs([data, ZProfileUpdateInput]); + const profile = await prisma.user.create({ + data: data, + select: responseSelection, + }); + + revalidateTag(getProfileByEmailCacheTag(profile.email)); + revalidateTag(getProfileCacheTag(profile.id)); + + return profile; +}; + // function to delete a user's profile including teams export const deleteProfile = async (userId: string): Promise => { validateInputs([userId, ZId]); diff --git a/packages/lib/services/team.ts b/packages/lib/services/team.ts index a9417cd90a..63883dca4b 100644 --- a/packages/lib/services/team.ts +++ b/packages/lib/services/team.ts @@ -107,7 +107,20 @@ export const getTeamByEnvironmentId = async (environmentId: string): Promise => { +export const createTeam = async (teamInput: TTeamUpdateInput): Promise => { + try { + const team = await prisma.team.create({ + data: teamInput, + select, + }); + + return team; + } catch (error) { + throw error; + } +}; + +export const updateTeam = async (teamId: string, data: Partial): Promise => { try { const updatedTeam = await prisma.team.update({ where: { diff --git a/packages/lib/survey/auth.ts b/packages/lib/survey/auth.ts new file mode 100644 index 0000000000..263231af5d --- /dev/null +++ b/packages/lib/survey/auth.ts @@ -0,0 +1,24 @@ +import { ZId } from "@formbricks/types/v1/environment"; +import { validateInputs } from "../utils/validate"; +import { hasUserEnvironmentAccess } from "../environment/auth"; +import { getSurvey, getSurveyCacheTag } from "./service"; +import { unstable_cache } from "next/cache"; + +export const canUserAccessSurvey = async (userId: string, surveyId: string): Promise => + await unstable_cache( + async () => { + validateInputs([surveyId, ZId], [userId, ZId]); + + if (!userId) return false; + + const survey = await getSurvey(surveyId); + if (!survey) throw new Error("Survey not found"); + + const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId); + if (!hasAccessToEnvironment) return false; + + return true; + }, + [`users-${userId}-surveys-${surveyId}`], + { revalidate: 30 * 60, tags: [getSurveyCacheTag(surveyId)] } + )(); // 30 minutes diff --git a/packages/lib/services/survey.ts b/packages/lib/survey/service.ts similarity index 96% rename from packages/lib/services/survey.ts rename to packages/lib/survey/service.ts index b2cd638c03..b6d2ff5ed0 100644 --- a/packages/lib/services/survey.ts +++ b/packages/lib/survey/service.ts @@ -15,22 +15,15 @@ import { revalidateTag, unstable_cache } from "next/cache"; import { z } from "zod"; import { captureTelemetry } from "../telemetry"; import { validateInputs } from "../utils/validate"; -import { getDisplaysCacheTag } from "./displays"; +import { getDisplaysCacheTag } from "../services/displays"; import { getResponsesCacheTag } from "../response/service"; // surveys cache key and tags -const getSurveysCacheKey = (environmentId: string): string => `environments-${environmentId}-surveys`; const getSurveysCacheTag = (environmentId: string): string => `environments-${environmentId}-surveys`; // survey cache key and tags -export const getSurveyCacheKey = (surveyId: string): string => `surveys-${surveyId}`; export const getSurveyCacheTag = (surveyId: string): string => `surveys-${surveyId}`; -// survey with analytics cache key -const getSurveysWithAnalyticsCacheKey = (environmentId: string): string => - `environments-${environmentId}-surveysWithAnalytics`; -const getSurveyWithAnalyticsCacheKey = (surveyId: string): string => `surveyWithAnalytics-${surveyId}`; - export const selectSurvey = { id: true, createdAt: true, @@ -145,7 +138,7 @@ export const getSurveyWithAnalytics = async (surveyId: string): Promise => { throw new ValidationError("Data validation of survey failed"); } }, - [getSurveyCacheKey(surveyId)], + [`surveys-${surveyId}`], { tags: [getSurveyCacheTag(surveyId)], revalidate: 60 * 30, @@ -329,7 +322,7 @@ export const getSurveys = async (environmentId: string): Promise => { throw new ValidationError("Data validation of survey failed"); } }, - [getSurveysCacheKey(environmentId)], + [`environments-${environmentId}-surveys`], { tags: [getSurveysCacheTag(environmentId)], revalidate: 60 * 30, @@ -393,7 +386,7 @@ export const getSurveysWithAnalytics = async (environmentId: string): Promise; + export type TActionClassInput = z.infer; diff --git a/packages/types/v1/attributeClasses.ts b/packages/types/v1/attributeClasses.ts index 2b67849d65..6b492ae669 100644 --- a/packages/types/v1/attributeClasses.ts +++ b/packages/types/v1/attributeClasses.ts @@ -22,12 +22,20 @@ export const ZAttributeClassInput = z.object({ environmentId: z.string(), }); +export const ZAttributeClassAutomaticInput = z.object({ + name: z.string(), + description: z.string(), + type: z.enum(["automatic"]), +}); + export const ZAttributeClassUpdateInput = z.object({ name: z.string(), description: z.string().optional(), archived: z.boolean().optional(), }); +export type TAttributeClassAutomaticInput = z.infer; + export type TAttributeClassUpdateInput = z.infer; export type TAttributeClassInput = z.infer; diff --git a/packages/types/v1/environment.ts b/packages/types/v1/environment.ts index 3910cfb437..90a31bdb1e 100644 --- a/packages/types/v1/environment.ts +++ b/packages/types/v1/environment.ts @@ -25,4 +25,11 @@ export const ZEnvironmentUpdateInput = z.object({ export const ZId = z.string().cuid2(); +export const ZEnvironmentCreateInput = z.object({ + type: z.enum(["development", "production"]).optional(), + widgetSetupCompleted: z.boolean().optional(), +}); + +export type TEnvironmentCreateInput = z.infer; + export type TEnvironmentUpdateInput = z.infer; diff --git a/packages/types/v1/product.ts b/packages/types/v1/product.ts index c686b612f7..4f95cec1de 100644 --- a/packages/types/v1/product.ts +++ b/packages/types/v1/product.ts @@ -11,7 +11,7 @@ export const ZProduct = z.object({ highlightBorderColor: z .string() .regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) - .nullish(), + .nullable(), recontactDays: z.number().int(), formbricksSignature: z.boolean(), placement: z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]), @@ -26,7 +26,6 @@ export const ZProductUpdateInput = ZProduct.omit({ id: true, createdAt: true, updatedAt: true, - environments: true, }); export type TProductUpdateInput = z.infer; diff --git a/packages/types/v1/profile.ts b/packages/types/v1/profile.ts index 62a1e3be00..0130acc3bb 100644 --- a/packages/types/v1/profile.ts +++ b/packages/types/v1/profile.ts @@ -21,11 +21,21 @@ export const ZProfile = z.object({ export type TProfile = z.infer; export const ZProfileUpdateInput = z.object({ - name: z.string().nullable(), - email: z.string(), - onboardingCompleted: z.boolean(), - role: ZRole.nullable(), - objective: ZObjective.nullable(), + name: z.string().nullish(), + email: z.string().optional(), + onboardingCompleted: z.boolean().optional(), + role: ZRole.optional(), + objective: ZObjective.optional(), }); export type TProfileUpdateInput = z.infer; + +export const ZProfileCreateInput = z.object({ + name: z.string().optional(), + email: z.string(), + onboardingCompleted: z.boolean().optional(), + role: ZRole.optional(), + objective: ZObjective.optional(), +}); + +export type TProfileCreateInput = z.infer; diff --git a/packages/types/v1/teams.ts b/packages/types/v1/teams.ts index 3eef5b8e07..617058a15a 100644 --- a/packages/types/v1/teams.ts +++ b/packages/types/v1/teams.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { Prisma } from "@prisma/client"; export const ZTeam = z.object({ id: z.string().cuid2(), @@ -10,6 +9,12 @@ export const ZTeam = z.object({ stripeCustomerId: z.string().nullable(), }); -export type TTeamUpdateInput = Prisma.TeamUpdateInput; +export const ZTeamUpdateInput = z.object({ + name: z.string(), + plan: z.enum(["free", "pro"]).optional(), + stripeCustomerId: z.string().nullish(), +}); + +export type TTeamUpdateInput = z.infer; export type TTeam = z.infer; diff --git a/packages/ui/components/Accordion.tsx b/packages/ui/components/Accordion.tsx new file mode 100644 index 0000000000..314965e23b --- /dev/null +++ b/packages/ui/components/Accordion.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; +import { cn } from "@formbricks/lib/cn"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props}> + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 7b8fe4c492..c45d6e986b 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -1,3 +1,4 @@ +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/Accordion"; export { AdvancedOptionToggle } from "./components/AdvancedOptionToggle"; export { Alert, AlertDescription, AlertTitle } from "./components/Alert"; export { PersonAvatar, ProfileAvatar } from "./components/Avatars"; @@ -50,6 +51,7 @@ export { export { ErrorComponent } from "./components/ErrorComponent"; export { Input } from "./components/Input"; export { Label } from "./components/Label"; +export { NoMobileOverlay } from "./components/NoMobileOverlay"; export { Pagination } from "./components/Pagination"; export { PasswordInput } from "./components/PasswordInput"; export { Popover, PopoverContent, PopoverTrigger } from "./components/Popover"; @@ -67,12 +69,11 @@ export { SelectTrigger, SelectValue, } from "./components/Select"; +export { Skeleton } from "./components/Skeleton"; export { Switch } from "./components/Switch"; export { TabBar } from "./components/TabBar"; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/Tooltip"; export { AddVariablesDropdown, Editor } from "./components/editor"; -export { Skeleton } from "./components/Skeleton"; -export { NoMobileOverlay } from "./components/NoMobileOverlay"; /* Icons */ export { AngryBirdRage2Icon } from "./components/icons/AngryBirdRage2Icon"; diff --git a/packages/ui/package.json b/packages/ui/package.json index db423f33de..5efb234cdb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,6 +29,7 @@ "@lexical/react": "^0.12.2", "@lexical/rich-text": "^0.12.2", "@lexical/table": "^0.12.2", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cd149db3a..ad4dd99137 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -717,6 +717,9 @@ importers: '@lexical/table': specifier: ^0.12.2 version: 0.12.2(lexical@0.12.2) + '@radix-ui/react-accordion': + specifier: ^1.1.2 + version: 1.1.2(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-checkbox': specifier: ^1.0.4 version: 1.0.4(react-dom@18.2.0)(react@18.2.0) @@ -4748,6 +4751,33 @@ packages: '@babel/runtime': 7.21.0 dev: false + /@radix-ui/react-accordion@1.1.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.21.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collapsible': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.23)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.23)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-arrow@1.0.3(@types/react@18.2.23)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: