fix conflicts
25
.env.example
@@ -11,33 +11,26 @@ WEBAPP_URL=http://localhost:3000
|
||||
# Required for next-auth. Should be the same as WEBAPP_URL
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Set this if you want to have a shorter link for surveys
|
||||
SHORT_URL_BASE=
|
||||
|
||||
# Encryption keys
|
||||
# Please set both for now, we will change this in the future
|
||||
|
||||
# You can use: `openssl rand -hex 32` to generate one
|
||||
ENCRYPTION_KEY=
|
||||
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
# API Secret for running cron jobs. (mandatory)
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
CRON_SECRET=
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
|
||||
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
|
||||
|
||||
###############
|
||||
# NEXT AUTH #
|
||||
###############
|
||||
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
NEXTAUTH_SECRET=RANDOM_STRING
|
||||
|
||||
# API Secret for running cron jobs. (mandatory)
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
CRON_SECRET=RANDOM_STRING
|
||||
|
||||
################
|
||||
# MAIL SETUP #
|
||||
################
|
||||
@@ -77,6 +70,8 @@ S3_BUCKET_NAME=
|
||||
# Configure a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
|
||||
# e.g., https://gateway.storjshare.io
|
||||
S3_ENDPOINT_URL=
|
||||
# Force path style for S3 compatible storage (0 for disabled, 1 for enabled)
|
||||
S3_FORCE_PATH_STYLE=0
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
|
||||
4
.github/actions/cache-build-web/action.yml
vendored
@@ -53,10 +53,12 @@ runs:
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENCRYPTION_KE, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=${{ inputs.e2e_testing_mode }}" >> .env
|
||||
shell: bash
|
||||
|
||||
50
.github/workflows/e2e.yml
vendored
@@ -2,6 +2,8 @@ name: E2E Tests
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
TELEMETRY_DISABLED: 1
|
||||
jobs:
|
||||
build:
|
||||
name: Run E2E Tests
|
||||
@@ -25,29 +27,35 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Build & Cache Web Binaries
|
||||
uses: ./.github/actions/cache-build-web
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
e2e_testing_mode: "1"
|
||||
|
||||
- name: Check if pnpm is installed
|
||||
id: pnpm-check
|
||||
run: |
|
||||
if pnpm --version; then
|
||||
echo "pnpm is installed."
|
||||
echo "PNPM_INSTALLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "pnpm is not installed."
|
||||
echo "PNPM_INSTALLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.PNPM_INSTALLED == 'false'
|
||||
run: pnpm install
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
shell: bash
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
shell: bash
|
||||
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=1" >> .env
|
||||
shell: bash
|
||||
|
||||
- name: Build App
|
||||
run: |
|
||||
pnpm build --filter=@formbricks/web...
|
||||
|
||||
- name: Apply Prisma Migrations
|
||||
run: |
|
||||
@@ -70,15 +78,7 @@ jobs:
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- name: Cache Playwright
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install Playwright
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run E2E Tests
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY and fill in .env
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
8
.github/workflows/test.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY and fill in .env
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${ENCRYPTION_KEY}/" .env
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
2
.gitignore
vendored
@@ -56,5 +56,5 @@ Zone.Identifier
|
||||
packages/lib/uploads
|
||||
|
||||
# Vite Timestamps
|
||||
vite.config.*.timestamp-*
|
||||
*vite.config.*.timestamp-*
|
||||
|
||||
|
||||
@@ -144,6 +144,12 @@ Or you can also deploy Formbricks on [RepoCloud](https://repocloud.io) using the
|
||||
|
||||
[](https://repocloud.io/details/?app_id=254)
|
||||
|
||||
##### Zeabur
|
||||
|
||||
Or you can also deploy Formbricks on [Zeabur](https://zeabur.com) using the button below.
|
||||
|
||||
[](https://zeabur.com/templates/G4TUJL)
|
||||
|
||||
<a id="development"></a>
|
||||
|
||||
## 👨💻 Development
|
||||
|
||||
@@ -62,7 +62,7 @@ const AppPage = ({}) => {
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col justify-between md:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<SurveySwitch value="app" formbricks={formbricks} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
|
||||
@@ -58,7 +58,7 @@ const AppPage = ({}) => {
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col items-center justify-between md:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<SurveySwitch value="website" formbricks={formbricks} />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,68 +0,0 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Custom Start & End Conditions for Surveys",
|
||||
description:
|
||||
"Optimize your survey management with custom Start & End Conditions in Formbricks. This feature allows you to control exactly when your survey is available for responses and when it should close, making it ideal for time-sensitive or number-of-response-limited surveys.",
|
||||
};
|
||||
|
||||
# Custom Start & End Conditions
|
||||
|
||||
Optimize your survey management with custom Start & End Conditions in Formbricks. This feature allows you to control exactly when your survey is available for responses and when it should close, making it ideal for time-sensitive or number-of-response-limited surveys.
|
||||
|
||||
Configure your surveys to open and close based on specific criteria. Here’s how to set up these conditions:
|
||||
|
||||
### **Opening a Survey**
|
||||
|
||||
**1. Schedule a Start Date:**
|
||||
|
||||
- **How to Set**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Release Survey on Date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepOne}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Details**: Choose the date and time when the survey should become available to respondents. All times follow UTC timezone.
|
||||
- **Use Case**: This is useful for launching surveys in alignment with events, product releases, or specific marketing campaigns.
|
||||
|
||||
### **Closing a Survey**
|
||||
|
||||
**1. Limit by Number of Responses:**
|
||||
|
||||
- **How to Set**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on response limit”.
|
||||
{" "}
|
||||
<MdxImage
|
||||
src={StepTwo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Set a specific number of responses after which the survey automatically closes.
|
||||
- **Use Case**: Perfect for limited offers, exclusive surveys, or when you need a precise sample size for statistical significance.
|
||||
|
||||
**2. Schedule an End Date:**
|
||||
|
||||
- **How to Set**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Define a specific date and time for the survey to close. This also follows UTC timezone. - **Use
|
||||
Case**: Essential for surveys linked to time-bound events or studies where data collection needs to end at a specific
|
||||
point.
|
||||
|
||||
### **Summary**
|
||||
|
||||
Setting up Custom Start & End Conditions in Formbricks allows you to control the availability and duration of your surveys with precision. Whether you are conducting academic research, market analysis, or gathering event feedback, these settings help ensure that your data collection aligns perfectly with your objectives.
|
||||
|
||||
---
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
28
apps/docs/app/global/limit-submissions/page.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Set a Maximum Number of Submissions for Surveys",
|
||||
description:
|
||||
"Limit the number of responses your survey can receive.",
|
||||
};
|
||||
|
||||
# Limit by Number of Submissions
|
||||
|
||||
Automatically close your survey after a specific number of responses with Formbricks. This feature is perfect for limited offers, exclusive surveys, or when you need a precise sample size for statistical significance.
|
||||
|
||||
- **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on response limit”.
|
||||
{" "}
|
||||
<MdxImage
|
||||
src={StepTwo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Set a specific number of responses after which the survey automatically closes.
|
||||
- **Use Case**: Perfect for limited offers, exclusive surveys, or when you need a precise sample size for statistical significance.
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
51
apps/docs/app/global/schedule-start-end-dates/page.mdx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Schedule Start & End Dates for Surveys",
|
||||
description:
|
||||
"Automatically release and close surveys based on specific dates.",
|
||||
};
|
||||
|
||||
# Schedule Start & End Dates
|
||||
|
||||
Optimize your survey management with custom Start & End Conditions in Formbricks. This feature allows you to control exactly when your survey is available for responses and when it should close, making it ideal for time-sensitive or number-of-response-limited surveys.
|
||||
|
||||
Configure your surveys to open and close based on specific criteria. Here’s how to set up these conditions:
|
||||
|
||||
## **Schedule a Survey Release**
|
||||
|
||||
- **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Release Survey on Date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepOne}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Details**: Choose the date and time when the survey should become available to respondents. All times follow UTC timezone.
|
||||
- **Use Case**: This is useful for launching surveys in alignment with events, product releases, or specific marketing campaigns.
|
||||
|
||||
## **Automatically Closing a Survey**
|
||||
|
||||
- **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to Response Options, Toggle the “Close survey on date”.
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Details**: Define a specific date and time for the survey to close. This also follows UTC timezone. - **Use
|
||||
Case**: Essential for surveys linked to time-bound events or studies where data collection needs to end at a specific
|
||||
point.
|
||||
|
||||
### **Summary**
|
||||
|
||||
Setting up Start & End Dates in Formbricks allows you to control the availability and duration of your surveys with precision. Whether you are conducting academic research, market analysis, or gathering event feedback, these settings help ensure that your data collection aligns perfectly with your objectives.
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -1,3 +1,16 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import EntraIDAppReg01 from "./images/entra_app_reg_01.png";
|
||||
import EntraIDAppReg02 from "./images/entra_app_reg_02.png";
|
||||
import EntraIDAppReg03 from "./images/entra_app_reg_03.png";
|
||||
import EntraIDAppReg04 from "./images/entra_app_reg_04.png";
|
||||
import EntraIDAppReg05 from "./images/entra_app_reg_05.png";
|
||||
import EntraIDAppReg06 from "./images/entra_app_reg_06.png";
|
||||
import EntraIDAppReg07 from "./images/entra_app_reg_07.png";
|
||||
import EntraIDAppReg08 from "./images/entra_app_reg_08.png";
|
||||
import EntraIDAppReg09 from "./images/entra_app_reg_09.png";
|
||||
import EntraIDAppReg10 from "./images/entra_app_reg_10.png";
|
||||
|
||||
export const metadata = {
|
||||
title: "Configure Formbricks with External auth providers",
|
||||
description:
|
||||
@@ -48,8 +61,6 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | |
|
||||
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
|
||||
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
|
||||
| DEFAULT_ORGANIZATION_ROLE | Role of the user in the default organization. | optional | admin |
|
||||
@@ -135,26 +146,141 @@ GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
|
||||
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
|
||||
|
||||
### Azure SSO OAuth
|
||||
### Microsoft Entra ID (Azure Active Directory) SSO OAuth
|
||||
|
||||
Have an Azure Active Directory (AAD) instance? Integrate it with your Formbricks instance to allow users to log in using their existing AAD credentials. This guide will walk you through the process of setting up Azure SSO for your Formbricks instance.
|
||||
Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks instance to allow users to log in using their existing Microsoft credentials. This guide will walk you through the process of setting up an Application Registration for your Formbricks instance.
|
||||
|
||||
### Requirements
|
||||
#### Requirements
|
||||
|
||||
- An Azure Active Directory (AAD) instance.
|
||||
- A Microsoft Entra ID Tenant populated with users. [Create a tenant as per Microsoft's documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
|
||||
- A Formbricks instance running and accessible.
|
||||
- The callback URI for your Formbricks instance: `{WEBAPP_URL}/api/auth/callback/azure-ad`
|
||||
|
||||
### Steps
|
||||
#### Creating an App Registration
|
||||
|
||||
1. Create a new Tenant in Azure Active Directory as per their [official documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
|
||||
2. Add Users & Groups to your AAD instance.
|
||||
3. Now we need to fill the below environment variables in our Formbricks instance so get them from your AD configuration:
|
||||
- `AZUREAD_CLIENT_ID`
|
||||
- `AZUREAD_CLIENT_SECRET`
|
||||
- `AZUREAD_TENANT_ID`
|
||||
4. Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
|
||||
5. Restart your Formbricks instance.
|
||||
6. You're all set! Users can now signup & log in using their AAD credentials.
|
||||
1. Login to the [Microsoft Entra admin center](https://entra.microsoft.com/).
|
||||
2. Go to **Applications** > **App registrations** in the left menu.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg01}
|
||||
alt="App Registration Name Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Click the **New registration** button at the top.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg02}
|
||||
alt="App Registration Name Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. Name your application something descriptive, such as `Formbricks SSO`.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg03}
|
||||
alt="App Registration Name Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
5. If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg04}
|
||||
alt="Supported Account Types List"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
6. Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above).
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg05}
|
||||
alt="Redirect URI Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
7. Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created.
|
||||
|
||||
8. On the _Overview_ page, under **Essentials**:
|
||||
- Copy the entry for **Application (client) ID** to populate the `AZUREAD_CLIENT_ID` variable.
|
||||
- Copy the entry for **Directory (tenant) ID** to populate the `AZUREAD_TENANT_ID` variable.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg06}
|
||||
alt="Client and Tenant ID Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
9. From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg07}
|
||||
alt="Certificates & secrets link"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
10. Make sure you have the **Client secrets** tab active, and click **New client secret**.
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg08}
|
||||
alt="New Client Secret Tab & Button"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
11. Enter a **Description**, set an **Expires** period, then click **Add**.
|
||||
|
||||
<Note>
|
||||
You will need to create a new client secret using these steps whenever your chosen expiry period ends.
|
||||
</Note>
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg09}
|
||||
alt="Description & Expires Fields"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
12. Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable.
|
||||
|
||||
<Note>
|
||||
Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply start from step 9 to create a new secret.
|
||||
</Note>
|
||||
|
||||
<MdxImage
|
||||
src={EntraIDAppReg10}
|
||||
alt="Client Secret Value Field"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
13. Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
|
||||
|
||||
<Note>
|
||||
You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., `"THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters.
|
||||
</Note>
|
||||
|
||||
An example `.env` for Microsoft Entra ID in Formbricks would look like:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Formbricks Env for Microsoft Entra ID SSO">
|
||||
```yml {{ title: ".env" }}
|
||||
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
|
||||
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
|
||||
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
14. Restart your Formbricks instance.
|
||||
15. You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
|
||||
|
||||
## OpenID Configuration
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const metadata = {
|
||||
|
||||
## Overview
|
||||
|
||||
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hostesr under a separate Enterprise License.
|
||||
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hosters under a separate Enterprise License.
|
||||
|
||||
<Note>
|
||||
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form
|
||||
|
||||
@@ -191,12 +191,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.3.0 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.3" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.3.0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -286,12 +286,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.2 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.2" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.2
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -390,12 +390,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.1.0 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.1" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.1.0
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -504,12 +504,12 @@ docker compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker pull ghcr.io/formbricks/data-migrations:v2.0.3 && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
-e UPGRADE_TO_VERSION="v2.0" \
|
||||
ghcr.io/formbricks/data-migrations:latest
|
||||
ghcr.io/formbricks/data-migrations:v2.0.3
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -50,9 +50,10 @@ The script will prompt you for the following information:
|
||||
<CodeGroup title="Docker GPG Keys Overwrite Prompt">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
@@ -64,48 +65,21 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N)
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. **Email Address**: Provide your email address for SSL certificate registration with Let's Encrypt.
|
||||
2. **Domain Name**: You will be asked to enter the domain name where you want to host Formbricks. This domain will be used to generate an SSL certificate.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Email Prompt">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. Youre now ready to run your Formbricks instance!
|
||||
🚗 Installing Traefik...
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks. Please make sure that port 80 and 443 are open in your VM's Security Group to allow Traefik to create the SSL certificate.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Domain Name for SSL certificate Prompt">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Dockers official GPG key and setting up the stable repository.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
@@ -113,13 +87,186 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
🚗 Installing Traefik...
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. **HTTPS Certificate Prompt**: The script will ask if you want to create an HTTPS certificate for your domain. Enter Y to proceed. This is highly recommended for secure access to your Formbricks instance.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. **DNS Setup Prompt**: Ensure that your domain's DNS is correctly configured and ports 80 and 443 are open. Confirm this by entering Y. This step is crucial for proper SSL certificate issuance and secure server access.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. **Email Address**: Provide an email address for SSL certificate registration. This email will be used for notifications regarding your SSL certificate from Let's Encrypt.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Email Prompt">
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
6. **Enforce HTTPS (HSTS) Prompt**: Enforcing HTTPS with HSTS is a good security practice, as it ensures all communication with your server is encrypted. Enter Y to enable this setting.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Domain Name for SSL certificate Prompt">
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
💡 Created traefik.yaml file with your provided email address.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
🔗 Do you want to enforce HTTPS (HSTS)? [Y/n]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
7. **Email Service Setup Prompt**: The script will ask if you want to set up the email service. Enter `Y` to proceed.(default is `N`). You can skip this step if you don't want to set up the email service. You will still be able to use Formbricks without setting up the email service.
|
||||
|
||||
<Col>
|
||||
<CodeGroup>
|
||||
|
||||
```bash
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
🔗 Do you want to enforce HTTPS (HSTS)? [Y/n]
|
||||
Y
|
||||
🚗 Configuring Traefik...
|
||||
💡 Created traefik.yaml and traefik-dynamic.yaml file.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
📧 Do you want to set up the email service? You will need SMTP credentials for the same! [y/N]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -131,39 +278,56 @@ docs@formbricks.com
|
||||
<CodeGroup title="Successfully setup Formbricks on your Ubuntu machine">
|
||||
|
||||
```bash
|
||||
🧱 Welcome to the Formbricks single instance installer
|
||||
🚀 Executing default step of installing Formbricks
|
||||
🧱 Welcome to the Formbricks Setup Script
|
||||
|
||||
🛸 Fasten your seatbelts! Were setting up your Formbricks environment on your Ubuntu 22.04.2 LTS server.
|
||||
🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your Ubuntu 24.04 LTS server.
|
||||
|
||||
🧹 Time to sweep away any old Docker installations.
|
||||
🔄 Updating your package list.
|
||||
📦 Installing the necessary dependencies.
|
||||
🔑 Adding Dockers official GPG key and setting up the stable repository.
|
||||
🔑 Adding Docker's official GPG key and setting up the stable repository.
|
||||
File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N) y
|
||||
🔄 Updating your package list again.
|
||||
🐳 Installing Docker.
|
||||
🚀 Testing your Docker installation.
|
||||
🎉 Docker is installed!
|
||||
🐳 Adding your user to the Docker group to avoid using sudo with docker commands.
|
||||
🎉 Hooray! Docker is all set and ready to go. Youre now ready to run your Formbricks instance!
|
||||
🚗 Installing Traefik...
|
||||
🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!
|
||||
📁 Created Formbricks Quickstart directory at ./formbricks.
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
💡 Created traefik.yaml file with your provided email address.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):
|
||||
my.hosted.url.com
|
||||
🚙 Updating NEXTAUTH_SECRET in the Formbricks container...
|
||||
🔗 Do you want us to set up an HTTPS certificate for you? [Y/n]
|
||||
Y
|
||||
🔗 Please make sure that the domain points to the server's IP address and that ports 80 & 443 are open in your server's firewall. Is everything set up? [Y/n]
|
||||
Y
|
||||
💡 Please enter your email address for the SSL certificate:
|
||||
docs@formbricks.com
|
||||
🔗 Do you want to enforce HTTPS (HSTS)? [Y/n]
|
||||
Y
|
||||
🚗 Configuring Traefik...
|
||||
💡 Created traefik.yaml and traefik-dynamic.yaml file.
|
||||
💡 Created acme.json file with correct permissions.
|
||||
📧 Do you want to set up the email service? You will need SMTP credentials for the same! [y/N] N
|
||||
📥 Downloading docker-compose.yml from Formbricks GitHub repository...
|
||||
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||
Dload Upload Total Spent Left Speed
|
||||
100 6632 100 6632 0 0 24280 0 --:--:-- --:--:-- --:--:-- 24382
|
||||
🚙 Updating docker-compose.yml with your custom inputs...
|
||||
🚗 NEXTAUTH_SECRET updated successfully!
|
||||
🚗 ENCRYPTION_KEY updated successfully!
|
||||
🚗 CRON_SECRET updated successfully!
|
||||
|
||||
[+] Running 4/4
|
||||
✔ Network formbricks_default Created 0.1s
|
||||
✔ Container formbricks-postgres-1 Started 0.5s
|
||||
✔ Container formbricks-formbricks-1 Started 0.7s
|
||||
✔ Container traefik Started 1.1s
|
||||
✔ Network formbricks_default Created 0.2s
|
||||
✔ Container formbricks-postgres-1 Started 1.0s
|
||||
✔ Container formbricks-formbricks-1 Started 1.6s
|
||||
✔ Container traefik Started 2.8s
|
||||
🔗 To edit more variables and deeper config, go to the formbricks/docker-compose.yml, edit the file, and restart the container!
|
||||
🚨 Make sure you have set up the DNS records as well as inbound rules for the domain name and IP address of this instance.
|
||||
|
||||
🎉 All done! Check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.'
|
||||
🎉 All done! Please setup your Formbricks instance by visiting your domain at https://my.hosted.url.com. You can check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.'
|
||||
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -15,6 +15,7 @@ const TopLevelNavItem = ({ href, children }: { href: string; children: React.Rea
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="text-sm leading-5 text-slate-600 transition hover:text-slate-900 dark:text-slate-400 dark:hover:text-white">
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
@@ -171,6 +171,13 @@ const NavigationGroup = ({
|
||||
|
||||
const isParentOpen = (title: string) => openGroups.includes(title);
|
||||
|
||||
const sortedLinks = group.links.map((link) => {
|
||||
if (link.children) {
|
||||
link.children.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
return link;
|
||||
});
|
||||
|
||||
return (
|
||||
<li className={clsx("relative mt-6", className)}>
|
||||
<motion.h2 layout="position" className="font-semibold text-slate-900 dark:text-white">
|
||||
@@ -185,7 +192,7 @@ const NavigationGroup = ({
|
||||
{isActiveGroup && <ActivePageMarker group={group} pathname={pathname || "/docs"} />}
|
||||
</AnimatePresence>
|
||||
<ul role="list" className="border-l border-transparent">
|
||||
{group.links.map((link) => (
|
||||
{sortedLinks.map((link) => (
|
||||
<motion.li key={link.title} layout="position" className="relative">
|
||||
{link.href ? (
|
||||
<NavLink
|
||||
|
||||
@@ -40,7 +40,8 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
],
|
||||
@@ -63,7 +64,8 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
],
|
||||
@@ -89,7 +91,8 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" },
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" },
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" },
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" },
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" },
|
||||
{ title: "Verify Email before Survey", href: "/link-surveys/verify-email-before-survey" },
|
||||
{ title: "PIN Protected Surveys", href: "/link-surveys/pin-protected-surveys" },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import nextMDX from "@next/mdx";
|
||||
|
||||
import { recmaPlugins } from "./mdx/recma.mjs";
|
||||
import { rehypePlugins } from "./mdx/rehype.mjs";
|
||||
import { remarkPlugins } from "./mdx/remark.mjs";
|
||||
@@ -105,17 +104,18 @@ const nextConfig = {
|
||||
destination: "/app-surveys/user-identification",
|
||||
permanent: true,
|
||||
},
|
||||
// Global Features
|
||||
{
|
||||
source: "/global/custom-start-end-conditions",
|
||||
destination: "/global/schedule-start-end-dates",
|
||||
permanent: true,
|
||||
},
|
||||
// Integrations
|
||||
{
|
||||
source: "/integrations/:path",
|
||||
destination: "/developer-docs/integrations/:path",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/global/custom-styling",
|
||||
destination: "/global/overwrite-styling",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,23 +19,23 @@
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.6.1",
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@storybook/addon-a11y": "^8.2.7",
|
||||
"@storybook/addon-essentials": "^8.2.7",
|
||||
"@storybook/addon-interactions": "^8.2.7",
|
||||
"@storybook/addon-links": "^8.2.7",
|
||||
"@storybook/addon-onboarding": "^8.2.7",
|
||||
"@storybook/blocks": "^8.2.7",
|
||||
"@storybook/react": "^8.2.7",
|
||||
"@storybook/react-vite": "^8.2.7",
|
||||
"@storybook/test": "^8.2.7",
|
||||
"@storybook/addon-a11y": "^8.2.9",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/addon-onboarding": "^8.2.9",
|
||||
"@storybook/blocks": "^8.2.9",
|
||||
"@storybook/react": "^8.2.9",
|
||||
"@storybook/react-vite": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.2.7",
|
||||
"tsup": "^8.2.3",
|
||||
"vite": "^5.3.5"
|
||||
"storybook": "^8.2.9",
|
||||
"tsup": "^8.2.4",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ RUN PRISMA_VERSION=$(cat prisma_version.txt) && npm install -g prisma@$PRISMA_VE
|
||||
|
||||
EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
USER nextjs
|
||||
# USER nextjs
|
||||
|
||||
# Prepare volume for uploads
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
|
||||
@@ -82,7 +82,6 @@ export const ConnectWithFormbricks = ({
|
||||
</div>
|
||||
<Button
|
||||
id="finishOnboarding"
|
||||
className="text-slate-400 hover:text-slate-700"
|
||||
variant={widgetSetupCompleted ? "primary" : "minimal"}
|
||||
onClick={handleFinishOnboarding}
|
||||
EndIcon={ArrowRight}>
|
||||
|
||||
@@ -36,7 +36,12 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
|
||||
const handleInvite = async (data: TInviteOrganizationMemberDetails) => {
|
||||
try {
|
||||
await inviteOrganizationMemberAction(organization.id, data.email, "developer", data.inviteMessage);
|
||||
await inviteOrganizationMemberAction({
|
||||
organizationId: organization.id,
|
||||
email: data.email,
|
||||
role: "developer",
|
||||
inviteMessage: data.inviteMessage,
|
||||
});
|
||||
toast.success("Invite sent successful");
|
||||
await finishOnboarding();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { CircleUserRoundIcon, EarthIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
@@ -15,14 +15,14 @@ const Page = async ({ params }: ChannelPageProps) => {
|
||||
{
|
||||
title: "Public website",
|
||||
description: "Run well-timed pop-up surveys.",
|
||||
icon: EarthIcon,
|
||||
icon: GlobeIcon,
|
||||
iconText: "Built for scale",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=website`,
|
||||
},
|
||||
{
|
||||
title: "App with sign up",
|
||||
description: "Run highly-targeted micro-surveys.",
|
||||
icon: CircleUserRoundIcon,
|
||||
icon: GlobeLockIcon,
|
||||
iconText: "Enrich user profiles",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=app`,
|
||||
},
|
||||
|
||||
@@ -1,68 +1,54 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { inviteUser } from "@formbricks/lib/invite/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { ZMembershipRole } from "@formbricks/types/memberships";
|
||||
|
||||
export const inviteOrganizationMemberAction = async (
|
||||
organizationId: string,
|
||||
email: string,
|
||||
role: TMembershipRole,
|
||||
inviteMessage: string
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ZInviteOrganizationMemberAction = z.object({
|
||||
organizationId: ZId,
|
||||
email: z.string(),
|
||||
role: ZMembershipRole,
|
||||
inviteMessage: z.string(),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
.schema(ZInviteOrganizationMemberAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["membership", "create"],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const invite = await inviteUser({
|
||||
organizationId: parsedInput.organizationId,
|
||||
invitee: {
|
||||
email: parsedInput.email,
|
||||
name: "",
|
||||
role: parsedInput.role,
|
||||
},
|
||||
});
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
parsedInput.inviteMessage
|
||||
);
|
||||
}
|
||||
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId,
|
||||
invitee: {
|
||||
email,
|
||||
name: "",
|
||||
role,
|
||||
},
|
||||
return invite;
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
email,
|
||||
user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
inviteMessage
|
||||
);
|
||||
}
|
||||
|
||||
return invite;
|
||||
};
|
||||
|
||||
@@ -1,208 +1,206 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { createActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { actionClient, authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromProductId,
|
||||
getOrganizationIdFromSegmentId,
|
||||
getOrganizationIdFromSurveyId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { getProduct } from "@formbricks/lib/product/service";
|
||||
import {
|
||||
cloneSegment,
|
||||
createSegment,
|
||||
deleteSegment,
|
||||
getSegment,
|
||||
resetSegmentInSurvey,
|
||||
updateSegment,
|
||||
} from "@formbricks/lib/segment/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import {
|
||||
deleteSurvey,
|
||||
getSurvey,
|
||||
loadNewSegmentInSurvey,
|
||||
updateSurvey,
|
||||
} from "@formbricks/lib/survey/service";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TBaseFilters, TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { loadNewSegmentInSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZBaseFilters, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
import { ZSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const surveyMutateAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
|
||||
export const updateSurveyAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
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");
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(survey.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(survey!.environmentId, session.user.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const refetchProductAction = async (productId: string): Promise<TProduct | null> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const product = await getProduct(productId);
|
||||
return product;
|
||||
};
|
||||
|
||||
export const createBasicSegmentAction = async ({
|
||||
description,
|
||||
environmentId,
|
||||
filters,
|
||||
isPrivate,
|
||||
surveyId,
|
||||
title,
|
||||
}: {
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
isPrivate: boolean;
|
||||
filters: TBaseFilters;
|
||||
}) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const segment = await createSegment({
|
||||
environmentId,
|
||||
surveyId,
|
||||
title,
|
||||
description: description || "",
|
||||
isPrivate,
|
||||
filters,
|
||||
export const updateSurveyAction = authenticatedActionClient
|
||||
.schema(ZSurvey)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.id),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
return await updateSurvey(parsedInput);
|
||||
});
|
||||
surveyCache.revalidate({ id: surveyId });
|
||||
|
||||
return segment;
|
||||
};
|
||||
const ZRefetchProductAction = z.object({
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = async (
|
||||
environmentId: string,
|
||||
segmentId: string,
|
||||
data: TSegmentUpdateInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const refetchProductAction = authenticatedActionClient
|
||||
.schema(ZRefetchProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "read"],
|
||||
});
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
return await getProduct(parsedInput.productId);
|
||||
});
|
||||
|
||||
const { filters } = data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
const ZCreateBasicSegmentAction = z.object({
|
||||
description: z.string().optional(),
|
||||
environmentId: ZId,
|
||||
filters: ZBaseFilters,
|
||||
isPrivate: z.boolean(),
|
||||
surveyId: ZId,
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export const createBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZCreateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["segment", "create"],
|
||||
});
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(parsedInput.filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(segmentId, data);
|
||||
};
|
||||
const segment = await createSegment({
|
||||
environmentId: parsedInput.environmentId,
|
||||
surveyId: parsedInput.surveyId,
|
||||
title: parsedInput.title,
|
||||
description: parsedInput.description || "",
|
||||
isPrivate: parsedInput.isPrivate,
|
||||
filters: parsedInput.filters,
|
||||
});
|
||||
surveyCache.revalidate({ id: parsedInput.surveyId });
|
||||
|
||||
export const loadNewBasicSegmentAction = async (surveyId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await loadNewSegmentInSurvey(surveyId, segmentId);
|
||||
};
|
||||
|
||||
export const cloneBasicSegmentAction = async (segmentId: string, surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneSegment(segmentId, surveyId);
|
||||
return clonedSegment;
|
||||
} catch (err: any) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const foundSegment = await getSegment(segmentId);
|
||||
|
||||
if (!foundSegment) {
|
||||
throw new Error(`Segment with id ${segmentId} not found`);
|
||||
}
|
||||
|
||||
return await deleteSegment(segmentId);
|
||||
};
|
||||
|
||||
export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await resetSegmentInSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const getImagesFromUnsplashAction = async (searchQuery: string, page: number = 1) => {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
const baseUrl = "https://api.unsplash.com/search/photos";
|
||||
const params = new URLSearchParams({
|
||||
query: searchQuery,
|
||||
client_id: UNSPLASH_ACCESS_KEY,
|
||||
orientation: "landscape",
|
||||
per_page: "9",
|
||||
page: page.toString(),
|
||||
return segment;
|
||||
});
|
||||
|
||||
try {
|
||||
const ZUpdateBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
data: ZSegmentUpdateInput,
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZUpdateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
const { filters } = parsedInput.data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(parsedInput.segmentId, parsedInput.data);
|
||||
});
|
||||
|
||||
const ZLoadNewBasicSegmentAction = z.object({
|
||||
surveyId: ZId,
|
||||
segmentId: ZId,
|
||||
});
|
||||
|
||||
export const loadNewBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZLoadNewBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.surveyId),
|
||||
rules: ["segment", "read"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
|
||||
});
|
||||
|
||||
const ZCloneBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const cloneBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZCloneBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "create"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZResetBasicSegmentFiltersAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const resetBasicSegmentFiltersAction = authenticatedActionClient
|
||||
.schema(ZResetBasicSegmentFiltersAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
return await resetSegmentInSurvey(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZGetImagesFromUnsplashAction = z.object({
|
||||
searchQuery: z.string(),
|
||||
page: z.number().optional(),
|
||||
});
|
||||
|
||||
export const getImagesFromUnsplashAction = actionClient
|
||||
.schema(ZGetImagesFromUnsplashAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
const baseUrl = "https://api.unsplash.com/search/photos";
|
||||
const params = new URLSearchParams({
|
||||
query: parsedInput.searchQuery,
|
||||
client_id: UNSPLASH_ACCESS_KEY,
|
||||
orientation: "landscape",
|
||||
per_page: "9",
|
||||
page: (parsedInput.page || 1).toString(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}?${params}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -227,14 +225,16 @@ export const getImagesFromUnsplashAction = async (searchQuery: string, page: num
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Error getting images from Unsplash");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) => {
|
||||
try {
|
||||
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
const ZTriggerDownloadUnsplashImageAction = z.object({
|
||||
downloadUrl: z.string(),
|
||||
});
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = actionClient
|
||||
.schema(ZTriggerDownloadUnsplashImageAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const response = await fetch(`${parsedInput.downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -245,20 +245,20 @@ export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) =>
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new Error("Error downloading image from Unsplash");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const createActionClassAction = async (environmentId: string, action: TActionClassInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateActionClassAction = z.object({
|
||||
action: ZActionClassInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, action.environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createActionClassAction = authenticatedActionClient
|
||||
.schema(ZCreateActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.action.environmentId),
|
||||
rules: ["actionClass", "create"],
|
||||
});
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createActionClass(action.environmentId, action);
|
||||
};
|
||||
return await createActionClass(parsedInput.action.environmentId, parsedInput.action);
|
||||
});
|
||||
|
||||
@@ -135,10 +135,14 @@ export const CreateNewActionTab = ({
|
||||
};
|
||||
}
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(
|
||||
environmentId,
|
||||
updatedAction as TActionClassInput
|
||||
);
|
||||
// const newActionClass: TActionClass =
|
||||
const createActionClassResposne = await createActionClassAction({
|
||||
action: updatedAction as TActionClassInput,
|
||||
});
|
||||
|
||||
if (!createActionClassResposne?.data) return;
|
||||
|
||||
const newActionClass = createActionClassResposne.data;
|
||||
if (setActionClasses) {
|
||||
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
|
||||
}
|
||||
|
||||
@@ -213,8 +213,6 @@ export const EditorCardMenu = ({
|
||||
|
||||
<DropdownMenuSubContent className="ml-4 border border-slate-200">
|
||||
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
|
||||
if (type === card.type) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
|
||||
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { getDefaultEndingCard } from "@formbricks/lib/templates";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
@@ -258,18 +258,19 @@ export const QuestionsView = ({
|
||||
toast.success("Question duplicated.");
|
||||
};
|
||||
|
||||
const addQuestion = (question: any, index?: number) => {
|
||||
const addQuestion = (question: TSurveyQuestion, index?: number) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const translatedQuestion = translateQuestion(question, languageSymbols);
|
||||
const updatedQuestion = addMultiLanguageLabels(question, languageSymbols);
|
||||
|
||||
if (index) {
|
||||
updatedSurvey.questions.splice(index, 0, { ...translatedQuestion, isDraft: true });
|
||||
updatedSurvey.questions.splice(index, 0, { ...updatedQuestion, isDraft: true });
|
||||
} else {
|
||||
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
|
||||
updatedSurvey.questions.push({ ...updatedQuestion, isDraft: true });
|
||||
}
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
|
||||
@@ -68,9 +68,9 @@ export const SurveyEditor = ({
|
||||
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
|
||||
|
||||
const fetchLatestProduct = useCallback(async () => {
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
}
|
||||
}, [localProduct.id]);
|
||||
|
||||
@@ -95,9 +95,9 @@ export const SurveyEditor = ({
|
||||
const listener = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
const fetchLatestProduct = async () => {
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
}
|
||||
};
|
||||
fetchLatestProduct();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { createSegmentAction } from "@formbricks/ee/advanced-targeting/lib/actions";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -132,7 +133,7 @@ export const SurveyMenuBar = ({
|
||||
title: localSurvey.id,
|
||||
});
|
||||
|
||||
return newSegment;
|
||||
return newSegment?.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -224,12 +225,16 @@ export const SurveyMenuBar = ({
|
||||
});
|
||||
|
||||
const segment = await handleSegmentUpdate();
|
||||
const updatedSurvey = await updateSurveyAction({ ...localSurvey, segment });
|
||||
const updatedSurveyResponse = await updateSurveyAction({ ...localSurvey, segment });
|
||||
|
||||
setIsSurveySaving(false);
|
||||
setLocalSurvey(updatedSurvey);
|
||||
|
||||
toast.success("Changes saved.");
|
||||
if (updatedSurveyResponse?.data) {
|
||||
setLocalSurvey(updatedSurveyResponse.data);
|
||||
toast.success("Changes saved.");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
|
||||
@@ -81,34 +82,38 @@ export const TargetingCard = ({
|
||||
const handleCloneSegment = async () => {
|
||||
if (!segment) return;
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneBasicSegmentAction(segment.id, localSurvey.id);
|
||||
const cloneBasicSegmentResponse = await cloneBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
|
||||
setSegment(clonedSegment);
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
if (cloneBasicSegmentResponse?.data) {
|
||||
setSegment(cloneBasicSegmentResponse.data);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(cloneBasicSegmentResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadNewSegment = async (surveyId: string, segmentId: string) => {
|
||||
const updatedSurvey = await loadNewBasicSegmentAction(surveyId, segmentId);
|
||||
return updatedSurvey;
|
||||
const loadNewBasicSegmentResponse = await loadNewBasicSegmentAction({ surveyId, segmentId });
|
||||
return loadNewBasicSegmentResponse?.data as TSurvey;
|
||||
};
|
||||
|
||||
const handleSegmentUpdate = async (environmentId: string, segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updatedSegment = await updateBasicSegmentAction(environmentId, segmentId, data);
|
||||
return updatedSegment;
|
||||
const handleSegmentUpdate = async (segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updateBasicSegmentResponse = await updateBasicSegmentAction({ segmentId, data });
|
||||
return updateBasicSegmentResponse?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSegmentCreate = async (data: TSegmentCreateInput) => {
|
||||
const createdSegment = await createBasicSegmentAction(data);
|
||||
return createdSegment;
|
||||
return createdSegment?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
|
||||
try {
|
||||
if (!segment) throw new Error("Invalid segment");
|
||||
await updateBasicSegmentAction(environmentId, segment?.id, data);
|
||||
await updateBasicSegmentAction({ segmentId: segment?.id, data });
|
||||
|
||||
router.refresh();
|
||||
toast.success("Segment saved successfully");
|
||||
@@ -122,7 +127,10 @@ export const TargetingCard = ({
|
||||
|
||||
const handleResetAllFilters = async () => {
|
||||
try {
|
||||
return await resetBasicSegmentFiltersAction(localSurvey.id);
|
||||
const resetBasicSegmentFiltersResponse = await resetBasicSegmentFiltersAction({
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
return resetBasicSegmentFiltersResponse?.data;
|
||||
} catch (err) {
|
||||
toast.error("Error resetting filters");
|
||||
}
|
||||
|
||||
@@ -123,7 +123,13 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
const fetchData = async (searchQuery: string, currentPage: number) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
|
||||
const getImagesFromUnsplashResponse = await getImagesFromUnsplashAction({
|
||||
searchQuery: searchQuery,
|
||||
page: currentPage,
|
||||
});
|
||||
if (!getImagesFromUnsplashResponse?.data) return;
|
||||
|
||||
const imagesFromUnsplash = getImagesFromUnsplashResponse.data;
|
||||
for (let i = 0; i < imagesFromUnsplash.length; i++) {
|
||||
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
|
||||
"authorName"
|
||||
@@ -163,7 +169,7 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
try {
|
||||
handleBgChange(imageUrl, "image");
|
||||
if (downloadImageUrl) {
|
||||
await triggerDownloadUnsplashImageAction(downloadImageUrl);
|
||||
await triggerDownloadUnsplashImageAction({ downloadUrl: downloadImageUrl });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
|
||||
@@ -44,7 +44,6 @@ const Page = async ({ params }) => {
|
||||
getServerSession(authOptions),
|
||||
getSegments(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { canUserAccessAttributeClass } from "@formbricks/lib/attributeClass/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import {
|
||||
getOrganizationIdFromAttributeClassId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
export const getSegmentsByAttributeClassAction = async (
|
||||
environmentId: string,
|
||||
attributeClass: TAttributeClass
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetSegmentsByAttributeClassAction = z.object({
|
||||
environmentId: ZId,
|
||||
attributeClass: ZAttributeClass,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessAttributeClass(session.user.id, attributeClass.id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const segments = await getSegmentsByAttributeClassName(environmentId, attributeClass.name);
|
||||
export const getSegmentsByAttributeClassAction = authenticatedActionClient
|
||||
.schema(ZGetSegmentsByAttributeClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromAttributeClassId(parsedInput.attributeClass.id),
|
||||
rules: ["attributeClass", "read"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["environment", "read"],
|
||||
});
|
||||
|
||||
const segments = await getSegmentsByAttributeClassName(
|
||||
parsedInput.environmentId,
|
||||
parsedInput.attributeClass.name
|
||||
);
|
||||
|
||||
// segments is an array of segments, each segment has a survey array with objects with properties: id, name and status.
|
||||
// We need the name of the surveys only and we need to filter out the surveys that are both in progress and not in progress.
|
||||
@@ -34,8 +51,4 @@ export const getSegmentsByAttributeClassAction = async (
|
||||
.flat();
|
||||
|
||||
return { activeSurveys, inactiveSurveys };
|
||||
} catch (err) {
|
||||
console.error(`Error getting segments by attribute class: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { TagIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -24,20 +25,20 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
|
||||
setLoading(true);
|
||||
|
||||
const getSurveys = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassName = await getSegmentsByAttributeClassAction(
|
||||
attributeClass.environmentId,
|
||||
attributeClass
|
||||
);
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassNameResponse = await getSegmentsByAttributeClassAction({
|
||||
environmentId: attributeClass.environmentId,
|
||||
attributeClass,
|
||||
});
|
||||
|
||||
setActiveSurveys(segmentsWithAttributeClassName.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassName.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (segmentsWithAttributeClassNameResponse?.data) {
|
||||
setActiveSurveys(segmentsWithAttributeClassNameResponse.data.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassNameResponse.data.inactiveSurveys);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(segmentsWithAttributeClassNameResponse);
|
||||
setError(new Error(errorMessage));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
getSurveys();
|
||||
|
||||
@@ -5,9 +5,10 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromPersonId } from "@formbricks/lib/organization/utils";
|
||||
import { deletePerson } from "@formbricks/lib/person/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
const ZPersonDeleteAction = z.object({
|
||||
personId: z.string(),
|
||||
personId: ZId,
|
||||
});
|
||||
|
||||
export const deletePersonAction = authenticatedActionClient
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { deleteSegment, getSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSegmentId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
|
||||
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
});
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
export const deleteBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZDeleteBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "delete"],
|
||||
});
|
||||
|
||||
const foundSegment = await getSegment(segmentId);
|
||||
return await deleteSegment(parsedInput.segmentId);
|
||||
});
|
||||
|
||||
if (!foundSegment) {
|
||||
throw new Error(`Segment with id ${segmentId} not found`);
|
||||
}
|
||||
const ZUpdateBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
data: ZSegmentUpdateInput,
|
||||
});
|
||||
|
||||
return await deleteSegment(segmentId);
|
||||
};
|
||||
export const updateBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZUpdateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = async (
|
||||
environmentId: string,
|
||||
segmentId: string,
|
||||
data: TSegmentUpdateInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const { filters } = parsedInput.data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const { filters } = data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(segmentId, data);
|
||||
};
|
||||
return await updateSegment(parsedInput.segmentId, parsedInput.data);
|
||||
});
|
||||
|
||||
@@ -70,11 +70,14 @@ export const BasicSegmentSettings = ({
|
||||
|
||||
try {
|
||||
setIsUpdatingSegment(true);
|
||||
await updateBasicSegmentAction(segment.environmentId, segment.id, {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
await updateBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
data: {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
},
|
||||
});
|
||||
|
||||
setIsUpdatingSegment(false);
|
||||
@@ -99,7 +102,7 @@ export const BasicSegmentSettings = ({
|
||||
const handleDeleteSegment = async () => {
|
||||
try {
|
||||
setIsDeletingSegment(true);
|
||||
await deleteBasicSegmentAction(segment.environmentId, segment.id);
|
||||
await deleteBasicSegmentAction({ segmentId: segment.id });
|
||||
|
||||
setIsDeletingSegment(false);
|
||||
toast.success("Segment deleted successfully!");
|
||||
|
||||
@@ -1,68 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import { Organization } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled)
|
||||
throw new OperationNotAllowedError(
|
||||
"Creating Multiple organization is restricted on your instance of Formbricks"
|
||||
);
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) throw new Error("User not found");
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrganizationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled)
|
||||
throw new OperationNotAllowedError(
|
||||
"Creating Multiple organization is restricted on your instance of Formbricks"
|
||||
);
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: organizationName,
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const product = await createProduct(newOrganization.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...ctx.user.notificationSettings,
|
||||
alert: {
|
||||
...ctx.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...ctx.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: Array.from(
|
||||
new Set([...(ctx.user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id])
|
||||
),
|
||||
};
|
||||
|
||||
await updateUser(ctx.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, session.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const product = await createProduct(newOrganization.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...user.notificationSettings,
|
||||
alert: {
|
||||
...user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: Array.from(
|
||||
new Set([...(user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id])
|
||||
),
|
||||
};
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
};
|
||||
|
||||
const ZCreateProductAction = z.object({
|
||||
organizationId: z.string(),
|
||||
organizationId: ZId,
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,76 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { canUserUpdateActionClass, verifyUserRoleAccess } from "@formbricks/lib/actionClass/auth";
|
||||
import { deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { z } from "zod";
|
||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromActionClassId } from "@formbricks/lib/organization/utils";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const deleteActionClassAction = async (environmentId, actionClassId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteActionClassAction = z.object({
|
||||
actionClassId: ZId,
|
||||
});
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
export const deleteActionClassAction = authenticatedActionClient
|
||||
.schema(ZDeleteActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromActionClassId(parsedInput.actionClassId),
|
||||
rules: ["actionClass", "delete"],
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
await deleteActionClass(parsedInput.actionClassId);
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const ZUpdateActionClassAction = z.object({
|
||||
actionClassId: ZId,
|
||||
updatedAction: ZActionClassInput,
|
||||
});
|
||||
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
export const updateActionClassAction = authenticatedActionClient
|
||||
.schema(ZUpdateActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const actionClass = await getActionClass(parsedInput.actionClassId);
|
||||
if (actionClass === null) {
|
||||
throw new ResourceNotFoundError("ActionClass", parsedInput.actionClassId);
|
||||
}
|
||||
|
||||
await deleteActionClass(environmentId, actionClassId);
|
||||
};
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromActionClassId(parsedInput.actionClassId),
|
||||
rules: ["actionClass", "update"],
|
||||
});
|
||||
|
||||
export const updateActionClassAction = async (
|
||||
environmentId: string,
|
||||
actionClassId: string,
|
||||
updatedAction: Partial<TActionClassInput>
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return await updateActionClass(
|
||||
actionClass.environmentId,
|
||||
parsedInput.actionClassId,
|
||||
parsedInput.updatedAction
|
||||
);
|
||||
});
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
const ZGetActiveInactiveSurveysAction = z.object({
|
||||
actionClassId: ZId,
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
export const getActiveInactiveSurveysAction = authenticatedActionClient
|
||||
.schema(ZGetActiveInactiveSurveysAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromActionClassId(parsedInput.actionClassId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateActionClass(environmentId, actionClassId, updatedAction);
|
||||
};
|
||||
|
||||
export const getActiveInactiveSurveysAction = async (
|
||||
actionClassId: string,
|
||||
environmentId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const surveys = await getSurveysByActionClassId(actionClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name),
|
||||
};
|
||||
return response;
|
||||
};
|
||||
const surveys = await getSurveysByActionClassId(parsedInput.actionClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name),
|
||||
};
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
@@ -25,16 +26,18 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
|
||||
setLoading(true);
|
||||
|
||||
const updateState = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const activeInactiveSurveys = await getActiveInactiveSurveysAction(actionClass.id, environmentId);
|
||||
setActiveSurveys(activeInactiveSurveys.activeSurveys);
|
||||
setInactiveSurveys(activeInactiveSurveys.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const getActiveInactiveSurveysResponse = await getActiveInactiveSurveysAction({
|
||||
actionClassId: actionClass.id,
|
||||
});
|
||||
if (getActiveInactiveSurveysResponse?.data) {
|
||||
setActiveSurveys(getActiveInactiveSurveysResponse.data.activeSurveys);
|
||||
setInactiveSurveys(getActiveInactiveSurveysResponse.data.inactiveSurveys);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(getActiveInactiveSurveysResponse);
|
||||
setError(new Error(errorMessage));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
updateState();
|
||||
|
||||
@@ -31,7 +31,6 @@ export const ActionDetailModal = ({
|
||||
title: "Settings",
|
||||
children: (
|
||||
<ActionSettingsTab
|
||||
environmentId={environmentId}
|
||||
actionClass={actionClass}
|
||||
actionClasses={actionClasses}
|
||||
setOpen={setOpen}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { CodeActionForm } from "@formbricks/ui/organisms/CodeActionForm";
|
||||
import { NoCodeActionForm } from "@formbricks/ui/organisms/NoCodeActionForm";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
actionClass: TActionClass;
|
||||
actionClasses: TActionClass[];
|
||||
setOpen: (v: boolean) => void;
|
||||
@@ -31,7 +30,6 @@ interface ActionSettingsTabProps {
|
||||
}
|
||||
|
||||
export const ActionSettingsTab = ({
|
||||
environmentId,
|
||||
actionClass,
|
||||
actionClasses,
|
||||
setOpen,
|
||||
@@ -104,7 +102,10 @@ export const ActionSettingsTab = ({
|
||||
},
|
||||
}),
|
||||
};
|
||||
await updateActionClassAction(environmentId, actionClass.id, updatedData);
|
||||
await updateActionClassAction({
|
||||
actionClassId: actionClass.id,
|
||||
updatedAction: updatedData,
|
||||
});
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
toast.success("Action updated successfully");
|
||||
@@ -118,7 +119,7 @@ export const ActionSettingsTab = ({
|
||||
const handleDeleteAction = async () => {
|
||||
try {
|
||||
setIsDeletingAction(true);
|
||||
await deleteActionClassAction(environmentId, actionClass.id);
|
||||
await deleteActionClassAction({ actionClassId: actionClass.id });
|
||||
router.refresh();
|
||||
toast.success("Action deleted successfully");
|
||||
setOpen(false);
|
||||
|
||||
@@ -5,11 +5,15 @@ import { formbricksLogout } from "@/app/lib/formbricks";
|
||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
BlendIcon,
|
||||
BlocksIcon,
|
||||
ChevronRightIcon,
|
||||
Cog,
|
||||
CreditCardIcon,
|
||||
GlobeIcon,
|
||||
GlobeLockIcon,
|
||||
KeyIcon,
|
||||
LinkIcon,
|
||||
LogOutIcon,
|
||||
MessageCircle,
|
||||
MousePointerClick,
|
||||
@@ -27,7 +31,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/utils/strings";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
@@ -115,7 +119,24 @@ export const MainNavigation = ({
|
||||
}, [organizations]);
|
||||
|
||||
const sortedProducts = useMemo(() => {
|
||||
return [...products].sort((a, b) => a.name.localeCompare(b.name));
|
||||
const channelOrder: (string | null)[] = ["website", "app", "link", null];
|
||||
|
||||
const groupedProducts = products.reduce(
|
||||
(acc, product) => {
|
||||
const channel = product.config.channel;
|
||||
const key = channel !== null ? channel : "null";
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(product);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof products>
|
||||
);
|
||||
|
||||
Object.keys(groupedProducts).forEach((channel) => {
|
||||
groupedProducts[channel].sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
return channelOrder.flatMap((channel) => groupedProducts[channel !== null ? channel : "null"] || []);
|
||||
}, [products]);
|
||||
|
||||
const handleEnvironmentChangeByProduct = (productId: string) => {
|
||||
@@ -277,18 +298,27 @@ export const MainNavigation = ({
|
||||
<div
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-row items-center space-x-5",
|
||||
"flex cursor-pointer flex-row items-center space-x-3",
|
||||
isCollapsed ? "pl-2" : "pl-4"
|
||||
)}>
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900 p-1.5 font-bold text-slate-50">
|
||||
XM
|
||||
<div className="rounded-lg bg-slate-900 p-1.5 text-slate-50">
|
||||
{product.config.channel === "website" ? (
|
||||
<GlobeIcon strokeWidth={1.5} />
|
||||
) : product.config.channel === "app" ? (
|
||||
<GlobeLockIcon strokeWidth={1.5} />
|
||||
) : product.config.channel === "link" ? (
|
||||
<LinkIcon strokeWidth={1.5} />
|
||||
) : (
|
||||
<BlendIcon strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
{!isCollapsed && !isTextVisible && (
|
||||
<>
|
||||
<div>
|
||||
<p
|
||||
title={product.name}
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700 transition-opacity duration-200",
|
||||
"ph-no-capture ph-no-capture -mb-0.5 max-w-28 truncate text-sm font-bold text-slate-700 transition-opacity duration-200",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
{product.name}
|
||||
@@ -298,7 +328,9 @@ export const MainNavigation = ({
|
||||
"text-sm text-slate-500 transition-opacity duration-200",
|
||||
isTextVisible ? "opacity-0" : "opacity-100"
|
||||
)}>
|
||||
Product
|
||||
{product.config.channel === "link"
|
||||
? "Link & Email"
|
||||
: capitalizeFirstLetter(product.config.channel)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
@@ -312,7 +344,7 @@ export const MainNavigation = ({
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 space-y-1 rounded-xl border border-slate-200 shadow-sm"
|
||||
className="w-fit space-y-1 rounded-xl border border-slate-200 shadow-sm"
|
||||
id="userDropdownInnerContentWrapper"
|
||||
side="right"
|
||||
sideOffset={10}
|
||||
@@ -324,15 +356,28 @@ export const MainNavigation = ({
|
||||
{sortedProducts.map((product) => (
|
||||
<DropdownMenuRadioItem
|
||||
value={product.id}
|
||||
className="cursor-pointer break-all rounded-lg"
|
||||
className="cursor-pointer break-all rounded-lg font-normal"
|
||||
key={product.id}>
|
||||
{product?.name}
|
||||
<div>
|
||||
{product.config.channel === "website" ? (
|
||||
<GlobeIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
) : product.config.channel === "app" ? (
|
||||
<GlobeLockIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
) : product.config.channel === "link" ? (
|
||||
<LinkIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
) : (
|
||||
<BlendIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
<div className="">{product?.name}</div>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
{isOwnerOrAdmin && (
|
||||
<DropdownMenuItem onClick={() => handleAddProduct(organization.id)} className="rounded-lg">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleAddProduct(organization.id)}
|
||||
className="rounded-lg font-normal">
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
<span>Add product</span>
|
||||
</DropdownMenuItem>
|
||||
@@ -350,7 +395,7 @@ export const MainNavigation = ({
|
||||
<div
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
"flex cursor-pointer flex-row items-center space-x-5",
|
||||
"flex cursor-pointer flex-row items-center space-x-3",
|
||||
isCollapsed ? "pl-2" : "pl-4"
|
||||
)}>
|
||||
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
|
||||
@@ -358,16 +403,15 @@ export const MainNavigation = ({
|
||||
<>
|
||||
<div className={cn(isTextVisible ? "opacity-0" : "opacity-100")}>
|
||||
<p
|
||||
title={user?.email}
|
||||
className={cn(
|
||||
"ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700"
|
||||
"ph-no-capture ph-no-capture -mb-0.5 max-w-28 truncate text-sm font-bold text-slate-700"
|
||||
)}>
|
||||
{user?.name ? (
|
||||
<span>{truncate(user?.name, 30)}</span>
|
||||
) : (
|
||||
<span>{truncate(user?.email, 30)}</span>
|
||||
)}
|
||||
{user?.name ? <span>{user?.name}</span> : <span>{user?.email}</span>}
|
||||
</p>
|
||||
<p className={cn("text-sm text-slate-500")}>
|
||||
<p
|
||||
title={capitalizeFirstLetter(organization?.name)}
|
||||
className="max-w-28 truncate text-sm text-slate-500">
|
||||
{capitalizeFirstLetter(organization?.name)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessIntegration } from "@formbricks/lib/integration/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZIntegrationInput } from "@formbricks/types/integration";
|
||||
|
||||
export const createOrUpdateIntegrationAction = async (
|
||||
environmentId: string,
|
||||
integrationData: TIntegrationInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authenticated");
|
||||
const ZCreateOrUpdateIntegrationAction = z.object({
|
||||
environmentId: ZId,
|
||||
integrationData: ZIntegrationInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createOrUpdateIntegrationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrUpdateIntegrationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["integration", "create"],
|
||||
});
|
||||
|
||||
return await createOrUpdateIntegration(environmentId, integrationData);
|
||||
};
|
||||
return await createOrUpdateIntegration(parsedInput.environmentId, parsedInput.integrationData);
|
||||
});
|
||||
|
||||
export const deleteIntegrationAction = async (integrationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteIntegrationAction = z.object({
|
||||
integrationId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessIntegration(session.user.id, integrationId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteIntegrationAction = authenticatedActionClient
|
||||
.schema(ZDeleteIntegrationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.integrationId),
|
||||
rules: ["integration", "delete"],
|
||||
});
|
||||
|
||||
return await deleteIntegration(integrationId);
|
||||
};
|
||||
return await deleteIntegration(parsedInput.integrationId);
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
|
||||
export const refreshTablesAction = async (environmentId: string) => {
|
||||
return await getAirtableTables(environmentId);
|
||||
};
|
||||
@@ -144,7 +144,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
const actionMessage = isEditMode ? "updated" : "added";
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, airtableIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: airtableIntegrationData });
|
||||
toast.success(`Integration ${actionMessage} successfully`);
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
@@ -176,7 +176,7 @@ export const AddIntegrationModal = ({
|
||||
const integrationData = structuredClone(airtableIntegrationData);
|
||||
integrationData.config.data.splice(index, 1);
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, integrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData });
|
||||
handleClose();
|
||||
router.refresh();
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(airtableIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: airtableIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export const AddIntegrationModal = ({
|
||||
// create action
|
||||
googleSheetIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction(environmentId, googleSheetIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -172,7 +172,7 @@ export const AddIntegrationModal = ({
|
||||
googleSheetIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, googleSheetIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(googleSheetIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: googleSheetIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const refreshDatabasesAction = async (environmentId: string) => {
|
||||
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");
|
||||
|
||||
return await getNotionDatabases(environmentId);
|
||||
};
|
||||
@@ -202,7 +202,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, notionIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -217,7 +217,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, notionIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(notionIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: notionIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { getSlackChannels } from "@formbricks/lib/slack/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
export const refreshChannelsAction = async (environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZRefreshChannelsAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const refreshChannelsAction = authenticatedActionClient
|
||||
.schema(ZRefreshChannelsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["integration", "update"],
|
||||
});
|
||||
|
||||
return await getSlackChannels(environmentId);
|
||||
};
|
||||
return await getSlackChannels(parsedInput.environmentId);
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ export const AddChannelMappingModal = ({
|
||||
// create action
|
||||
slackIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction(environmentId, slackIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -155,7 +155,7 @@ export const AddChannelMappingModal = ({
|
||||
slackIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, slackIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(slackIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: slackIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -40,8 +40,11 @@ export const SlackWrapper = ({
|
||||
>(null);
|
||||
|
||||
const refreshChannels = async () => {
|
||||
const latestSlackChannels = await refreshChannelsAction(environment.id);
|
||||
setSlackChannels(latestSlackChannels);
|
||||
const refreshChannelsResponse = await refreshChannelsAction({ environmentId: environment.id });
|
||||
|
||||
if (refreshChannelsResponse?.data) {
|
||||
setSlackChannels(refreshChannelsResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlackAuthorization = async () => {
|
||||
|
||||
@@ -1,58 +1,77 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessWebhook } from "@formbricks/lib/webhook/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromWebhookId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { testEndpoint } from "@formbricks/lib/webhook/utils";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/webhooks";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZWebhookInput } from "@formbricks/types/webhooks";
|
||||
|
||||
export const createWebhookAction = async (
|
||||
environmentId: string,
|
||||
webhookInput: TWebhookInput
|
||||
): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateWebhookAction = z.object({
|
||||
environmentId: ZId,
|
||||
webhookInput: ZWebhookInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createWebhookAction = authenticatedActionClient
|
||||
.schema(ZCreateWebhookAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["webhook", "create"],
|
||||
});
|
||||
|
||||
return await createWebhook(environmentId, webhookInput);
|
||||
};
|
||||
return await createWebhook(parsedInput.environmentId, parsedInput.webhookInput);
|
||||
});
|
||||
|
||||
export const deleteWebhookAction = async (id: string): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteWebhookAction = z.object({
|
||||
id: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessWebhook(session.user.id, id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteWebhookAction = authenticatedActionClient
|
||||
.schema(ZDeleteWebhookAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromWebhookId(parsedInput.id),
|
||||
rules: ["webhook", "delete"],
|
||||
});
|
||||
|
||||
return await deleteWebhook(id);
|
||||
};
|
||||
return await deleteWebhook(parsedInput.id);
|
||||
});
|
||||
|
||||
export const updateWebhookAction = async (
|
||||
environmentId: string,
|
||||
webhookId: string,
|
||||
webhookInput: Partial<TWebhookInput>
|
||||
): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZUpdateWebhookAction = z.object({
|
||||
webhookId: ZId,
|
||||
webhookInput: ZWebhookInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessWebhook(session.user.id, webhookId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const updateWebhookAction = authenticatedActionClient
|
||||
.schema(ZUpdateWebhookAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromWebhookId(parsedInput.webhookId),
|
||||
rules: ["webhook", "update"],
|
||||
});
|
||||
|
||||
return await updateWebhook(environmentId, webhookId, webhookInput);
|
||||
};
|
||||
return await updateWebhook(parsedInput.webhookId, parsedInput.webhookInput);
|
||||
});
|
||||
|
||||
export const testEndpointAction = async (url: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZTestEndpointAction = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
const res = await testEndpoint(url);
|
||||
export const testEndpointAction = authenticatedActionClient
|
||||
.schema(ZTestEndpointAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const res = await testEndpoint(parsedInput.url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw res.error;
|
||||
}
|
||||
};
|
||||
if (!res.ok) {
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction(testEndpointInput);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
@@ -107,7 +107,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
|
||||
await createWebhookAction(environmentId, updatedData);
|
||||
await createWebhookAction({ environmentId, webhookInput: updatedData });
|
||||
router.refresh();
|
||||
setOpenWithStates(false);
|
||||
toast.success("Webhook added successfully.");
|
||||
|
||||
@@ -6,14 +6,13 @@ import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
|
||||
|
||||
interface WebhookModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
}
|
||||
|
||||
export const WebhookModal = ({ environmentId, open, setOpen, webhook, surveys }: WebhookModalProps) => {
|
||||
export const WebhookModal = ({ open, setOpen, webhook, surveys }: WebhookModalProps) => {
|
||||
const tabs = [
|
||||
{
|
||||
title: "Overview",
|
||||
@@ -21,14 +20,7 @@ export const WebhookModal = ({ environmentId, open, setOpen, webhook, surveys }:
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<WebhookSettingsTab
|
||||
environmentId={environmentId}
|
||||
webhook={webhook}
|
||||
surveys={surveys}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
children: <WebhookSettingsTab webhook={webhook} surveys={surveys} setOpen={setOpen} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -19,13 +19,12 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }: ActionSettingsTabProps) => {
|
||||
export const WebhookSettingsTab = ({ webhook, surveys, setOpen }: ActionSettingsTabProps) => {
|
||||
const router = useRouter();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
@@ -48,7 +47,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction(testEndpointInput);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
@@ -113,7 +112,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
setIsUpdatingWebhook(true);
|
||||
await updateWebhookAction(environmentId, webhook.id, updatedData);
|
||||
await updateWebhookAction({ webhookId: webhook.id, webhookInput: updatedData });
|
||||
toast.success("Webhook updated successfully.");
|
||||
router.refresh();
|
||||
setIsUpdatingWebhook(false);
|
||||
@@ -232,7 +231,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
onDelete={async () => {
|
||||
setOpen(false);
|
||||
try {
|
||||
await deleteWebhookAction(webhook.id);
|
||||
await deleteWebhookAction({ id: webhook.id });
|
||||
router.refresh();
|
||||
toast.success("Webhook deleted successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -67,7 +67,6 @@ export const WebhookTable = ({
|
||||
</div>
|
||||
)}
|
||||
<WebhookModal
|
||||
environmentId={environment.id}
|
||||
open={isWebhookDetailModalOpen}
|
||||
setOpen={setWebhookDetailModalOpen}
|
||||
webhook={activeWebhook}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
const ZUpdateProductAction = z.object({
|
||||
productId: ZId,
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
export const updateProductAction = authenticatedActionClient
|
||||
.schema(ZUpdateProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
schema: ZProductUpdateInput,
|
||||
data: parsedInput.data,
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "update"],
|
||||
});
|
||||
|
||||
return await updateProduct(parsedInput.productId, parsedInput.data);
|
||||
});
|
||||
@@ -1,28 +1,45 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { canUserAccessApiKey } from "@formbricks/lib/apiKey/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { createApiKey, deleteApiKey } from "@formbricks/lib/apiKey/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { TApiKeyCreateInput } from "@formbricks/types/api-keys";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import {
|
||||
getOrganizationIdFromApiKeyId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { ZApiKeyCreateInput } from "@formbricks/types/api-keys";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
export const deleteApiKeyAction = async (id: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteApiKeyAction = z.object({
|
||||
id: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessApiKey(session.user.id, id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteApiKeyAction = authenticatedActionClient
|
||||
.schema(ZDeleteApiKeyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromApiKeyId(parsedInput.id),
|
||||
rules: ["apiKey", "delete"],
|
||||
});
|
||||
|
||||
return await deleteApiKey(id);
|
||||
};
|
||||
export const createApiKeyAction = async (environmentId: string, apiKeyData: TApiKeyCreateInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return await deleteApiKey(parsedInput.id);
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateApiKeyAction = z.object({
|
||||
environmentId: ZId,
|
||||
apiKeyData: ZApiKeyCreateInput,
|
||||
});
|
||||
|
||||
return await createApiKey(environmentId, apiKeyData);
|
||||
};
|
||||
export const createApiKeyAction = authenticatedActionClient
|
||||
.schema(ZCreateApiKeyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["apiKey", "create"],
|
||||
});
|
||||
|
||||
return await createApiKey(parsedInput.environmentId, parsedInput.apiKeyData);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FilesIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TApiKey } from "@formbricks/types/api-keys";
|
||||
@@ -35,7 +36,7 @@ export const EditAPIKeys = ({
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
try {
|
||||
await deleteApiKeyAction(activeKey.id);
|
||||
await deleteApiKeyAction({ id: activeKey.id });
|
||||
const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || [];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success("API Key deleted");
|
||||
@@ -47,16 +48,20 @@ export const EditAPIKeys = ({
|
||||
};
|
||||
|
||||
const handleAddAPIKey = async (data) => {
|
||||
try {
|
||||
const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label });
|
||||
const updatedApiKeys = [...apiKeysLocal!, apiKey];
|
||||
const createApiKeyResponse = await createApiKeyAction({
|
||||
environmentId: environmentTypeId,
|
||||
apiKeyData: { label: data.label },
|
||||
});
|
||||
if (createApiKeyResponse?.data) {
|
||||
const updatedApiKeys = [...apiKeysLocal!, createApiKeyResponse.data];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success("API key created");
|
||||
} catch (e) {
|
||||
toast.error("Unable to create API Key");
|
||||
} finally {
|
||||
setOpenAddAPIKeyModal(false);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createApiKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
setOpenAddAPIKeyModal(false);
|
||||
};
|
||||
|
||||
const ApiKeyDisplay = ({ apiKey }) => {
|
||||
|
||||
@@ -4,30 +4,11 @@ import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
const ZUpdateProductAction = z.object({
|
||||
productId: z.string(),
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
export const updateProductAction = authenticatedActionClient
|
||||
.schema(ZUpdateProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
schema: ZProductUpdateInput,
|
||||
data: parsedInput.data,
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "update"],
|
||||
});
|
||||
|
||||
return await updateProduct(parsedInput.productId, parsedInput.data);
|
||||
});
|
||||
import { deleteProduct, getProducts } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
const ZProductDeleteAction = z.object({
|
||||
productId: z.string(),
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const deleteProductAction = authenticatedActionClient
|
||||
|
||||
@@ -4,6 +4,7 @@ import { deleteProductAction } from "@/app/(app)/environments/[environmentId]/pr
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { truncate } from "@formbricks/lib/utils/strings";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -24,19 +25,17 @@ export const DeleteProductRender = ({
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const handleDeleteProduct = async () => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const deletedProductActionResult = await deleteProductAction({ productId: product.id });
|
||||
if (deletedProductActionResult?.data) {
|
||||
toast.success("Product deleted successfully.");
|
||||
router.push("/");
|
||||
}
|
||||
setIsDeleting(false);
|
||||
} catch (err) {
|
||||
setIsDeleting(false);
|
||||
toast.error("Could not delete product.");
|
||||
setIsDeleting(true);
|
||||
const deleteProductResponse = await deleteProductAction({ productId: product.id });
|
||||
if (deleteProductResponse?.data) {
|
||||
toast.success("Product deleted successfully.");
|
||||
router.push("/");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteProductResponse);
|
||||
toast.error(errorMessage);
|
||||
setIsDeleteDialogOpen(false);
|
||||
}
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type EditProductNameProps = {
|
||||
product: TProduct;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type EditWaitingTimeProps = {
|
||||
product: TProduct;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessProduct, verifyUserRoleAccess } from "@formbricks/lib/product/auth";
|
||||
import { getProduct, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
export const updateProductAction = async (productId: string, inputProduct: TProductUpdateInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const product = await getProduct(productId);
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateProduct(productId, inputProduct);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
interface EditFormbricksBrandingProps {
|
||||
type: "linkSurvey" | "inAppSurvey";
|
||||
@@ -34,7 +34,7 @@ export const EditFormbricksBranding = ({
|
||||
let inputProduct: Partial<TProductUpdateInput> = {
|
||||
[type === "linkSurvey" ? "linkSurveyBranding" : "inAppSurveyBranding"]: newBrandingState,
|
||||
};
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
await updateProductAction({ productId: product.id, data: inputProduct });
|
||||
toast.success(newBrandingState ? "Formbricks branding is shown." : "Formbricks branding is hidden.");
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
interface EditLogoProps {
|
||||
product: TProduct;
|
||||
@@ -61,7 +61,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
const updatedProduct: TProductUpdateInput = {
|
||||
logo: { url: logoUrl, bgColor: isBgColorEnabled ? logoBgColor : undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
await updateProductAction({ productId: product.id, data: updatedProduct });
|
||||
toast.success("Logo updated successfully");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update the logo");
|
||||
@@ -83,7 +83,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
const updatedProduct: TProductUpdateInput = {
|
||||
logo: { url: undefined, bgColor: undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
await updateProductAction({ productId: product.id, data: updatedProduct });
|
||||
toast.success("Logo removed successfully", { icon: "🗑️" });
|
||||
} catch (error) {
|
||||
toast.error("Failed to remove the logo");
|
||||
|
||||
@@ -11,7 +11,7 @@ import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@form
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
const placements = [
|
||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||
@@ -53,10 +53,13 @@ export const EditPlacementForm = ({ product }: EditPlacementProps) => {
|
||||
|
||||
const onSubmit: SubmitHandler<EditPlacementFormValues> = async (data) => {
|
||||
try {
|
||||
await updateProductAction(product.id, {
|
||||
placement: data.placement,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
placement: data.placement,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Placement updated successfully.");
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { COLOR_DEFAULTS, PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling, ZProductStyling } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
FormProvider,
|
||||
} from "@formbricks/ui/Form";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type ThemeStylingProps = {
|
||||
product: TProduct;
|
||||
@@ -111,8 +112,11 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
},
|
||||
};
|
||||
|
||||
await updateProductAction(product.id, {
|
||||
styling: { ...defaultStyling },
|
||||
await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
styling: { ...defaultStyling },
|
||||
},
|
||||
});
|
||||
|
||||
form.reset({ ...defaultStyling });
|
||||
@@ -122,15 +126,19 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
}, [form, product.id, router]);
|
||||
|
||||
const onSubmit: SubmitHandler<TProductStyling> = async (data) => {
|
||||
try {
|
||||
const updatedProduct = await updateProductAction(product.id, {
|
||||
const updatedProductResponse = await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
styling: data,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
form.reset({ ...updatedProduct.styling });
|
||||
if (updatedProductResponse?.data) {
|
||||
form.reset({ ...updatedProductResponse.data.styling });
|
||||
toast.success("Styling updated successfully.");
|
||||
} catch (err) {
|
||||
toast.error("Error updating styling.");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedProductResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,50 +1,64 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessTag, verifyUserRoleAccess } from "@formbricks/lib/tag/auth";
|
||||
import { deleteTag, getTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromTagId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
export const deleteTagAction = async (tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteTagAction = z.object({
|
||||
tagId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessTag(session.user.id, tagId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteTagAction = authenticatedActionClient
|
||||
.schema(ZDeleteTagAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.tagId),
|
||||
rules: ["tag", "delete"],
|
||||
});
|
||||
|
||||
const tag = await getTag(tagId);
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(tag!.environmentId, session.user!.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
return await deleteTag(parsedInput.tagId);
|
||||
});
|
||||
|
||||
return await deleteTag(tagId);
|
||||
};
|
||||
const ZUpdateTagNameAction = z.object({
|
||||
tagId: ZId,
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const updateTagNameAction = async (tagId: string, name: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const updateTagNameAction = authenticatedActionClient
|
||||
.schema(ZUpdateTagNameAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.tagId),
|
||||
rules: ["tag", "update"],
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessTag(session.user.id, tagId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
return await updateTagName(parsedInput.tagId, parsedInput.name);
|
||||
});
|
||||
|
||||
const tag = await getTag(tagId);
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(tag!.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZMergeTagsAction = z.object({
|
||||
originalTagId: ZId,
|
||||
newTagId: ZId,
|
||||
});
|
||||
|
||||
return await updateTagName(tagId, name);
|
||||
};
|
||||
export const mergeTagsAction = authenticatedActionClient
|
||||
.schema(ZMergeTagsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.originalTagId),
|
||||
rules: ["tag", "update"],
|
||||
});
|
||||
|
||||
export const mergeTagsAction = async (originalTagId: string, newTagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.newTagId),
|
||||
rules: ["tag", "update"],
|
||||
});
|
||||
|
||||
const isAuthorizedForOld = await canUserAccessTag(session.user.id, originalTagId);
|
||||
const isAuthorizedForNew = await canUserAccessTag(session.user.id, newTagId);
|
||||
if (!isAuthorizedForOld || !isAuthorizedForNew) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const tag = await getTag(originalTagId);
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(tag!.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await mergeTags(originalTagId, newTagId);
|
||||
};
|
||||
return await mergeTags(parsedInput.originalTagId, parsedInput.newTagId);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AlertCircleIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TTag, TTagsCount } from "@formbricks/types/tags";
|
||||
@@ -45,16 +46,16 @@ const SingleTag: React.FC<{
|
||||
const [isMergingTags, setIsMergingTags] = useState(false);
|
||||
const [openDeleteTagDialog, setOpenDeleteTagDialog] = useState(false);
|
||||
|
||||
const confirmDeleteTag = () => {
|
||||
deleteTagAction(tagId)
|
||||
.then((response) => {
|
||||
toast.success(`${response?.name ?? "Tag"} tag deleted`);
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
});
|
||||
const confirmDeleteTag = async () => {
|
||||
const deleteTagResponse = await deleteTagAction({ tagId });
|
||||
if (deleteTagResponse?.data) {
|
||||
toast.success(`${deleteTagResponse?.data.name ?? "Tag"} tag deleted`);
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteTagResponse);
|
||||
toast.error(errorMessage ?? "Something went wrong");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -71,24 +72,25 @@ const SingleTag: React.FC<{
|
||||
)}
|
||||
defaultValue={tagName}
|
||||
onBlur={(e) => {
|
||||
updateTagNameAction(tagId, e.target.value.trim())
|
||||
.then(() => {
|
||||
updateTagNameAction({ tagId, name: e.target.value.trim() }).then((updateTagNameResponse) => {
|
||||
if (updateTagNameResponse?.data) {
|
||||
setUpdateTagError(false);
|
||||
toast.success("Tag updated");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.message.includes("Unique constraint failed on the fields")) {
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateTagNameResponse);
|
||||
if (errorMessage.includes("Unique constraint failed on the fields")) {
|
||||
toast.error("Tag already exists", {
|
||||
duration: 2000,
|
||||
icon: <AlertCircleIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
} else {
|
||||
toast.error(error?.message ?? "Something went wrong", {
|
||||
toast.error(errorMessage ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
setUpdateTagError(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,18 +115,17 @@ const SingleTag: React.FC<{
|
||||
}
|
||||
onSelect={(newTagId) => {
|
||||
setIsMergingTags(true);
|
||||
mergeTagsAction(tagId, newTagId)
|
||||
.then(() => {
|
||||
mergeTagsAction({ originalTagId: tagId, newTagId }).then((mergeTagsResponse) => {
|
||||
if (mergeTagsResponse?.data) {
|
||||
toast.success("Tags merged");
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(mergeTagsResponse);
|
||||
toast.error(errorMessage ?? "Something went wrong");
|
||||
}
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export const updateNotificationSettingsAction = async (notificationSettings: TUserNotificationSettings) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new AuthorizationError("Not authenticated");
|
||||
}
|
||||
const ZUpdateNotificationSettingsAction = z.object({
|
||||
notificationSettings: ZUserNotificationSettings,
|
||||
});
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings,
|
||||
export const updateNotificationSettingsAction = authenticatedActionClient
|
||||
.schema(ZUpdateNotificationSettingsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await updateUser(ctx.user.id, {
|
||||
notificationSettings: parsedInput.notificationSettings,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const NotificationSwitch = ({
|
||||
!updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId];
|
||||
}
|
||||
|
||||
await updateNotificationSettingsAction(updatedNotificationSettings);
|
||||
await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings });
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,105 +1,78 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { disableTwoFactorAuth, enableTwoFactorAuth, setupTwoFactorAuth } from "@formbricks/lib/auth/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteFile } from "@formbricks/lib/storage/service";
|
||||
import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserUpdateInput } from "@formbricks/types/user";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZUserUpdateInput } from "@formbricks/types/user";
|
||||
|
||||
export const updateUserAction = async (data: Partial<TUserUpdateInput>) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const updateUserAction = authenticatedActionClient
|
||||
.schema(ZUserUpdateInput.partial())
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await updateUser(ctx.user.id, parsedInput);
|
||||
});
|
||||
|
||||
return await updateUser(session.user.id, data);
|
||||
};
|
||||
const ZSetupTwoFactorAuthAction = z.object({
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export const setupTwoFactorAuthAction = async (password: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
export const setupTwoFactorAuthAction = authenticatedActionClient
|
||||
.schema(ZSetupTwoFactorAuthAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await setupTwoFactorAuth(ctx.user.id, parsedInput.password);
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
const ZEnableTwoFactorAuthAction = z.object({
|
||||
code: z.string(),
|
||||
});
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
export const enableTwoFactorAuthAction = authenticatedActionClient
|
||||
.schema(ZEnableTwoFactorAuthAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await enableTwoFactorAuth(ctx.user.id, parsedInput.code);
|
||||
});
|
||||
|
||||
return await setupTwoFactorAuth(session.user.id, password);
|
||||
};
|
||||
const ZDisableTwoFactorAuthAction = z.object({
|
||||
code: z.string(),
|
||||
password: z.string(),
|
||||
backupCode: z.string().optional(),
|
||||
});
|
||||
|
||||
export const enableTwoFactorAuthAction = async (code: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
export const disableTwoFactorAuthAction = authenticatedActionClient
|
||||
.schema(ZDisableTwoFactorAuthAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await disableTwoFactorAuth(ctx.user.id, parsedInput);
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
const ZUpdateAvatarAction = z.object({
|
||||
avatarUrl: z.string(),
|
||||
});
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
export const updateAvatarAction = authenticatedActionClient
|
||||
.schema(ZUpdateAvatarAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await updateUser(ctx.user.id, { imageUrl: parsedInput.avatarUrl });
|
||||
});
|
||||
|
||||
return await enableTwoFactorAuth(session.user.id, code);
|
||||
};
|
||||
const ZRemoveAvatarAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
type TDisableTwoFactorAuthParams = {
|
||||
code: string;
|
||||
password: string;
|
||||
backupCode?: string;
|
||||
};
|
||||
export const removeAvatarAction = authenticatedActionClient
|
||||
.schema(ZRemoveAvatarAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["environment", "read"],
|
||||
});
|
||||
|
||||
export const disableTwoFactorAuthAction = async (params: TDisableTwoFactorAuthParams) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return await disableTwoFactorAuth(session.user.id, params);
|
||||
};
|
||||
|
||||
export const updateAvatarAction = async (avatarUrl: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return await updateUser(session.user.id, { imageUrl: avatarUrl });
|
||||
};
|
||||
|
||||
export const removeAvatarAction = async (environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isUserAuthorized) {
|
||||
throw new Error("Not Authorized");
|
||||
}
|
||||
|
||||
try {
|
||||
const imageUrl = user.imageUrl;
|
||||
const imageUrl = ctx.user.imageUrl;
|
||||
if (!imageUrl) {
|
||||
throw new Error("Image not found");
|
||||
}
|
||||
@@ -109,12 +82,9 @@ export const removeAvatarAction = async (environmentId: string) => {
|
||||
throw new Error("Invalid filename");
|
||||
}
|
||||
|
||||
const deletionResult = await deleteFile(environmentId, "public", fileName);
|
||||
const deletionResult = await deleteFile(parsedInput.environmentId, "public", fileName);
|
||||
if (!deletionResult.success) {
|
||||
throw new Error("Deletion failed");
|
||||
}
|
||||
return await updateUser(session.user.id, { imageUrl: null });
|
||||
} catch (error) {
|
||||
throw new Error(`${"Deletion failed"}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
return await updateUser(ctx.user.id, { imageUrl: null });
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
@@ -43,14 +44,16 @@ export const DisableTwoFactorModal = ({ open, setOpen }: TDisableTwoFactorModalP
|
||||
const onSubmit: SubmitHandler<TDisableTwoFactorFormState> = async (data) => {
|
||||
const { code, password, backupCode } = data;
|
||||
|
||||
try {
|
||||
const { message } = await disableTwoFactorAuthAction({ code, password, backupCode });
|
||||
toast.success(message);
|
||||
const disableTwoFactorAuthResponse = await disableTwoFactorAuthAction({ code, password, backupCode });
|
||||
|
||||
if (disableTwoFactorAuthResponse?.data) {
|
||||
toast.success(disableTwoFactorAuthResponse.data.message);
|
||||
|
||||
router.refresh();
|
||||
resetState();
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(disableTwoFactorAuthResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
try {
|
||||
if (imageUrl) {
|
||||
// If avatar image already exists, then remove it before update action
|
||||
await removeAvatarAction(environmentId);
|
||||
await removeAvatarAction({ environmentId });
|
||||
}
|
||||
const { url, error } = await handleFileUpload(file, environmentId);
|
||||
|
||||
@@ -70,7 +70,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAvatarAction(url);
|
||||
await updateAvatarAction({ avatarUrl: url });
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast.error("Avatar update failed. Please try again.");
|
||||
@@ -84,7 +84,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await removeAvatarAction(environmentId);
|
||||
await removeAvatarAction({ environmentId });
|
||||
} catch (err) {
|
||||
toast.error("Avatar update failed. Please try again.");
|
||||
} finally {
|
||||
|
||||