Compare commits

...

36 Commits

Author SHA1 Message Date
Matti Nannt
ca5ea315d6 chore: determine formbricks version on release (#4985) 2025-03-18 11:49:12 +01:00
Piyush Gupta
646fe9c67f feat: optional cron jobs check (#4966) 2025-03-18 10:13:31 +00:00
StepSecurity Bot
6a123a2399 fix: Harden GitHub Actions (#4982)
Signed-off-by: StepSecurity Bot <bot@stepsecurity.io>
2025-03-18 11:23:10 +01:00
Piyush Jain
39aa9f0941 chore(infra-updates): updates and fixes (#4976) 2025-03-17 17:45:32 +00:00
Jakob Schott
625a4dcfae fix: changed 'Download example CSV'-link to a button (#4975) 2025-03-17 16:51:43 +00:00
Harsh Shrikant Bhat
7971681d02 docs: Remove duplicate titles for better SEO (#4962) 2025-03-17 09:50:17 -07:00
Johannes
3dea241d7a docs: tweak docs for sso (#4974) 2025-03-17 06:46:54 -07:00
Peter Pesti-Varga
e5ce6532f5 fix: Fix Android build setting (#4967) 2025-03-17 13:13:05 +01:00
victorvhs017
aa910ca3f0 fix: updated docker file with redis and minio containers (#4909) 2025-03-17 09:33:02 +00:00
Piyush Gupta
c2d237a99a fix: google sheet integration error message (#4899) 2025-03-16 16:10:51 +00:00
Piyush Jain
a371bdaedd chore(terraform): fix (#4963) 2025-03-15 13:32:05 +00:00
Piyush Jain
dbbd77a8eb chore(env): add new env variables (#4959) 2025-03-15 12:20:07 +00:00
Matti Nannt
c28de7c079 chore: prepare 3.4.0 release (#4950) 2025-03-13 20:38:32 +01:00
Matti Nannt
05f1068e01 chore: prepare 3.3.2 release (#4930) 2025-03-13 20:35:51 +01:00
Matti Nannt
7103ec9877 fix: survey preview stuck in sending (#4941) 2025-03-13 20:34:45 +01:00
Johannes
9cd7a25343 fix: fix except last (#4942) 2025-03-13 14:13:23 +00:00
IllimarR
2d028d18e5 feat: possibility to set mail from name (#4864)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2025-03-13 05:50:15 -07:00
Johannes
0164eca206 fix: survey display and card width (#4937) 2025-03-13 11:27:57 +00:00
Piyush Jain
f227c9e97e feat: introduce updated helm chart (#4896)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2025-03-13 10:30:17 +01:00
StepSecurity Bot
aecedfd082 fix: Apply security best practices (#4876)
Signed-off-by: StepSecurity Bot <bot@stepsecurity.io>
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2025-03-13 10:12:15 +01:00
Dhruwang Jariwala
e0f180bf04 feat: open telemetry for prometheus (#4922)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-03-12 20:16:08 +01:00
Anshuman Pandey
5d0c435a33 feat: @formbricks/react-native v2.1.0 release (#4927) 2025-03-12 18:21:14 +00:00
Dhruwang Jariwala
daa7e7b56a fix: Update tolgee.yml (#4928) 2025-03-12 14:04:10 +00:00
Matti Nannt
655f319083 chore: add bug label to new bug issues (#4929) 2025-03-12 14:39:53 +01:00
dependabot[bot]
fcfe5682da chore(deps): bump the npm_and_yarn group across 2 directories with 1 update (#4926)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 14:17:43 +01:00
Johannes
e1140ac436 fix: update instructions and docs link (#4921)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-03-12 13:00:52 +00:00
Anshuman Pandey
1529f5d478 fix: click outside is working even if the placement is not center (#4925) 2025-03-12 12:57:15 +00:00
Johannes
4870dc8d45 fix: update project config navbar + wording (#4918) 2025-03-12 12:40:01 +00:00
dependabot[bot]
a25e5dcfcd chore(deps-dev): bump esbuild from 0.25.0 to 0.25.1 in the npm_and_yarn group across 1 directory (#4911)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 13:51:57 +01:00
Anshuman Pandey
828e23b5c6 fix: survey autoclose on inactivity fix (#4916)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-03-12 11:52:15 +00:00
Dhruwang Jariwala
1921312445 fix: spacing tweaks (#4913) 2025-03-12 11:36:01 +00:00
Matti Nannt
0b9a884364 fix: smtp missing logs (#4917) 2025-03-11 11:13:22 +00:00
Dhruwang Jariwala
da4211f0b0 fix: Actions for contributors (#4905) 2025-03-10 16:04:17 +00:00
Dhruwang Jariwala
b21827cb32 fix: file input should not allow duplicate files (#4900) 2025-03-10 12:04:03 +00:00
Dhruwang Jariwala
4424a8a21d feat: pt-PT translations (#4874) 2025-03-10 09:22:26 +00:00
Dhruwang Jariwala
eb030f9ed6 fix: Address/Contact question accessibility (#4884) 2025-03-10 09:21:44 +00:00
194 changed files with 4753 additions and 2414 deletions

View File

@@ -39,6 +39,7 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
# See optional configurations below if you want to disable these features.
MAIL_FROM=noreply@example.com
MAIL_FROM_NAME=Formbricks
SMTP_HOST=localhost
SMTP_PORT=1025
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
@@ -96,6 +97,9 @@ PASSWORD_RESET_DISABLED=1
# Organization Invite. Disable the ability for invited users to create an account.
# INVITE_DISABLED=1
# Docker cron jobs. Disable the supercronic cron jobs in the Docker image (useful for cluster setups).
# DOCKER_CRON_ENABLED=1
##########
# Other #
##########
@@ -184,7 +188,7 @@ ENTERPRISE_LICENSE_KEY=
UNSPLASH_ACCESS_KEY=
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL=redis://localhost:6379
REDIS_URL=redis://localhost:6379
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
# REDIS_HTTP_URL:
@@ -202,4 +206,9 @@ UNKEY_ROOT_KEY=
# AI_AZURE_LLM_DEPLOYMENT_ID=
# NEXT_PUBLIC_INTERCOM_APP_ID=
# INTERCOM_SECRET_KEY=
# INTERCOM_SECRET_KEY=
# Enable Prometheus metrics
# PROMETHEUS_ENABLED=
# PROMETHEUS_EXPORTER_PORT=

View File

@@ -1,6 +1,7 @@
name: Bug report
description: "Found a bug? Please fill out the sections below. \U0001F44D"
type: bug
labels: ["bug"]
body:
- type: textarea
id: issue-summary

View File

@@ -5,6 +5,9 @@ on:
types:
- opened
permissions:
contents: read
jobs:
label_on_pr:
runs-on: ubuntu-latest
@@ -15,8 +18,13 @@ jobs:
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Apply labels from linked issue to PR
uses: actions/github-script@v5
uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -12,7 +12,12 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Build & Cache Web Binaries

View File

@@ -11,19 +11,24 @@ jobs:
name: Run Chromatic
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 20
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Run Chromatic
uses: chromaui/action@latest
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # latest
with:
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@@ -18,6 +18,11 @@ jobs:
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |

View File

@@ -7,6 +7,9 @@ on:
schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
- cron: "0 8 * * 1"
permissions:
contents: read
jobs:
cron-weeklySummary:
permissions:
@@ -16,6 +19,10 @@ jobs:
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
with:
egress-policy: audit
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |

27
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0

View File

@@ -43,16 +43,21 @@ jobs:
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v3
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
@@ -112,7 +117,7 @@ jobs:
- name: Azure login
if: env.AZURE_ENABLED == 'true'
uses: azure/login@v2
uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -130,14 +135,14 @@ jobs:
run: |
pnpm test:e2e
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
if: failure()
with:
name: app-logs

View File

@@ -4,6 +4,9 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
labeler:
name: Pull Request Labeler
@@ -12,7 +15,12 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481

View File

@@ -12,6 +12,11 @@ jobs:
timeout-minutes: 15
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: ./.github/actions/dangerous-git-checkout

View File

@@ -50,6 +50,10 @@ jobs:
checks: write
statuses: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
with:
egress-policy: audit
- name: fail if conditional jobs failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
run: exit 1

View File

@@ -1,62 +0,0 @@
name: Prepare release
run-name: Prepare release ${{ inputs.next_version }}
on:
workflow_dispatch:
inputs:
next_version:
required: true
type: string
description: "Version name"
permissions:
contents: write
pull-requests: write
jobs:
prepare_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: ./.github/actions/dangerous-git-checkout
- name: Configure git
run: |
git config --local user.email "github-actions@github.com"
git config --local user.name "GitHub Actions"
- name: Setup Node.js 20.x
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Bump version
run: |
cd apps/web
pnpm version ${{ inputs.next_version }} --no-workspaces-update
- name: Commit changes and create a branch
run: |
branch_name="release-v${{ inputs.next_version }}"
git checkout -b "$branch_name"
git add .
git commit -m "chore: release v${{ inputs.next_version }}"
git push origin "$branch_name"
- name: Create pull request
run: |
gh pr create \
--base main \
--head "release-v${{ inputs.next_version }}" \
--title "chore: bump version to v${{ inputs.next_version }}" \
--body "This PR contains the changes for the v${{ inputs.next_version }} release."
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -26,23 +26,28 @@ jobs:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout Repo
uses: actions/checkout@v2
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
with:
node-version: 18.x
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
- name: Install Dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish
publish: pnpm release

View File

@@ -17,6 +17,9 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -28,23 +31,28 @@ jobs:
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Set up Depot CLI
uses: depot/setup-action@v1
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3 # v3.0.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -54,7 +62,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5 # v5.0.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -62,7 +70,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: depot/build-push-action@v1
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
project: tw0fqmsx3c
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}

View File

@@ -20,6 +20,9 @@ env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -31,23 +34,40 @@ jobs:
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Get Release Tag
id: extract_release_tag
run: |
TAG=${{ github.ref }}
TAG=${TAG#refs/tags/v}
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
- name: Update package.json version
run: |
sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json
cat ./apps/web/package.json | grep version
- name: Set up Depot CLI
uses: depot/setup-action@v1
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3 # v3.0.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -57,7 +77,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5 # v5.0.0
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -65,7 +85,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: depot/build-push-action@v1
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
with:
project: tw0fqmsx3c
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}

View File

@@ -5,6 +5,9 @@ on:
tags:
- "v*"
permissions:
contents: read
jobs:
release-image-on-dockerhub:
name: Release on Dockerhub
@@ -16,17 +19,13 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
egress-policy: audit
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Checkout Repo
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Get Release Tag
id: extract_release_tag
@@ -35,8 +34,22 @@ jobs:
TAG=${TAG#refs/tags/v}
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
- name: Update package.json version
run: |
sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json
cat ./apps/web/package.json | grep version
- name: Log in to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1
with:
context: .
file: ./apps/web/Dockerfile

View File

@@ -0,0 +1,51 @@
name: Publish Helm Chart
on:
release:
types:
- published
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Extract release version
run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
- name: Set up Helm
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5
with:
version: latest
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin
- name: Install YQ
uses: dcarbone/install-yq-action@4075b4dca348d74bd83f2bf82d30f25d7c54539b # v1.3.1
- name: Update Chart.yaml with new version
run: |
yq -i ".version = \"${VERSION#v}\"" helm-chart/Chart.yaml
yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml
- name: Package Helm chart
run: |
helm package ./helm-chart
- name: Push Helm chart to GitHub Container Registry
run: |
helm push formbricks-${VERSION#v}.tgz oci://ghcr.io/formbricks/helm-charts

View File

@@ -34,6 +34,11 @@ jobs:
# actions: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
@@ -71,6 +76,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
sarif_file: results.sarif

View File

@@ -16,7 +16,12 @@ jobs:
name: PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -35,7 +40,7 @@ jobs:
revert
ossgg
- uses: marocchino/sticky-pull-request-comment@v2
- uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
@@ -54,7 +59,7 @@ jobs:
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
with:
header: pr-title-lint-error
message: |

View File

@@ -14,7 +14,12 @@ jobs:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

View File

@@ -0,0 +1,74 @@
name: 'Terraform'
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
id-token: write
contents: write
jobs:
terraform:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
aws-region: "eu-central-1"
- name: Setup Terraform
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
- name: Terraform Format
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
working-directory: infra/terraform
- name: Terraform Init
id: init
run: terraform init
working-directory: infra/terraform
- name: Terraform Validate
id: validate
run: terraform validate
working-directory: infra/terraform
- name: Terraform Plan
id: plan
run: terraform plan -out .planfile
working-directory: infra/terraform
- name: Post PR comment
uses: borchero/terraform-plan-comment@3399d8dbae8b05185e815e02361ede2949cd99c4 # v2.4.0
if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
with:
token: ${{ github.token }}
planfile: .planfile
working-directory: "infra/terraform"
skip-comment: true
- name: Terraform Apply
id: apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply .planfile
working-directory: "infra/terraform"

View File

@@ -1,6 +1,9 @@
name: Tests
on:
workflow_call:
permissions:
contents: read
jobs:
build:
name: Unit Tests
@@ -10,16 +13,21 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v3
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64

View File

@@ -5,18 +5,30 @@ permissions:
on:
workflow_dispatch:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
check-missing-translations:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.pull_request.base.ref }}
- name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 18

View File

@@ -3,7 +3,7 @@ permissions:
contents: read
on:
pull_request:
pull_request_target:
types: [closed]
branches:
- main
@@ -15,30 +15,33 @@ jobs:
if: github.event.pull_request.merged == true
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # This ensures we get the full git history
- name: Get source branch name
id: branch-name
run: |
# For PR merges, use the head ref from the pull request event
SOURCE_BRANCH="${{ github.head_ref }}"
RAW_BRANCH="${{ github.head_ref }}"
SOURCE_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9._\/-]//g')
# Only remove username prefix if needed
if [[ "$SOURCE_BRANCH" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]+/ ]]; then
PREFIX=${SOURCE_BRANCH%%/*}
if [[ ! "$PREFIX" =~ ^(feature|fix|bugfix|hotfix|release|chore|docs|test|refactor|style|perf|build|ci|revert)$ ]]; then
SOURCE_BRANCH=${SOURCE_BRANCH#*/}
fi
fi
echo "SOURCE_BRANCH=$SOURCE_BRANCH" >> $GITHUB_ENV
# Safely add to environment variables using GitHub's recommended method
# This prevents environment variable injection attacks
echo "SOURCE_BRANCH<<EOF" >> $GITHUB_ENV
echo "$SOURCE_BRANCH" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "Detected source branch: $SOURCE_BRANCH"
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: 18 # Ensure compatibility with your project
@@ -77,7 +80,7 @@ jobs:
--yes
- name: Upload backup as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: tolgee-backup-${{ github.sha }}
path: ./tolgee-backup

View File

@@ -17,7 +17,12 @@ jobs:
timeout-minutes: 10
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/first-interaction@3c71ce730280171fd1cfb57c00c774f8998586f7 # v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-

20
.gitignore vendored
View File

@@ -53,4 +53,22 @@ yarn-error.log*
packages/lib/uploads
apps/web/public/js
packages/database/migrations
branch.json
branch.json
.vercel
# Terraform
infra/terraform/.terraform/
**/.terraform.lock.hcl
**/terraform.tfstate
**/terraform.tfstate.*
**/crash.log
**/override.tf
**/override.tf.json
**/*.tfvars
**/*.tfvars.json
**/.terraformrc
**/terraform.rc
# IntelliJ IDEA
/.idea/
/*.iml

View File

@@ -1,6 +1,6 @@
#!/bin/bash
images=($(yq eval '.services.*.image' packages/database/docker-compose.yml))
images=($(yq eval '.services.*.image' docker-compose.dev.yml))
pull_image() {
docker pull "$1"

View File

@@ -27,6 +27,10 @@
{
"language": "zh-Hant-TW",
"path": "./packages/lib/messages/zh-Hant-TW.json"
},
{
"language": "pt-PT",
"path": "./packages/lib/messages/pt-PT.json"
}
],
"forceMode": "OVERRIDE"

View File

@@ -30,7 +30,7 @@
"@typescript-eslint/eslint-plugin": "8.18.0",
"@typescript-eslint/parser": "8.18.0",
"@vitejs/plugin-react": "4.3.4",
"esbuild": "0.25.0",
"esbuild": "0.25.1",
"eslint-plugin-storybook": "0.11.1",
"prop-types": "15.8.1",
"storybook": "8.4.7",

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine3.20 AS base
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
#
## step 1: Prune monorepo
@@ -111,7 +111,12 @@ VOLUME /home/nextjs/apps/web/uploads/
RUN mkdir -p /home/nextjs/apps/web/saml-connection
VOLUME /home/nextjs/apps/web/saml-connection
CMD supercronic -quiet /app/docker/cronjobs & \
CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
echo "Starting cron jobs..."; \
supercronic -quiet /app/docker/cronjobs & \
else \
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \
fi; \
(cd packages/database && npm run db:migrate:deploy) && \
(cd packages/database && npm run db:create-saml-database:deploy) && \
exec node apps/web/server.js

View File

@@ -231,6 +231,7 @@ export const ProjectSettings = ({
<p className="text-sm text-slate-400">{t("common.preview")}</p>
<div className="z-0 h-3/4 w-3/4">
<SurveyInline
isPreviewMode={true}
survey={previewSurvey(projectName || "my Product", t)}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}

View File

@@ -1,25 +1,41 @@
"use server";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
export async function getSpreadsheetNameByIdAction(
googleSheetIntegration: TIntegrationGoogleSheets,
environmentId: string,
spreadsheetId: string
) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const ZGetSpreadsheetNameByIdAction = z.object({
googleSheetIntegration: ZIntegrationGoogleSheets,
environmentId: z.string(),
spreadsheetId: z.string(),
});
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const integrationData = structuredClone(googleSheetIntegration);
integrationData.config.data.forEach((data) => {
data.createdAt = new Date(data.createdAt);
export const getSpreadsheetNameByIdAction = authenticatedActionClient
.schema(ZGetSpreadsheetNameByIdAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
minPermission: "readWrite",
},
],
});
const integrationData = structuredClone(parsedInput.googleSheetIntegration);
integrationData.config.data.forEach((data) => {
data.createdAt = new Date(data.createdAt);
});
return await getSpreadsheetNameById(integrationData, parsedInput.spreadsheetId);
});
return await getSpreadsheetNameById(integrationData, spreadsheetId);
}

View File

@@ -8,6 +8,7 @@ import {
isValidGoogleSheetsUrl,
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
@@ -115,11 +116,18 @@ export const AddIntegrationModal = ({
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
}
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
const spreadsheetName = await getSpreadsheetNameByIdAction(
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
googleSheetIntegration,
environmentId,
spreadsheetId
);
spreadsheetId,
});
if (!spreadsheetNameResponse?.data) {
const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse);
throw new Error(errorMessage);
}
const spreadsheetName = spreadsheetNameResponse.data;
setIsLinkingSheet(true);
integrationData.spreadsheetId = spreadsheetId;

View File

@@ -27,7 +27,7 @@ export const EditAlerts = ({
return (
<>
{memberships.map((membership) => (
<>
<div key={membership.organization.id}>
<div className="mb-5 grid grid-cols-6 items-center space-x-3">
<div className="col-span-3 flex items-center space-x-3">
<UsersIcon className="h-6 w-7 text-slate-600" />
@@ -110,7 +110,7 @@ export const EditAlerts = ({
</Link>
</p>
</div>
</>
</div>
))}
</>
);

View File

@@ -18,7 +18,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
return (
<>
{memberships.map((membership) => (
<>
<div key={membership.organization.id}>
<div className="mb-5 flex items-center space-x-3 text-sm font-medium">
<UsersIcon className="h-6 w-7 text-slate-600" />
@@ -52,7 +52,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
</Link>
</p>
</div>
</>
</div>
))}
</>
);

View File

@@ -9,8 +9,10 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import {
getOrganizationByEnvironmentId,
getOrganizationsWhereUserIsSingleOwner,
} from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { SettingsCard } from "../../components/SettingsCard";
import { DeleteAccount } from "./components/DeleteAccount";
@@ -71,7 +73,9 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
description={t("environments.settings.profile.two_factor_authentication_description")}
buttons={[
{
text: t("common.start_free_trial"),
text: IS_FORMBRICKS_CLOUD
? t("common.start_free_trial")
: t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${params.environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -1,11 +1,12 @@
"use client";
import { MobileAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab";
import { WebAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
import { useState } from "react";
export const AppTab = ({ environmentId }) => {
export const AppTab = () => {
const { t } = useTranslate();
const [selectedTab, setSelectedTab] = useState("webapp");
@@ -20,79 +21,7 @@ export const AppTab = ({ environmentId }) => {
handleOptionChange={(value) => setSelectedTab(value)}
/>
<div className="mt-4">
{selectedTab === "webapp" ? <WebAppTab environmentId={environmentId} /> : <MobileAppTab />}
</div>
</div>
);
};
const MobileAppTab = () => {
const { t } = useTranslate();
return (
<div>
<p className="text-lg font-semibold text-slate-800">
{t("environments.surveys.summary.how_to_embed_a_survey_on_your_react_native_app")}
</p>
<ol className="mt-4 list-decimal space-y-2 pl-5 text-sm text-slate-700">
<li>
{t("common.follow_these")}{" "}
<Link
href="https://formbricks.com/docs/developer-docs/react-native-in-app-surveys"
target="_blank"
className="decoration-brand-dark font-medium underline underline-offset-2">
{t("environments.surveys.summary.setup_instructions_for_react_native_apps")}
</Link>{" "}
{t("environments.surveys.summary.to_connect_your_app_with_formbricks")}
</li>
</ol>
<div className="mt-2 text-sm italic text-slate-700">
{t("environments.surveys.summary.were_working_on_sdks_for_flutter_swift_and_kotlin")}
</div>
</div>
);
};
const WebAppTab = ({ environmentId }) => {
const { t } = useTranslate();
return (
<div>
<p className="text-lg font-semibold text-slate-800">
{t("environments.surveys.summary.how_to_embed_a_survey_on_your_web_app")}
</p>
<ol className="mt-4 list-decimal space-y-2 pl-5 text-sm text-slate-700">
<li>
{t("common.follow_these")}{" "}
<Link
href={`/environments/${environmentId}/project/app-connection`}
target="_blank"
className="decoration-brand-dark font-medium underline underline-offset-2">
{t("environments.surveys.summary.setup_instructions")}
</Link>{" "}
{t("environments.surveys.summary.to_connect_your_web_app_with_formbricks")}
</li>
<li>
{t("environments.surveys.summary.learn_how_to")}{" "}
<Link
href="https://formbricks.com/docs/app-surveys/user-identification"
target="_blank"
className="decoration-brand-dark font-medium underline underline-offset-2">
{t("environments.surveys.summary.identify_users_and_set_attributes")}
</Link>{" "}
{t("environments.surveys.summary.to_run_highly_targeted_surveys")}.
</li>
<li>
{t("environments.surveys.summary.make_sure_the_survey_type_is_set_to")}{" "}
<b>{t("common.app_survey")}</b>
</li>
<li>{t("environments.surveys.summary.define_when_and_where_the_survey_should_pop_up")}</li>
</ol>
<div className="mt-4">
<video autoPlay loop muted className="w-full rounded-xl border border-slate-200">
<source src="/video/tooltips/change-survey-type-app.mp4" type="video/mp4" />
{t("environments.surveys.summary.unsupported_video_tag_warning")}
</video>
</div>
<div className="mt-4">{selectedTab === "webapp" ? <WebAppTab /> : <MobileAppTab />}</div>
</div>
);
};

View File

@@ -88,7 +88,7 @@ export const EmbedView = ({
locale={locale}
/>
) : activeId === "app" ? (
<AppTab environmentId={environmentId} />
<AppTab />
) : null}
<div className="mt-2 rounded-md p-3 text-center lg:hidden">
{tabs.slice(0, 2).map((tab) => (

View File

@@ -0,0 +1,25 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
export const MobileAppTab = () => {
const { t } = useTranslate();
return (
<Alert>
<AlertTitle>{t("environments.surveys.summary.quickstart_mobile_apps")}</AlertTitle>
<AlertDescription>
{t("environments.surveys.summary.quickstart_mobile_apps_description")}
<Button asChild className="w-fit" size="sm" variant="link">
<Link
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides"
target="_blank">
{t("common.learn_more")}
</Link>
</Button>
</AlertDescription>
</Alert>
);
};

View File

@@ -85,8 +85,10 @@ export const PanelInfoView = ({ disableBack, handleInitialPageButton }: PanelInf
</p>
</div>
<Button className="justify-center" asChild>
<Link href="https://formbricks.com/docs/link-surveys/market-research-panel" target="_blank">
{t("common.get_started")}
<Link
href="https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/market-research-panel"
target="_blank">
{t("common.learn_more")}
</Link>
</Button>
</div>

View File

@@ -0,0 +1,25 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
export const WebAppTab = () => {
const { t } = useTranslate();
return (
<Alert>
<AlertTitle>{t("environments.surveys.summary.quickstart_web_apps")}</AlertTitle>
<AlertDescription>
{t("environments.surveys.summary.quickstart_web_apps_description")}
<Button asChild className="w-fit" size="sm" variant="link">
<Link
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/quickstart"
target="_blank">
{t("common.learn_more")}
</Link>
</Button>
</AlertDescription>
</Alert>
);
};

View File

@@ -11,7 +11,7 @@ const createTimeoutPromise = (ms, rejectReason) => {
CacheHandler.onCreation(async () => {
let client;
if (process.env.REDIS_URL && process.env.ENTERPRISE_LICENSE_KEY) {
if (process.env.REDIS_URL) {
try {
// Create a Redis client.
client = createClient({
@@ -45,8 +45,6 @@ CacheHandler.onCreation(async () => {
});
}
}
} else if (process.env.REDIS_URL) {
console.log("Redis clustering requires an Enterprise License. Falling back to LRU cache.");
}
/** @type {import("@neshca/cache-handler").Handler | null} */

View File

@@ -0,0 +1,58 @@
// instrumentation-node.ts
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { HostMetrics } from "@opentelemetry/host-metrics";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { RuntimeNodeInstrumentation } from "@opentelemetry/instrumentation-runtime-node";
import {
Resource,
detectResourcesSync,
envDetector,
hostDetector,
processDetector,
} from "@opentelemetry/resources";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { env } from "@formbricks/lib/env";
const exporter = new PrometheusExporter({
port: env.PROMETHEUS_EXPORTER_PORT ? parseInt(env.PROMETHEUS_EXPORTER_PORT) : 9464,
endpoint: "/metrics",
host: "0.0.0.0", // Listen on all network interfaces
});
const detectedResources = detectResourcesSync({
detectors: [envDetector, processDetector, hostDetector],
});
const customResources = new Resource({});
const resources = detectedResources.merge(customResources);
const meterProvider = new MeterProvider({
readers: [exporter],
resource: resources,
});
const hostMetrics = new HostMetrics({
name: `otel-metrics`,
meterProvider,
});
registerInstrumentations({
meterProvider,
instrumentations: [new HttpInstrumentation(), new RuntimeNodeInstrumentation()],
});
hostMetrics.start();
process.on("SIGTERM", async () => {
try {
// Stop collecting metrics or flush them if needed
await meterProvider.shutdown();
// Possibly close other instrumentation resources
} catch (e) {
console.error("Error during graceful shutdown:", e);
} finally {
process.exit(0);
}
});

View File

@@ -1,25 +1,8 @@
import { registerOTel } from "@vercel/otel";
import { LangfuseExporter } from "langfuse-vercel";
import { env } from "@formbricks/lib/env";
export async function register() {
if (env.LANGFUSE_SECRET_KEY && env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_BASEURL) {
registerOTel({
serviceName: "formbricks-cloud-dev",
traceExporter: new LangfuseExporter({
debug: false,
secretKey: env.LANGFUSE_SECRET_KEY,
publicKey: env.LANGFUSE_PUBLIC_KEY,
baseUrl: env.LANGFUSE_BASEURL,
}),
});
// instrumentation.ts
export const register = async () => {
if (process.env.NEXT_RUNTIME === "nodejs" && env.PROMETHEUS_ENABLED) {
await import("./instrumentation-node");
}
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
};

View File

View File

@@ -360,13 +360,11 @@ export const UploadContactsCSVButton = ({
)}
</div>
{!csvResponse.length && (
<p>
<a
onClick={handleDownloadExampleCSV}
className="cursor-pointer text-right text-sm text-slate-500">
{t("environments.contacts.upload_contacts_modal_download_example_csv")}{" "}
</a>
</p>
<div className="flex justify-start">
<Button onClick={handleDownloadExampleCSV} variant="secondary">
{t("environments.contacts.upload_contacts_modal_download_example_csv")}
</Button>
</div>
)}
</div>

View File

@@ -95,7 +95,7 @@ export const ContactsPage = async ({
description={t("environments.contacts.unlock_contacts_description")}
buttons={[
{
text: t("common.start_free_trial"),
text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${params.environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -100,7 +100,7 @@ export const SegmentsPage = async ({
description={t("environments.segments.unlock_segments_description")}
buttons={[
{
text: t("common.start_free_trial"),
text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${params.environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -11,7 +11,7 @@ export const LanguagesLoading = () => {
const { t } = useTranslate();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="languages" loading />
</PageHeader>
<SettingsCard

View File

@@ -1,9 +1,6 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
import { EditLanguage } from "@/modules/ee/multi-language-surveys/components/edit-language";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
@@ -12,7 +9,7 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganization } from "@formbricks/lib/organization/service";
@@ -35,11 +32,6 @@ export const LanguagesPage = async (props: { params: Promise<{ environmentId: st
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
if (!isMultiLanguageAllowed) {
notFound();
}
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const session = await getServerSession(authOptions);
@@ -63,18 +55,20 @@ export const LanguagesPage = async (props: { params: Promise<{ environmentId: st
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="languages"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="languages" />
</PageHeader>
<SettingsCard
title={t("environments.project.languages.multi_language_surveys")}
description={t("environments.project.languages.multi_language_surveys_description")}>
<EditLanguage project={project} locale={user.locale} isReadOnly={isReadOnly} />
<EditLanguage
project={project}
locale={user.locale}
isReadOnly={isReadOnly}
isMultiLanguageAllowed={isMultiLanguageAllowed}
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
/>
</SettingsCard>
</PageContentWrapper>
);

View File

@@ -4,9 +4,9 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
import { ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { Language } from "@prisma/client";
import { useTranslate } from "@tolgee/react";
import { TFnType } from "@tolgee/react";
import { TFnType, useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
@@ -26,6 +26,9 @@ interface EditLanguageProps {
project: TProject;
locale: TUserLocale;
isReadOnly: boolean;
isMultiLanguageAllowed: boolean;
environmentId: string;
isFormbricksCloud: boolean;
}
const checkIfDuplicateExists = (arr: string[]) => {
@@ -57,7 +60,7 @@ const validateLanguages = (languages: Language[], t: TFnType) => {
return false;
}
// Check if the chosen alias matches an ISO identifier of a language that hasnt been added
// Check if the chosen alias matches an ISO identifier of a language that hasn't been added
for (const alias of languageAliases) {
if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) {
toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), {
@@ -70,7 +73,14 @@ const validateLanguages = (languages: Language[], t: TFnType) => {
return true;
};
export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps) {
export function EditLanguage({
project,
locale,
isReadOnly,
isMultiLanguageAllowed,
environmentId,
isFormbricksCloud,
}: EditLanguageProps) {
const { t } = useTranslate();
const [languages, setLanguages] = useState<Language[]>(project.languages);
const [isEditing, setIsEditing] = useState(false);
@@ -150,6 +160,21 @@ export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps)
setIsEditing(false);
};
const buttons: [ModalButton, ModalButton] = [
{
text: isFormbricksCloud ? t("common.start_free_trial") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
];
const handleSaveChanges = async () => {
if (!validateLanguages(languages, t)) return;
await Promise.all(
@@ -179,63 +204,75 @@ export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps)
) : null;
return (
<div className="flex flex-col space-y-4">
<div className="space-y-4">
{languages.length > 0 ? (
<>
<LanguageLabels />
{languages.map((language, index) => (
<LanguageRow
index={index}
isEditing={isEditing}
key={language.id}
language={language}
locale={locale}
onDelete={() => handleDeleteLanguage(language.id)}
onLanguageChange={(newLanguage: Language) => {
const updatedLanguages = [...languages];
updatedLanguages[index] = newLanguage;
setLanguages(updatedLanguages);
}}
/>
))}
</>
) : (
<p className="text-sm italic text-slate-500">
{t("environments.project.languages.no_language_found")}
</p>
)}
<AddLanguageButton onClick={handleAddLanguage} />
</div>
<EditSaveButtons
isEditing={isEditing}
onCancel={handleCancelChanges}
disabled={isReadOnly}
onEdit={() => {
setIsEditing(true);
}}
onSave={handleSaveChanges}
t={t}
/>
{isReadOnly && (
<Alert variant="warning" className="mt-4">
<AlertDescription>
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
</AlertDescription>
</Alert>
<>
{isMultiLanguageAllowed ? (
<div className="flex flex-col space-y-4">
<div className="space-y-4">
{languages.length > 0 ? (
<>
<LanguageLabels />
{languages.map((language, index) => (
<LanguageRow
index={index}
isEditing={isEditing}
key={language.id}
language={language}
locale={locale}
onDelete={() => handleDeleteLanguage(language.id)}
onLanguageChange={(newLanguage: Language) => {
const updatedLanguages = [...languages];
updatedLanguages[index] = newLanguage;
setLanguages(updatedLanguages);
}}
/>
))}
</>
) : (
<p className="text-sm italic text-slate-500">
{t("environments.project.languages.no_language_found")}
</p>
)}
<AddLanguageButton onClick={handleAddLanguage} />
</div>
<EditSaveButtons
isEditing={isEditing}
onCancel={handleCancelChanges}
disabled={isReadOnly}
onEdit={() => {
setIsEditing(true);
}}
onSave={handleSaveChanges}
t={t}
/>
{isReadOnly && (
<Alert variant="warning" className="mt-4">
<AlertDescription>
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
</AlertDescription>
</Alert>
)}
<ConfirmationModal
buttonText={t("environments.project.languages.remove_language")}
isButtonDisabled={confirmationModal.isButtonDisabled}
onConfirm={() => performLanguageDeletion(confirmationModal.languageId)}
open={confirmationModal.isOpen}
setOpen={() => {
setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen }));
}}
text={confirmationModal.text}
title={t("environments.project.languages.remove_language")}
/>
</div>
) : (
<UpgradePrompt
title={t("environments.settings.general.use_multi_language_surveys_with_a_higher_plan")}
description={t(
"environments.settings.general.use_multi_language_surveys_with_a_higher_plan_description"
)}
buttons={buttons}
/>
)}
<ConfirmationModal
buttonText={t("environments.project.languages.remove_language")}
isButtonDisabled={confirmationModal.isButtonDisabled}
onConfirm={() => performLanguageDeletion(confirmationModal.languageId)}
open={confirmationModal.isOpen}
setOpen={() => {
setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen }));
}}
text={confirmationModal.text}
title={t("environments.project.languages.remove_language")}
/>
</div>
</>
);
}

View File

@@ -230,7 +230,9 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
description={t("environments.surveys.edit.upgrade_notice_description")}
buttons={[
{
text: t("common.start_free_trial"),
text: isFormbricksCloud
? t("common.start_free_trial")
: t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request",

View File

@@ -0,0 +1,36 @@
"use client";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { useTranslate } from "@tolgee/react";
export const TeamsLoading = () => {
const { t } = useTranslate();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="teams" loading />
</PageHeader>
<div className="p-4">
<div className="mb-4">
<div className="h-6 w-1/3 animate-pulse rounded bg-slate-200" />
</div>
<div className="space-y-4">
{[...Array(3)].map((_, idx) => (
<div
key={idx}
className="flex animate-pulse items-center space-x-4 rounded border border-slate-200 p-4">
<div className="h-10 w-10 rounded-full bg-slate-300" />
<div className="flex-1 space-y-2">
<div className="h-4 w-3/4 rounded bg-slate-200" />
<div className="h-4 w-1/2 rounded bg-slate-200" />
</div>
</div>
))}
</div>
</div>
</PageContentWrapper>
);
};

View File

@@ -1,8 +1,4 @@
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { AccessView } from "@/modules/ee/teams/project-teams/components/access-view";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -37,9 +33,6 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const teams = await getTeamsByProjectId(project.id);
if (!teams) {
@@ -50,13 +43,8 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="teams"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="teams" />
</PageHeader>
<AccessView environmentId={params.environmentId} teams={teams} isOwnerOrManager={isOwnerOrManager} />
</PageContentWrapper>

View File

@@ -37,7 +37,7 @@ export const TeamsView = async ({
const buttons: [ModalButton, ModalButton] = [
{
text: t("common.start_free_trial"),
text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request",

View File

@@ -162,7 +162,7 @@ export const EmailCustomizationSettings = ({
const buttons: [ModalButton, ModalButton] = [
{
text: t("common.start_free_trial"),
text: isFormbricksCloud ? t("common.start_free_trial") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -23,7 +23,7 @@ export const BrandingSettingsCard = async ({
const buttons: [ModalButton, ModalButton] = [
{
text: t("common.start_free_trial"),
text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -6,6 +6,7 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport";
import {
DEBUG,
MAIL_FROM,
MAIL_FROM_NAME,
SMTP_AUTHENTICATED,
SMTP_HOST,
SMTP_PASSWORD,
@@ -69,12 +70,13 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
} as SMTPTransport.Options);
const emailDefaults = {
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
return true;
} catch (error) {
console.error("Error in sendEmail:", error);
throw new InvalidInputError("Incorrect SMTP credentials");
}
};

View File

@@ -35,7 +35,7 @@ export const AppConnectionLoading = () => {
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="app-connection" loading />
</PageHeader>
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>

View File

@@ -1,9 +1,5 @@
import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { EnvironmentIdField } from "@/modules/projects/settings/(setup)/components/environment-id-field";
import { SetupInstructions } from "@/modules/projects/settings/(setup)/components/setup-instructions";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
@@ -31,18 +27,10 @@ export const AppConnectionPage = async (props) => {
throw new Error(t("common.organization_not_found"));
}
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="app-connection"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="app-connection" />
</PageHeader>
<div className="space-y-4">
<EnvironmentNotice environmentId={params.environmentId} subPageUrl="/project/app-connection" />

View File

@@ -42,7 +42,7 @@ export const APIKeysLoading = () => {
const { t } = useTranslate();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="api-keys" loading />
</PageHeader>
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>

View File

@@ -1,9 +1,5 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
@@ -53,18 +49,10 @@ export const APIKeysPage = async (props) => {
const isReadOnly = isMember && !hasManageAccess;
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="api-keys"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="api-keys" />
</PageHeader>
<EnvironmentNotice environmentId={environment.id} subPageUrl="/project/api-keys" />
{environment.type === "development" ? (

View File

@@ -8,17 +8,13 @@ import { usePathname } from "next/navigation";
interface ProjectConfigNavigationProps {
activeId: string;
environmentId?: string;
isMultiLanguageAllowed?: boolean;
loading?: boolean;
canDoRoleManagement?: boolean;
}
export const ProjectConfigNavigation = ({
activeId,
environmentId,
isMultiLanguageAllowed,
loading,
canDoRoleManagement,
}: ProjectConfigNavigationProps) => {
const { t } = useTranslate();
const pathname = usePathname();
@@ -43,7 +39,6 @@ export const ProjectConfigNavigation = ({
label: t("common.survey_languages"),
icon: <LanguagesIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/project/languages`,
hidden: !isMultiLanguageAllowed,
current: pathname?.includes("/languages"),
},
{
@@ -70,8 +65,8 @@ export const ProjectConfigNavigation = ({
{
id: "teams",
label: t("common.team_access"),
icon: <UsersIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/project/teams`,
hidden: !canDoRoleManagement,
current: pathname?.includes("/teams"),
},
];

View File

@@ -28,7 +28,7 @@ export const GeneralSettingsLoading = () => {
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="general" loading />
</PageHeader>
{cards.map((card, index) => (

View File

@@ -1,9 +1,5 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
@@ -51,21 +47,12 @@ export const GeneralSettingsPage = async (props: { params: Promise<{ environment
const isReadOnly = isMember && !hasManageAccess;
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
const isOwnerOrManager = isOwner || isManager;
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.project_configuration")}>
{/* </PageHeader><PageHeader pageTitle={t("common.configuration")}> */}
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="general"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="general" />
</PageHeader>
<SettingsCard
title={t("common.project_name")}

View File

@@ -9,7 +9,7 @@ import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/ser
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
export const metadata: Metadata = {
title: "Config",
title: "Configuration",
};
export const ProjectSettingsLayout = async (props) => {

View File

@@ -24,7 +24,7 @@ export const ProjectLookSettingsLoading = () => {
const { t } = useTranslate();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="look" loading />
</PageHeader>
<SettingsCard

View File

@@ -1,10 +1,6 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
getWhiteLabelPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { BrandingSettingsCard } from "@/modules/ee/whitelabel/remove-branding/components/branding-settings-card";
@@ -51,18 +47,10 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
const isReadOnly = isMember && !hasManageAccess;
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="look"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="look" />
</PageHeader>
<SettingsCard
title={t("environments.project.look.theme")}

View File

@@ -10,7 +10,7 @@ export const TagsLoading = () => {
const { t } = useTranslate();
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation activeId="tags" />
</PageHeader>
<SettingsCard

View File

@@ -1,9 +1,5 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
getMultiLanguagePermission,
getRoleManagementPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
@@ -59,18 +55,10 @@ export const TagsPage = async (props) => {
const isReadOnly = isMember && !hasManageAccess;
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan);
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.configuration")}>
<ProjectConfigNavigation
environmentId={params.environmentId}
activeId="tags"
isMultiLanguageAllowed={isMultiLanguageAllowed}
canDoRoleManagement={canDoRoleManagement}
/>
<PageHeader pageTitle={t("common.project_configuration")}>
<ProjectConfigNavigation environmentId={params.environmentId} activeId="tags" />
</PageHeader>
<SettingsCard
title={t("environments.project.tags.manage_tags")}

View File

@@ -243,7 +243,6 @@ export const SurveyEditor = ({
environment={environment}
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
languageCode={selectedLanguageCode}
onFileUpload={async (file) => file.name}
/>
</aside>
</div>

View File

@@ -43,7 +43,7 @@ export const TargetingLockedCard = ({ isFormbricksCloud, environmentId }: Target
description={t("environments.surveys.edit.unlock_targeting_description")}
buttons={[
{
text: t("common.start_free_trial"),
text: isFormbricksCloud ? t("common.start_free_trial") : t("common.request_trial_license"),
href: isFormbricksCloud
? `/environments/${environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -21,7 +21,7 @@ export const LegalFooter = ({
return (
<div className="absolute bottom-0 z-[1500] h-10 w-full" role="contentinfo">
<div className="mx-auto flex h-full max-w-lg items-center justify-center p-2 text-center text-xs text-slate-500">
<div className="mx-auto flex h-full max-w-2xl items-center justify-center p-2 text-center text-xs text-slate-500">
{IMPRINT_URL && (
<Link href={IMPRINT_URL} target="_blank" className="hover:underline" tabIndex={-1}>
{t("common.imprint")}

View File

@@ -80,7 +80,7 @@ export const LinkSurveyWrapper = ({
onBackgroundLoaded={handleBackgroundLoaded}>
<div className="flex max-h-dvh min-h-dvh items-center justify-center overflow-clip">
{!styling.isLogoHidden && project.logo?.url && <ClientLogo projectLogo={project.logo} />}
<div className="h-full w-full max-w-lg space-y-6 px-1.5">
<div className="h-full w-full max-w-4xl space-y-6 px-1.5">
{isPreview && (
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
<div />

View File

@@ -170,14 +170,14 @@ export const LinkSurvey = ({
PRIVACY_URL={PRIVACY_URL}
isBrandingEnabled={project.linkSurveyBranding}>
<SurveyInline
apiHost={!isPreview ? webAppUrl : undefined}
environmentId={!isPreview ? survey.environmentId : undefined}
apiHost={webAppUrl}
environmentId={survey.environmentId}
isPreviewMode={isPreview}
survey={survey}
styling={determineStyling()}
languageCode={languageCode}
isBrandingEnabled={project.linkSurveyBranding}
shouldResetQuestionId={false}
onFileUpload={isPreview ? async (file) => `https://formbricks.com/${file.name}` : undefined}
// eslint-disable-next-line jsx-a11y/no-autofocus -- need it as focus behaviour is different in normal surveys and survey preview
autoFocus={autoFocus}
prefillResponseData={prefillValue}

View File

@@ -84,7 +84,6 @@ export const TemplateContainerWithPreview = ({
project={project}
environment={environment}
languageCode={"default"}
onFileUpload={async (file) => file.name}
/>
)}
</aside>

View File

@@ -8,8 +8,8 @@ export const SlackIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
fill="none"
stroke="currentColor"
strokeWidth="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
{...props}>
<rect width="3" height="8" x="13" y="2" rx="1.5" />
<path d="M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5" />

View File

@@ -166,7 +166,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
return (
<div
ref={ContentRef}
className={`relative h-[90%] max-h-[40rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
className={`relative h-[90%] max-h-[42rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
{/* below element is use to create notch for the mobile device mockup */}
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
{surveyType === "link" && renderBackground()}

View File

@@ -9,9 +9,7 @@ import { useTranslate } from "@tolgee/react";
import { Variants, motion } from "framer-motion";
import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { TJsFileUploadParams } from "@formbricks/types/js";
import { TProjectStyling } from "@formbricks/types/project";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
import { Modal } from "./components/modal";
import { TabOption } from "./components/tab-option";
@@ -25,7 +23,6 @@ interface PreviewSurveyProps {
project: Project;
environment: Pick<Environment, "id" | "appSetupCompleted">;
languageCode: string;
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
}
let surveyNameTemp: string;
@@ -66,7 +63,6 @@ export const PreviewSurvey = ({
project,
environment,
languageCode,
onFileUpload,
}: PreviewSurveyProps) => {
const [isModalOpen, setIsModalOpen] = useState(true);
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
@@ -265,11 +261,11 @@ export const PreviewSurvey = ({
borderRadius={styling?.roundness ?? 8}
background={styling?.cardBackgroundColor?.light}>
<SurveyInline
isPreviewMode={true}
survey={survey}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
onFileUpload={onFileUpload}
styling={styling}
isCardBorderVisible={!styling.highlightBorderColor?.light}
onClose={handlePreviewModalClose}
@@ -288,9 +284,9 @@ export const PreviewSurvey = ({
</div>
<div className="z-10 w-full max-w-md rounded-lg border border-transparent">
<SurveyInline
isPreviewMode={true}
survey={{ ...survey, type: "link" }}
isBrandingEnabled={project.linkSurveyBranding}
onFileUpload={onFileUpload}
languageCode={languageCode}
responseCount={42}
styling={styling}
@@ -367,11 +363,11 @@ export const PreviewSurvey = ({
borderRadius={styling.roundness ?? 8}
background={styling.cardBackgroundColor?.light}>
<SurveyInline
isPreviewMode={true}
survey={survey}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
languageCode={languageCode}
onFileUpload={onFileUpload}
styling={styling}
isCardBorderVisible={!styling.highlightBorderColor?.light}
onClose={handlePreviewModalClose}
@@ -392,12 +388,12 @@ export const PreviewSurvey = ({
<ClientLogo environmentId={environment.id} projectLogo={project.logo} previewSurvey />
)}
</div>
<div className="z-0 w-full max-w-lg rounded-lg border-transparent">
<div className="z-0 w-full max-w-4xl rounded-lg border-transparent">
<SurveyInline
isPreviewMode={true}
survey={{ ...survey, type: "link" }}
isBrandingEnabled={project.linkSurveyBranding}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
languageCode={languageCode}
responseCount={42}
styling={styling}

View File

@@ -81,7 +81,7 @@ export const QuestionToggleTable = ({
</th>
<th className="w-1/6 text-sm font-semibold">{t("common.show")}</th>
<th className="w-1/6 text-sm font-semibold">{t("environments.surveys.edit.required")}</th>
<th className="text-sm font-semibold">{t("common.placeholder")}</th>
<th className="text-sm font-semibold">{t("common.label")}</th>
</tr>
</thead>
<tbody>

View File

@@ -16,77 +16,72 @@ interface SecondaryNavbarProps {
export const SecondaryNavigation = ({ navigation, activeId, loading, ...props }: SecondaryNavbarProps) => {
return (
<div {...props}>
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{loading
? navigation.map((navElem) => (
<div className="group flex h-full flex-col" key={navElem.id}>
<div
aria-disabled="true"
className={cn(
navElem.id === activeId ? "font-semibold text-slate-900" : "text-slate-500",
"flex h-full items-center px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === activeId ? "page" : undefined}>
{navElem.label}
</div>
<div
className={cn(
"bottom-0 mt-auto h-[2px] w-full rounded-t-lg transition-all duration-150 ease-in-out",
navElem.id === activeId ? "bg-slate-300" : "bg-transparent group-hover:bg-slate-300",
navElem.hidden && "hidden"
)}
/>
<nav className="flex h-10 w-full items-center space-x-4" aria-label="Tabs">
{loading
? navigation.map((navElem) => (
<div className="group flex h-full flex-col truncate" key={navElem.id}>
<div
aria-disabled="true"
className={cn(
navElem.id === activeId ? "font-semibold text-slate-900" : "text-slate-500",
"flex h-full items-center truncate px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === activeId ? "page" : undefined}>
{navElem.label}
</div>
))
: navigation.map(
(navElem) =>
!navElem.hidden && (
<div className="group flex h-full flex-col" key={navElem.id}>
{navElem.href ? (
<Link
href={navElem.href}
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
className={cn(
navElem.id === activeId
? "font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700",
"flex h-full items-center px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === activeId ? "page" : undefined}>
{navElem.label}
</Link>
) : (
<button
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
className={cn(
navElem.id === activeId
? "font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700",
"grow items-center px-3 text-sm font-medium transition-all duration-150 ease-in-out",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === activeId ? "page" : undefined}>
{navElem.label}
</button>
)}
<div
<div
className={cn(
"bottom-0 mt-auto h-[2px] w-full rounded-t-lg transition-all duration-150 ease-in-out",
navElem.id === activeId ? "bg-slate-300" : "bg-transparent group-hover:bg-slate-300",
navElem.hidden && "hidden"
)}
/>
</div>
))
: navigation.map(
(navElem) =>
!navElem.hidden && (
<div className="group flex h-full flex-col truncate" key={navElem.id}>
{navElem.href ? (
<Link
href={navElem.href}
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
className={cn(
"bottom-0 mt-auto h-[2px] w-full rounded-t-lg transition-all duration-150 ease-in-out",
navElem.id === activeId
? "bg-brand-dark"
: "bg-transparent group-hover:bg-slate-300",
? "font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700",
"flex h-full items-center px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
/>
</div>
)
)}
</nav>
<div className="justify-self-end"></div>
</div>
aria-current={navElem.id === activeId ? "page" : undefined}>
{navElem.label}
</Link>
) : (
<button
{...(navElem.onClick ? { onClick: navElem.onClick } : {})}
className={cn(
navElem.id === activeId
? "font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700",
"grow items-center px-3 text-sm font-medium transition-all duration-150 ease-in-out",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === activeId ? "page" : undefined}>
{navElem.label}
</button>
)}
<div
className={cn(
"bottom-0 mt-auto h-[2px] w-full rounded-t-lg transition-all duration-150 ease-in-out",
navElem.id === activeId ? "bg-brand-dark" : "bg-transparent group-hover:bg-slate-300",
navElem.hidden && "hidden"
)}
/>
</div>
)
)}
</nav>
</div>
);
};

View File

@@ -162,6 +162,7 @@ export const ThemeStylingPreviewSurvey = ({
borderRadius={project.styling.roundness ?? 8}>
<Fragment key={surveyFormKey}>
<SurveyInline
isPreviewMode={true}
survey={{ ...survey, type: "app" }}
isBrandingEnabled={project.inAppSurveyBranding}
isRedirectDisabled={true}
@@ -187,6 +188,7 @@ export const ThemeStylingPreviewSurvey = ({
key={surveyFormKey}
className={`${project.logo?.url && !project.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
<SurveyInline
isPreviewMode={true}
survey={{ ...survey, type: "link" }}
isBrandingEnabled={project.linkSurveyBranding}
isRedirectDisabled={true}

View File

@@ -23,11 +23,14 @@ const nextConfig = {
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
},
i18n: {
locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW"],
locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW", "pt-PT"],
localeDetection: false,
defaultLocale: "en-US",
},
experimental: {},
experimental: {
instrumentationHook: true,
serverComponentsExternalPackages: ["@opentelemetry/instrumentation"],
},
transpilePackages: ["@formbricks/database", "@formbricks/lib"],
images: {
remotePatterns: [
@@ -108,6 +111,10 @@ const nextConfig = {
},
],
});
config.resolve.fallback = {
http: false, // Prevents Next.js from trying to bundle 'http'
https: false,
};
return config;
},
async headers() {

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "3.3.1",
"version": "0.0.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
@@ -41,8 +41,13 @@
"@lexical/rich-text": "0.21.0",
"@lexical/table": "0.21.0",
"@opentelemetry/api-logs": "0.56.0",
"@opentelemetry/exporter-prometheus": "0.57.2",
"@opentelemetry/host-metrics": "0.35.5",
"@opentelemetry/instrumentation": "0.56.0",
"@opentelemetry/instrumentation-http": "0.57.2",
"@opentelemetry/instrumentation-runtime-node": "0.12.2",
"@opentelemetry/sdk-logs": "0.56.0",
"@opentelemetry/sdk-metrics": "1.30.1",
"@paralleldrive/cuid2": "2.2.2",
"@prisma/client": "6.0.1",
"@radix-ui/react-accordion": "1.2.2",
@@ -104,11 +109,12 @@
"next-safe-action": "7.10.2",
"node-fetch": "3.3.2",
"nodemailer": "6.9.16",
"opentelemetry": "0.1.0",
"optional": "0.1.4",
"otplib": "12.0.1",
"papaparse": "5.4.1",
"posthog-js": "1.200.2",
"prismjs": "1.29.0",
"prismjs": "1.30.0",
"react": "19.0.0",
"react-colorful": "5.6.1",
"react-confetti": "6.1.0",

View File

@@ -205,22 +205,20 @@ test.describe("Survey Create & Submit Response without logic", async () => {
// Address Question
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
await expect(
page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
).toBeVisible();
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)).toBeVisible();
await page
.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)
.fill("This is my Address");
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
await page.getByLabel(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
// Contact Info Question
await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible();
await expect(page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
await expect(page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
await page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click();
// Ranking Question
@@ -866,21 +864,17 @@ test.describe("Testing Survey with advanced logic", async () => {
// Address Question
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
).toBeVisible();
await page
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
.fill("This is my Address");
await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
).toBeVisible();
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)).toBeVisible();
await page
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)
.fill("This is my city");
await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip)
).toBeVisible();
await page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip)).toBeVisible();
await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
// loading spinner -> wait for it to disappear

5
apps/web/prometheus.yml Normal file
View File

@@ -0,0 +1,5 @@
scrape_configs:
- job_name: "nodejs-app"
scrape_interval: 5s
static_configs:
- targets: ["host.docker.internal:9464"]

View File

@@ -4,7 +4,7 @@ import { DevTools, Tolgee } from "@tolgee/web";
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;
export const ALL_LANGUAGES = ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW"];
export const ALL_LANGUAGES = ["en-US", "de-DE", "fr-FR", "pt-BR", "pt-PT", "zh-Hant-TW"];
export const DEFAULT_LANGUAGE = "en-US";
@@ -20,6 +20,7 @@ export function TolgeeBase() {
"de-DE": () => import("@formbricks/lib/messages/de-DE.json"),
"fr-FR": () => import("@formbricks/lib/messages/fr-FR.json"),
"pt-BR": () => import("@formbricks/lib/messages/pt-BR.json"),
"pt-PT": () => import("@formbricks/lib/messages/pt-PT.json"),
"zh-Hant-TW": () => import("@formbricks/lib/messages/zh-Hant-TW.json"),
},
});

44
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,44 @@
services:
postgres:
image: pgvector/pgvector:pg17
volumes:
- postgres:/var/lib/postgresql/data
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- 5432:5432
mailhog:
image: arjenz/mailhog # Copy of mailhog/MailHog to support linux/arm64
ports:
- 8025:8025 # web ui
- 1025:1025 # smtp server
redis:
image: redis:7.0.11
ports:
- 6379:6379
volumes:
- redis-data:/data
minio:
image: minio/minio:RELEASE.2025-02-28T09-55-16Z
command: server /data --console-address ":9001"
environment:
- MINIO_ROOT_USER=devminio
- MINIO_ROOT_PASSWORD=devminio123
ports:
- "9000:9000" # S3 API
- "9001:9001" # Console
volumes:
- minio-data:/data
volumes:
postgres:
driver: local
redis-data:
driver: local
minio-data:
driver: local

View File

@@ -36,6 +36,7 @@ x-environment: &environment
# Email Configuration
# MAIL_FROM:
# MAIL_FROM_NAME:
# SMTP_HOST:
# SMTP_PORT:
# SMTP_USER:
@@ -68,6 +69,9 @@ x-environment: &environment
# Set the below to your Unsplash API Key for their Survey Backgrounds
# UNSPLASH_ACCESS_KEY:
# Set the below to 0 to disable cron jobs
# DOCKER_CRON_ENABLED: 1
################################################### OPTIONAL (STORAGE) ###################################################
# Set the below to set a custom Upload Directory

View File

@@ -224,6 +224,9 @@ EOT
echo -n "Enter your SMTP configured Email ID: "
read mail_from
echo -n "Enter your SMTP configured Email Name: "
read mail_from_name
echo -n "Enter your SMTP Host URL: "
read smtp_host
@@ -244,6 +247,7 @@ EOT
else
mail_from=""
mail_from_name=""
smtp_host=""
smtp_port=""
smtp_user=""
@@ -270,6 +274,7 @@ EOT
if [[ -n $mail_from ]]; then
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
sed -i "s|# MAIL_FROM_NAME:|MAIL_FROM_NAME: \"$mail_from_name\"|" docker-compose.yml
sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml
sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml
sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml

View File

@@ -262,7 +262,9 @@
"group": "Auth & SSO",
"icon": "lock",
"pages": [
"self-hosting/configuration/auth-sso/oauth",
"self-hosting/configuration/auth-sso/open-id-connect",
"self-hosting/configuration/auth-sso/azure-ad-oauth",
"self-hosting/configuration/auth-sso/google-oauth",
"self-hosting/configuration/auth-sso/saml-sso"
]
},

View File

@@ -1039,6 +1039,7 @@ x-environment: &environment
# Email Configuration
MAIL_FROM:
MAIL_FROM_NAME:
SMTP_HOST:
SMTP_PORT:
SMTP_SECURE_ENABLED:

View File

@@ -0,0 +1,109 @@
---
title: Azure AD OAuth
description: "Configure Microsoft Entra ID (Azure AD) OAuth for secure Single Sign-On with your Formbricks instance. Use enterprise-grade authentication for your survey platform."
icon: "microsoft"
---
<Note>
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license).
</Note>
### Microsoft Entra ID
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
- 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`
## How to connect your Formbricks instance to Microsoft Entra
<Steps>
<Step title="Access the Microsoft Entra admin center">
- Login to the [Microsoft Entra admin center](https://entra.microsoft.com/).
- Go to **Applications** > **App registrations** in the left menu.
![first](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250153/image_tobdth.jpg)
</Step>
<Step title="Create a new app registration">
- Click the **New registration** button at the top.
![second](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250228/image_dmz75t.jpg)
</Step>
<Step title="Configure the application">
- Name your application something descriptive, such as `Formbricks SSO`.
![third](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250292/image_rooa3w.jpg)
- If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_.
![fourth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250542/image_nyndzo.jpg)
- Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above).
![fifth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250776/image_s3pgb6.jpg)
- Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created.
</Step>
<Step title="Collect application credentials">
- 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.
![sixth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250876/image_dj2vi5.jpg)
</Step>
<Step title="Create a client secret">
- From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**.
![seventh](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250913/image_p4zknw.jpg)
- Make sure you have the **Client secrets** tab active, and click **New client secret**.
![eighth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250973/image_kyjray.jpg)
- 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>
![ninth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251467/image_bkirq4.jpg)
- 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 create a new secret.
</Note>
![tenth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251234/image_jen6tp.jpg)
</Step>
<Step title="Update environment variables">
- 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 this:
```yml Formbricks Env for Microsoft Entra ID SSO
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
```
</Step>
<Step title="Restart and test">
- Restart your Formbricks instance.
- You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
</Step>
</Steps>

View File

@@ -0,0 +1,81 @@
---
title: "Google OAuth"
description: "Configure Google OAuth for secure Single Sign-On with your Formbricks instance. Implement enterprise-grade authentication for your survey platform with Google credentials."
icon: "google"
---
<Note>
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license).
</Note>
### Google OAuth
Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance.
### Requirements
- A Google Cloud Platform (GCP) account
- A Formbricks instance running
### How to connect your Formbricks instance to Google
<Steps>
<Step title="Create a GCP Project">
- Navigate to the [GCP Console](https://console.cloud.google.com/).
- From the projects list, select a project or create a new one.
</Step>
<Step title="Setting up OAuth 2.0">
- If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**.
- On the left, click **Credentials**.
- Click **Create Credentials**, then select **OAuth client ID**.
</Step>
<Step title="Configure OAuth Consent Screen">
- If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**.
- Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted.
</Step>
<Step title="Create OAuth 2.0 Client IDs">
- Select the application type **Web application** for your project and enter any additional information required.
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
```
Authorized JavaScript origins: {WEBAPP_URL}
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
```
</Step>
<Step title="Update Environment Variables in Docker">
- To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container.
- In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform:
```sh
GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
```
- Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID):
```sh
docker exec -it container_id /bin/bash
export GOOGLE_CLIENT_ID=your-client-id-here
export GOOGLE_CLIENT_SECRET=your-client-secret-here
exit
```
</Step>
<Step title="Restart Your Formbricks Instance">
<Note>
Restarting your Docker containers may cause a brief period of downtime. Plan accordingly.
</Note>
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication.
- 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.
</Step>
</Steps>

View File

@@ -1,208 +0,0 @@
---
title: OAuth
description: "OAuth for Formbricks"
icon: "key"
---
<Note>
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Entra ID, Github and OpenID Connect, requires a valid Formbricks Enterprise License.
</Note>
### Google OAuth
Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance.
#### Requirements:
- A Google Cloud Platform (GCP) account.
- A Formbricks instance running and accessible.
#### Steps:
1. **Create a GCP Project**:
- Navigate to the [GCP Console](https://console.cloud.google.com/).
- From the projects list, select a project or create a new one.
2. **Setting up OAuth 2.0**:
- If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**.
- On the left, click **Credentials**.
- Click **Create Credentials**, then select **OAuth client ID**.
3. **Configure OAuth Consent Screen**:
- If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**.
- Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted.
4. **Create OAuth 2.0 Client IDs**:
- Select the application type **Web application** for your project and enter any additional information required.
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
```{{ Redirect & Origin URLs
Authorized JavaScript origins: {WEBAPP_URL}
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
```
- **Update Environment Variables in Docker**:
- To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container.
- In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform:
- Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID):
```sh Shell commands
docker exec -it container_id /bin/bash
export GOOGLE_CLIENT_ID=your-client-id-here
export GOOGLE_CLIENT_SECRET=your-client-secret-here
exit
```
```sh env file
GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
```
1. **Restart Your Formbricks Instance**:
- **Note:** Restarting your Docker containers may cause a brief period of downtime. Plan accordingly.
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it:
- 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:
### Microsoft Entra ID (Azure Active Directory) SSO OAuth
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
- 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`
#### Creating an App Registration
- Login to the [Microsoft Entra admin center](https://entra.microsoft.com/).
- Go to **Applications** > **App registrations** in the left menu.
![first](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250153/image_tobdth.jpg)
- Click the **New registration** button at the top.
![second](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250228/image_dmz75t.jpg)
- Name your application something descriptive, such as `Formbricks SSO`.
![third](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250292/image_rooa3w.jpg)
- If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_.
![fourth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250542/image_nyndzo.jpg)
- Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above).
![fifth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250776/image_s3pgb6.jpg)
- Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created.
- 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.
![sixth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250876/image_dj2vi5.jpg)
- From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**.
![seventh](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250913/image_p4zknw.jpg)
- Make sure you have the **Client secrets** tab active, and click **New client secret**.
![eighth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250973/image_kyjray.jpg)
- 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>
![ninth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251467/image_bkirq4.jpg)
- 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>
![tenth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251234/image_jen6tp.jpg)
- 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:
```yml Formbricks Env for Microsoft Entra ID SSO
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
```
- Restart your Formbricks instance.
- You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
## OpenID Configuration
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
- Configure your OIDC provider & get the following variables:
- `OIDC_CLIENT_ID`
- `OIDC_CLIENT_SECRET`
- `OIDC_ISSUER`
- `OIDC_SIGNING_ALGORITHM`
<Note>
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
</Note>
- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
```yml Formbricks Env for FusionAuth OIDC
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
OIDC_ISSUER=http://localhost:9011
OIDC_DISPLAY_NAME=FusionAuth
OIDC_SIGNING_ALGORITHM=HS256
```
- Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
- Restart your Formbricks instance.
- You're all set! Users can now sign up & log in using their OIDC credentials.

View File

@@ -0,0 +1,45 @@
---
title: "Open ID Connect"
description: "Configure Open ID Connect for secure Single Sign-On with your Formbricks instance. Implement enterprise-grade authentication for your survey platform with Open ID Connect."
icon: "key"
---
<Note>
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license).
</Note>
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
- Configure your OIDC provider & get the following variables:
- `OIDC_CLIENT_ID`
- `OIDC_CLIENT_SECRET`
- `OIDC_ISSUER`
- `OIDC_SIGNING_ALGORITHM`
<Note>
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
</Note>
- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
```yml Formbricks Env for FusionAuth OIDC
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
OIDC_ISSUER=http://localhost:9011
OIDC_DISPLAY_NAME=FusionAuth
OIDC_SIGNING_ALGORITHM=HS256
```
- Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
- Restart your Formbricks instance.
- You're all set! Users can now sign up & log in using their OIDC credentials.

View File

@@ -1,7 +1,7 @@
---
title: "SAML SSO"
title: "SAML SSO - Self-hosted"
icon: "user-shield"
description: "How to set up SAML SSO for Formbricks"
description: "Configure SAML Single Sign-On (SSO) for secure enterprise authentication with your Formbricks instance."
---
<Note>You require an Enterprise License along with a SAML SSO add-on to avail this feature.</Note>
@@ -12,7 +12,7 @@ Formbricks supports SAML Single Sign-On (SSO) to enable secure, centralized auth
To learn more about SAML Jackson, please refer to the [BoxyHQ SAML Jackson documentation](https://boxyhq.com/docs/jackson/deploy).
## How SAML Works in Formbricks
## How SAML works in Formbricks
SAML (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between an Identity Provider (IdP) and Formbricks. Here's how the integration works with BoxyHQ Jackson embedded into the flow:
@@ -37,7 +37,7 @@ SAML (Security Assertion Markup Language) is an XML-based standard for exchangin
7. **Access Granted:**
Formbricks logs the user in using the verified information.
## SAML Authentication Flow Sequence Diagram
## SAML Auth Flow Sequence Diagram
Below is a sequence diagram illustrating the complete SAML authentication flow with BoxyHQ Jackson integrated:
@@ -67,12 +67,31 @@ sequenceDiagram
To configure SAML SSO in Formbricks, follow these steps:
1. **Database Setup:** Configure a dedicated database for SAML by setting the `SAML_DATABASE_URL` environment variable in your `docker-compose.yml` file (e.g., `postgres://postgres:postgres@postgres:5432/formbricks-saml`). If you're using a self-signed certificate for Postgres, include the `sslmode=disable` parameter.
2. **IdP Application:** Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers))
3. **User Provisioning:** Provision users in your IdP and configure access to the IdP SAML app for all your users (who need access to Formbricks).
4. **Metadata:** Keep the XML metadata from your IdP handy for the next step.
5. **Metadata Setup:** Create a file called `connection.xml` in your self-hosted Formbricks instance's `formbricks/saml-connection` directory and paste the XML metadata from your IdP into it. Please create the directory if it doesn't exist. Your metadata file should start with a tag like this: `<?xml version="1.0" encoding="UTF-8"?><...>` or `<md:EntityDescriptor entityID="...">`. Please remove any extra text from the metadata.
6. **Restart Formbricks:** Restart Formbricks to apply the changes. You can do this by running `docker compose down` and then `docker compose up -d`.
<Steps>
<Step title="Database Setup">
Configure a dedicated database for SAML by setting the `SAML_DATABASE_URL` environment variable in your `docker-compose.yml` file (e.g., `postgres://postgres:postgres@postgres:5432/formbricks-saml`). If you're using a self-signed certificate for Postgres, include the `sslmode=disable` parameter.
</Step>
<Step title="IdP Application">
Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers))
</Step>
<Step title="User Provisioning">
Provision users in your IdP and configure access to the IdP SAML app for all your users (who need access to Formbricks).
</Step>
<Step title="Metadata">
Keep the XML metadata from your IdP handy for the next step.
</Step>
<Step title="Metadata Setup">
Create a file called `connection.xml` in your self-hosted Formbricks instance's `formbricks/saml-connection` directory and paste the XML metadata from your IdP into it. Please create the directory if it doesn't exist. Your metadata file should start with a tag like this: `<?xml version="1.0" encoding="UTF-8"?><...>` or `<md:EntityDescriptor entityID="...">`. Please remove any extra text from the metadata.
</Step>
<Step title="Restart Formbricks">
Restart Formbricks to apply the changes. You can do this by running `docker compose down` and then `docker compose up -d`.
</Step>
</Steps>
<Note>
We don't support multiple SAML connections yet. You can only have one SAML connection at a time. If you

View File

@@ -33,6 +33,7 @@ These variables are present inside your machines docker-compose file. Restart
| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | |
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | |
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | |
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
@@ -58,7 +59,10 @@ These variables are present inside your machines docker-compose file. Restart
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | |
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 |
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | |
| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | |
| CUSTOM_CACHE_DISABLED | Disables custom cache handler if set to 1 (required for deployment on Vercel) | optional | |
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Github Discussions and well try our best to work out a solution with you.

View File

@@ -33,6 +33,7 @@ To enable email functionality, configure the following environment variables:
```bash
# Basic SMTP Configuration
MAIL_FROM=noreply@yourdomain.com
MAIL_FROM_NAME=Formbricks
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=your_username
@@ -75,6 +76,7 @@ If you're using the one-click setup with Docker Compose, you can either:
environment:
# Email Configuration
MAIL_FROM: noreply@yourdomain.com
MAIL_FROM_NAME: Formbricks
SMTP_HOST: smtp.yourprovider.com
SMTP_PORT: 587
SMTP_USER: your_username
@@ -95,6 +97,7 @@ environment:
```bash
MAIL_FROM=noreply@yourdomain.com
MAIL_FROM_NAME=Formbricks
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
@@ -105,6 +108,7 @@ SMTP_PASSWORD=your_sendgrid_api_key
```bash
MAIL_FROM=noreply@yourdomain.com
MAIL_FROM_NAME=Formbricks
SMTP_HOST=email-smtp.us-east-1.amazonaws.com
SMTP_PORT=587
SMTP_USER=your_ses_access_key
@@ -115,6 +119,7 @@ SMTP_PASSWORD=your_ses_secret_key
```bash
MAIL_FROM=your_email@gmail.com
MAIL_FROM_NAME=Formbricks
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com

Some files were not shown because too many files have changed in this diff Show More