Compare commits

..

1 Commits

Author SHA1 Message Date
Piyush Gupta
eae5d67a2b fix: weekly summary 2025-02-26 10:34:33 +05:30
698 changed files with 4816 additions and 32293 deletions

View File

@@ -1,56 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
# **/node_modules
.pnp
.pnp.js
.pnpm-store/
# testing
**/coverage
coverage
# next.js
**/.next/
**/out/
**/.next
**/out
**/build
# node
**/dist/
**/dist
# misc
**/.DS_Store
.DS_Store
*.pem
Zone.Identifier
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
**/.env
**/.env.local
**/.env.development.local
**/.env.test.local
**/.env.production.local
!packages/database/.env
!apps/web/.env
# build tools
# turbo
.turbo
**/*vite.config.*.timestamp-*
# environment specific
# nixos stuff
.direnv
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
.vscode
.github
**/.turbo
# project specific
packages/lib/uploads
apps/web/public/js
packages/database/migrations
branch.json
.env

View File

@@ -39,7 +39,6 @@ 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)
@@ -97,9 +96,6 @@ 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 #
##########
@@ -134,9 +130,6 @@ AZUREAD_TENANT_ID=
# OIDC_DISPLAY_NAME=
# OIDC_SIGNING_ALGORITHM=
# Configure SAML SSO
# SAML_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/formbricks-saml
# Configure this when you want to ship JS & CSS files from a complete URL instead of the current domain
# ASSET_PREFIX_URL=
@@ -188,14 +181,11 @@ ENTERPRISE_LICENSE_KEY=
UNSPLASH_ACCESS_KEY=
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
REDIS_URL=redis://localhost:6379
# REDIS_URL=redis://localhost:6379
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
# REDIS_HTTP_URL:
# 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
@@ -206,9 +196,4 @@ UNKEY_ROOT_KEY=
# AI_AZURE_LLM_DEPLOYMENT_ID=
# NEXT_PUBLIC_INTERCOM_APP_ID=
# INTERCOM_SECRET_KEY=
# Enable Prometheus metrics
# PROMETHEUS_ENABLED=
# PROMETHEUS_EXPORTER_PORT=
# INTERCOM_SECRET_KEY=

View File

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

View File

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

View File

@@ -12,12 +12,7 @@ jobs:
timeout-minutes: 30
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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- name: Build & Cache Web Binaries

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,21 +43,16 @@ jobs:
--health-timeout=5s
--health-retries=5
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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
uses: actions/setup-node@v3
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
@@ -89,7 +84,7 @@ jobs:
- name: Run App
run: |
NODE_ENV=test pnpm start --filter=@formbricks/web | tee app.log 2>&1 &
NODE_ENV=test pnpm start --filter=@formbricks/web &
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
@@ -117,7 +112,7 @@ jobs:
- name: Azure login
if: env.AZURE_ENABLED == 'true'
uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -135,19 +130,9 @@ jobs:
run: |
pnpm test:e2e
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
- uses: actions/upload-artifact@v4
if: always()
with:
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

@@ -4,9 +4,6 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
labeler:
name: Pull Request Labeler
@@ -15,12 +12,7 @@ jobs:
pull-requests: write
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/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481

View File

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

View File

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

62
.github/workflows/prepare-release.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,12 +16,7 @@ jobs:
name: PR title
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: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -40,7 +35,7 @@ jobs:
revert
ossgg
- uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
@@ -59,7 +54,7 @@ jobs:
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
message: |

View File

@@ -6,7 +6,6 @@ on:
- main
pull_request:
types: [opened, synchronize, reopened]
merge_group:
permissions:
contents: read
jobs:
@@ -14,12 +13,7 @@ jobs:
name: SonarQube
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 # v4.2.2
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

View File

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

View File

@@ -1,9 +1,6 @@
name: Tests
on:
workflow_call:
permissions:
contents: read
jobs:
build:
name: Unit Tests
@@ -13,21 +10,16 @@ jobs:
contents: read
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@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
uses: actions/setup-node@v3
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64

View File

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

View File

