mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 02:55:04 -05:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 928c586e75 | |||
| fc066551b5 | |||
| 7b1c7f95de | |||
| 0164eca206 | |||
| f227c9e97e | |||
| aecedfd082 | |||
| e0f180bf04 | |||
| 5d0c435a33 | |||
| daa7e7b56a | |||
| 655f319083 | |||
| fcfe5682da | |||
| e1140ac436 | |||
| 1529f5d478 | |||
| 4870dc8d45 | |||
| a25e5dcfcd | |||
| 828e23b5c6 | |||
| 1921312445 | |||
| 0b9a884364 | |||
| da4211f0b0 | |||
| b21827cb32 | |||
| 4424a8a21d | |||
| eb030f9ed6 | |||
| 333372d61c | |||
| 48a92f3e55 | |||
| ddc767e53e | |||
| 432425ea59 | |||
| 6075fd3ef8 | |||
| f099a46f83 | |||
| fe54ef66c6 | |||
| 4eb0e930f6 | |||
| fae925aa25 | |||
| 764a3d2fde | |||
| b5a51f1304 | |||
| 140aee749b | |||
| 4113dd1873 | |||
| 0e0d3780d3 | |||
| 38ff01aedc | |||
| cdf687ad80 | |||
| a399fc7f80 | |||
| c54a48e70b | |||
| 884b6f12ae | |||
| 5cae0febc9 | |||
| 0e898db710 | |||
| 40d54d60d4 | |||
| 269e026381 | |||
| 8245f2f6af | |||
| 8c07e8b1a8 | |||
| e94b0845a2 | |||
| 4acc85bd12 | |||
| ffa534d5eb | |||
| fccf0f1e39 | |||
| a5d80d1f02 | |||
| 803a73afb6 | |||
| 1eb8049d04 | |||
| f9ed0c487f | |||
| fa7d33351f | |||
| e3084760b8 | |||
| 8e5addad5c | |||
| 6e741018e5 | |||
| 98c7c78421 | |||
| 16c588138c | |||
| 1373863af5 | |||
| 75315ea2c5 | |||
| 9f6fb8a387 | |||
| b84d3d5806 | |||
| 5c2c1bbfcd | |||
| 54e84858b5 | |||
| 833d0789d7 | |||
| 1a974f3dd8 | |||
| 146173883f | |||
| ebb02a5723 | |||
| c96f7fed18 | |||
| 861eff3cd2 | |||
| b66c0d17d0 | |||
| 0e748050f3 | |||
| ae3524b79f | |||
| 0ce58b592a | |||
| 578346840e | |||
| 56bcb46d6c | |||
| 91405c48e0 | |||
| b40dff621a | |||
| 7d4409e2b4 |
+30
-13
@@ -1,39 +1,56 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
# **/node_modules
|
**/node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
coverage
|
**/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
**/.next
|
**/.next/
|
||||||
**/out
|
**/out/
|
||||||
**/build
|
**/build
|
||||||
|
|
||||||
# node
|
# node
|
||||||
**/dist
|
**/dist/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
**/.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
Zone.Identifier
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# turbo
|
# local env files
|
||||||
.turbo
|
**/.env
|
||||||
|
**/.env.local
|
||||||
|
**/.env.development.local
|
||||||
|
**/.env.test.local
|
||||||
|
**/.env.production.local
|
||||||
|
!packages/database/.env
|
||||||
|
!apps/web/.env
|
||||||
|
|
||||||
# nixos stuff
|
# build tools
|
||||||
|
.turbo
|
||||||
|
**/*vite.config.*.timestamp-*
|
||||||
|
|
||||||
|
# environment specific
|
||||||
.direnv
|
.direnv
|
||||||
|
|
||||||
.vscode
|
# Playwright
|
||||||
.github
|
/test-results/
|
||||||
**/.turbo
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|
||||||
.env
|
# project specific
|
||||||
|
packages/lib/uploads
|
||||||
|
apps/web/public/js
|
||||||
|
packages/database/migrations
|
||||||
|
branch.json
|
||||||
+11
-1
@@ -130,6 +130,9 @@ AZUREAD_TENANT_ID=
|
|||||||
# OIDC_DISPLAY_NAME=
|
# OIDC_DISPLAY_NAME=
|
||||||
# OIDC_SIGNING_ALGORITHM=
|
# 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
|
# Configure this when you want to ship JS & CSS files from a complete URL instead of the current domain
|
||||||
# ASSET_PREFIX_URL=
|
# ASSET_PREFIX_URL=
|
||||||
|
|
||||||
@@ -186,6 +189,9 @@ UNSPLASH_ACCESS_KEY=
|
|||||||
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
|
# 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:
|
# 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)
|
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||||
# CUSTOM_CACHE_DISABLED=1
|
# CUSTOM_CACHE_DISABLED=1
|
||||||
|
|
||||||
@@ -196,4 +202,8 @@ UNSPLASH_ACCESS_KEY=
|
|||||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||||
|
|
||||||
# NEXT_PUBLIC_INTERCOM_APP_ID=
|
# NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||||
# INTERCOM_SECRET_KEY=
|
# INTERCOM_SECRET_KEY=
|
||||||
|
|
||||||
|
# Enable Prometheus metrics
|
||||||
|
# PROMETHEUS_ENABLED=
|
||||||
|
# PROMETHEUS_EXPORTER_PORT=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: "Found a bug? Please fill out the sections below. \U0001F44D"
|
description: "Found a bug? Please fill out the sections below. \U0001F44D"
|
||||||
type: bug
|
type: bug
|
||||||
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: issue-summary
|
id: issue-summary
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ on:
|
|||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label_on_pr:
|
label_on_pr:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -15,8 +18,13 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
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
|
- name: Apply labels from linked issue to PR
|
||||||
uses: actions/github-script@v5
|
uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Build & Cache Web Binaries
|
- name: Build & Cache Web Binaries
|
||||||
|
|||||||
@@ -11,19 +11,24 @@ jobs:
|
|||||||
name: Run Chromatic
|
name: Run Chromatic
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
- name: Run Chromatic
|
- name: Run Chromatic
|
||||||
uses: chromaui/action@latest
|
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # latest
|
||||||
with:
|
with:
|
||||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ jobs:
|
|||||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@v2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: cURL request
|
- name: cURL request
|
||||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
|
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
|
||||||
- cron: "0 8 * * 1"
|
- cron: "0 8 * * 1"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cron-weeklySummary:
|
cron-weeklySummary:
|
||||||
permissions:
|
permissions:
|
||||||
@@ -16,6 +19,10 @@ jobs:
|
|||||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
- name: cURL request
|
- name: cURL request
|
||||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Dependency Review Action
|
||||||
|
#
|
||||||
|
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||||
|
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||||
|
# Once installed, if the workflow run is marked as required,
|
||||||
|
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||||
|
#
|
||||||
|
# Source repository: https://github.com/actions/dependency-review-action
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||||
@@ -43,16 +43,21 @@ jobs:
|
|||||||
--health-timeout=5s
|
--health-timeout=5s
|
||||||
--health-retries=5
|
--health-retries=5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Setup Node.js 20.x
|
- name: Setup Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
@@ -84,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run App
|
- name: Run App
|
||||||
run: |
|
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
|
sleep 10 # Optional: gives some buffer for the app to start
|
||||||
for attempt in {1..10}; do
|
for attempt in {1..10}; do
|
||||||
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
|
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
|
||||||
@@ -112,7 +117,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Azure login
|
- name: Azure login
|
||||||
if: env.AZURE_ENABLED == 'true'
|
if: env.AZURE_ENABLED == 'true'
|
||||||
uses: azure/login@v2
|
uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
@@ -130,9 +135,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pnpm test:e2e
|
pnpm test:e2e
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: app-logs
|
||||||
|
path: app.log
|
||||||
|
|
||||||
|
- name: Output App Logs
|
||||||
|
if: failure()
|
||||||
|
run: cat app.log
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ on:
|
|||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
name: Pull Request Labeler
|
name: Pull Request Labeler
|
||||||
@@ -12,7 +15,12 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
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: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
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
|
- name: fail if conditional jobs failed
|
||||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
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: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|||||||
@@ -26,23 +26,28 @@ jobs:
|
|||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
steps:
|
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
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|
||||||
- name: Create Release Pull Request or Publish to npm
|
- name: Create Release Pull Request or Publish to npm
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
|
||||||
with:
|
with:
|
||||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||||
publish: pnpm release
|
publish: pnpm release
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -28,23 +31,28 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
|
||||||
- name: Set up Depot CLI
|
- name: Set up Depot CLI
|
||||||
uses: depot/setup-action@v1
|
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||||
|
|
||||||
# Install the cosign tool except on PR
|
# Install the cosign tool except on PR
|
||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@v3.5.0
|
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3 # v3.0.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -54,7 +62,7 @@ jobs:
|
|||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5 # v5.0.0
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
@@ -62,7 +70,7 @@ jobs:
|
|||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: depot/build-push-action@v1
|
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||||
with:
|
with:
|
||||||
project: tw0fqmsx3c
|
project: tw0fqmsx3c
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ env:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -31,23 +34,28 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
|
||||||
- name: Set up Depot CLI
|
- name: Set up Depot CLI
|
||||||
uses: depot/setup-action@v1
|
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||||
|
|
||||||
# Install the cosign tool except on PR
|
# Install the cosign tool except on PR
|
||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@v3.5.0
|
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3 # v3.0.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -57,7 +65,7 @@ jobs:
|
|||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5 # v5.0.0
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
@@ -65,7 +73,7 @@ jobs:
|
|||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: depot/build-push-action@v1
|
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||||
with:
|
with:
|
||||||
project: tw0fqmsx3c
|
project: tw0fqmsx3c
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-image-on-dockerhub:
|
release-image-on-dockerhub:
|
||||||
name: Release on Dockerhub
|
name: Release on Dockerhub
|
||||||
@@ -16,17 +19,22 @@ jobs:
|
|||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||||
steps:
|
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
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
|
||||||
|
|
||||||
- name: Get Release Tag
|
- name: Get Release Tag
|
||||||
id: extract_release_tag
|
id: extract_release_tag
|
||||||
@@ -36,7 +44,7 @@ jobs:
|
|||||||
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./apps/web/Dockerfile
|
file: ./apps/web/Dockerfile
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ jobs:
|
|||||||
# actions: read
|
# actions: read
|
||||||
|
|
||||||
steps:
|
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"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
@@ -71,6 +76,6 @@ jobs:
|
|||||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ jobs:
|
|||||||
name: PR title
|
name: PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
||||||
id: lint_pr_title
|
id: lint_pr_title
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -35,7 +40,7 @@ jobs:
|
|||||||
revert
|
revert
|
||||||
ossgg
|
ossgg
|
||||||
|
|
||||||
- uses: marocchino/sticky-pull-request-comment@v2
|
- uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
||||||
# When the previous steps fails, the workflow would stop. By adding this
|
# When the previous steps fails, the workflow would stop. By adding this
|
||||||
# condition you can continue the execution with the populated error message.
|
# condition you can continue the execution with the populated error message.
|
||||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||||
@@ -54,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
# Delete a previous comment when the issue has been resolved
|
# Delete a previous comment when the issue has been resolved
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
||||||
with:
|
with:
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
message: |
|
message: |
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
merge_group:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
@@ -13,7 +14,12 @@ jobs:
|
|||||||
name: SonarQube
|
name: SonarQube
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Unit Tests
|
name: Unit Tests
|
||||||
@@ -10,16 +13,21 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Setup Node.js 20.x
|
- name: Setup Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|||||||
@@ -5,18 +5,30 @@ permissions:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-missing-translations:
|
check-missing-translations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request_target:
|
||||||
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
@@ -11,13 +12,36 @@ jobs:
|
|||||||
tag-production-keys:
|
tag-production-keys:
|
||||||
name: Tag Production Keys
|
name: Tag Production Keys
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # This ensures we get the full git history
|
||||||
|
|
||||||
|
- name: Get source branch name
|
||||||
|
id: branch-name
|
||||||
|
run: |
|
||||||
|
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"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||||
with:
|
with:
|
||||||
node-version: 18 # Ensure compatibility with your project
|
node-version: 18 # Ensure compatibility with your project
|
||||||
|
|
||||||
@@ -26,17 +50,38 @@ jobs:
|
|||||||
|
|
||||||
- name: Tag Production Keys
|
- name: Tag Production Keys
|
||||||
run: |
|
run: |
|
||||||
BRANCH_NAME=${GITHUB_REF##*/}
|
|
||||||
npx tolgee tag \
|
npx tolgee tag \
|
||||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||||
--filter-extracted \
|
--filter-extracted \
|
||||||
--filter-tag "draft: ${BRANCH_NAME}" \
|
--filter-tag "draft:${SOURCE_BRANCH}" \
|
||||||
--tag production \
|
--tag production \
|
||||||
--untag "draft: ${BRANCH_NAME}"
|
--untag "draft:${SOURCE_BRANCH}"
|
||||||
|
|
||||||
- name: Tag Deprecated Keys
|
- name: Tag unused production keys as Deprecated
|
||||||
run: |
|
run: |
|
||||||
npx tolgee tag \
|
npx tolgee tag \
|
||||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||||
--filter-not-extracted --filter-tag production \
|
--filter-not-extracted --filter-tag production \
|
||||||
--tag deprecated --untag production
|
--tag deprecated --untag production
|
||||||
|
|
||||||
|
- name: Tag unused draft:current-branch keys as Deprecated
|
||||||
|
run: |
|
||||||
|
npx tolgee tag \
|
||||||
|
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||||
|
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
|
||||||
|
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
|
||||||
|
|
||||||
|
- name: Sync with backup
|
||||||
|
run: |
|
||||||
|
npx tolgee sync \
|
||||||
|
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||||
|
--backup ./tolgee-backup \
|
||||||
|
--continue-on-warning \
|
||||||
|
--yes
|
||||||
|
|
||||||
|
- name: Upload backup as artifact
|
||||||
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
|
with:
|
||||||
|
name: tolgee-backup-${{ github.sha }}
|
||||||
|
path: ./tolgee-backup
|
||||||
|
retention-days: 90
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
if: github.event.action == 'opened'
|
if: github.event.action == 'opened'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/first-interaction@v1
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/first-interaction@3c71ce730280171fd1cfb57c00c774f8998586f7 # v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pr-message: |-
|
pr-message: |-
|
||||||
|
|||||||
+35
-23
@@ -1,25 +1,26 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
**/node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
coverage
|
**/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
.next/
|
**/.next/
|
||||||
out/
|
**/out/
|
||||||
build
|
**/build
|
||||||
|
|
||||||
# node
|
# node
|
||||||
dist/
|
**/dist/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
**/.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
Zone.Identifier
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
@@ -27,36 +28,47 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env
|
**/.env
|
||||||
.env.local
|
**/.env.local
|
||||||
.env.development.local
|
**/.env.development.local
|
||||||
.env.test.local
|
**/.env.test.local
|
||||||
.env.production.local
|
**/.env.production.local
|
||||||
!packages/database/.env
|
!packages/database/.env
|
||||||
!apps/web/.env
|
!apps/web/.env
|
||||||
|
|
||||||
# turbo
|
# build tools
|
||||||
.turbo
|
.turbo
|
||||||
|
**/*vite.config.*.timestamp-*
|
||||||
|
|
||||||
# nixos stuff
|
# environment specific
|
||||||
.direnv
|
.direnv
|
||||||
|
|
||||||
Zone.Identifier
|
|
||||||
|
|
||||||
# Playwright
|
# Playwright
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
|
||||||
# uploads
|
# project specific
|
||||||
packages/lib/uploads
|
packages/lib/uploads
|
||||||
|
|
||||||
# Vite Timestamps
|
|
||||||
*vite.config.*.timestamp-*
|
|
||||||
|
|
||||||
# js compiled assets
|
|
||||||
apps/web/public/js
|
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
|
||||||
|
|
||||||
packages/database/migrations
|
# IntelliJ IDEA
|
||||||
|
/.idea/
|
||||||
|
/*.iml
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
|
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json
|
||||||
|
prettier --write ./branch.json
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
|
|
||||||
+20
-4
@@ -1,5 +1,21 @@
|
|||||||
pnpm lint-staged
|
#!/bin/sh
|
||||||
pnpm tolgee-pull || true
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
echo "{\"branchName\": \"main\"}" > ../branch.json
|
|
||||||
git add branch.json packages/lib/messages/*.json
|
|
||||||
|
|
||||||
|
# Load environment variables from .env files
|
||||||
|
if [ -f .env ]; then
|
||||||
|
set -a
|
||||||
|
. .env
|
||||||
|
set +a
|
||||||
|
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
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
{
|
{
|
||||||
"language": "zh-Hant-TW",
|
"language": "zh-Hant-TW",
|
||||||
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "pt-PT",
|
||||||
|
"path": "./packages/lib/messages/pt-PT.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"forceMode": "OVERRIDE"
|
"forceMode": "OVERRIDE"
|
||||||
|
|||||||
Vendored
+3
-1
@@ -6,6 +6,8 @@
|
|||||||
"dbaeumer.vscode-eslint", // eslint plugin
|
"dbaeumer.vscode-eslint", // eslint plugin
|
||||||
"esbenp.prettier-vscode", // prettier plugin
|
"esbenp.prettier-vscode", // prettier plugin
|
||||||
"Prisma.prisma", // syntax|format|completion for prisma
|
"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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
|
|||||||
Portions of this software are licensed as follows:
|
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 "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.
|
- 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.
|
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<h3 align="center">Formbricks</h3>
|
<h3 align="center">Formbricks</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Harvest user-insights, build irresistible experiences.
|
The Open Source Qualtrics Alternative
|
||||||
<br />
|
<br />
|
||||||
<a href="https://formbricks.com/">Website</a>
|
<a href="https://formbricks.com/">Website</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "8.18.0",
|
"@typescript-eslint/eslint-plugin": "8.18.0",
|
||||||
"@typescript-eslint/parser": "8.18.0",
|
"@typescript-eslint/parser": "8.18.0",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.3.4",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.1",
|
||||||
"eslint-plugin-storybook": "0.11.1",
|
"eslint-plugin-storybook": "0.11.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"storybook": "8.4.7",
|
"storybook": "8.4.7",
|
||||||
|
|||||||
@@ -48,3 +48,6 @@ uploads/
|
|||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
|
# SAML Preloaded Connections
|
||||||
|
saml-connection/
|
||||||
+13
-6
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-alpine3.20 AS base
|
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
## step 1: Prune monorepo
|
## step 1: Prune monorepo
|
||||||
@@ -33,6 +33,9 @@ ENV CRON_SECRET="placeholder_for_cron_secret_of_64_chars_get_overwritten_at_runt
|
|||||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||||
ARG SENTRY_AUTH_TOKEN
|
ARG SENTRY_AUTH_TOKEN
|
||||||
|
|
||||||
|
# Increase Node.js memory limit
|
||||||
|
# ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -41,19 +44,17 @@ WORKDIR /app
|
|||||||
# COPY --from=builder /app/out/json/ .
|
# COPY --from=builder /app/out/json/ .
|
||||||
# COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
# COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
|
|
||||||
# Install the dependencies
|
|
||||||
# RUN pnpm install
|
|
||||||
|
|
||||||
# Prepare the build
|
# Prepare the build
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Create a .env file
|
# Create a .env file
|
||||||
RUN touch apps/web/.env
|
RUN touch apps/web/.env
|
||||||
|
|
||||||
|
# Install the dependencies
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
# RUN pnpm post-install --filter=@formbricks/web...
|
RUN NODE_OPTIONS="--max_old_space_size=4096" pnpm build --filter=@formbricks/web...
|
||||||
RUN pnpm build --filter=@formbricks/web...
|
|
||||||
|
|
||||||
# Extract Prisma version
|
# Extract Prisma version
|
||||||
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
|
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
|
||||||
@@ -76,6 +77,7 @@ WORKDIR /home/nextjs
|
|||||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||||
COPY --from=installer /app/apps/web/package.json .
|
COPY --from=installer /app/apps/web/package.json .
|
||||||
# Leverage output traces to reduce image size
|
# 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/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/.next/static ./apps/web/.next/static
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||||
@@ -105,6 +107,11 @@ ENV HOSTNAME "0.0.0.0"
|
|||||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||||
VOLUME /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 supercronic -quiet /app/docker/cronjobs & \
|
CMD supercronic -quiet /app/docker/cronjobs & \
|
||||||
(cd packages/database && npm run db:migrate:deploy) && \
|
(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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
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 { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
@@ -15,7 +16,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service";
|
|||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
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 { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
@@ -19,7 +20,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service";
|
|||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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)],
|
||||||
|
}
|
||||||
|
)()
|
||||||
|
);
|
||||||
+1
-1
@@ -7,6 +7,7 @@ import {
|
|||||||
UNSUPPORTED_TYPES_BY_NOTION,
|
UNSUPPORTED_TYPES_BY_NOTION,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
||||||
import NotionLogo from "@/images/notion.png";
|
import NotionLogo from "@/images/notion.png";
|
||||||
|
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
@@ -19,7 +20,6 @@ import { useForm } from "react-hook-form";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
@@ -21,7 +22,6 @@ import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/
|
|||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
@@ -14,7 +15,6 @@ import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
|||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@ export const EditAlerts = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{memberships.map((membership) => (
|
{memberships.map((membership) => (
|
||||||
<>
|
<div key={membership.organization.id}>
|
||||||
<div className="mb-5 grid grid-cols-6 items-center space-x-3">
|
<div className="mb-5 grid grid-cols-6 items-center space-x-3">
|
||||||
<div className="col-span-3 flex 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" />
|
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||||
@@ -110,7 +110,7 @@ export const EditAlerts = ({
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{memberships.map((membership) => (
|
{memberships.map((membership) => (
|
||||||
<>
|
<div key={membership.organization.id}>
|
||||||
<div className="mb-5 flex items-center space-x-3 text-sm font-medium">
|
<div className="mb-5 flex items-center space-x-3 text-sm font-medium">
|
||||||
<UsersIcon className="h-6 w-7 text-slate-600" />
|
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
|||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||||
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
|
import {
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
getOrganizationByEnvironmentId,
|
||||||
|
getOrganizationsWhereUserIsSingleOwner,
|
||||||
|
} from "@formbricks/lib/organization/service";
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
import { SettingsCard } from "../../components/SettingsCard";
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
import { DeleteAccount } from "./components/DeleteAccount";
|
import { DeleteAccount } from "./components/DeleteAccount";
|
||||||
@@ -71,7 +73,9 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
description={t("environments.settings.profile.two_factor_authentication_description")}
|
description={t("environments.settings.profile.two_factor_authentication_description")}
|
||||||
buttons={[
|
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
|
href: IS_FORMBRICKS_CLOUD
|
||||||
? `/environments/${params.environmentId}/settings/billing`
|
? `/environments/${params.environmentId}/settings/billing`
|
||||||
: "https://formbricks.com/upgrade-self-hosting-license",
|
: "https://formbricks.com/upgrade-self-hosting-license",
|
||||||
|
|||||||
+133
@@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+3
-1
@@ -12,7 +12,8 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
|||||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
import React from "react";
|
||||||
|
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
@@ -84,6 +85,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
isReadOnly={!isOwnerOrManager}
|
isReadOnly={!isOwnerOrManager}
|
||||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||||
|
fbLogoUrl={FB_LOGO_URL}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
{isMultiOrgEnabled && (
|
{isMultiOrgEnabled && (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Badge } from "@/modules/ui/components/badge";
|
import { Badge } from "@/modules/ui/components/badge";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
|
import React from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
|
||||||
export const SettingsCard = ({
|
export const SettingsCard = ({
|
||||||
|
|||||||
+1
-8
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
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 { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
@@ -12,7 +13,6 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|||||||
import { processResponseData } from "@formbricks/lib/responses";
|
import { processResponseData } from "@formbricks/lib/responses";
|
||||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||||
import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime";
|
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 { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||||
import { TResponseTableData } from "@formbricks/types/responses";
|
import { TResponseTableData } from "@formbricks/types/responses";
|
||||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||||
@@ -200,13 +200,6 @@ export const generateResponseTableColumns = (
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" className="font-normal">
|
<TooltipContent side="bottom" className="font-normal">
|
||||||
{t("environments.surveys.responses.how_to_identify_users")}
|
{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
|
<Link
|
||||||
className="underline underline-offset-2 hover:text-slate-900"
|
className="underline underline-offset-2 hover:text-slate-900"
|
||||||
href="https://formbricks.com/docs/app-surveys/user-identification"
|
href="https://formbricks.com/docs/app-surveys/user-identification"
|
||||||
|
|||||||
+2
-2
@@ -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="col-span-2 pl-4 md:pl-6">{t("common.response")}</div>
|
||||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||||
</div>
|
</div>
|
||||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
{questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||||
<div
|
<div
|
||||||
key={response.value}
|
key={`${response.value}-${idx}`}
|
||||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
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">
|
<div className="pl-4 md:pl-6">
|
||||||
{response.contact ? (
|
{response.contact ? (
|
||||||
|
|||||||
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { InboxIcon } from "lucide-react";
|
import { InboxIcon } from "lucide-react";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
|
||||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||||
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getQuestionIcon } from "@/modules/survey/lib/questions";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { TimerIcon } from "lucide-react";
|
import { TimerIcon } from "lucide-react";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { getQuestionIcon } from "@formbricks/lib/utils/questions";
|
|
||||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||||
import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||||
|
|
||||||
|
|||||||
+4
-75
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"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 { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const AppTab = ({ environmentId }) => {
|
export const AppTab = () => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const [selectedTab, setSelectedTab] = useState("webapp");
|
const [selectedTab, setSelectedTab] = useState("webapp");
|
||||||
|
|
||||||
@@ -20,79 +21,7 @@ export const AppTab = ({ environmentId }) => {
|
|||||||
handleOptionChange={(value) => setSelectedTab(value)}
|
handleOptionChange={(value) => setSelectedTab(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">{selectedTab === "webapp" ? <WebAppTab /> : <MobileAppTab />}</div>
|
||||||
{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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@ export const EmbedView = ({
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
) : activeId === "app" ? (
|
) : activeId === "app" ? (
|
||||||
<AppTab environmentId={environmentId} />
|
<AppTab />
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-2 rounded-md p-3 text-center lg:hidden">
|
<div className="mt-2 rounded-md p-3 text-center lg:hidden">
|
||||||
{tabs.slice(0, 2).map((tab) => (
|
{tabs.slice(0, 2).map((tab) => (
|
||||||
|
|||||||
-5
@@ -17,11 +17,6 @@ interface LinkTabProps {
|
|||||||
export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }: LinkTabProps) => {
|
export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }: LinkTabProps) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const docsLinks = [
|
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"),
|
title: t("environments.surveys.summary.data_prefilling"),
|
||||||
description: t("environments.surveys.summary.data_prefilling_description"),
|
description: t("environments.surveys.summary.data_prefilling_description"),
|
||||||
|
|||||||
+25
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-2
@@ -85,8 +85,10 @@ export const PanelInfoView = ({ disableBack, handleInitialPageButton }: PanelInf
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button className="justify-center" asChild>
|
<Button className="justify-center" asChild>
|
||||||
<Link href="https://formbricks.com/docs/link-surveys/market-research-panel" target="_blank">
|
<Link
|
||||||
{t("common.get_started")}
|
href="https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/market-research-panel"
|
||||||
|
target="_blank">
|
||||||
|
{t("common.learn_more")}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+25
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+5
-2
@@ -380,7 +380,7 @@ export const getQuestionSummary = async (
|
|||||||
|
|
||||||
let hasValidAnswer = false;
|
let hasValidAnswer = false;
|
||||||
|
|
||||||
if (Array.isArray(answer)) {
|
if (Array.isArray(answer) && question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||||
answer.forEach((value) => {
|
answer.forEach((value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
totalSelectionCount++;
|
totalSelectionCount++;
|
||||||
@@ -396,7 +396,10 @@ export const getQuestionSummary = async (
|
|||||||
hasValidAnswer = true;
|
hasValidAnswer = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (typeof answer === "string") {
|
} else if (
|
||||||
|
typeof answer === "string" &&
|
||||||
|
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle
|
||||||
|
) {
|
||||||
if (answer) {
|
if (answer) {
|
||||||
totalSelectionCount++;
|
totalSelectionCount++;
|
||||||
if (questionChoices.includes(answer)) {
|
if (questionChoices.includes(answer)) {
|
||||||
|
|||||||
+20
-1
@@ -14,6 +14,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/modules/ui/components/dropdown-menu";
|
} from "@/modules/ui/components/dropdown-menu";
|
||||||
|
import { Input } from "@/modules/ui/components/input";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
||||||
@@ -48,6 +49,7 @@ export const QuestionFilterComboBox = ({
|
|||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [openFilterValue, setOpenFilterValue] = React.useState<boolean>(false);
|
const [openFilterValue, setOpenFilterValue] = React.useState<boolean>(false);
|
||||||
const commandRef = React.useRef(null);
|
const commandRef = React.useRef(null);
|
||||||
|
const [searchQuery, setSearchQuery] = React.useState<string>("");
|
||||||
const defaultLanguageCode = "default";
|
const defaultLanguageCode = "default";
|
||||||
useClickOutside(commandRef, () => setOpen(false));
|
useClickOutside(commandRef, () => setOpen(false));
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
@@ -73,6 +75,12 @@ export const QuestionFilterComboBox = ({
|
|||||||
(type === TSurveyQuestionTypeEnum.NPS || type === TSurveyQuestionTypeEnum.Rating) &&
|
(type === TSurveyQuestionTypeEnum.NPS || type === TSurveyQuestionTypeEnum.Rating) &&
|
||||||
(filterValue === "Submitted" || filterValue === "Skipped");
|
(filterValue === "Submitted" || filterValue === "Skipped");
|
||||||
|
|
||||||
|
const filteredOptions = options?.filter((o) =>
|
||||||
|
(typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex w-full flex-row">
|
<div className="inline-flex w-full flex-row">
|
||||||
{filterOptions && filterOptions?.length <= 1 ? (
|
{filterOptions && filterOptions?.length <= 1 ? (
|
||||||
@@ -160,10 +168,21 @@ export const QuestionFilterComboBox = ({
|
|||||||
{open && (
|
{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">
|
<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>
|
<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>
|
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options?.map((o) => (
|
{filteredOptions?.map((o, index) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
key={`option-${typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o}-${index}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
!isMultiple
|
!isMultiple
|
||||||
? onChangeFilterComboBoxValue(
|
? onChangeFilterComboBoxValue(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { hasOrganizationAccess } from "@/app/lib/api/apiHelper";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { hasOrganizationAccess } from "@formbricks/lib/auth";
|
||||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
@@ -16,7 +16,7 @@ export const GET = async (_: Request, context: { params: Promise<{ organizationI
|
|||||||
// check auth
|
// check auth
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session) throw new AuthenticationError("Not authenticated");
|
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");
|
if (!hasAccess) throw new AuthorizationError("Unauthorized");
|
||||||
|
|
||||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organizationId);
|
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organizationId);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { hasOrganizationAccess } from "@/app/lib/api/apiHelper";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { hasOrganizationAccess } from "@formbricks/lib/auth";
|
||||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
import { getProject } from "@formbricks/lib/project/service";
|
import { getProject } from "@formbricks/lib/project/service";
|
||||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
|
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");
|
if (!session) throw new AuthenticationError("Not authenticated");
|
||||||
const project = await getProject(projectId);
|
const project = await getProject(projectId);
|
||||||
if (!project) return notFound();
|
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");
|
if (!hasAccess) throw new AuthorizationError("Unauthorized");
|
||||||
// redirect to project's production environment
|
// redirect to project's production environment
|
||||||
const environments = await getEnvironments(project.id);
|
const environments = await getEnvironments(project.id);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { GET } from "@/modules/ee/auth/saml/api/authorize/route";
|
||||||
|
|
||||||
|
export { GET };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { POST } from "@/modules/ee/auth/saml/api/callback/route";
|
||||||
|
|
||||||
|
export { POST };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { POST } from "@/modules/ee/auth/saml/api/token/route";
|
||||||
|
|
||||||
|
export { POST };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { GET } from "@/modules/ee/auth/saml/api/userinfo/route";
|
||||||
|
|
||||||
|
export { GET };
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key";
|
||||||
import { responses } from "@/app/lib/api/response";
|
import { responses } from "@/app/lib/api/response";
|
||||||
|
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
|
||||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { getEnvironmentIdFromApiKey } from "./lib/api-key";
|
|
||||||
|
|
||||||
export const authenticateRequest = async (request: Request): Promise<TAuthenticationApiKey | null> => {
|
export const authenticateRequest = async (request: Request): Promise<TAuthenticationApiKey | null> => {
|
||||||
const apiKey = request.headers.get("x-api-key");
|
const apiKey = request.headers.get("x-api-key");
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
||||||
if (environmentId) {
|
if (environmentId) {
|
||||||
|
const hashedApiKey = hashApiKey(apiKey);
|
||||||
const authentication: TAuthenticationApiKey = {
|
const authentication: TAuthenticationApiKey = {
|
||||||
type: "apiKey",
|
type: "apiKey",
|
||||||
environmentId,
|
environmentId,
|
||||||
|
hashedApiKey,
|
||||||
};
|
};
|
||||||
return authentication;
|
return authentication;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const getSurveysForEnvironmentState = reactCache(
|
|||||||
displayLimit: true,
|
displayLimit: true,
|
||||||
displayOption: true,
|
displayOption: true,
|
||||||
hiddenFields: true,
|
hiddenFields: true,
|
||||||
|
isBackButtonHidden: true,
|
||||||
triggers: {
|
triggers: {
|
||||||
select: {
|
select: {
|
||||||
actionClass: {
|
actionClass: {
|
||||||
@@ -72,6 +73,7 @@ export const getSurveysForEnvironmentState = reactCache(
|
|||||||
},
|
},
|
||||||
displayPercentage: true,
|
displayPercentage: true,
|
||||||
delay: true,
|
delay: true,
|
||||||
|
projectOverwrites: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { cache } from "@formbricks/lib/cache";
|
|||||||
import { getHash } from "@formbricks/lib/crypto";
|
import { getHash } from "@formbricks/lib/crypto";
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||||
import { ZString } from "@formbricks/types/common";
|
import { ZString } from "@formbricks/types/common";
|
||||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { InvalidInputError } from "@formbricks/types/errors";
|
|
||||||
|
|
||||||
export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Promise<string | null> => {
|
export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Promise<string | null> => {
|
||||||
const hashedKey = getHash(apiKey);
|
const hashedKey = getHash(apiKey);
|
||||||
@@ -42,7 +41,7 @@ export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Pro
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[`getEnvironmentIdFromApiKey-${apiKey}`],
|
[`management-api-getEnvironmentIdFromApiKey-${apiKey}`],
|
||||||
{
|
{
|
||||||
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
|
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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 { headers } from "next/headers";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {
|
||||||
|
OPTIONS,
|
||||||
|
PUT,
|
||||||
|
} from "@/modules/ee/contacts/api/client/[environmentId]/contacts/[userId]/attributes/route";
|
||||||
|
|
||||||
|
export { OPTIONS, PUT };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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)],
|
||||||
|
}
|
||||||
|
)()
|
||||||
|
);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { GET, OPTIONS } from "@/app/api/v1/client/[environmentId]/environment/route";
|
||||||
|
|
||||||
|
export { OPTIONS, GET };
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {
|
||||||
|
GET,
|
||||||
|
OPTIONS,
|
||||||
|
} from "@/modules/ee/contacts/api/client/[environmentId]/identify/contacts/[userId]/route";
|
||||||
|
|
||||||
|
export { GET, OPTIONS };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { OPTIONS, PUT } from "@/app/api/v1/client/[environmentId]/responses/[responseId]/route";
|
||||||
|
|
||||||
|
export { OPTIONS, PUT };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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)],
|
||||||
|
}
|
||||||
|
)()
|
||||||
|
);
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { OPTIONS, POST } from "@/app/api/v1/client/[environmentId]/storage/local/route";
|
||||||
|
|
||||||
|
export { OPTIONS, POST };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { OPTIONS, POST } from "@/app/api/v1/client/[environmentId]/storage/route";
|
||||||
|
|
||||||
|
export { OPTIONS, POST };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { OPTIONS, POST } from "@/modules/ee/contacts/api/client/[environmentId]/user/route";
|
||||||
|
|
||||||
|
export { POST, OPTIONS };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { DELETE, GET, PUT } from "@/modules/api/v2/management/responses/[responseId]/route";
|
||||||
|
|
||||||
|
export { GET, PUT, DELETE };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { GET, POST } from "@/modules/api/v2/management/responses/route";
|
||||||
|
|
||||||
|
export { GET, POST };
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
"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;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export async function GET() {
|
||||||
|
return Response.json({ status: "ok" });
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@@ -15,7 +15,8 @@ interface ApiErrorResponse {
|
|||||||
| "unauthorized"
|
| "unauthorized"
|
||||||
| "method_not_allowed"
|
| "method_not_allowed"
|
||||||
| "not_authenticated"
|
| "not_authenticated"
|
||||||
| "forbidden";
|
| "forbidden"
|
||||||
|
| "too_many_requests";
|
||||||
message: string;
|
message: string;
|
||||||
details: {
|
details: {
|
||||||
[key: string]: string | string[] | number | number[] | boolean | boolean[];
|
[key: string]: string | string[] | number | number[] | boolean | boolean[];
|
||||||
@@ -247,7 +248,7 @@ const tooManyRequestsResponse = (
|
|||||||
|
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
code: "internal_server_error",
|
code: "too_many_requests",
|
||||||
message,
|
message,
|
||||||
details: {},
|
details: {},
|
||||||
} as ApiErrorResponse,
|
} as ApiErrorResponse,
|
||||||
|
|||||||
@@ -7064,5 +7064,6 @@ export const previewSurvey = (projectName: string, t: TFnType) => {
|
|||||||
triggers: [],
|
triggers: [],
|
||||||
showLanguageSwitch: false,
|
showLanguageSwitch: false,
|
||||||
followUps: [],
|
followUps: [],
|
||||||
|
isBackButtonHidden: false,
|
||||||
} as TSurvey;
|
} as TSurvey;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export const isClientSideApiRoute = (url: string): boolean => {
|
|||||||
return regex.test(url);
|
return regex.test(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isManagementApiRoute = (url: string): boolean => {
|
||||||
|
const regex = /^\/api\/v\d+\/management\//;
|
||||||
|
return regex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
export const isShareUrlRoute = (url: string): boolean => {
|
export const isShareUrlRoute = (url: string): boolean => {
|
||||||
const regex = /\/share\/[A-Za-z0-9]+\/(?:summary|responses)/;
|
const regex = /\/share\/[A-Za-z0-9]+\/(?:summary|responses)/;
|
||||||
return regex.test(url);
|
return regex.test(url);
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// instrumentation-node.ts
|
||||||
|
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||||
|
import { HostMetrics } from "@opentelemetry/host-metrics";
|
||||||
|
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||||
|
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
||||||
|
import { RuntimeNodeInstrumentation } from "@opentelemetry/instrumentation-runtime-node";
|
||||||
|
import {
|
||||||
|
Resource,
|
||||||
|
detectResourcesSync,
|
||||||
|
envDetector,
|
||||||
|
hostDetector,
|
||||||
|
processDetector,
|
||||||
|
} from "@opentelemetry/resources";
|
||||||
|
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||||
|
import { env } from "@formbricks/lib/env";
|
||||||
|
|
||||||
|
const exporter = new PrometheusExporter({
|
||||||
|
port: env.PROMETHEUS_EXPORTER_PORT ? parseInt(env.PROMETHEUS_EXPORTER_PORT) : 9464,
|
||||||
|
endpoint: "/metrics",
|
||||||
|
host: "0.0.0.0", // Listen on all network interfaces
|
||||||
|
});
|
||||||
|
|
||||||
|
const detectedResources = detectResourcesSync({
|
||||||
|
detectors: [envDetector, processDetector, hostDetector],
|
||||||
|
});
|
||||||
|
|
||||||
|
const customResources = new Resource({});
|
||||||
|
|
||||||
|
const resources = detectedResources.merge(customResources);
|
||||||
|
|
||||||
|
const meterProvider = new MeterProvider({
|
||||||
|
readers: [exporter],
|
||||||
|
resource: resources,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostMetrics = new HostMetrics({
|
||||||
|
name: `otel-metrics`,
|
||||||
|
meterProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
registerInstrumentations({
|
||||||
|
meterProvider,
|
||||||
|
instrumentations: [new HttpInstrumentation(), new RuntimeNodeInstrumentation()],
|
||||||
|
});
|
||||||
|
|
||||||
|
hostMetrics.start();
|
||||||
|
|
||||||
|
process.on("SIGTERM", async () => {
|
||||||
|
try {
|
||||||
|
// Stop collecting metrics or flush them if needed
|
||||||
|
await meterProvider.shutdown();
|
||||||
|
// Possibly close other instrumentation resources
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error during graceful shutdown:", e);
|
||||||
|
} finally {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,25 +1,8 @@
|
|||||||
import { registerOTel } from "@vercel/otel";
|
|
||||||
import { LangfuseExporter } from "langfuse-vercel";
|
|
||||||
import { env } from "@formbricks/lib/env";
|
import { env } from "@formbricks/lib/env";
|
||||||
|
|
||||||
export async function register() {
|
// instrumentation.ts
|
||||||
if (env.LANGFUSE_SECRET_KEY && env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_BASEURL) {
|
export const register = async () => {
|
||||||
registerOTel({
|
if (process.env.NEXT_RUNTIME === "nodejs" && env.PROMETHEUS_ENABLED) {
|
||||||
serviceName: "formbricks-cloud-dev",
|
await import("./instrumentation-node");
|
||||||
traceExporter: new LangfuseExporter({
|
|
||||||
debug: false,
|
|
||||||
secretKey: env.LANGFUSE_SECRET_KEY,
|
|
||||||
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
|
||||||
baseUrl: env.LANGFUSE_BASEURL,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
||||||
await import("./sentry.server.config");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NEXT_RUNTIME === "edge") {
|
|
||||||
await import("./sentry.edge.config");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+84
-32
@@ -12,22 +12,36 @@ import {
|
|||||||
isClientSideApiRoute,
|
isClientSideApiRoute,
|
||||||
isForgotPasswordRoute,
|
isForgotPasswordRoute,
|
||||||
isLoginRoute,
|
isLoginRoute,
|
||||||
|
isManagementApiRoute,
|
||||||
isShareUrlRoute,
|
isShareUrlRoute,
|
||||||
isSignupRoute,
|
isSignupRoute,
|
||||||
isSyncWithUserIdentificationEndpoint,
|
isSyncWithUserIdentificationEndpoint,
|
||||||
isVerifyEmailRoute,
|
isVerifyEmailRoute,
|
||||||
} from "@/app/middleware/endpoint-validator";
|
} from "@/app/middleware/endpoint-validator";
|
||||||
|
import { logApiError } from "@/modules/api/v2/lib/utils";
|
||||||
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { ipAddress } from "@vercel/functions";
|
import { ipAddress } from "@vercel/functions";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import type { NextRequest } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { RATE_LIMITING_DISABLED, WEBAPP_URL } from "@formbricks/lib/constants";
|
import { E2E_TESTING, IS_PRODUCTION, RATE_LIMITING_DISABLED, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||||
import { isValidCallbackUrl } from "@formbricks/lib/utils/url";
|
import { isValidCallbackUrl } from "@formbricks/lib/utils/url";
|
||||||
|
|
||||||
export const middleware = async (request: NextRequest) => {
|
const enforceHttps = (request: NextRequest): Response | null => {
|
||||||
// issue with next auth types; let's review when new fixes are available
|
const forwardedProto = request.headers.get("x-forwarded-proto") ?? "http";
|
||||||
const token = await getToken({ req: request as any });
|
if (IS_PRODUCTION && !E2E_TESTING && forwardedProto !== "https") {
|
||||||
|
const apiError: ApiErrorResponseV2 = {
|
||||||
|
type: "forbidden",
|
||||||
|
details: [{ field: "", issue: "Only HTTPS connections are allowed on the management endpoint." }],
|
||||||
|
};
|
||||||
|
logApiError(request, apiError);
|
||||||
|
return NextResponse.json(apiError, { status: 403 });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuth = async (request: NextRequest): Promise<Response | null> => {
|
||||||
|
const token = await getToken({ req: request as any });
|
||||||
if (isAuthProtectedRoute(request.nextUrl.pathname) && !token) {
|
if (isAuthProtectedRoute(request.nextUrl.pathname) && !token) {
|
||||||
const loginUrl = `${WEBAPP_URL}/auth/login?callbackUrl=${encodeURIComponent(WEBAPP_URL + request.nextUrl.pathname + request.nextUrl.search)}`;
|
const loginUrl = `${WEBAPP_URL}/auth/login?callbackUrl=${encodeURIComponent(WEBAPP_URL + request.nextUrl.pathname + request.nextUrl.search)}`;
|
||||||
return NextResponse.redirect(loginUrl);
|
return NextResponse.redirect(loginUrl);
|
||||||
@@ -35,13 +49,62 @@ export const middleware = async (request: NextRequest) => {
|
|||||||
|
|
||||||
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
||||||
if (callbackUrl && !isValidCallbackUrl(callbackUrl, WEBAPP_URL)) {
|
if (callbackUrl && !isValidCallbackUrl(callbackUrl, WEBAPP_URL)) {
|
||||||
return NextResponse.json({ error: "Invalid callback URL" });
|
return NextResponse.json({ error: "Invalid callback URL" }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (token && callbackUrl) {
|
if (token && callbackUrl) {
|
||||||
return NextResponse.redirect(WEBAPP_URL + callbackUrl);
|
return NextResponse.redirect(WEBAPP_URL + callbackUrl);
|
||||||
}
|
}
|
||||||
if (process.env.NODE_ENV !== "production" || RATE_LIMITING_DISABLED) {
|
return null;
|
||||||
return NextResponse.next();
|
};
|
||||||
|
|
||||||
|
const applyRateLimiting = (request: NextRequest, ip: string) => {
|
||||||
|
if (isLoginRoute(request.nextUrl.pathname)) {
|
||||||
|
loginLimiter(`login-${ip}`);
|
||||||
|
} else if (isSignupRoute(request.nextUrl.pathname)) {
|
||||||
|
signupLimiter(`signup-${ip}`);
|
||||||
|
} else if (isVerifyEmailRoute(request.nextUrl.pathname)) {
|
||||||
|
verifyEmailLimiter(`verify-email-${ip}`);
|
||||||
|
} else if (isForgotPasswordRoute(request.nextUrl.pathname)) {
|
||||||
|
forgotPasswordLimiter(`forgot-password-${ip}`);
|
||||||
|
} else if (isClientSideApiRoute(request.nextUrl.pathname)) {
|
||||||
|
clientSideApiEndpointsLimiter(`client-side-api-${ip}`);
|
||||||
|
const envIdAndUserId = isSyncWithUserIdentificationEndpoint(request.nextUrl.pathname);
|
||||||
|
if (envIdAndUserId) {
|
||||||
|
const { environmentId, userId } = envIdAndUserId;
|
||||||
|
syncUserIdentificationLimiter(`sync-${environmentId}-${userId}`);
|
||||||
|
}
|
||||||
|
} else if (isShareUrlRoute(request.nextUrl.pathname)) {
|
||||||
|
shareUrlLimiter(`share-${ip}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const middleware = async (originalRequest: NextRequest) => {
|
||||||
|
// Create a new Request object to override headers and add a unique request ID header
|
||||||
|
const request = new NextRequest(originalRequest, {
|
||||||
|
headers: new Headers(originalRequest.headers),
|
||||||
|
});
|
||||||
|
|
||||||
|
request.headers.set("x-request-id", uuidv4());
|
||||||
|
|
||||||
|
// Create a new NextResponse object to forward the new request with headers
|
||||||
|
const nextResponseWithCustomHeader = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: request.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enforce HTTPS for management endpoints
|
||||||
|
if (isManagementApiRoute(request.nextUrl.pathname)) {
|
||||||
|
const httpsResponse = enforceHttps(request);
|
||||||
|
if (httpsResponse) return httpsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle authentication
|
||||||
|
const authResponse = await handleAuth(request);
|
||||||
|
if (authResponse) return authResponse;
|
||||||
|
|
||||||
|
if (!IS_PRODUCTION || RATE_LIMITING_DISABLED) {
|
||||||
|
return nextResponseWithCustomHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ip =
|
let ip =
|
||||||
@@ -51,32 +114,19 @@ export const middleware = async (request: NextRequest) => {
|
|||||||
|
|
||||||
if (ip) {
|
if (ip) {
|
||||||
try {
|
try {
|
||||||
if (isLoginRoute(request.nextUrl.pathname)) {
|
applyRateLimiting(request, ip);
|
||||||
await loginLimiter(`login-${ip}`);
|
return nextResponseWithCustomHeader;
|
||||||
} else if (isSignupRoute(request.nextUrl.pathname)) {
|
|
||||||
await signupLimiter(`signup-${ip}`);
|
|
||||||
} else if (isVerifyEmailRoute(request.nextUrl.pathname)) {
|
|
||||||
await verifyEmailLimiter(`verify-email-${ip}`);
|
|
||||||
} else if (isForgotPasswordRoute(request.nextUrl.pathname)) {
|
|
||||||
await forgotPasswordLimiter(`forgot-password-${ip}`);
|
|
||||||
} else if (isClientSideApiRoute(request.nextUrl.pathname)) {
|
|
||||||
await clientSideApiEndpointsLimiter(`client-side-api-${ip}`);
|
|
||||||
|
|
||||||
const envIdAndUserId = isSyncWithUserIdentificationEndpoint(request.nextUrl.pathname);
|
|
||||||
if (envIdAndUserId) {
|
|
||||||
const { environmentId, userId } = envIdAndUserId;
|
|
||||||
await syncUserIdentificationLimiter(`sync-${environmentId}-${userId}`);
|
|
||||||
}
|
|
||||||
} else if (isShareUrlRoute(request.nextUrl.pathname)) {
|
|
||||||
await shareUrlLimiter(`share-${ip}`);
|
|
||||||
}
|
|
||||||
return NextResponse.next();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Rate Limiting IP: ${ip}`);
|
const apiError: ApiErrorResponseV2 = {
|
||||||
return NextResponse.json({ error: "Too many requests, Please try after a while!" }, { status: 429 });
|
type: "too_many_requests",
|
||||||
|
details: [{ field: "", issue: "Too many requests. Please try again later." }],
|
||||||
|
};
|
||||||
|
logApiError(request, apiError);
|
||||||
|
return NextResponse.json(apiError, { status: 429 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NextResponse.next();
|
|
||||||
|
return nextResponseWithCustomHeader;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -94,5 +144,7 @@ export const config = {
|
|||||||
"/api/packages/:path*",
|
"/api/packages/:path*",
|
||||||
"/auth/verification-requested",
|
"/auth/verification-requested",
|
||||||
"/auth/forgot-password",
|
"/auth/forgot-password",
|
||||||
|
"/api/v1/management/:path*",
|
||||||
|
"/api/v2/management/:path*",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ export const SurveyLinkDisplay = ({ surveyUrl }: SurveyLinkDisplayProps) => {
|
|||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="mt-2 w-full min-w-96 text-ellipsis rounded-lg border bg-white px-4 py-2 text-center text-slate-800 caret-transparent"
|
className="mt-2 w-full min-w-96 text-ellipsis rounded-lg border bg-white px-4 py-2 text-slate-800 caret-transparent"
|
||||||
defaultValue={surveyUrl}
|
defaultValue={surveyUrl}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user