Compare commits

..

73 Commits

Author SHA1 Message Date
dependabot[bot]
6a2a8b74c8 chore(deps): bump the npm_and_yarn group across 3 directories with 1 update (#5035)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2025-03-22 04:24:15 +01:00
Harsh Shrikant Bhat
43d5d3d719 chore: small email tweaks (#5019) 2025-03-21 06:14:56 -07:00
Piyush Gupta
5527f184b7 feat: adds configurable logging (#4914)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-03-21 06:09:13 -07:00
Piyush Jain
7dd5cf8b6e release: pre steps switch from vercel to aws (#5030) 2025-03-21 08:36:53 +01:00
Piyush Gupta
aec697f5b9 fix: role escalation in org settings (#4901)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-03-21 05:38:51 +00:00
Piyush Jain
aa2588dd89 chore(terraform): fix terraform certs (#5023) 2025-03-20 09:08:17 +00:00
victorvhs017
ed886e1794 fix: add membership checks in [environmentId] route (#5020)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-03-20 02:07:19 -07:00
Dhruwang Jariwala
452709dec7 fix: recall in email embed (#4971)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-03-20 05:22:42 +00:00
Dhruwang Jariwala
a5cac35cfd fix: single use link generation (#5004) 2025-03-20 04:31:36 +00:00
Peter Pesti-Varga
3ee8485ef0 fix: Android build changes + close survey window on js exception (#5016) 2025-03-19 09:11:31 -07:00
Dhruwang Jariwala
673f61be17 fix: layout breaking when adding note to response (#5007) 2025-03-19 05:22:24 -07:00
Piyush Jain
db86247510 chore(observability): add observability tools permissions (#5003) 2025-03-19 09:57:02 +00:00
Harsh Shrikant Bhat
090f6eef71 docs: add enterprise hint for all EE features in docs (#5000)
Co-authored-by: Johannes <johannes@formbricks.com>
2025-03-19 01:34:53 -07:00
Matti Nannt
214d18616f feat: personalized survey links (#4870)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-03-19 07:30:39 +00:00
Piyush Gupta
3b126291a6 docs: removed XM & Survey -> SAML SSO (#4999) 2025-03-19 07:06:46 +00:00
Piyush Jain
55a230e127 chore: updates to aws cloud resources (#4996) 2025-03-18 19:01:44 +01:00
Anshuman Pandey
2a107ece7f chore: js-core sdk refactor (#4815)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2025-03-18 15:58:50 +00:00
victorvhs017
7a3ef93a18 chore: Refactored the intercom next public env variable and added test files (#4960) 2025-03-18 15:04:08 +00:00
Anshuman Pandey
6255c9baad fix: handling invalid csv files (#4991) 2025-03-18 14:30:28 +00:00
Piyush Jain
c322a963ab fix(helm-chart): missing envFrom when using secret.enabled (#4992) 2025-03-18 15:41:16 +01:00
Paribesh Nepal
b1e8cb5a07 feat: added qr code feature (#4951) 2025-03-18 07:21:32 -07:00
Harsh Shrikant Bhat
a391089efc docs: Missing page descriptions. (#4980) 2025-03-18 07:20:13 -07:00
victorvhs017
1894bbe4f7 feat: add custom TTL for cache records (#4912) 2025-03-18 12:33:52 +00:00
Peter Pesti-Varga
07dba90679 fix: Android build fixes (#4984) 2025-03-18 13:14:25 +01:00
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
Piyush Gupta
333372d61c fix: removed link survey identification from share modal (#4898)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-03-10 06:45:28 +00:00
Piyush Gupta
48a92f3e55 feat: OIDC name fields added (#4872)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-03-09 08:42:00 +00:00
Piyush Gupta
ddc767e53e fix: logo on follow up email (#4837)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-03-08 06:40:40 +00:00
Matti Nannt
432425ea59 chore: add licenses to ios and android SDK (#4893) 2025-03-07 18:18:31 +01:00
Peter Pesti-Varga
6075fd3ef8 feat: Mobile SDK unit tests and Readme files (#4892) 2025-03-07 16:54:19 +01:00
Anshuman Pandey
f099a46f83 feat: surveys package integration with v2 apis (#4882)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-03-07 12:41:14 +00:00
Dhruwang Jariwala
fe54ef66c6 docs: activepieces docs for self hosters (#4891) 2025-03-07 12:27:56 +00:00
Piyush Gupta
4eb0e930f6 fix: improve multiple choice question summary calculation (#4829)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-03-07 12:24:12 +00:00
Dhruwang Jariwala
fae925aa25 fix: typo in email (#4880) 2025-03-07 10:07:23 +00:00
victorvhs017
764a3d2fde fix: added the test-key reference back (#4883) 2025-03-07 09:48:11 +00:00
Anshuman Pandey
b5a51f1304 fix: surveys pkg android file uploads (#4869)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-03-07 04:38:49 +00:00
Piyush Gupta
140aee749b feat: new management api crud endpoint for responses (#4716)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Victor Santos <victor@formbricks.com>
Co-authored-by: victorvhs017 <115753265+victorvhs017@users.noreply.github.com>
2025-03-06 17:16:06 +00:00
Dhruwang Jariwala
4113dd1873 fix: signup vulnerability (#4859) 2025-03-06 12:08:40 +00:00
716 changed files with 33836 additions and 8531 deletions

View File

@@ -25,6 +25,9 @@ NEXTAUTH_SECRET=
# You can use: `openssl rand -hex 32` to generate a secure one
CRON_SECRET=
# Set the minimum log level(debug, info, warn, error, fatal)
LOG_LEVEL=info
##############
# DATABASE #
##############
@@ -39,6 +42,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 +100,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,11 +191,16 @@ 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
# You can also add more configuration to Redis using the redis.conf file in the root directory
REDIS_URL=redis://localhost:6379
REDIS_DEFAULT_TTL=86400 # 1 day
# 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:
# The below is used for Rate Limiting for management API
UNKEY_ROOT_KEY=
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
# CUSTOM_CACHE_DISABLED=1
@@ -198,5 +210,10 @@ UNSPLASH_ACCESS_KEY=
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
# AI_AZURE_LLM_DEPLOYMENT_ID=
# NEXT_PUBLIC_INTERCOM_APP_ID=
# INTERCOM_SECRET_KEY=
# INTERCOM_APP_ID=
# 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

@@ -1,96 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/demo-react-native
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/demo
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/storybook
schedule:
interval: daily
- package-ecosystem: docker
directory: /apps/web
schedule:
interval: daily
- package-ecosystem: npm
directory: /apps/web
schedule:
interval: daily
- package-ecosystem: npm
directory: /
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/api
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/config-eslint
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/config-prettier
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/config-typescript
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/database
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/js-core
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/js
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/lib
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/react-native
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/surveys
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/types
schedule:
interval: daily
- package-ecosystem: npm
directory: /packages/vite-plugins
schedule:
interval: daily

View File

@@ -1,78 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: ["main"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
schedule:
- cron: "0 0 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript", "typescript"]
# CodeQL supports [ $supported-codeql-languages ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
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
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
category: "/language:${{matrix.language}}"

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@v2
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit

View File

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

View File

@@ -89,7 +89,7 @@ jobs:
- name: Run App
run: |
NODE_ENV=test pnpm start --filter=@formbricks/web &
NODE_ENV=test pnpm start --filter=@formbricks/web | tee app.log 2>&1 &
sleep 10 # Optional: gives some buffer for the app to start
for attempt in {1..10}; do
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
@@ -141,3 +141,13 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
if: failure()
with:
name: app-logs
path: app.log
- name: Output App Logs
if: failure()
run: cat app.log

View File

@@ -51,10 +51,9 @@ jobs:
statuses: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@v2
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,67 +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:
- 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
- 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

@@ -42,6 +42,18 @@ jobs:
- name: Checkout repository
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@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0

View File

@@ -27,6 +27,18 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.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: Log in to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
@@ -36,13 +48,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.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: Build and push Docker image
uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1
with:

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

@@ -46,11 +46,7 @@ jobs:
- name: Run tests with coverage
run: |
cd apps/web
pnpm test:coverage
cd ../../
# The Vitest coverage config is in your vite.config.mts
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203
env:

View File

@@ -0,0 +1,75 @@
name: 'Terraform'
on:
workflow_dispatch:
# TODO: enable it back when migration is completed.
# 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

@@ -5,7 +5,7 @@ permissions:
on:
workflow_dispatch:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
@@ -18,7 +18,14 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0

View File

@@ -3,7 +3,7 @@ permissions:
contents: read
on:
pull_request:
pull_request_target:
types: [closed]
branches:
- main
@@ -28,18 +28,16 @@ jobs:
- 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

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

@@ -1,18 +0,0 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.38.0
hooks:
- id: eslint
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace

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

@@ -6,6 +6,8 @@
"dbaeumer.vscode-eslint", // eslint plugin
"esbenp.prettier-vscode", // prettier plugin
"Prisma.prisma", // syntax|format|completion for prisma
"yzhang.markdown-all-in-one" // nicer markdown support
"yzhang.markdown-all-in-one", // nicer markdown support
"vitest.explorer", // run tests directly from the code window
"sonarsource.sonarlint-vscode" // sonarqube linter for vscode
]
}

View File

@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
Portions of this software are licensed as follows:
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
- All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All content that resides under the "packages/js/", "packages/react-native/", "packages/android/", "packages/ios/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"lucide-react": "0.468.0",
"next": "15.1.2",
"next": "15.2.3",
"react": "19.0.0",
"react-dom": "19.0.0"
},

View File

@@ -9,6 +9,12 @@ declare const window: Window;
export default function AppPage(): React.JSX.Element {
const [darkMode, setDarkMode] = useState(false);
const router = useRouter();
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
const userAttributes = {
"Attribute 1": "one",
"Attribute 2": "two",
"Attribute 3": "three",
};
useEffect(() => {
if (darkMode) {
@@ -33,18 +39,9 @@ export default function AppPage(): React.JSX.Element {
addFormbricksDebugParam();
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
const userInitAttributes = {
language: "de",
"Init Attribute 1": "eight",
"Init Attribute 2": "two",
};
void formbricks.init({
void formbricks.setup({
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
userId,
attributes: userInitAttributes,
appUrl: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
});
}
@@ -126,19 +123,19 @@ export default function AppPage(): React.JSX.Element {
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
Set a user ID / pull data from Formbricks app
</h3>
<p className="text-slate-700 dark:text-slate-300">
On formbricks.reset() the local state will <strong>be deleted</strong> and formbricks gets{" "}
<strong>reinitialized</strong>.
On formbricks.setUserId() the user state will <strong>be fetched from Formbricks</strong> and
the local state gets <strong>updated with the user state</strong>.
</p>
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
type="button"
onClick={() => {
void formbricks.reset();
void formbricks.setUserId(userId);
}}>
Reset
Set user ID
</button>
<p className="text-xs text-slate-700 dark:text-slate-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Reset&apos; and
@@ -158,7 +155,7 @@ export default function AppPage(): React.JSX.Element {
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a
href="https://formbricks.com/docs/actions/no-code"
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions"
rel="noopener noreferrer"
className="underline dark:text-blue-500"
target="_blank">
@@ -166,7 +163,7 @@ export default function AppPage(): React.JSX.Element {
</a>{" "}
as long as you created it beforehand in the Formbricks App.{" "}
<a
href="https://formbricks.com/docs/actions/no-code"
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions"
rel="noopener noreferrer"
target="_blank"
className="underline dark:text-blue-500">
@@ -175,6 +172,7 @@ export default function AppPage(): React.JSX.Element {
</p>
</div>
</div>
<div className="p-6">
<div>
<button
@@ -190,7 +188,7 @@ export default function AppPage(): React.JSX.Element {
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/custom-attributes"
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
@@ -215,7 +213,7 @@ export default function AppPage(): React.JSX.Element {
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/custom-attributes"
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
@@ -240,7 +238,7 @@ export default function AppPage(): React.JSX.Element {
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/identify-users"
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
@@ -250,6 +248,110 @@ export default function AppPage(): React.JSX.Element {
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setAttributes(userAttributes);
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Multiple Attributes
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
user attributes
</a>{" "}
to &apos;one&apos;, &apos;two&apos;, &apos;three&apos;.
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
onClick={() => {
void formbricks.setLanguage("de");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Language to &apos;de&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/general-features/multi-language-surveys"
target="_blank"
rel="noopener noreferrer"
className="underline dark:text-blue-500">
language
</a>{" "}
to &apos;de&apos;.
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
void formbricks.track("code");
}}>
Code Action
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions"
rel="noopener noreferrer"
className="underline dark:text-blue-500"
target="_blank">
Code Action
</a>{" "}
as long as you created it beforehand in the Formbricks App.{" "}
<a
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions"
rel="noopener noreferrer"
target="_blank"
className="underline dark:text-blue-500">
Here are instructions on how to do it.
</a>
</p>
</div>
</div>
<div className="p-6">
<div>
<button
type="button"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
void formbricks.logout();
}}>
Logout
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button logs out the user and syncs the local state with Formbricks. (Only works if a
userId is set)
</p>
</div>
</div>
</div>
</div>
</div>

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

@@ -85,6 +85,8 @@ COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
# Copy Prisma-specific generated files
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
@@ -93,14 +95,16 @@ COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_mod
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
COPY /docker/cronjobs /app/docker/cronjobs
# Copy only @paralleldrive/cuid2 and @noble/hashes
# Copy required dependencies
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
COPY --from=installer /app/node_modules/zod ./node_modules/zod
RUN npm install -g tsx typescript prisma
RUN npm install -g tsx typescript prisma pino-pretty
EXPOSE 3000
ENV HOSTNAME "0.0.0.0"
ENV NODE_ENV="production"
# USER nextjs
# Prepare volume for uploads
@@ -111,7 +115,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
exec node apps/web/server.js

View File

@@ -34,10 +34,9 @@ export const OnboardingSetupInstructions = ({
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){
var apiHost = "${webAppUrl}";
var appUrl = "${webAppUrl}";
var environmentId = "${environmentId}";
var userId = "testUser";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.setup({environmentId: environmentId, appUrl: appUrl})},500)}();
</script>
<!-- END Formbricks Surveys -->
`;
@@ -45,9 +44,9 @@ export const OnboardingSetupInstructions = ({
const htmlSnippetForWebsiteSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){
var apiHost = "${webAppUrl}";
var appUrl = "${webAppUrl}";
var environmentId = "${environmentId}";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}();
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.setup({environmentId: environmentId, appUrl: appUrl })},500)}();
</script>
<!-- END Formbricks Surveys -->
`;
@@ -56,10 +55,9 @@ export const OnboardingSetupInstructions = ({
import formbricks from "@formbricks/js";
if (typeof window !== "undefined") {
formbricks.init({
formbricks.setup({
environmentId: "${environmentId}",
apiHost: "${webAppUrl}",
userId: "testUser",
appUrl: "${webAppUrl}",
});
}
@@ -75,9 +73,9 @@ export const OnboardingSetupInstructions = ({
import formbricks from "@formbricks/js";
if (typeof window !== "undefined") {
formbricks.init({
formbricks.setup({
environmentId: "${environmentId}",
apiHost: "${webAppUrl}",
appUrl: "${webAppUrl}",
});
}

View File

@@ -1,13 +1,10 @@
import { getDefaultEndingCard } from "@/app/lib/templates";
import { createId } from "@paralleldrive/cuid2";
import { TFnType } from "@tolgee/react";
import { logger } from "@formbricks/logger";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
function logError(error: Error, context: string) {
console.error(`Error in ${context}:`, error);
}
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
try {
return {
@@ -19,7 +16,7 @@ export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
},
};
} catch (error) {
logError(error, "getXMSurveyDefault");
logger.error(error, "Failed to create default XM survey template");
throw error; // Re-throw after logging
}
};
@@ -449,7 +446,7 @@ export const getXMTemplates = (t: TFnType): TXMTemplate[] => {
enpsSurvey(t),
];
} catch (error) {
logError(error, "getXMTemplates");
logger.error(error, "Unable to load XM templates, returning empty array");
return []; // Return an empty array or handle as needed
}
};

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

@@ -12,12 +12,12 @@ export const FormbricksClient = ({ userId, email }: { userId: string; email: str
useEffect(() => {
if (formbricksEnabled && userId) {
formbricks.init({
formbricks.setup({
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
userId,
appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
});
formbricks.setUserId(userId);
formbricks.setEmail(email);
}
}, [userId, email]);

View File

@@ -2,21 +2,14 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
export const metadata: Metadata = {
@@ -25,51 +18,24 @@ export const metadata: Metadata = {
const Page = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const { isReadOnly, project, isBilling, environment } = await getEnvironmentAuth(params.environmentId);
const t = await getTranslate();
const [actionClasses, organization, project] = await Promise.all([
getActionClasses(params.environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
]);
const [actionClasses] = await Promise.all([getActionClasses(params.environmentId)]);
const locale = await findMatchingLocale();
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
if (!project) {
throw new Error(t("common.project_not_found"));
}
const environments = await getEnvironments(project.id);
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
if (!currentEnvironment) {
throw new Error(t("common.environment_not_found"));
}
const otherEnvironment = environments.filter((env) => env.id !== params.environmentId)[0];
const otherEnvActionClasses = await getActionClasses(otherEnvironment.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
if (isBilling) {
return redirect(`/environments/${params.environmentId}/settings/billing`);
}
const isReadOnly = isMember && hasReadAccess;
const renderAddActionButton = () => (
<AddActionModal
environmentId={params.environmentId}
@@ -82,7 +48,7 @@ const Page = async (props) => {
<PageContentWrapper>
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
<ActionClassesTable
environment={currentEnvironment}
environment={environment}
otherEnvironment={otherEnvironment}
otherEnvActionClasses={otherEnvActionClasses}
environmentId={params.environmentId}

View File

@@ -1,3 +1,4 @@
import { logger } from "@formbricks/logger";
import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable";
export const fetchTables = async (environmentId: string, baseId: string) => {
@@ -17,7 +18,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
});
if (!res.ok) {
console.error(res.text);
const errorText = await res.text();
logger.error({ errorText }, "authorize: Could not fetch airtable config");
throw new Error("Could not create response");
}
const resJSON = await res.json();

View File

@@ -1,21 +1,14 @@
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { getAirtableTables } from "@formbricks/lib/airtable/service";
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
@@ -24,48 +17,25 @@ const Page = async (props) => {
const params = await props.params;
const t = await getTranslate();
const isEnabled = !!AIRTABLE_CLIENT_ID;
const [session, surveys, integrations, environment] = await Promise.all([
getServerSession(authOptions),
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
const [surveys, integrations] = await Promise.all([
getSurveys(params.environmentId),
getIntegrations(params.environmentId),
getEnvironment(params.environmentId),
]);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
(integration): integration is TIntegrationAirtable => integration.type === "airtable"
);
let airtableArray: TIntegrationItem[] = [];
if (airtableIntegration && airtableIntegration.config.key) {
if (airtableIntegration?.config.key) {
airtableArray = await getAirtableTables(params.environmentId);
}
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
if (isReadOnly) {
redirect("./");
}

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

@@ -1,3 +1,5 @@
import { logger } from "@formbricks/logger";
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
const res = await fetch(`${apiHost}/api/google-sheet`, {
method: "GET",
@@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
});
if (!res.ok) {
console.error(res.text);
const errorText = await res.text();
logger.error({ errorText }, "authorize: Could not fetch google sheet config");
throw new Error("Could not create response");
}
const resJSON = await res.json();

View File

@@ -1,13 +1,10 @@
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import {
GOOGLE_SHEETS_CLIENT_ID,
@@ -15,11 +12,7 @@ import {
GOOGLE_SHEETS_REDIRECT_URL,
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
@@ -27,43 +20,20 @@ const Page = async (props) => {
const params = await props.params;
const t = await getTranslate();
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
const [session, surveys, integrations, environment] = await Promise.all([
getServerSession(authOptions),
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
const [surveys, integrations] = await Promise.all([
getSurveys(params.environmentId),
getIntegrations(params.environmentId),
getEnvironment(params.environmentId),
]);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
);
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
if (isReadOnly) {
redirect("./");
}

View File

@@ -7,6 +7,7 @@ import { surveyCache } from "@formbricks/lib/survey/cache";
import { selectSurvey } from "@formbricks/lib/survey/service";
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -34,7 +35,7 @@ export const getSurveys = reactCache(
return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey<TSurvey>(surveyPrisma));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error({ error }, "getSurveys: Could not fetch surveys");
throw new DatabaseError(error.message);
}
throw error;

View File

@@ -1,3 +1,5 @@
import { logger } from "@formbricks/logger";
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
const res = await fetch(`${apiHost}/api/v1/integrations/notion`, {
method: "GET",
@@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
});
if (!res.ok) {
console.error(res.text);
const errorText = await res.text();
logger.error({ errorText }, "authorize: Could not fetch notion config");
throw new Error("Could not create response");
}
const resJSON = await res.json();

View File

@@ -1,13 +1,10 @@
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import {
NOTION_AUTH_URL,
@@ -16,12 +13,8 @@ import {
NOTION_REDIRECT_URI,
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getNotionDatabases } from "@formbricks/lib/notion/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
@@ -34,44 +27,20 @@ const Page = async (props) => {
NOTION_AUTH_URL &&
NOTION_REDIRECT_URI
);
const [session, surveys, notionIntegration, environment] = await Promise.all([
getServerSession(authOptions),
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
const [surveys, notionIntegration] = await Promise.all([
getSurveys(params.environmentId),
getIntegrationByType(params.environmentId, "notion"),
getEnvironment(params.environmentId),
]);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
let databasesArray: TIntegrationNotionDatabase[] = [];
if (notionIntegration && (notionIntegration as TIntegrationNotion).config.key?.bot_id) {
databasesArray = (await getNotionDatabases(environment.id)) ?? [];
}
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
if (isReadOnly) {
redirect("./");
}

View File

@@ -9,71 +9,40 @@ import notionLogo from "@/images/notion.png";
import SlackLogo from "@/images/slacklogo.png";
import WebhookLogo from "@/images/webhook.png";
import ZapierLogo from "@/images/zapier-small.png";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { Card } from "@/modules/ui/components/integration-card";
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 Image from "next/image";
import { redirect } from "next/navigation";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { TIntegrationType } from "@formbricks/types/integration";
const Page = async (props) => {
const params = await props.params;
const environmentId = params.environmentId;
const t = await getTranslate();
const { isReadOnly, environment, isBilling } = await getEnvironmentAuth(params.environmentId);
const [
environment,
integrations,
organization,
session,
userWebhookCount,
zapierWebhookCount,
makeWebhookCount,
n8nwebhookCount,
activePiecesWebhookCount,
] = await Promise.all([
getEnvironment(environmentId),
getIntegrations(environmentId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getWebhookCountBySource(environmentId, "user"),
getWebhookCountBySource(environmentId, "zapier"),
getWebhookCountBySource(environmentId, "make"),
getWebhookCountBySource(environmentId, "n8n"),
getWebhookCountBySource(environmentId, "activepieces"),
getIntegrations(params.environmentId),
getWebhookCountBySource(params.environmentId, "user"),
getWebhookCountBySource(params.environmentId, "zapier"),
getWebhookCountBySource(params.environmentId, "make"),
getWebhookCountBySource(params.environmentId, "n8n"),
getWebhookCountBySource(params.environmentId, "activepieces"),
]);
const isIntegrationConnected = (type: TIntegrationType) =>
integrations.some((integration) => integration.type === type);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
if (isBilling) {
return redirect(`/environments/${params.environmentId}/settings/billing`);
@@ -244,7 +213,7 @@ const Page = async (props) => {
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
docsText: t("common.docs"),
docsNewTab: true,
connectHref: `/environments/${environmentId}/project/app-connection`,
connectHref: `/environments/${params.environmentId}/project/app-connection`,
connectText: t("common.connect"),
connectNewTab: false,
label: "Javascript SDK",

View File

@@ -1,3 +1,5 @@
import { logger } from "@formbricks/logger";
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
const res = await fetch(`${apiHost}/api/v1/integrations/slack`, {
method: "GET",
@@ -5,7 +7,8 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
});
if (!res.ok) {
console.error(res.text);
const errorText = await res.text();
logger.error({ errorText }, "authorize: Could not fetch slack config");
throw new Error("Could not create response");
}
const resJSON = await res.json();

View File

@@ -1,20 +1,13 @@
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
@@ -23,40 +16,16 @@ const Page = async (props) => {
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
const t = await getTranslate();
const [session, surveys, slackIntegration, environment] = await Promise.all([
getServerSession(authOptions),
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
const [surveys, slackIntegration] = await Promise.all([
getSurveys(params.environmentId),
getIntegrationByType(params.environmentId, "slack"),
getEnvironment(params.environmentId),
]);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
project.organizationId
);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
if (isReadOnly) {
redirect("./");
}

View File

@@ -4,7 +4,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { redirect } from "next/navigation";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
@@ -49,7 +49,10 @@ const EnvLayout = async (props: {
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
if (!membership) return notFound();
if (!membership) {
throw new Error(t("common.membership_not_found"));
}
return (
<>

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

@@ -157,6 +157,10 @@ const Page = async (props) => {
throw new Error(t("common.user_not_found"));
}
if (!memberships) {
throw new Error(t("common.membership_not_found"));
}
if (user?.notificationSettings) {
user.notificationSettings = setCompleteNotificationSettings(user.notificationSettings, memberships);
}

View File

@@ -1,16 +1,14 @@
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { SettingsId } from "@/modules/ui/components/settings-id";
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 { getUser } from "@formbricks/lib/user/service";
import { SettingsCard } from "../../components/SettingsCard";
import { DeleteAccount } from "./components/DeleteAccount";
@@ -23,20 +21,16 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslate();
const { environmentId } = params;
const session = await getServerSession(authOptions);
if (!session) {
throw new Error(t("common.session_not_found"));
}
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const { session } = await getEnvironmentAuth(params.environmentId);
const organizationsWithSingleOwner = await getOrganizationsWhereUserIsSingleOwner(session.user.id);
const user = session && session.user ? await getUser(session.user.id) : null;
const user = session?.user ? await getUser(session.user.id) : null;
if (!user) {
throw new Error(t("common.user_not_found"));
}
return (
<PageContentWrapper>
@@ -71,7 +65,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,18 +1,14 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { CheckIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import Link from "next/link";
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 { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
const Page = async (props) => {
const params = await props.params;
@@ -21,20 +17,8 @@ const Page = async (props) => {
notFound();
}
const session = await getServerSession(authOptions);
const { isMember, currentUserMembership } = await getEnvironmentAuth(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const isPricingDisabled = isMember;
if (isPricingDisabled) {

View File

@@ -0,0 +1,133 @@
import {
getIsMultiOrgEnabled,
getIsOrganizationAIReady,
getWhiteLabelPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import Page from "./page";
vi.mock("@formbricks/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
FB_LOGO_URL: "https://example.com/mock-logo.png",
ENCRYPTION_KEY: "mock-encryption-key",
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
GITHUB_ID: "mock-github-id",
GITHUB_SECRET: "mock-github-secret",
GOOGLE_CLIENT_ID: "mock-google-client-id",
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
OIDC_CLIENT_ID: "mock-oidc-client-id",
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
OIDC_ISSUER: "mock-oidc-issuer",
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
SAML_DATABASE_URL: "mock-saml-database-url",
WEBAPP_URL: "mock-webapp-url",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "mock-smtp-port",
}));
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
getTranslate: vi.fn(),
}));
vi.mock("@formbricks/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@formbricks/lib/organization/service", () => ({
getOrganizationByEnvironmentId: vi.fn(),
}));
vi.mock("@formbricks/lib/membership/service", () => ({
getMembershipByUserIdOrganizationId: vi.fn(),
}));
vi.mock("@formbricks/lib/membership/utils", () => ({
getAccessFlags: vi.fn(),
}));
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getIsMultiOrgEnabled: vi.fn(),
getIsOrganizationAIReady: vi.fn(),
getWhiteLabelPermission: vi.fn(),
}));
describe("Page", () => {
const mockParams = { environmentId: "test-environment-id" };
const mockSession = { user: { id: "test-user-id" } };
const mockUser = { id: "test-user-id" } as TUser;
const mockOrganization = { id: "test-organization-id", billing: { plan: "free" } } as TOrganization;
const mockMembership = { role: "owner" } as TMembership;
const mockTranslate = vi.fn((key) => key);
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
vi.mocked(getAccessFlags).mockReturnValue({
isOwner: true,
isManager: false,
isBilling: false,
isMember: false,
});
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
vi.mocked(getIsOrganizationAIReady).mockResolvedValue(true);
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
});
it("renders the page with organization settings", async () => {
const props = {
params: Promise.resolve({ environmentId: "env-123" }),
};
const result = await Page(props);
expect(result).toBeTruthy();
});
it("renders if session user id is null", async () => {
vi.mocked(getServerSession).mockResolvedValue({ user: { id: null } });
const props = {
params: Promise.resolve({ environmentId: "env-123" }),
};
const result = await Page(props);
expect(result).toBeTruthy();
});
it("throws an error if the session is not found", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
await expect(Page({ params: Promise.resolve(mockParams) })).rejects.toThrow("common.session_not_found");
});
it("throws an error if the organization is not found", async () => {
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
await expect(Page({ params: Promise.resolve(mockParams) })).rejects.toThrow(
"common.organization_not_found"
);
});
});

View File

@@ -12,7 +12,7 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { SettingsId } from "@/modules/ui/components/settings-id";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
@@ -84,6 +84,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
environmentId={params.environmentId}
isReadOnly={!isOwnerOrManager}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
fbLogoUrl={FB_LOGO_URL}
user={user}
/>
{isMultiOrgEnabled && (

View File

@@ -200,13 +200,6 @@ export const generateResponseTableColumns = (
</TooltipTrigger>
<TooltipContent side="bottom" className="font-normal">
{t("environments.surveys.responses.how_to_identify_users")}
<Link
className="underline underline-offset-2 hover:text-slate-900"
href="https://formbricks.com/docs/link-surveys/user-identification"
target="_blank">
{t("common.link_surveys")}
</Link>{" "}
or{" "}
<Link
className="underline underline-offset-2 hover:text-slate-900"
href="https://formbricks.com/docs/app-surveys/user-identification"

View File

@@ -60,7 +60,7 @@ export const ShareEmbedSurvey = ({
const [activeId, setActiveId] = useState(survey.type === "link" ? tabs[0].id : tabs[3].id);
const [showView, setShowView] = useState<"start" | "embed" | "panel">("start");
const [surveyUrl, setSurveyUrl] = useState(webAppUrl + "/s/" + survey.id);
const [surveyUrl, setSurveyUrl] = useState("");
useEffect(() => {
if (survey.type !== "link") {

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

@@ -16,12 +16,8 @@ interface LinkTabProps {
export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }: LinkTabProps) => {
const { t } = useTranslate();
const docsLinks = [
{
title: t("environments.surveys.summary.identify_users"),
description: t("environments.surveys.summary.identify_users_description"),
link: "https://formbricks.com/docs/link-surveys/user-identification",
},
{
title: t("environments.surveys.summary.data_prefilling"),
description: t("environments.surveys.summary.data_prefilling_description"),
@@ -53,6 +49,7 @@ export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }:
locale={locale}
/>
</div>
<div className="flex flex-wrap justify-between gap-2">
<p className="pt-2 font-semibold text-slate-700">
{t("environments.surveys.summary.you_can_do_a_lot_more_with_links_surveys")} 💡

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

@@ -0,0 +1,36 @@
import { Options } from "qr-code-styling";
export const getQRCodeOptions = (width: number, height: number): Options => ({
width,
height,
type: "svg",
data: "",
margin: 0,
qrOptions: {
typeNumber: 0,
mode: "Byte",
errorCorrectionLevel: "L",
},
imageOptions: {
saveAsBlob: true,
hideBackgroundDots: false,
imageSize: 0,
margin: 0,
},
dotsOptions: {
type: "extra-rounded",
color: "#000000",
roundSize: true,
},
backgroundOptions: {
color: "#ffffff",
},
cornersSquareOptions: {
type: "dot",
color: "#000000",
},
cornersDotOptions: {
type: "dot",
color: "#000000",
},
});

View File

@@ -0,0 +1,44 @@
"use client";
import { getQRCodeOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options";
import { useTranslate } from "@tolgee/react";
import QRCodeStyling from "qr-code-styling";
import { useEffect, useRef } from "react";
import { toast } from "react-hot-toast";
export const useSurveyQRCode = (surveyUrl: string) => {
const qrCodeRef = useRef<HTMLDivElement>(null);
const qrInstance = useRef<QRCodeStyling | null>(null);
const { t } = useTranslate();
useEffect(() => {
try {
if (!qrInstance.current) {
qrInstance.current = new QRCodeStyling(getQRCodeOptions(70, 70));
}
if (surveyUrl && qrInstance.current) {
qrInstance.current.update({ data: surveyUrl });
if (qrCodeRef.current) {
qrCodeRef.current.innerHTML = "";
qrInstance.current.append(qrCodeRef.current);
}
}
} catch (error) {
toast.error(t("environments.surveys.summary.failed_to_generate_qr_code"));
}
}, [surveyUrl]);
const downloadQRCode = () => {
try {
const downloadInstance = new QRCodeStyling(getQRCodeOptions(500, 500));
downloadInstance.update({ data: surveyUrl });
downloadInstance.download({ name: "survey-qr", extension: "png" });
} catch (error) {
toast.error(t("environments.surveys.summary.failed_to_generate_qr_code"));
}
};
return { qrCodeRef, downloadQRCode };
};

View File

@@ -380,7 +380,7 @@ export const getQuestionSummary = async (
let hasValidAnswer = false;
if (Array.isArray(answer)) {
if (Array.isArray(answer) && question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
answer.forEach((value) => {
if (value) {
totalSelectionCount++;
@@ -396,7 +396,10 @@ export const getQuestionSummary = async (
hasValidAnswer = true;
}
});
} else if (typeof answer === "string") {
} else if (
typeof answer === "string" &&
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle
) {
if (answer) {
totalSelectionCount++;
if (questionChoices.includes(answer)) {

View File

@@ -0,0 +1,92 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { getServerSession } from "next-auth";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getUser } from "@formbricks/lib/user/service";
import { TUser } from "@formbricks/types/user";
import AppLayout from "./layout";
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@formbricks/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@formbricks/lib/constants", () => ({
INTERCOM_SECRET_KEY: "test-secret-key",
IS_INTERCOM_CONFIGURED: true,
INTERCOM_APP_ID: "test-app-id",
ENCRYPTION_KEY: "test-encryption-key",
ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key",
GITHUB_ID: "test-github-id",
GITHUB_SECRET: "test-githubID",
GOOGLE_CLIENT_ID: "test-google-client-id",
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
AZUREAD_CLIENT_ID: "test-azuread-client-id",
AZUREAD_CLIENT_SECRET: "test-azure",
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
OIDC_DISPLAY_NAME: "test-oidc-display-name",
OIDC_CLIENT_ID: "test-oidc-client-id",
OIDC_ISSUER: "test-oidc-issuer",
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
WEBAPP_URL: "test-webapp-url",
}));
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
FormbricksClient: () => <div data-testid="formbricks-client" />,
}));
vi.mock("@/app/intercom/IntercomClientWrapper", () => ({
IntercomClientWrapper: () => <div data-testid="mock-intercom-wrapper" />,
}));
vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
NoMobileOverlay: () => <div data-testid="no-mobile-overlay" />,
}));
vi.mock("@/modules/ui/components/post-hog-client", () => ({
PHProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="ph-provider">{children}</div>
),
PostHogPageview: () => <div data-testid="ph-pageview" />,
}));
vi.mock("@/modules/ui/components/toaster-client", () => ({
ToasterClient: () => <div data-testid="toaster-client" />,
}));
describe("(app) AppLayout", () => {
afterEach(() => {
cleanup();
});
it("renders child content and all sub-components when user exists", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
// Because AppLayout is async, call it like a function
const element = await AppLayout({
children: <div data-testid="child-content">Hello from children</div>,
});
render(element);
expect(screen.getByTestId("no-mobile-overlay")).toBeInTheDocument();
expect(screen.getByTestId("ph-pageview")).toBeInTheDocument();
expect(screen.getByTestId("ph-provider")).toBeInTheDocument();
expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("toaster-client")).toBeInTheDocument();
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children");
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
});
it("skips FormbricksClient if no user is present", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
const element = await AppLayout({
children: <div data-testid="child-content">Hello from children</div>,
});
render(element);
expect(screen.queryByTestId("formbricks-client")).not.toBeInTheDocument();
});
});

View File

@@ -1,12 +1,11 @@
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
import { IntercomClient } from "@/app/IntercomClient";
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getServerSession } from "next-auth";
import { Suspense } from "react";
import { INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
import { getUser } from "@formbricks/lib/user/service";
const AppLayout = async ({ children }) => {
@@ -22,11 +21,7 @@ const AppLayout = async ({ children }) => {
<PHProvider>
<>
{user ? <FormbricksClient userId={user.id} email={user.email} /> : null}
<IntercomClient
isIntercomConfigured={IS_INTERCOM_CONFIGURED}
intercomSecretKey={INTERCOM_SECRET_KEY}
user={user}
/>
<IntercomClientWrapper user={user} />
<ToasterClient />
{children}
</>

View File

@@ -0,0 +1,34 @@
import "@testing-library/jest-dom/vitest";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import AppLayout from "../(auth)/layout";
vi.mock("@formbricks/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_INTERCOM_CONFIGURED: true,
INTERCOM_SECRET_KEY: "mock-intercom-secret-key",
INTERCOM_APP_ID: "mock-intercom-app-id",
}));
vi.mock("@/app/intercom/IntercomClientWrapper", () => ({
IntercomClientWrapper: () => <div data-testid="mock-intercom-wrapper" />,
}));
vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
NoMobileOverlay: () => <div data-testid="mock-no-mobile-overlay" />,
}));
describe("(auth) AppLayout", () => {
it("renders the NoMobileOverlay and IntercomClient, plus children", async () => {
const appLayoutElement = await AppLayout({
children: <div data-testid="child-content">Hello from children!</div>,
});
const childContentText = "Hello from children!";
render(appLayoutElement);
expect(screen.getByTestId("mock-no-mobile-overlay")).toBeInTheDocument();
expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument();
expect(screen.getByTestId("child-content")).toHaveTextContent(childContentText);
});
});

View File

@@ -1,12 +1,11 @@
import { IntercomClient } from "@/app/IntercomClient";
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
import { INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
const AppLayout = async ({ children }) => {
return (
<>
<NoMobileOverlay />
<IntercomClient isIntercomConfigured={IS_INTERCOM_CONFIGURED} intercomSecretKey={INTERCOM_SECRET_KEY} />
<IntercomClientWrapper />
{children}
</>
);

View File

@@ -1,8 +1,8 @@
import { hasOrganizationAccess } from "@/app/lib/api/apiHelper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { notFound } from "next/navigation";
import { hasOrganizationAccess } from "@formbricks/lib/auth";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -16,7 +16,7 @@ export const GET = async (_: Request, context: { params: Promise<{ organizationI
// check auth
const session = await getServerSession(authOptions);
if (!session) throw new AuthenticationError("Not authenticated");
const hasAccess = await hasOrganizationAccess(session.user, organizationId);
const hasAccess = await hasOrganizationAccess(session.user.id, organizationId);
if (!hasAccess) throw new AuthorizationError("Unauthorized");
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organizationId);

View File

@@ -1,7 +1,7 @@
import { hasOrganizationAccess } from "@/app/lib/api/apiHelper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { hasOrganizationAccess } from "@formbricks/lib/auth";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getProject } from "@formbricks/lib/project/service";
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
@@ -15,7 +15,7 @@ export const GET = async (_: Request, context: { params: Promise<{ projectId: st
if (!session) throw new AuthenticationError("Not authenticated");
const project = await getProject(projectId);
if (!project) return notFound();
const hasAccess = await hasOrganizationAccess(session.user, project.organizationId);
const hasAccess = await hasOrganizationAccess(session.user.id, project.organizationId);
if (!hasAccess) throw new AuthorizationError("Unauthorized");
// redirect to project's production environment
const environments = await getEnvironments(project.id);

View File

@@ -2,6 +2,7 @@ import { getMetadataForLinkSurvey } from "@/modules/survey/link/metadata";
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { getShortUrl } from "@formbricks/lib/shortUrl/service";
import { logger } from "@formbricks/logger";
import { TShortUrl, ZShortUrlId } from "@formbricks/types/short-url";
export const generateMetadata = async (props): Promise<Metadata> => {
@@ -44,7 +45,7 @@ const Page = async (props) => {
try {
shortUrl = await getShortUrl(params.shortUrlId);
} catch (error) {
console.error(error);
logger.error(error, "Could not fetch short url");
notFound();
}

View File

@@ -3,6 +3,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { AsyncParser } from "@json2csv/node";
import { getServerSession } from "next-auth";
import { NextRequest } from "next/server";
import { logger } from "@formbricks/logger";
export const POST = async (request: NextRequest) => {
const session = await getServerSession(authOptions);
@@ -28,7 +29,7 @@ export const POST = async (request: NextRequest) => {
try {
csv = await parser.parse(json).promise();
} catch (err) {
console.error(err);
logger.error({ error: err, url: request.url }, "Failed to convert to CSV");
throw new Error("Failed to convert to CSV");
}

View File

@@ -4,6 +4,7 @@ import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { doesSurveyHasOpenTextQuestion } from "@formbricks/lib/survey/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse } from "@formbricks/types/responses";
@@ -80,7 +81,7 @@ export const generateInsightsEnabledForSurveyQuestions = async (
return { success: false };
} catch (error) {
console.error("Error generating insights for surveys:", error);
logger.error(error, "Error generating insights for surveys");
throw error;
}
};

View File

@@ -5,6 +5,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { headers } from "next/headers";
import { z } from "zod";
import { CRON_SECRET } from "@formbricks/lib/constants";
import { logger } from "@formbricks/logger";
import { generateInsightsEnabledForSurveyQuestions } from "./lib/utils";
export const maxDuration = 300; // This function can run for a maximum of 300 seconds
@@ -25,7 +26,7 @@ export const POST = async (request: Request) => {
const inputValidation = ZGenerateInsightsInput.safeParse(jsonInput);
if (!inputValidation.success) {
console.error(inputValidation.error);
logger.error({ error: inputValidation.error, url: request.url }, "Error in POST /api/insights");
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),

View File

@@ -9,6 +9,7 @@ import { writeDataToSlack } from "@formbricks/lib/slack/service";
import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime";
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
import { truncateText } from "@formbricks/lib/utils/strings";
import { logger } from "@formbricks/logger";
import { Result } from "@formbricks/types/error-handlers";
import { TIntegration, TIntegrationType } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
@@ -83,13 +84,13 @@ export const handleIntegrations = async (
survey
);
if (!googleResult.ok) {
console.error("Error in google sheets integration: ", googleResult.error);
logger.error(googleResult.error, "Error in google sheets integration");
}
break;
case "slack":
const slackResult = await handleSlackIntegration(integration as TIntegrationSlack, data, survey);
if (!slackResult.ok) {
console.error("Error in slack integration: ", slackResult.error);
logger.error(slackResult.error, "Error in slack integration");
}
break;
case "airtable":
@@ -99,13 +100,13 @@ export const handleIntegrations = async (
survey
);
if (!airtableResult.ok) {
console.error("Error in airtable integration: ", airtableResult.error);
logger.error(airtableResult.error, "Error in airtable integration");
}
break;
case "notion":
const notionResult = await handleNotionIntegration(integration as TIntegrationNotion, data, survey);
if (!notionResult.ok) {
console.error("Error in notion integration: ", notionResult.error);
logger.error(notionResult.error, "Error in notion integration");
}
break;
}
@@ -418,7 +419,7 @@ const getValue = (colType: string, value: string | string[] | Date | number | Re
return typeof value === "string" ? value : (value as string[]).join(", ");
}
} catch (error) {
console.error(error);
logger.error(error, "Payload build failed!");
throw new Error("Payload build failed!");
}
};

View File

@@ -1,6 +1,7 @@
import { sendFollowUpEmail } from "@/modules/email";
import { z } from "zod";
import { TSurveyFollowUpAction } from "@formbricks/database/types/survey-follow-up";
import { logger } from "@formbricks/logger";
import { TOrganization } from "@formbricks/types/organizations";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -89,6 +90,6 @@ export const sendSurveyFollowUps = async (
.map((result) => `FollowUp ${result.followUpId} failed: ${result.error}`);
if (errors.length > 0) {
console.error("Follow-up processing errors:", errors);
logger.error(errors, "Follow-up processing errors");
}
};

View File

@@ -19,6 +19,7 @@ import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { convertDatesInObject } from "@formbricks/lib/time";
import { getPromptText } from "@formbricks/lib/utils/ai";
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
import { logger } from "@formbricks/logger";
import { handleIntegrations } from "./lib/handleIntegrations";
export const POST = async (request: Request) => {
@@ -34,7 +35,10 @@ export const POST = async (request: Request) => {
const inputValidation = ZPipelineInput.safeParse(convertedJsonInput);
if (!inputValidation.success) {
console.error(inputValidation.error);
logger.error(
{ error: inputValidation.error, url: request.url },
"Error in POST /api/(internal)/pipeline"
);
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
@@ -87,7 +91,7 @@ export const POST = async (request: Request) => {
data: response,
}),
}).catch((error) => {
console.error(`Webhook call to ${webhook.url} failed:`, error);
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
})
);
@@ -100,7 +104,7 @@ export const POST = async (request: Request) => {
]);
if (!survey) {
console.error(`Survey with id ${surveyId} not found`);
logger.error({ url: request.url, surveyId }, `Survey with id ${surveyId} not found`);
return new Response("Survey not found", { status: 404 });
}
@@ -172,7 +176,10 @@ export const POST = async (request: Request) => {
const emailPromises = usersWithNotifications.map((user) =>
sendResponseFinishedEmail(user.email, environmentId, survey, response, responseCount).catch((error) => {
console.error(`Failed to send email to ${user.email}:`, error);
logger.error(
{ error, url: request.url, userEmail: user.email },
`Failed to send email to ${user.email}`
);
})
);
@@ -188,7 +195,7 @@ export const POST = async (request: Request) => {
const results = await Promise.allSettled([...webhookPromises, ...emailPromises]);
results.forEach((result) => {
if (result.status === "rejected") {
console.error("Promise rejected:", result.reason);
logger.error({ error: result.reason, url: request.url }, "Promise rejected");
}
});
@@ -228,7 +235,7 @@ export const POST = async (request: Request) => {
text,
});
} catch (e) {
console.error(e);
logger.error({ error: e, url: request.url }, "Error creating document and assigning insight");
}
}
}
@@ -240,7 +247,7 @@ export const POST = async (request: Request) => {
const results = await Promise.allSettled(webhookPromises);
results.forEach((result) => {
if (result.status === "rejected") {
console.error("Promise rejected:", result.reason);
logger.error({ error: result.reason, url: request.url }, "Promise rejected");
}
});
}

View File

@@ -1,16 +1,19 @@
import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key";
import { responses } from "@/app/lib/api/response";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { TAuthenticationApiKey } from "@formbricks/types/auth";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getEnvironmentIdFromApiKey } from "./lib/api-key";
export const authenticateRequest = async (request: Request): Promise<TAuthenticationApiKey | null> => {
const apiKey = request.headers.get("x-api-key");
if (apiKey) {
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
if (environmentId) {
const hashedApiKey = hashApiKey(apiKey);
const authentication: TAuthenticationApiKey = {
type: "apiKey",
environmentId,
hashedApiKey,
};
return authentication;
}

View File

@@ -21,6 +21,7 @@ import {
} from "@formbricks/lib/posthogServer";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { logger } from "@formbricks/logger";
import { ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -103,7 +104,7 @@ export const GET = async (
},
});
} catch (error) {
console.error(`Error sending plan limits reached event to Posthog: ${error}`);
logger.error({ error, url: request.url }, `Error sending plan limits reached event to Posthog`);
}
}
}
@@ -187,7 +188,10 @@ export const GET = async (
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);
logger.error(
{ error, url: request.url },
"Error in GET /api/v1/client/[environmentId]/app/sync/[userId]"
);
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
}
};

View File

@@ -14,6 +14,7 @@ import { getSurveys } from "@formbricks/lib/survey/service";
import { anySurveyHasFilters } from "@formbricks/lib/survey/utils";
import { diffInDays } from "@formbricks/lib/utils/datetime";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -150,7 +151,7 @@ export const getSyncSurveys = reactCache(
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error(error);
throw new DatabaseError(error.message);
}

View File

@@ -2,6 +2,7 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
import { logger } from "@formbricks/logger";
import { ZDisplayCreateInput } from "@formbricks/types/displays";
import { InvalidInputError } from "@formbricks/types/errors";
import { createDisplay } from "./lib/display";
@@ -48,7 +49,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error in POST /api/v1/client/[environmentId]/displays");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -15,6 +15,7 @@ import {
} from "@formbricks/lib/posthogServer";
import { projectCache } from "@formbricks/lib/project/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TJsEnvironmentState } from "@formbricks/types/js";
import { getActionClassesForEnvironmentState } from "./actionClass";
@@ -89,7 +90,7 @@ export const getEnvironmentState = async (
},
});
} catch (err) {
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}

View File

@@ -4,6 +4,7 @@ import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { projectCache } from "@formbricks/lib/project/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TJsEnvironmentStateProject } from "@formbricks/types/js";
@@ -35,7 +36,7 @@ export const getProjectForEnvironmentState = reactCache(
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error(error, "Error getting project for environment state");
throw new DatabaseError(error.message);
}
throw error;

View File

@@ -5,6 +5,7 @@ import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
@@ -80,7 +81,7 @@ export const getSurveysForEnvironmentState = reactCache(
return surveysPrisma.map((survey) => transformPrismaSurvey<TJsEnvironmentStateSurvey>(survey));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error(error, "Error getting surveys for environment state");
throw new DatabaseError(error.message);
}
throw error;

View File

@@ -3,6 +3,7 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { NextRequest } from "next/server";
import { environmentCache } from "@formbricks/lib/environment/cache";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZJsSyncInput } from "@formbricks/types/js";
@@ -11,7 +12,7 @@ export const OPTIONS = async (): Promise<Response> => {
};
export const GET = async (
_: NextRequest,
request: NextRequest,
props: {
params: Promise<{
environmentId: string;
@@ -58,11 +59,14 @@ export const GET = async (
return responses.notFoundResponse(err.resourceType, err.resourceId);
}
console.error(err);
logger.error(
{ error: err, url: request.url },
"Error in GET /api/v1/client/[environmentId]/environment"
);
return responses.internalServerErrorResponse(err.message, true);
}
} catch (error) {
console.error(error);
logger.error({ error, url: request.url }, "Error in GET /api/v1/client/[environmentId]/environment");
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
}
};

View File

@@ -3,6 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { updateResponse } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZResponseUpdateInput } from "@formbricks/types/responses";
@@ -45,7 +46,10 @@ export const PUT = async (
return responses.badRequestResponse(error.message);
}
if (error instanceof DatabaseError) {
console.error(error);
logger.error(
{ error, url: request.url },
"Error in PUT /api/v1/client/[environmentId]/responses/[responseId]"
);
return responses.internalServerErrorResponse(error.message);
}
}
@@ -59,7 +63,10 @@ export const PUT = async (
return responses.badRequestResponse(error.message);
}
if (error instanceof DatabaseError) {
console.error(error);
logger.error(
{ error, url: request.url },
"Error in PUT /api/v1/client/[environmentId]/responses/[responseId]"
);
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -12,6 +12,7 @@ import { calculateTtcTotal } from "@formbricks/lib/response/utils";
import { responseNoteCache } from "@formbricks/lib/responseNote/cache";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
@@ -178,7 +179,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
});
} catch (err) {
// Log error but do not throw
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
}

View File

@@ -6,6 +6,7 @@ import { headers } from "next/headers";
import { UAParser } from "ua-parser-js";
import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
@@ -107,7 +108,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error creating response");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -9,6 +9,7 @@ import { validateLocalSignedUrl } from "@formbricks/lib/crypto";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { putFileToLocalStorage } from "@formbricks/lib/storage/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
interface Context {
params: Promise<{
@@ -125,7 +126,7 @@ export const POST = async (req: NextRequest, context: Context): Promise<Response
message: "File uploaded successfully",
});
} catch (err) {
console.error("err: ", err);
logger.error({ error: err, url: req.url }, "Error in POST /api/v1/client/[environmentId]/upload");
if (err.name === "FileTooLargeError") {
return responses.badRequestResponse(err.message);
}

View File

@@ -7,6 +7,7 @@ import { fetchAirtableAuthToken } from "@formbricks/lib/airtable/service";
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { createOrUpdateIntegration } from "@formbricks/lib/integration/service";
import { logger } from "@formbricks/logger";
const getEmail = async (token: string) => {
const req_ = await fetch("https://api.airtable.com/v0/meta/whoami", {
@@ -77,7 +78,7 @@ export const GET = async (req: NextRequest) => {
await createOrUpdateIntegration(environmentId, airtableIntegrationInput);
return Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/airtable`);
} catch (error) {
console.error(error);
logger.error({ error, url: req.url }, "Error in GET /api/v1/integrations/airtable/callback");
responses.internalServerErrorResponse(error);
}
responses.badRequestResponse("unknown error occurred");

View File

@@ -6,8 +6,7 @@ import { cache } from "@formbricks/lib/cache";
import { getHash } from "@formbricks/lib/crypto";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZString } from "@formbricks/types/common";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { InvalidInputError } from "@formbricks/types/errors";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Promise<string | null> => {
const hashedKey = getHash(apiKey);
@@ -42,7 +41,7 @@ export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Pro
throw error;
}
},
[`getEnvironmentIdFromApiKey-${apiKey}`],
[`management-api-getEnvironmentIdFromApiKey-${apiKey}`],
{
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
}

View File

@@ -2,6 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
import { logger } from "@formbricks/logger";
import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes";
import { TAuthenticationApiKey } from "@formbricks/types/auth";
@@ -54,7 +55,7 @@ export const PUT = async (
try {
actionClassUpdate = await request.json();
} catch (error) {
console.error(`Error parsing JSON: ${error}`);
logger.error({ error, url: request.url }, "Error parsing JSON");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}

View File

@@ -2,6 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createActionClass, getActionClasses } from "@formbricks/lib/actionClass/service";
import { logger } from "@formbricks/logger";
import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes";
import { DatabaseError } from "@formbricks/types/errors";
@@ -28,7 +29,7 @@ export const POST = async (request: Request): Promise<Response> => {
try {
actionClassInput = await request.json();
} catch (error) {
console.error(`Error parsing JSON input: ${error}`);
logger.error({ error, url: request.url }, "Error parsing JSON input");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}

View File

@@ -0,0 +1,15 @@
import { authOptions } from "@/modules/auth/lib/authOptions";
import { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import { getServerSession } from "next-auth";
export const getSessionUser = async (req?: NextApiRequest, res?: NextApiResponse) => {
// check for session (browser usage)
let session: Session | null;
if (req && res) {
session = await getServerSession(req, res, authOptions);
} else {
session = await getServerSession(authOptions);
}
if (session && "user" in session) return session.user;
};

View File

@@ -1,4 +1,5 @@
import { getSessionUser, hashApiKey } from "@/app/lib/api/apiHelper";
import { getSessionUser } from "@/app/api/v1/management/me/lib/utils";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";

View File

@@ -4,6 +4,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { TResponse, ZResponseUpdateInput } from "@formbricks/types/responses";
const fetchAndValidateResponse = async (authentication: any, responseId: string): Promise<TResponse> => {
@@ -77,7 +78,7 @@ export const PUT = async (
try {
responseUpdate = await request.json();
} catch (error) {
console.error(`Error parsing JSON: ${error}`);
logger.error({ error, url: request.url }, "Error parsing JSON");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}

View File

@@ -12,6 +12,7 @@ import { calculateTtcTotal } from "@formbricks/lib/response/utils";
import { responseNoteCache } from "@formbricks/lib/responseNote/cache";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
@@ -178,7 +179,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
});
} catch (err) {
// Log error but do not throw
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
logger.error(err, "Error sending plan limits reached event to Posthog");
}
}
}

View File

@@ -4,6 +4,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { NextRequest } from "next/server";
import { getResponses, getResponsesByEnvironmentId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
import { createResponse } from "./lib/response";
@@ -45,7 +46,7 @@ export const POST = async (request: Request): Promise<Response> => {
try {
jsonInput = await request.json();
} catch (err) {
console.error(`Error parsing JSON input: ${err}`);
logger.error({ error: err, url: request.url }, "Error parsing JSON input");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}
@@ -92,7 +93,7 @@ export const POST = async (request: Request): Promise<Response> => {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
logger.error({ error, url: request.url }, "Error in POST /api/v1/management/responses");
return responses.internalServerErrorResponse(error.message);
}
}

View File

@@ -3,6 +3,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { NextRequest } from "next/server";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { logger } from "@formbricks/logger";
import { getSignedUrlForPublicFile } from "./lib/getSignedUrl";
// api endpoint for uploading public files
@@ -17,7 +18,7 @@ export const POST = async (req: NextRequest): Promise<Response> => {
try {
storageInput = await req.json();
} catch (error) {
console.error(`Error parsing JSON input: ${error}`);
logger.error({ error, url: req.url }, "Error parsing JSON input");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}

View File

@@ -5,6 +5,7 @@ import { segmentCache } from "@formbricks/lib/cache/segment";
import { responseCache } from "@formbricks/lib/response/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { DatabaseError } from "@formbricks/types/errors";
export const deleteSurvey = async (surveyId: string) => {
@@ -67,7 +68,7 @@ export const deleteSurvey = async (surveyId: string) => {
return deletedSurvey;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
logger.error({ error, surveyId }, "Error deleting survey");
throw new DatabaseError(error.message);
}

View File

@@ -6,6 +6,7 @@ import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils
import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { TSurvey, ZSurveyUpdateInput } from "@formbricks/types/surveys/types";
const fetchAndAuthorizeSurvey = async (authentication: any, surveyId: string): Promise<TSurvey | null> => {
@@ -79,7 +80,7 @@ export const PUT = async (
try {
surveyUpdate = await request.json();
} catch (error) {
console.error(`Error parsing JSON input: ${error}`);
logger.error({ error, url: request.url }, "Error parsing JSON input");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}

View File

@@ -5,6 +5,7 @@ import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils
import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
import { logger } from "@formbricks/logger";
import { DatabaseError } from "@formbricks/types/errors";
import { ZSurveyCreateInput } from "@formbricks/types/surveys/types";
@@ -41,7 +42,7 @@ export const POST = async (request: Request): Promise<Response> => {
try {
surveyInput = await request.json();
} catch (error) {
console.error(`Error parsing JSON: ${error}`);
logger.error({ error, url: request.url }, "Error parsing JSON");
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}

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