@@ -3,8 +3,7 @@ permissions:
contents: read
on:
pull_request_target:
types: [closed]
push:
branches:
- main
@@ -12,36 +11,13 @@ jobs:
tag-production-keys:
name: Tag Production Keys
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # This ensures we get the full git history
- name: Get source branch name
id: branch-name
run: |
RAW_BRANCH="${{ github.head_ref }}"
SOURCE_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9._\/-]//g')
# 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"
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: actions/setup-node@v4
with:
node-version: 18 # Ensure compatibility with your project
@@ -50,12 +26,13 @@ jobs:
- name: Tag Production Keys
run: |
BRANCH_NAME=${GITHUB_REF##*/}
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-extracted \
--filter-tag "draft:${SOURCE_BRANCH}" \
--filter-tag "draft:${BRANCH_NAME}" \
--tag production \
--untag "draft:${SOURCE_BRANCH}"
--untag "draft:${BRANCH_NAME}"
- name: Tag unused production keys as Deprecated
run: |
@@ -66,10 +43,11 @@ jobs:
- name: Tag unused draft:current-branch keys as Deprecated
run: |
BRANCH_NAME=${GITHUB_REF##*/}
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
--filter-not-extracted --filter-tag "draft:${BRANCH_NAME}" \
--tag deprecated --untag "draft:${BRANCH_NAME}"
- name: Sync with backup
run: |
@@ -80,7 +58,7 @@ jobs:
--yes
- name: Upload backup as artifact
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
uses: actions/upload-artifact@v4
with:
name: tolgee-backup-${{ github.sha }}
path: ./tolgee-backup

View File

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

61
.gitignore vendored
View File

@@ -1,26 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
node_modules
.pnp
.pnp.js
.pnpm-store/
# testing
**/coverage
coverage
# next.js
**/.next/
**/out/
**/build
.next/
out/
build
# node
**/dist/
dist/
# misc
**/.DS_Store
.DS_Store
*.pem
Zone.Identifier
# debug
npm-debug.log*
@@ -28,47 +27,39 @@ yarn-debug.log*
yarn-error.log*
# local env files
**/.env
**/.env.local
**/.env.development.local
**/.env.test.local
**/.env.production.local
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
!packages/database/.env
!apps/web/.env
# build tools
# turbo
.turbo
**/*vite.config.*.timestamp-*
# environment specific
# nixos stuff
.direnv
Zone.Identifier
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# project specific
# uploads
packages/lib/uploads
# Vite Timestamps
*vite.config.*.timestamp-*
# js compiled assets
apps/web/public/js
packages/database/migrations
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
# tolgee
branch.json

View File

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

View File

@@ -1,2 +1 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json
prettier --write ./branch.json
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json

1
.husky/post-commit Normal file
View File

@@ -0,0 +1 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json

View File

@@ -3,19 +3,15 @@
# Load environment variables from .env files
if [ -f .env ]; then
set -a
. .env
set +a
export $(cat .env | grep -v '#' | xargs)
fi
pnpm lint-staged
# Run tolgee-pull if branch.json exists and NEXT_PUBLIC_TOLGEE_API_KEY is not set
if [ -f branch.json ]; then
if [ -z "$NEXT_PUBLIC_TOLGEE_API_KEY" ]; then
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
else
pnpm run tolgee-pull
git add packages/lib/messages
fi
fi
if [ -z "$NEXT_PUBLIC_TOLGEE_API_KEY" ]; then
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
else
pnpm run tolgee-pull
git add packages/lib/messages
fi

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
vercel.json

View File

@@ -27,10 +27,6 @@
{
"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,8 +6,6 @@
"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
"vitest.explorer", // run tests directly from the code window
"sonarsource.sonarlint-vscode" // sonarqube linter for vscode
"yzhang.markdown-all-in-one" // nicer markdown support
]
}

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/", "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 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 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 @@
<h3 align="center">Formbricks</h3>
<p align="center">
The Open Source Qualtrics Alternative
Harvest user-insights, build irresistible experiences.
<br />
<a href="https://formbricks.com/">Website</a>
</p>

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.1",
"esbuild": "0.25.0",
"eslint-plugin-storybook": "0.11.1",
"prop-types": "15.8.1",
"storybook": "8.4.7",

3
apps/web/.gitignore vendored
View File

@@ -48,6 +48,3 @@ uploads/
# Sentry Config File
.sentryclirc
# SAML Preloaded Connections
saml-connection/

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
FROM node:22-alpine3.20 AS base
#
## step 1: Prune monorepo
@@ -33,9 +33,6 @@ ENV CRON_SECRET="placeholder_for_cron_secret_of_64_chars_get_overwritten_at_runt
ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN
# Increase Node.js memory limit
# ENV NODE_OPTIONS="--max_old_space_size=4096"
# Set the working directory
WORKDIR /app
@@ -44,17 +41,19 @@ WORKDIR /app
# COPY --from=builder /app/out/json/ .
# COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
# Install the dependencies
# RUN pnpm install
# Prepare the build
COPY . .
# Create a .env file
RUN touch apps/web/.env
# Install the dependencies
RUN pnpm install
# Build the project
RUN NODE_OPTIONS="--max_old_space_size=4096" pnpm build --filter=@formbricks/web...
# RUN pnpm post-install --filter=@formbricks/web...
RUN pnpm build --filter=@formbricks/web...
# Extract Prisma version
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
@@ -77,7 +76,6 @@ WORKDIR /home/nextjs
COPY --from=installer /app/apps/web/next.config.mjs .
COPY --from=installer /app/apps/web/package.json .
# Leverage output traces to reduce image size
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
@@ -107,16 +105,6 @@ ENV HOSTNAME "0.0.0.0"
RUN mkdir -p /home/nextjs/apps/web/uploads/
VOLUME /home/nextjs/apps/web/uploads/
# Prepare volume for SAML preloaded connection
RUN mkdir -p /home/nextjs/apps/web/saml-connection
VOLUME /home/nextjs/apps/web/saml-connection
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; \
CMD supercronic -quiet /app/docker/cronjobs & \
(cd packages/database && npm run db:migrate:deploy) && \
(cd packages/database && npm run db:create-saml-database:deploy) && \
exec node apps/web/server.js

View File

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

View File

@@ -1,5 +1,4 @@
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";
@@ -16,6 +15,7 @@ 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 { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";

View File

@@ -1,41 +1,25 @@
"use server";
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 { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { AuthorizationError } from "@formbricks/types/errors";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
const ZGetSpreadsheetNameByIdAction = z.object({
googleSheetIntegration: ZIntegrationGoogleSheets,
environmentId: z.string(),
spreadsheetId: z.string(),
});
export async function getSpreadsheetNameByIdAction(
googleSheetIntegration: TIntegrationGoogleSheets,
environmentId: string,
spreadsheetId: string
) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
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);
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);
});
return await getSpreadsheetNameById(integrationData, spreadsheetId);
}

View File

@@ -8,7 +8,6 @@ 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";
@@ -116,18 +115,11 @@ export const AddIntegrationModal = ({
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
}
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
const spreadsheetName = await getSpreadsheetNameByIdAction(
googleSheetIntegration,
environmentId,
spreadsheetId,
});
if (!spreadsheetNameResponse?.data) {
const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse);
throw new Error(errorMessage);
}
const spreadsheetName = spreadsheetNameResponse.data;
spreadsheetId
);
setIsLinkingSheet(true);
integrationData.spreadsheetId = spreadsheetId;

View File

@@ -1,5 +1,4 @@
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";
@@ -20,6 +19,7 @@ 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 { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";

View File

@@ -1,48 +0,0 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
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 { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
export const getSurveys = reactCache(
async (environmentId: string): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
status: {
not: "completed",
},
},
select: selectSurvey,
orderBy: {
updatedAt: "desc",
},
});
return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey<TSurvey>(surveyPrisma));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getSurveys-${environmentId}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
)()
);

View File

@@ -7,7 +7,6 @@ import {
UNSUPPORTED_TYPES_BY_NOTION,
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
import NotionLogo from "@/images/notion.png";
import { getQuestionTypes } from "@/modules/survey/lib/questions";
import { Button } from "@/modules/ui/components/button";
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
import { Label } from "@/modules/ui/components/label";
@@ -20,6 +19,7 @@ import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TIntegrationInput } from "@formbricks/types/integration";
import {

View File

@@ -1,4 +1,3 @@
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";
@@ -22,6 +21,7 @@ import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getNotionDatabases } from "@formbricks/lib/notion/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";

View File

@@ -1,4 +1,3 @@
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";
@@ -15,6 +14,7 @@ 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 { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";

View File

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

View File

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

View File

@@ -9,10 +9,8 @@ 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 {
getOrganizationByEnvironmentId,
getOrganizationsWhereUserIsSingleOwner,
} from "@formbricks/lib/organization/service";
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";
@@ -73,9 +71,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
description={t("environments.settings.profile.two_factor_authentication_description")}
buttons={[
{
text: IS_FORMBRICKS_CLOUD
? t("common.start_free_trial")
: t("common.request_trial_license"),
text: t("common.start_free_trial"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${params.environmentId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",

View File

@@ -1,133 +0,0 @@
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,8 +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 React from "react";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
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";
@@ -85,7 +84,6 @@ 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

@@ -2,7 +2,6 @@
import { Badge } from "@/modules/ui/components/badge";
import { useTranslate } from "@tolgee/react";
import React from "react";
import { cn } from "@formbricks/lib/cn";
export const SettingsCard = ({

View File

@@ -1,7 +1,6 @@
"use client";
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions";
import { getSelectionColumn } from "@/modules/ui/components/data-table";
import { ResponseBadges } from "@/modules/ui/components/response-badges";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
@@ -13,6 +12,7 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { processResponseData } from "@formbricks/lib/responses";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime";
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TResponseTableData } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
@@ -200,6 +200,13 @@ 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

@@ -51,9 +51,9 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
<div className="col-span-2 pl-4 md:pl-6">{t("common.response")}</div>
<div className="px-4 md:px-6">{t("common.time")}</div>
</div>
{questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
<div
key={`${response.value}-${idx}`}
key={response.value}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="pl-4 md:pl-6">
{response.contact ? (

View File

@@ -1,9 +1,9 @@
"use client";
import { getQuestionTypes } from "@/modules/survey/lib/questions";
import { useTranslate } from "@tolgee/react";
import { InboxIcon } from "lucide-react";
import type { JSX } from "react";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";

View File

@@ -1,10 +1,10 @@
"use client";
import { getQuestionIcon } from "@/modules/survey/lib/questions";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
import { TimerIcon } from "lucide-react";
import { JSX } from "react";
import { getQuestionIcon } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";

View File

@@ -1,12 +1,11 @@
"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 = () => {
export const AppTab = ({ environmentId }) => {
const { t } = useTranslate();
const [selectedTab, setSelectedTab] = useState("webapp");
@@ -21,7 +20,79 @@ export const AppTab = () => {
handleOptionChange={(value) => setSelectedTab(value)}
/>
<div className="mt-4">{selectedTab === "webapp" ? <WebAppTab /> : <MobileAppTab />}</div>
<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>
);
};

View File

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

View File

@@ -17,6 +17,11 @@ 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"),

View File

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

View File

@@ -1,25 +0,0 @@
"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

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

View File

@@ -14,7 +14,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { Input } from "@/modules/ui/components/input";
import { useTranslate } from "@tolgee/react";
import clsx from "clsx";
import { ChevronDown, ChevronUp, X } from "lucide-react";
@@ -49,7 +48,6 @@ export const QuestionFilterComboBox = ({
const [open, setOpen] = React.useState(false);
const [openFilterValue, setOpenFilterValue] = React.useState<boolean>(false);
const commandRef = React.useRef(null);
const [searchQuery, setSearchQuery] = React.useState<string>("");
const defaultLanguageCode = "default";
useClickOutside(commandRef, () => setOpen(false));
const { t } = useTranslate();
@@ -75,12 +73,6 @@ export const QuestionFilterComboBox = ({
(type === TSurveyQuestionTypeEnum.NPS || type === TSurveyQuestionTypeEnum.Rating) &&
(filterValue === "Submitted" || filterValue === "Skipped");
const filteredOptions = options?.filter((o) =>
(typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o)
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
return (
<div className="inline-flex w-full flex-row">
{filterOptions && filterOptions?.length <= 1 ? (
@@ -168,21 +160,10 @@ export const QuestionFilterComboBox = ({
{open && (
<div className="animate-in bg-popover absolute top-0 z-10 max-h-52 w-full overflow-auto rounded-md bg-white outline-none">
<CommandList>
<div className="p-2">
<Input
type="text"
autoFocus
placeholder={t("common.search") + "..."}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-md border border-slate-300 p-2 text-sm focus:border-slate-300"
/>
</div>
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
<CommandGroup>
{filteredOptions?.map((o, index) => (
{options?.map((o) => (
<CommandItem
key={`option-${typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o}-${index}`}
onSelect={() => {
!isMultiple
? onChangeFilterComboBoxValue(

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.id, organizationId);
const hasAccess = await hasOrganizationAccess(session.user, 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.id, project.organizationId);
const hasAccess = await hasOrganizationAccess(session.user, project.organizationId);
if (!hasAccess) throw new AuthorizationError("Unauthorized");
// redirect to project's production environment
const environments = await getEnvironments(project.id);

View File

@@ -1,3 +0,0 @@
import { GET } from "@/modules/ee/auth/saml/api/authorize/route";
export { GET };

View File

@@ -1,3 +0,0 @@
import { POST } from "@/modules/ee/auth/saml/api/callback/route";
export { POST };

View File

@@ -1,3 +0,0 @@
import { POST } from "@/modules/ee/auth/saml/api/token/route";
export { POST };

View File

@@ -1,3 +0,0 @@
import { GET } from "@/modules/ee/auth/saml/api/userinfo/route";
export { GET };

View File

@@ -1,19 +1,16 @@
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

@@ -61,7 +61,6 @@ export const getSurveysForEnvironmentState = reactCache(
displayLimit: true,
displayOption: true,
hiddenFields: true,
isBackButtonHidden: true,
triggers: {
select: {
actionClass: {

View File

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

View File

@@ -1,15 +0,0 @@
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,5 +1,4 @@
import { getSessionUser } from "@/app/api/v1/management/me/lib/utils";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { getSessionUser, hashApiKey } from "@/app/lib/api/apiHelper";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";

View File

@@ -1,6 +0,0 @@
import {
OPTIONS,
PUT,
} from "@/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route";
export { OPTIONS, PUT };

View File

@@ -1,26 +0,0 @@
import { contactCache } from "@/lib/cache/contact";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
export const doesContactExist = reactCache(
(id: string): Promise<boolean> =>
cache(
async () => {
const contact = await prisma.contact.findFirst({
where: {
id,
},
select: {
id: true,
},
});
return !!contact;
},
[`doesContactExistDisplaysApiV2-${id}`],
{
tags: [contactCache.tag.byId(id)],
}
)()
);

View File

@@ -1,54 +0,0 @@
import {
TDisplayCreateInputV2,
ZDisplayCreateInputV2,
} from "@/app/api/v2/client/[environmentId]/displays/types/display";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { displayCache } from "@formbricks/lib/display/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { DatabaseError } from "@formbricks/types/errors";
import { doesContactExist } from "./contact";
export const createDisplay = async (displayInput: TDisplayCreateInputV2): Promise<{ id: string }> => {
validateInputs([displayInput, ZDisplayCreateInputV2]);
const { environmentId, contactId, surveyId } = displayInput;
try {
const contactExists = contactId ? await doesContactExist(contactId) : false;
const display = await prisma.display.create({
data: {
survey: {
connect: {
id: surveyId,
},
},
...(contactExists && {
contact: {
connect: {
id: contactId,
},
},
}),
},
select: { id: true, contactId: true, surveyId: true },
});
displayCache.revalidate({
id: display.id,
contactId: display.contactId,
surveyId: display.surveyId,
environmentId,
});
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};

View File

@@ -1,55 +0,0 @@
import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displays/types/display";
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 { InvalidInputError } from "@formbricks/types/errors";
import { createDisplay } from "./lib/display";
interface Context {
params: Promise<{
environmentId: string;
}>;
}
export const OPTIONS = async (): Promise<Response> => {
return responses.successResponse({}, true);
};
export const POST = async (request: Request, context: Context): Promise<Response> => {
const params = await context.params;
const jsonInput = await request.json();
const inputValidation = ZDisplayCreateInputV2.safeParse({
...jsonInput,
environmentId: params.environmentId,
});
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
if (inputValidation.data.contactId) {
const isContactsEnabled = await getIsContactsEnabled();
if (!isContactsEnabled) {
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
}
}
try {
const response = await createDisplay(inputValidation.data);
await capturePosthogEnvironmentEvent(inputValidation.data.environmentId, "display created");
return responses.successResponse(response, true);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
return responses.internalServerErrorResponse(error.message);
}
}
};

View File

@@ -1,9 +0,0 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ZDisplayCreateInput } from "@formbricks/types/displays";
export const ZDisplayCreateInputV2 = ZDisplayCreateInput.omit({ userId: true }).extend({
contactId: ZId.optional(),
});
export type TDisplayCreateInputV2 = z.infer<typeof ZDisplayCreateInputV2>;

View File

@@ -1,3 +0,0 @@
import { GET, OPTIONS } from "@/app/api/v1/client/[environmentId]/environment/route";
export { OPTIONS, GET };

View File

@@ -1,6 +0,0 @@
import {
GET,
OPTIONS,
} from "@/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route";
export { GET, OPTIONS };

View File

@@ -1,3 +0,0 @@
import { OPTIONS, PUT } from "@/app/api/v1/client/[environmentId]/responses/[responseId]/route";
export { OPTIONS, PUT };

View File

@@ -1,42 +0,0 @@
import { contactCache } from "@/lib/cache/contact";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
export const getContact = reactCache((contactId: string) =>
cache(
async () => {
const contact = await prisma.contact.findUnique({
where: { id: contactId },
select: {
id: true,
attributes: {
select: {
attributeKey: { select: { key: true } },
value: true,
},
},
},
});
if (!contact) {
return null;
}
const contactAttributes = contact.attributes.reduce((acc, attr) => {
acc[attr.attributeKey.key] = attr.value;
return acc;
}, {}) as TContactAttributes;
return {
id: contact.id,
attributes: contactAttributes,
};
},
[`getContact-responses-api-${contactId}`],
{
tags: [contactCache.tag.byId(contactId)],
}
)()
);

View File

@@ -1,145 +0,0 @@
import "server-only";
import { responseSelection } from "@/app/api/v1/client/[environmentId]/responses/lib/response";
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import {
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
} from "@formbricks/lib/organization/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@formbricks/lib/posthogServer";
import { responseCache } from "@formbricks/lib/response/cache";
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 { TContactAttributes } from "@formbricks/types/contact-attribute";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { getContact } from "./contact";
export const createResponse = async (responseInput: TResponseInputV2): Promise<TResponse> => {
validateInputs([responseInput, ZResponseInput]);
captureTelemetry("response created");
const {
environmentId,
language,
contactId,
surveyId,
displayId,
finished,
data,
meta,
singleUseId,
variables,
ttc: initialTtc,
createdAt,
updatedAt,
} = responseInput;
try {
let contact: { id: string; attributes: TContactAttributes } | null = null;
let userId: string | undefined = undefined;
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new ResourceNotFoundError("Organization", environmentId);
}
if (contactId) {
contact = await getContact(contactId);
userId = contact?.attributes.userId;
}
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
const prismaData: Prisma.ResponseCreateInput = {
survey: {
connect: {
id: surveyId,
},
},
display: displayId ? { connect: { id: displayId } } : undefined,
finished: finished,
data: data,
language: language,
...(contact?.id && {
contact: {
connect: {
id: contact.id,
},
},
contactAttributes: contact.attributes,
}),
...(meta && ({ meta } as Prisma.JsonObject)),
singleUseId,
...(variables && { variables }),
ttc: ttc,
createdAt,
updatedAt,
};
const responsePrisma = await prisma.response.create({
data: prismaData,
select: responseSelection,
});
const response: TResponse = {
...responsePrisma,
contact: contact
? {
id: contact.id,
userId: contact.attributes.userId,
}
: null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
responseCache.revalidate({
environmentId,
id: response.id,
contactId: contact?.id,
...(singleUseId && { singleUseId }),
userId,
surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
if (IS_FORMBRICKS_CLOUD) {
const responsesCount = await getMonthlyOrganizationResponseCount(organization.id);
const responsesLimit = organization.billing.limits.monthly.responses;
if (responsesLimit && responsesCount >= responsesLimit) {
try {
await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, {
plan: organization.billing.plan,
limits: {
projects: null,
monthly: {
responses: responsesLimit,
miu: null,
},
},
});
} catch (err) {
// Log error but do not throw
console.error(`Error sending plan limits reached event to Posthog: ${err}`);
}
}
}
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};

View File

@@ -1,138 +0,0 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
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 { ZId } from "@formbricks/types/common";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponse } from "@formbricks/types/responses";
import { createResponse } from "./lib/response";
import { TResponseInputV2, ZResponseInputV2 } from "./types/response";
interface Context {
params: Promise<{
environmentId: string;
}>;
}
export const OPTIONS = async (): Promise<Response> => {
return responses.successResponse({}, true);
};
export const POST = async (request: Request, context: Context): Promise<Response> => {
const params = await context.params;
const requestHeaders = await headers();
let responseInput;
try {
responseInput = await request.json();
} catch (error) {
return responses.badRequestResponse("Invalid JSON in request body", { error: error.message }, true);
}
const { environmentId } = params;
const environmentIdValidation = ZId.safeParse(environmentId);
const responseInputValidation = ZResponseInputV2.safeParse({ ...responseInput, environmentId });
if (!environmentIdValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(environmentIdValidation.error),
true
);
}
if (!responseInputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(responseInputValidation.error),
true
);
}
const userAgent = request.headers.get("user-agent") || undefined;
const agent = new UAParser(userAgent);
const country =
requestHeaders.get("CF-IPCountry") ||
requestHeaders.get("X-Vercel-IP-Country") ||
requestHeaders.get("CloudFront-Viewer-Country") ||
undefined;
const responseInputData = responseInputValidation.data;
if (responseInputData.contactId) {
const isContactsEnabled = await getIsContactsEnabled();
if (!isContactsEnabled) {
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
}
}
// get and check survey
const survey = await getSurvey(responseInputData.surveyId);
if (!survey) {
return responses.notFoundResponse("Survey", responseInputData.surveyId, true);
}
if (survey.environmentId !== environmentId) {
return responses.badRequestResponse(
"Survey is part of another environment",
{
"survey.environmentId": survey.environmentId,
environmentId,
},
true
);
}
let response: TResponse;
try {
const meta: TResponseInputV2["meta"] = {
source: responseInputData?.meta?.source,
url: responseInputData?.meta?.url,
userAgent: {
browser: agent.getBrowser().name,
device: agent.getDevice().type || "desktop",
os: agent.getOS().name,
},
country: country,
action: responseInputData?.meta?.action,
};
response = await createResponse({
...responseInputData,
meta,
});
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
console.error(error);
return responses.internalServerErrorResponse(error.message);
}
}
sendToPipeline({
event: "responseCreated",
environmentId: survey.environmentId,
surveyId: response.surveyId,
response: response,
});
if (responseInput.finished) {
sendToPipeline({
event: "responseFinished",
environmentId: survey.environmentId,
surveyId: response.surveyId,
response: response,
});
}
await capturePosthogEnvironmentEvent(survey.environmentId, "response created", {
surveyId: response.surveyId,
surveyType: survey.type,
});
return responses.successResponse({ id: response.id }, true);
};

View File

@@ -1,6 +0,0 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ZResponseInput } from "@formbricks/types/responses";
export const ZResponseInputV2 = ZResponseInput.omit({ userId: true }).extend({ contactId: ZId.nullish() });
export type TResponseInputV2 = z.infer<typeof ZResponseInputV2>;

View File

@@ -1,3 +0,0 @@
import { OPTIONS, POST } from "@/app/api/v1/client/[environmentId]/storage/local/route";
export { OPTIONS, POST };

View File

@@ -1,3 +0,0 @@
import { OPTIONS, POST } from "@/app/api/v1/client/[environmentId]/storage/route";
export { OPTIONS, POST };

View File

@@ -1,3 +0,0 @@
import { OPTIONS, POST } from "@/modules/ee/contacts/api/client/[environmentId]/user/route";
export { POST, OPTIONS };

View File

@@ -1,3 +0,0 @@
import { DELETE, GET, PUT } from "@/modules/api/v2/management/responses/[responseId]/route";
export { GET, PUT, DELETE };

View File

@@ -1,3 +0,0 @@
import { GET, POST } from "@/modules/api/v2/management/responses/route";
export { GET, POST };

View File

@@ -0,0 +1,17 @@
"use client";
import { useTranslate } from "@tolgee/react";
import { XCircleIcon } from "lucide-react";
const Error = ({ error }: { error: Error & { digest?: string } }) => {
const { t } = useTranslate();
return (
<div className="mx-auto flex h-full max-w-xl flex-col items-center justify-center text-center">
<XCircleIcon height={40} color="red" />
<p className="text-md mt-4 font-bold text-zinc-900">{t("health.degraded")}</p>
<p className="text-sm text-zinc-900">{error.message}</p>
</div>
);
};
export default Error;

View File

@@ -0,0 +1,54 @@
import { getTranslate } from "@/tolgee/server";
import { BadgeCheckIcon } from "lucide-react";
import { Metadata } from "next";
import { prisma } from "@formbricks/database";
export const dynamic = "force-dynamic"; // no caching
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
googleBot: {
index: false,
follow: false,
},
},
};
const checkDatabaseConnection = async () => {
try {
await prisma.$queryRaw`SELECT 1`;
} catch (e) {
console.error("Database connection error:", e);
throw new Error("Database could not be reached");
}
};
/* const checkS3Connection = async () => {
if (!IS_S3_CONFIGURED) {
// dont try connecting if not in use
return;
}
try {
await testS3BucketAccess();
} catch (e) {
throw new Error("S3 Bucket cannot be accessed");
}
}; */
const Page = async () => {
const t = await getTranslate();
await checkDatabaseConnection();
// Skipping S3 check for now until it's fixed
// await checkS3Connection();
return (
<div className="mx-auto flex h-full max-w-xl flex-col items-center justify-center text-center">
<BadgeCheckIcon height={40} color="green" />
<p className="text-md mt-4 font-bold text-zinc-900">{t("health.healthy")}</p>
</div>
);
};
export default Page;

View File

@@ -1,3 +0,0 @@
export async function GET() {
return Response.json({ status: "ok" });
}

View File

@@ -0,0 +1,75 @@
import { authOptions } from "@/modules/auth/lib/authOptions";
import { createHash } from "crypto";
import { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import { getServerSession } from "next-auth";
import { prisma } from "@formbricks/database";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
export const hasEnvironmentAccess = async (
req: NextApiRequest,
res: NextApiResponse,
environmentId: string
) => {
if (req.headers["x-api-key"]) {
const ownership = await hasApiEnvironmentAccess(req.headers["x-api-key"].toString(), environmentId);
if (!ownership) {
return false;
}
} else {
const user = await getSessionUser(req, res);
if (!user) {
return false;
}
const ownership = await hasUserEnvironmentAccess(user.id, environmentId);
if (!ownership) {
return false;
}
}
return true;
};
export const hasApiEnvironmentAccess = async (apiKey, environmentId) => {
// write function to check if the API Key has access to the environment
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(apiKey),
},
select: {
environmentId: true,
},
});
if (apiKeyData?.environmentId === environmentId) {
return true;
}
return false;
};
export const hasOrganizationAccess = async (user, organizationId) => {
const membership = await prisma.membership.findUnique({
where: {
userId_organizationId: {
userId: user.id,
organizationId: organizationId,
},
},
});
if (membership) {
return true;
}
return false;
};
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

@@ -15,8 +15,7 @@ interface ApiErrorResponse {
| "unauthorized"
| "method_not_allowed"
| "not_authenticated"
| "forbidden"
| "too_many_requests";
| "forbidden";
message: string;
details: {
[key: string]: string | string[] | number | number[] | boolean | boolean[];
@@ -248,7 +247,7 @@ const tooManyRequestsResponse = (
return Response.json(
{
code: "too_many_requests",
code: "internal_server_error",
message,
details: {},
} as ApiErrorResponse,

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