Compare commits

...

8 Commits

Author SHA1 Message Date
Piyush Gupta
c2d237a99a fix: google sheet integration error message (#4899) 2025-03-16 16:10:51 +00:00
Piyush Jain
a371bdaedd chore(terraform): fix (#4963) 2025-03-15 13:32:05 +00:00
Piyush Jain
dbbd77a8eb chore(env): add new env variables (#4959) 2025-03-15 12:20:07 +00:00
Matti Nannt
c28de7c079 chore: prepare 3.4.0 release (#4950) 2025-03-13 20:38:32 +01:00
Matti Nannt
05f1068e01 chore: prepare 3.3.2 release (#4930) 2025-03-13 20:35:51 +01:00
Matti Nannt
7103ec9877 fix: survey preview stuck in sending (#4941) 2025-03-13 20:34:45 +01:00
Johannes
9cd7a25343 fix: fix except last (#4942) 2025-03-13 14:13:23 +00:00
IllimarR
2d028d18e5 feat: possibility to set mail from name (#4864)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2025-03-13 05:50:15 -07:00
34 changed files with 422 additions and 232 deletions

View File

@@ -39,6 +39,7 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
# See optional configurations below if you want to disable these features.
MAIL_FROM=noreply@example.com
MAIL_FROM_NAME=Formbricks
SMTP_HOST=localhost
SMTP_PORT=1025
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
@@ -207,3 +208,4 @@ UNKEY_ROOT_KEY=
# Enable Prometheus metrics
# PROMETHEUS_ENABLED=
# PROMETHEUS_EXPORTER_PORT=

View File

@@ -0,0 +1,43 @@
name: Publish Helm Chart
on:
release:
types:
- published
jobs:
publish:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract release version
run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: latest
- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin
- name: Install YQ
uses: dcarbone/install-yq-action@v1.3.1
- name: Update Chart.yaml with new version
run: |
yq -i ".version = \"${VERSION#v}\"" helm-chart/Chart.yaml
yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml
- name: Package Helm chart
run: |
helm package ./helm-chart
- name: Push Helm chart to GitHub Container Registry
run: |
helm push formbricks-${VERSION#v}.tgz oci://ghcr.io/formbricks/helm-charts

View File

@@ -0,0 +1,93 @@
name: 'Terraform'
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
paths:
- 'infra/terraform/**'
permissions:
id-token: write
contents: write
jobs:
terraform:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
aws-region: "eu-central-1"
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Format
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
working-directory: infra/terraform
# - name: Post Format
# if: always() && github.ref != 'refs/heads/main' && (steps.fmt.outcome == 'success' || steps.fmt.outcome == 'failure')
# uses: robburger/terraform-pr-commenter@v1
# with:
# commenter_type: fmt
# commenter_input: ${{ format('{0}{1}', steps.fmt.outputs.stdout, steps.fmt.outputs.stderr) }}
# commenter_exitcode: ${{ steps.fmt.outputs.exitcode }}
- name: Terraform Init
id: init
run: terraform init
working-directory: infra/terraform
# - name: Post Init
# if: always() && github.ref != 'refs/heads/main' && (steps.init.outcome == 'success' || steps.init.outcome == 'failure')
# uses: robburger/terraform-pr-commenter@v1
# with:
# commenter_type: init
# commenter_input: ${{ format('{0}{1}', steps.init.outputs.stdout, steps.init.outputs.stderr) }}
# commenter_exitcode: ${{ steps.init.outputs.exitcode }}
- name: Terraform Validate
id: validate
run: terraform validate
working-directory: infra/terraform
# - name: Post Validate
# if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
# uses: robburger/terraform-pr-commenter@v1
# with:
# commenter_type: validate
# commenter_input: ${{ format('{0}{1}', steps.validate.outputs.stdout, steps.validate.outputs.stderr) }}
# commenter_exitcode: ${{ steps.validate.outputs.exitcode }}
- name: Terraform Plan
id: plan
run: terraform plan -out .planfile
working-directory: infra/terraform
- name: Post PR comment
uses: borchero/terraform-plan-comment@v2
if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
with:
token: ${{ github.token }}
planfile: .planfile
working-directory: "infra/terraform"
skip-comment: true
- name: Terraform Apply
id: apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply .planfile
working-directory: "infra/terraform"

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport";
import {
DEBUG,
MAIL_FROM,
MAIL_FROM_NAME,
SMTP_AUTHENTICATED,
SMTP_HOST,
SMTP_PASSWORD,
@@ -69,7 +70,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
} as SMTPTransport.Options);
const emailDefaults = {
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "3.3.1",
"version": "3.4.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",

View File

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

View File

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

View File

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

View File

@@ -33,6 +33,7 @@ These variables are present inside your machines docker-compose file. Restart
| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | |
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | |
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | |
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |

View File

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

View File

@@ -5,8 +5,7 @@ description: A Helm chart for Formbricks with PostgreSQL, Redis
type: application
# Helm chart Version
version: 3.3.1
appVersion: v3.3.1
version: 0.0.0-dev
keywords:
- formbricks
@@ -18,7 +17,6 @@ maintainers:
- name: Formbricks
email: info@formbricks.com
dependencies:
- name: postgresql
version: "16.4.16"

View File

@@ -1,6 +1,6 @@
# formbricks
![Version: 3.3.1](https://img.shields.io/badge/Version-3.3.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.3.1](https://img.shields.io/badge/AppVersion-v3.3.1-informational?style=flat-square)
![Version: 0.0.0-dev](https://img.shields.io/badge/Version-0.0.0--dev-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
A Helm chart for Formbricks with PostgreSQL, Redis

View File

@@ -94,8 +94,12 @@ spec:
protocol: {{ $config.protocol | default "TCP" | quote }}
{{- end }}
{{- end }}
{{- if .Values.deployment.envFrom }}
{{- if or .Values.deployment.envFrom (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }}
envFrom:
{{- if or .Values.secret.enabled (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }}
- secretRef:
name: {{ template "formbricks.name" . }}-app-secrets
{{- end }}
{{- range $value := .Values.deployment.envFrom }}
{{- if (eq .type "configmap") }}
- configMapRef:
@@ -122,42 +126,8 @@ spec:
env:
{{- if and (.Values.enterprise.enabled) (ne .Values.enterprise.licenseKey "") }}
- name: ENTERPRISE_LICENSE_KEY
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: ENTERPRISE_LICENSE_KEY
{{- else if and (.Values.enterprise.enabled) (eq .Values.enterprise.licenseKey "") }}
- name: ENTERPRISE_LICENSE_KEY
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: ENTERPRISE_LICENSE_KEY
value: {{ .Values.enterprise.licenseKey | quote }}
{{- end }}
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: REDIS_URL
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: DATABASE_URL
- name: CRON_SECRET
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: CRON_SECRET
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: ENCRYPTION_KEY
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: {{ template "formbricks.name" . }}-app-secrets
key: NEXTAUTH_SECRET
{{- range $key, $value := .Values.deployment.env }}
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
{{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | indent 10 }}

View File

@@ -1,10 +1,6 @@
{{- if .Values.autoscaling.enabled }}
---
{{- if .Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler" }}
apiVersion: autoscaling/v2
{{- else }}
apiVersion: autoscaling/v2beta2
{{- end }}
kind: HorizontalPodAutoscaler
metadata:
name: {{ template "formbricks.name" . }}

View File

@@ -54,9 +54,9 @@ deployment:
# Environment variables from ConfigMaps or Secrets
envFrom:
# app-secrets:
# type: secret
# nameSuffix: app-secrets
# app-secrets:
# type: secret
# nameSuffix: app-secrets
# Environment variables passed to the app container
env:

View File

@@ -10,3 +10,11 @@ data "aws_eks_cluster_auth" "eks" {
data "aws_ecrpublic_authorization_token" "token" {
provider = aws.virginia
}
data "aws_iam_roles" "administrator" {
name_regex = "AWSReservedSSO_AdministratorAccess"
}
data "aws_iam_roles" "github" {
name_regex = "formbricks-prod-github"
}

View File

@@ -23,7 +23,7 @@ module "iam_github_oidc_role" {
"repo:formbricks/*:*",
]
policies = {
Administrator = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
Administrator = "arn:aws:iam::aws:policy/AdministratorAccess"
}
tags = local.tags

View File

@@ -249,7 +249,7 @@ module "eks" {
cluster_name = "${local.name}-eks"
cluster_version = "1.32"
enable_cluster_creator_admin_permissions = true
enable_cluster_creator_admin_permissions = false
cluster_endpoint_public_access = true
cluster_addons = {
@@ -271,6 +271,41 @@ module "eks" {
}
}
kms_key_administrators = [
tolist(data.aws_iam_roles.github.arns)[0],
tolist(data.aws_iam_roles.administrator.arns)[0]
]
kms_key_users = [
tolist(data.aws_iam_roles.github.arns)[0],
tolist(data.aws_iam_roles.administrator.arns)[0]
]
access_entries = {
administrator = {
principal_arn = tolist(data.aws_iam_roles.administrator.arns)[0]
policy_associations = {
Admin = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = {
type = "cluster"
}
}
}
}
github = {
principal_arn = tolist(data.aws_iam_roles.github.arns)[0]
policy_associations = {
Admin = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = {
type = "cluster"
}
}
}
}
}
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
control_plane_subnet_ids = module.vpc.intra_subnets
@@ -573,95 +608,69 @@ resource "helm_release" "formbricks" {
values = [
<<-EOT
postgresql:
enabled: false
redis:
enabled: false
ingress:
postgresql:
enabled: false
redis:
enabled: false
ingress:
enabled: true
ingressClassName: alb
hosts:
- host: "app.${local.domain}"
paths:
- path: /
pathType: "Prefix"
serviceName: "formbricks"
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn}
alb.ingress.kubernetes.io/healthcheck-path: "/health"
alb.ingress.kubernetes.io/group.name: formbricks
alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
secret:
enabled: false
rbac:
enabled: true
serviceAccount:
enabled: true
ingressClassName: alb
hosts:
- host: "app.${local.domain}"
paths:
- path: /
pathType: "Prefix"
serviceName: "formbricks"
name: formbricks
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn}
alb.ingress.kubernetes.io/healthcheck-path: "/health"
alb.ingress.kubernetes.io/group.name: formbricks
alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
secret:
enabled: false
rbac:
enabled: true
serviceAccount:
enabled: true
name: formbricks
annotations:
eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn}
serviceMonitor:
enabled: true
deployment:
image:
repository: "ghcr.io/formbricks/formbricks-experimental"
tag: "open-telemetry-for-prometheus"
pullPolicy: Always
env:
S3_BUCKET_NAME:
value: ${module.s3-bucket.s3_bucket_id}
RATE_LIMITING_DISABLED:
value: "1"
envFrom:
app-parameters:
type: secret
nameSuffix: {RELEASE.name}-app-parameters
annotations:
deployed_at: ${timestamp()}
externalSecret:
enabled: true # Enable/disable ExternalSecrets
secretStore:
name: aws-secrets-manager
kind: ClusterSecretStore
refreshInterval: "1h"
files:
app-parameters:
dataFrom:
key: "/prod/formbricks/env"
secretStore:
name: aws-parameter-store
kind: ClusterSecretStore
app-secrets:
data:
DATABASE_URL:
remoteRef:
key: "prod/formbricks/secrets"
property: DATABASE_URL
REDIS_URL:
remoteRef:
key: "prod/formbricks/secrets"
property: REDIS_URL
CRON_SECRET:
remoteRef:
key: "prod/formbricks/secrets"
property: CRON_SECRET
ENCRYPTION_KEY:
remoteRef:
key: "prod/formbricks/secrets"
property: ENCRYPTION_KEY
NEXTAUTH_SECRET:
remoteRef:
key: "prod/formbricks/secrets"
property: NEXTAUTH_SECRET
ENTERPRISE_LICENSE_KEY:
remoteRef:
key: "prod/formbricks/enterprise"
property: ENTERPRISE_LICENSE_KEY
EOT
eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn}
serviceMonitor:
enabled: true
deployment:
image:
repository: "ghcr.io/formbricks/formbricks-experimental"
tag: "open-telemetry-for-prometheus"
pullPolicy: Always
env:
S3_BUCKET_NAME:
value: ${module.s3-bucket.s3_bucket_id}
RATE_LIMITING_DISABLED:
value: "1"
envFrom:
app-env:
type: secret
nameSuffix: app-env
annotations:
deployed_at: ${timestamp()}
externalSecret:
enabled: true # Enable/disable ExternalSecrets
secretStore:
name: aws-secrets-manager
kind: ClusterSecretStore
refreshInterval: "1h"
files:
app-env:
dataFrom:
key: "prod/formbricks/environment"
app-secrets:
dataFrom:
key: "prod/formbricks/secrets"
EOT
]
}

View File

@@ -1,19 +1,3 @@
# Generate random secrets for formbricks
resource "random_password" "nextauth_secret" {
length = 32
special = false
}
resource "random_password" "encryption_key" {
length = 32
special = false
}
resource "random_password" "cron_secret" {
length = 32
special = false
}
# Create the first AWS Secrets Manager secret for environment variables
resource "aws_secretsmanager_secret" "formbricks_app_secrets" {
name = "prod/formbricks/secrets"
@@ -24,10 +8,7 @@ resource "aws_secretsmanager_secret" "formbricks_app_secrets" {
resource "aws_secretsmanager_secret_version" "formbricks_app_secrets" {
secret_id = aws_secretsmanager_secret.formbricks_app_secrets.id
secret_string = jsonencode({
NEXTAUTH_SECRET = random_password.nextauth_secret.result
ENCRYPTION_KEY = random_password.encryption_key.result
CRON_SECRET = random_password.cron_secret.result
DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks"
REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379"
DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks"
REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379"
})
}

View File

@@ -1,12 +1,7 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/
.DS_Store
/build
/captures

View File

@@ -83,6 +83,7 @@ export const SMTP_PASSWORD = env.SMTP_PASSWORD;
export const SMTP_AUTHENTICATED = env.SMTP_AUTHENTICATED !== "0";
export const SMTP_REJECT_UNAUTHORIZED_TLS = env.SMTP_REJECT_UNAUTHORIZED_TLS !== "0";
export const MAIL_FROM = env.MAIL_FROM;
export const MAIL_FROM_NAME = env.MAIL_FROM_NAME;
export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET;
export const ITEMS_PER_PAGE = 30;

View File

@@ -49,6 +49,7 @@ export const env = createEnv({
INTERCOM_SECRET_KEY: z.string().optional(),
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
MAIL_FROM: z.string().email().optional(),
MAIL_FROM_NAME: z.string().optional(),
NEXTAUTH_SECRET: z.string().min(1),
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
NOTION_OAUTH_CLIENT_SECRET: z.string().optional(),
@@ -173,6 +174,7 @@ export const env = createEnv({
INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY,
IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD,
MAIL_FROM: process.env.MAIL_FROM,
MAIL_FROM_NAME: process.env.MAIL_FROM_NAME,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,

View File

@@ -36,6 +36,7 @@ interface VariableStackEntry {
export function Survey({
apiHost,
environmentId,
isPreviewMode = false,
userId,
contactId,
mode,
@@ -150,7 +151,6 @@ export function Survey({
return localSurvey.questions[0]?.id;
});
const [showError, setShowError] = useState(false);
// flag state to store whether response processing has been completed or not, we ignore this check for survey editor preview and link survey preview where getSetIsResponseSendingFinished is undefined
const [isResponseSendingFinished, setIsResponseSendingFinished] = useState(
!getSetIsResponseSendingFinished
);
@@ -182,6 +182,11 @@ export function Survey({
};
const onFileUploadApi = async (file: TJsFileUploadParams["file"], params?: TUploadFileConfig) => {
if (isPreviewMode) {
// return mock url since an url is required for the preview
return `https://example.com/${file.name}`;
}
if (!apiClient) {
throw new Error("apiClient not initialized");
}
@@ -206,6 +211,17 @@ export function Survey({
}, [questionId]);
const createDisplay = useCallback(async () => {
// Skip display creation in preview mode but still trigger the onDisplayCreated callback
if (isPreviewMode) {
if (onDisplayCreated) {
onDisplayCreated();
}
if (onDisplay) {
onDisplay();
}
return;
}
if (apiClient && surveyState && responseQueue) {
try {
const display = await apiClient.createDisplay({
@@ -229,7 +245,17 @@ export function Survey({
console.error("error creating display: ", err);
}
}
}, [apiClient, surveyState, responseQueue, survey.id, userId, contactId, onDisplayCreated]);
}, [
apiClient,
surveyState,
responseQueue,
survey.id,
userId,
contactId,
onDisplayCreated,
isPreviewMode,
onDisplay,
]);
useEffect(() => {
// call onDisplay when component is mounted
@@ -385,6 +411,32 @@ export function Survey({
const onResponseCreateOrUpdate = useCallback(
(responseUpdate: TResponseUpdate) => {
// Always trigger the onResponse callback even in preview mode
if (!apiHost || !environmentId) {
onResponse?.({
data: responseUpdate.data,
ttc: responseUpdate.ttc,
finished: responseUpdate.finished,
variables: responseUpdate.variables,
language: responseUpdate.language,
endingId: responseUpdate.endingId,
});
return;
}
// Skip response creation in preview mode but still trigger the onResponseCreated callback
if (isPreviewMode) {
if (onResponseCreated) {
onResponseCreated();
}
// When in preview mode, set isResponseSendingFinished to true if the response is finished
if (responseUpdate.finished) {
setIsResponseSendingFinished(true);
}
return;
}
if (surveyState && responseQueue) {
if (contactId) {
surveyState.updateContactId(contactId);
@@ -415,7 +467,20 @@ export function Survey({
}
}
},
[surveyState, responseQueue, contactId, userId, survey, action, hiddenFieldsRecord, onResponseCreated]
[
apiHost,
environmentId,
isPreviewMode,
surveyState,
responseQueue,
contactId,
userId,
survey,
action,
hiddenFieldsRecord,
onResponseCreated,
onResponse,
]
);
useEffect(() => {
@@ -446,25 +511,14 @@ export function Survey({
onChange(surveyResponseData);
onChangeVariables(calculatedVariables);
if (apiHost && environmentId) {
onResponseCreateOrUpdate({
data: surveyResponseData,
ttc: responsettc,
finished,
variables: calculatedVariables,
language: selectedLanguage,
endingId,
});
} else {
onResponse?.({
data: surveyResponseData,
ttc: responsettc,
finished,
variables: calculatedVariables,
language: selectedLanguage,
endingId,
});
}
onResponseCreateOrUpdate({
data: surveyResponseData,
ttc: responsettc,
finished,
variables: calculatedVariables,
language: selectedLanguage,
endingId,
});
if (nextQuestionId) {
setQuestionId(nextQuestionId);
@@ -573,7 +627,7 @@ export function Survey({
onBack={onBack}
ttc={ttc}
setTtc={setTtc}
onFileUpload={apiHost && environmentId ? onFileUploadApi : onFileUpload!}
onFileUpload={onFileUpload ?? onFileUploadApi}
isFirstQuestion={question.id === localSurvey.questions[0]?.id}
skipPrefilled={skipPrefilled}
prefilledQuestionValue={getQuestionPrefillData(question.id, offset)}

View File

@@ -45,22 +45,23 @@ export const getShuffledChoicesIds = (
const otherOption = choices.find((choice) => {
return choice.id === "other";
});
const shuffledChoices = otherOption ? [...choices.filter((choice) => choice.id !== "other")] : [...choices];
if (shuffleOption === "all") {
shuffle(shuffledChoices);
} else if (shuffleOption === "exceptLast") {
if (otherOption) {
}
if (shuffleOption === "exceptLast") {
const lastElement = shuffledChoices.pop();
if (lastElement) {
shuffle(shuffledChoices);
} else {
const lastElement = shuffledChoices.pop();
if (lastElement) {
shuffle(shuffledChoices);
shuffledChoices.push(lastElement);
}
shuffledChoices.push(lastElement);
}
}
if (otherOption) shuffledChoices.push(otherOption);
if (otherOption) {
shuffledChoices.push(otherOption);
}
return shuffledChoices.map((choice) => choice.id);
};

View File

@@ -45,6 +45,7 @@ export interface SurveyModalProps extends SurveyBaseProps {
export interface SurveyContainerProps extends Omit<SurveyBaseProps, "onFileUpload"> {
apiHost?: string;
environmentId?: string;
isPreviewMode?: boolean;
userId?: string;
contactId?: string;
onDisplayCreated?: () => void | Promise<void>;

View File

@@ -119,6 +119,7 @@
"IS_FORMBRICKS_CLOUD",
"INTERCOM_SECRET_KEY",
"MAIL_FROM",
"MAIL_FROM_NAME",
"NEXT_PUBLIC_LAYER_API_KEY",
"NEXT_PUBLIC_DOCSEARCH_APP_ID",
"NEXT_PUBLIC_DOCSEARCH_API_KEY",