mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-22 00:21:18 -05:00
Compare commits
3 Commits
personaliz
...
randomize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
928c586e75 | ||
|
|
fc066551b5 | ||
|
|
7b1c7f95de |
@@ -39,7 +39,6 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
|
||||
# See optional configurations below if you want to disable these features.
|
||||
|
||||
MAIL_FROM=noreply@example.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=localhost
|
||||
SMTP_PORT=1025
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
@@ -185,7 +184,7 @@ ENTERPRISE_LICENSE_KEY=
|
||||
UNSPLASH_ACCESS_KEY=
|
||||
|
||||
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
|
||||
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
|
||||
# REDIS_HTTP_URL:
|
||||
@@ -208,4 +207,3 @@ UNKEY_ROOT_KEY=
|
||||
# Enable Prometheus metrics
|
||||
# PROMETHEUS_ENABLED=
|
||||
# PROMETHEUS_EXPORTER_PORT=
|
||||
|
||||
|
||||
43
.github/workflows/release-helm-chart.yml
vendored
43
.github/workflows/release-helm-chart.yml
vendored
@@ -1,43 +0,0 @@
|
||||
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
|
||||
69
.github/workflows/terrafrom-plan-and-apply.yml
vendored
69
.github/workflows/terrafrom-plan-and-apply.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: 'Terraform'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: 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: Terraform Init
|
||||
id: init
|
||||
run: terraform init
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Terraform Validate
|
||||
id: validate
|
||||
run: terraform validate
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Terraform Plan
|
||||
id: plan
|
||||
run: terraform plan -out .planfile
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Post PR comment
|
||||
uses: borchero/terraform-plan-comment@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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
images=($(yq eval '.services.*.image' docker-compose.dev.yml))
|
||||
images=($(yq eval '.services.*.image' packages/database/docker-compose.yml))
|
||||
|
||||
pull_image() {
|
||||
docker pull "$1"
|
||||
|
||||
@@ -231,7 +231,6 @@ export const ProjectSettings = ({
|
||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||
<div className="z-0 h-3/4 w-3/4">
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || "my Product", t)}
|
||||
styling={{ brandColor: { light: brandColor } }}
|
||||
isBrandingEnabled={false}
|
||||
|
||||
@@ -1,41 +1,25 @@
|
||||
"use server";
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
|
||||
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
|
||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||
googleSheetIntegration: ZIntegrationGoogleSheets,
|
||||
environmentId: z.string(),
|
||||
spreadsheetId: z.string(),
|
||||
});
|
||||
export async function getSpreadsheetNameByIdAction(
|
||||
googleSheetIntegration: TIntegrationGoogleSheets,
|
||||
environmentId: string,
|
||||
spreadsheetId: string
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export const getSpreadsheetNameByIdAction = authenticatedActionClient
|
||||
.schema(ZGetSpreadsheetNameByIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const integrationData = structuredClone(parsedInput.googleSheetIntegration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
|
||||
return await getSpreadsheetNameById(integrationData, parsedInput.spreadsheetId);
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const integrationData = structuredClone(googleSheetIntegration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
return await getSpreadsheetNameById(integrationData, spreadsheetId);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
isValidGoogleSheetsUrl,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
@@ -116,18 +115,11 @@ export const AddIntegrationModal = ({
|
||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
||||
}
|
||||
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
|
||||
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
|
||||
const spreadsheetName = await getSpreadsheetNameByIdAction(
|
||||
googleSheetIntegration,
|
||||
environmentId,
|
||||
spreadsheetId,
|
||||
});
|
||||
|
||||
if (!spreadsheetNameResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const spreadsheetName = spreadsheetNameResponse.data;
|
||||
spreadsheetId
|
||||
);
|
||||
|
||||
setIsLinkingSheet(true);
|
||||
integrationData.spreadsheetId = spreadsheetId;
|
||||
|
||||
@@ -11,7 +11,7 @@ const createTimeoutPromise = (ms, rejectReason) => {
|
||||
CacheHandler.onCreation(async () => {
|
||||
let client;
|
||||
|
||||
if (process.env.REDIS_URL) {
|
||||
if (process.env.REDIS_URL && process.env.ENTERPRISE_LICENSE_KEY) {
|
||||
try {
|
||||
// Create a Redis client.
|
||||
client = createClient({
|
||||
@@ -45,6 +45,8 @@ CacheHandler.onCreation(async () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (process.env.REDIS_URL) {
|
||||
console.log("Redis clustering requires an Enterprise License. Falling back to LRU cache.");
|
||||
}
|
||||
|
||||
/** @type {import("@neshca/cache-handler").Handler | null} */
|
||||
|
||||
@@ -360,11 +360,13 @@ export const UploadContactsCSVButton = ({
|
||||
)}
|
||||
</div>
|
||||
{!csvResponse.length && (
|
||||
<div className="flex justify-start">
|
||||
<Button onClick={handleDownloadExampleCSV} variant="secondary">
|
||||
{t("environments.contacts.upload_contacts_modal_download_example_csv")}
|
||||
</Button>
|
||||
</div>
|
||||
<p>
|
||||
<a
|
||||
onClick={handleDownloadExampleCSV}
|
||||
className="cursor-pointer text-right text-sm text-slate-500">
|
||||
{t("environments.contacts.upload_contacts_modal_download_example_csv")}{" "}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import {
|
||||
DEBUG,
|
||||
MAIL_FROM,
|
||||
MAIL_FROM_NAME,
|
||||
SMTP_AUTHENTICATED,
|
||||
SMTP_HOST,
|
||||
SMTP_PASSWORD,
|
||||
@@ -70,7 +69,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
const emailDefaults = {
|
||||
from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
||||
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
||||
};
|
||||
await transporter.sendMail({ ...emailDefaults, ...emailData });
|
||||
|
||||
|
||||
@@ -100,6 +100,11 @@ export const MatrixQuestionForm = ({
|
||||
label: t("environments.surveys.edit.randomize_all_except_last"),
|
||||
show: true,
|
||||
},
|
||||
exceptLastTwo: {
|
||||
id: "exceptLastTwo",
|
||||
label: t("environments.surveys.edit.randomize_all_except_last_two"),
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
/// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
@@ -70,6 +70,11 @@ export const MultipleChoiceQuestionForm = ({
|
||||
label: t("environments.surveys.edit.randomize_all_except_last"),
|
||||
show: true,
|
||||
},
|
||||
exceptLastTwo: {
|
||||
id: "exceptLastTwo",
|
||||
label: t("environments.surveys.edit.randomize_all_except_last_two"),
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
|
||||
@@ -243,6 +243,7 @@ export const SurveyEditor = ({
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
|
||||
languageCode={selectedLanguageCode}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -170,14 +170,14 @@ export const LinkSurvey = ({
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
isBrandingEnabled={project.linkSurveyBranding}>
|
||||
<SurveyInline
|
||||
apiHost={webAppUrl}
|
||||
environmentId={survey.environmentId}
|
||||
isPreviewMode={isPreview}
|
||||
apiHost={!isPreview ? webAppUrl : undefined}
|
||||
environmentId={!isPreview ? survey.environmentId : undefined}
|
||||
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}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const TemplateContainerWithPreview = ({
|
||||
project={project}
|
||||
environment={environment}
|
||||
languageCode={"default"}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -9,7 +9,9 @@ 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";
|
||||
@@ -23,6 +25,7 @@ interface PreviewSurveyProps {
|
||||
project: Project;
|
||||
environment: Pick<Environment, "id" | "appSetupCompleted">;
|
||||
languageCode: string;
|
||||
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||
}
|
||||
|
||||
let surveyNameTemp: string;
|
||||
@@ -63,6 +66,7 @@ export const PreviewSurvey = ({
|
||||
project,
|
||||
environment,
|
||||
languageCode,
|
||||
onFileUpload,
|
||||
}: PreviewSurveyProps) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||
@@ -261,11 +265,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}
|
||||
@@ -284,9 +288,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}
|
||||
@@ -363,11 +367,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}
|
||||
@@ -390,10 +394,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}
|
||||
|
||||
@@ -25,6 +25,7 @@ interface ShuffleOptionsTypes {
|
||||
none?: ShuffleOptionType;
|
||||
all?: ShuffleOptionType;
|
||||
exceptLast?: ShuffleOptionType;
|
||||
exceptLastTwo?: ShuffleOptionType;
|
||||
}
|
||||
|
||||
interface ShuffleOptionSelectProps {
|
||||
|
||||
@@ -162,7 +162,6 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
borderRadius={project.styling.roundness ?? 8}>
|
||||
<Fragment key={surveyFormKey}>
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "app" }}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
@@ -188,7 +187,6 @@ 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}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "3.4.0",
|
||||
"version": "3.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
mailhog:
|
||||
image: arjenz/mailhog # Copy of mailhog/MailHog to support linux/arm64
|
||||
ports:
|
||||
- 8025:8025 # web ui
|
||||
- 1025:1025 # smtp server
|
||||
|
||||
redis:
|
||||
image: redis:7.0.11
|
||||
ports:
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-02-28T09-55-16Z
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
- MINIO_ROOT_USER=devminio
|
||||
- MINIO_ROOT_PASSWORD=devminio123
|
||||
ports:
|
||||
- "9000:9000" # S3 API
|
||||
- "9001:9001" # Console
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
minio-data:
|
||||
driver: local
|
||||
@@ -36,7 +36,6 @@ x-environment: &environment
|
||||
|
||||
# Email Configuration
|
||||
# MAIL_FROM:
|
||||
# MAIL_FROM_NAME:
|
||||
# SMTP_HOST:
|
||||
# SMTP_PORT:
|
||||
# SMTP_USER:
|
||||
|
||||
@@ -224,9 +224,6 @@ 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
|
||||
|
||||
@@ -247,7 +244,6 @@ EOT
|
||||
|
||||
else
|
||||
mail_from=""
|
||||
mail_from_name=""
|
||||
smtp_host=""
|
||||
smtp_port=""
|
||||
smtp_user=""
|
||||
@@ -274,7 +270,6 @@ 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
|
||||
|
||||
@@ -262,9 +262,7 @@
|
||||
"group": "Auth & SSO",
|
||||
"icon": "lock",
|
||||
"pages": [
|
||||
"self-hosting/configuration/auth-sso/open-id-connect",
|
||||
"self-hosting/configuration/auth-sso/azure-ad-oauth",
|
||||
"self-hosting/configuration/auth-sso/google-oauth",
|
||||
"self-hosting/configuration/auth-sso/oauth",
|
||||
"self-hosting/configuration/auth-sso/saml-sso"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1039,7 +1039,6 @@ x-environment: &environment
|
||||
|
||||
# Email Configuration
|
||||
MAIL_FROM:
|
||||
MAIL_FROM_NAME:
|
||||
SMTP_HOST:
|
||||
SMTP_PORT:
|
||||
SMTP_SECURE_ENABLED:
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
title: Azure AD OAuth
|
||||
description: "Configure Microsoft Entra ID (Azure AD) OAuth for secure Single Sign-On with your Formbricks instance. Use enterprise-grade authentication for your survey platform."
|
||||
icon: "microsoft"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license).
|
||||
</Note>
|
||||
|
||||
### Microsoft Entra ID
|
||||
|
||||
Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks instance to allow users to log in using their existing Microsoft credentials. This guide will walk you through the process of setting up an Application Registration for your Formbricks instance.
|
||||
|
||||
### Requirements
|
||||
|
||||
- A Microsoft Entra ID Tenant populated with users. [Create a tenant as per Microsoft's documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
|
||||
|
||||
- A Formbricks instance running and accessible.
|
||||
|
||||
- The callback URI for your Formbricks instance: `{WEBAPP_URL}/api/auth/callback/azure-ad`
|
||||
|
||||
## How to connect your Formbricks instance to Microsoft Entra
|
||||
|
||||
<Steps>
|
||||
<Step title="Access the Microsoft Entra admin center">
|
||||
- Login to the [Microsoft Entra admin center](https://entra.microsoft.com/).
|
||||
- Go to **Applications** > **App registrations** in the left menu.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Create a new app registration">
|
||||
- Click the **New registration** button at the top.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Configure the application">
|
||||
- Name your application something descriptive, such as `Formbricks SSO`.
|
||||
|
||||

|
||||
|
||||
- If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_.
|
||||
|
||||

|
||||
|
||||
- Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above).
|
||||
|
||||

|
||||
|
||||
- Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created.
|
||||
</Step>
|
||||
|
||||
<Step title="Collect application credentials">
|
||||
- On the _Overview_ page, under **Essentials**:
|
||||
- Copy the entry for **Application (client) ID** to populate the `AZUREAD_CLIENT_ID` variable.
|
||||
- Copy the entry for **Directory (tenant) ID** to populate the `AZUREAD_TENANT_ID` variable.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Create a client secret">
|
||||
- From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**.
|
||||
|
||||

|
||||
|
||||
- Make sure you have the **Client secrets** tab active, and click **New client secret**.
|
||||
|
||||

|
||||
|
||||
- Enter a **Description**, set an **Expires** period, then click **Add**.
|
||||
|
||||
<Note>
|
||||
You will need to create a new client secret using these steps whenever your chosen expiry period ends.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
- Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable.
|
||||
|
||||
<Note>
|
||||
Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply create a new secret.
|
||||
</Note>
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Update environment variables">
|
||||
- Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
|
||||
|
||||
<Note>
|
||||
You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., "THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters.
|
||||
</Note>
|
||||
|
||||
An example `.env` for Microsoft Entra ID in Formbricks would look like this:
|
||||
|
||||
```yml Formbricks Env for Microsoft Entra ID SSO
|
||||
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
|
||||
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
|
||||
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Restart and test">
|
||||
- Restart your Formbricks instance.
|
||||
- You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
title: "Google OAuth"
|
||||
description: "Configure Google OAuth for secure Single Sign-On with your Formbricks instance. Implement enterprise-grade authentication for your survey platform with Google credentials."
|
||||
icon: "google"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license).
|
||||
</Note>
|
||||
|
||||
### Google OAuth
|
||||
|
||||
Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance.
|
||||
|
||||
### Requirements
|
||||
|
||||
- A Google Cloud Platform (GCP) account
|
||||
|
||||
- A Formbricks instance running
|
||||
|
||||
### How to connect your Formbricks instance to Google
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a GCP Project">
|
||||
- Navigate to the [GCP Console](https://console.cloud.google.com/).
|
||||
- From the projects list, select a project or create a new one.
|
||||
</Step>
|
||||
|
||||
<Step title="Setting up OAuth 2.0">
|
||||
- If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**.
|
||||
- On the left, click **Credentials**.
|
||||
- Click **Create Credentials**, then select **OAuth client ID**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure OAuth Consent Screen">
|
||||
- If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**.
|
||||
- Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted.
|
||||
</Step>
|
||||
|
||||
<Step title="Create OAuth 2.0 Client IDs">
|
||||
- Select the application type **Web application** for your project and enter any additional information required.
|
||||
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
|
||||
|
||||
```
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Update Environment Variables in Docker">
|
||||
- To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container.
|
||||
|
||||
- In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform:
|
||||
|
||||
```sh
|
||||
GOOGLE_CLIENT_ID=your-client-id-here
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
```
|
||||
|
||||
- Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID):
|
||||
|
||||
```sh
|
||||
docker exec -it container_id /bin/bash
|
||||
export GOOGLE_CLIENT_ID=your-client-id-here
|
||||
export GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
exit
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Restart Your Formbricks Instance">
|
||||
<Note>
|
||||
Restarting your Docker containers may cause a brief period of downtime. Plan accordingly.
|
||||
</Note>
|
||||
|
||||
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication.
|
||||
|
||||
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
|
||||
|
||||
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration.
|
||||
</Step>
|
||||
</Steps>
|
||||
208
docs/self-hosting/configuration/auth-sso/oauth.mdx
Normal file
208
docs/self-hosting/configuration/auth-sso/oauth.mdx
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: OAuth
|
||||
description: "OAuth for Formbricks"
|
||||
icon: "key"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Entra ID, Github and OpenID Connect, requires a valid Formbricks Enterprise License.
|
||||
</Note>
|
||||
|
||||
### Google OAuth
|
||||
|
||||
Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance.
|
||||
|
||||
#### Requirements:
|
||||
|
||||
- A Google Cloud Platform (GCP) account.
|
||||
|
||||
- A Formbricks instance running and accessible.
|
||||
|
||||
#### Steps:
|
||||
|
||||
1. **Create a GCP Project**:
|
||||
|
||||
- Navigate to the [GCP Console](https://console.cloud.google.com/).
|
||||
|
||||
- From the projects list, select a project or create a new one.
|
||||
|
||||
2. **Setting up OAuth 2.0**:
|
||||
|
||||
- If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**.
|
||||
|
||||
- On the left, click **Credentials**.
|
||||
|
||||
- Click **Create Credentials**, then select **OAuth client ID**.
|
||||
|
||||
3. **Configure OAuth Consent Screen**:
|
||||
|
||||
- If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**.
|
||||
|
||||
- Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted.
|
||||
|
||||
4. **Create OAuth 2.0 Client IDs**:
|
||||
|
||||
- Select the application type **Web application** for your project and enter any additional information required.
|
||||
|
||||
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
|
||||
|
||||
```{{ Redirect & Origin URLs
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
|
||||
```
|
||||
|
||||
- **Update Environment Variables in Docker**:
|
||||
|
||||
- To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container.
|
||||
|
||||
- In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform:
|
||||
|
||||
- Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID):
|
||||
|
||||
```sh Shell commands
|
||||
docker exec -it container_id /bin/bash
|
||||
export GOOGLE_CLIENT_ID=your-client-id-here
|
||||
export GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
exit
|
||||
```
|
||||
|
||||
```sh env file
|
||||
GOOGLE_CLIENT_ID=your-client-id-here
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
```
|
||||
|
||||
1. **Restart Your Formbricks Instance**:
|
||||
|
||||
- **Note:** Restarting your Docker containers may cause a brief period of downtime. Plan accordingly.
|
||||
|
||||
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it:
|
||||
|
||||
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
|
||||
|
||||
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
|
||||
|
||||
### Microsoft Entra ID (Azure Active Directory) SSO OAuth
|
||||
|
||||
Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks instance to allow users to log in using their existing Microsoft credentials. This guide will walk you through the process of setting up an Application Registration for your Formbricks instance.
|
||||
|
||||
#### Requirements
|
||||
|
||||
- A Microsoft Entra ID Tenant populated with users. [Create a tenant as per Microsoft's documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
|
||||
|
||||
- A Formbricks instance running and accessible.
|
||||
|
||||
- The callback URI for your Formbricks instance: `{WEBAPP_URL}/api/auth/callback/azure-ad`
|
||||
|
||||
#### Creating an App Registration
|
||||
|
||||
- Login to the [Microsoft Entra admin center](https://entra.microsoft.com/).
|
||||
|
||||
- Go to **Applications** > **App registrations** in the left menu.
|
||||
|
||||

|
||||
|
||||
- Click the **New registration** button at the top.
|
||||
|
||||

|
||||
|
||||
- Name your application something descriptive, such as `Formbricks SSO`.
|
||||
|
||||

|
||||
|
||||
- If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_.
|
||||
|
||||

|
||||
|
||||
- Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above).
|
||||
|
||||

|
||||
|
||||
- Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created.
|
||||
|
||||
- On the _Overview_ page, under **Essentials**:
|
||||
|
||||
- Copy the entry for **Application (client) ID** to populate the `AZUREAD_CLIENT_ID` variable.
|
||||
|
||||
- Copy the entry for **Directory (tenant) ID** to populate the `AZUREAD_TENANT_ID` variable.
|
||||
|
||||

|
||||
|
||||
- From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**.
|
||||
|
||||

|
||||
|
||||
- Make sure you have the **Client secrets** tab active, and click **New client secret**.
|
||||
|
||||

|
||||
|
||||
- Enter a **Description**, set an **Expires** period, then click **Add**.
|
||||
|
||||
<Note>
|
||||
You will need to create a new client secret using these steps whenever your chosen expiry period ends.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
- Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable.
|
||||
|
||||
<Note>
|
||||
Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply start from step 9 to create a new secret.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
- Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
|
||||
|
||||
<Note>
|
||||
You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., "THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters.
|
||||
</Note>
|
||||
|
||||
An example `.env` for Microsoft Entra ID in Formbricks would look like:
|
||||
|
||||
```yml Formbricks Env for Microsoft Entra ID SSO
|
||||
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
|
||||
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
|
||||
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
|
||||
```
|
||||
|
||||
- Restart your Formbricks instance.
|
||||
|
||||
- You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
|
||||
|
||||
## OpenID Configuration
|
||||
|
||||
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
|
||||
|
||||
- Configure your OIDC provider & get the following variables:
|
||||
|
||||
- `OIDC_CLIENT_ID`
|
||||
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
|
||||
- `OIDC_ISSUER`
|
||||
|
||||
- `OIDC_SIGNING_ALGORITHM`
|
||||
|
||||
<Note>
|
||||
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
|
||||
</Note>
|
||||
|
||||
- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
|
||||
|
||||
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
|
||||
|
||||
|
||||
```yml Formbricks Env for FusionAuth OIDC
|
||||
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
|
||||
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
|
||||
OIDC_ISSUER=http://localhost:9011
|
||||
OIDC_DISPLAY_NAME=FusionAuth
|
||||
OIDC_SIGNING_ALGORITHM=HS256
|
||||
```
|
||||
|
||||
|
||||
- Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
|
||||
|
||||
- Restart your Formbricks instance.
|
||||
|
||||
- You're all set! Users can now sign up & log in using their OIDC credentials.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: "Open ID Connect"
|
||||
description: "Configure Open ID Connect for secure Single Sign-On with your Formbricks instance. Implement enterprise-grade authentication for your survey platform with Open ID Connect."
|
||||
icon: "key"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license).
|
||||
</Note>
|
||||
|
||||
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
|
||||
|
||||
- Configure your OIDC provider & get the following variables:
|
||||
|
||||
- `OIDC_CLIENT_ID`
|
||||
|
||||
- `OIDC_CLIENT_SECRET`
|
||||
|
||||
- `OIDC_ISSUER`
|
||||
|
||||
- `OIDC_SIGNING_ALGORITHM`
|
||||
|
||||
<Note>
|
||||
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
|
||||
</Note>
|
||||
|
||||
- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
|
||||
|
||||
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
|
||||
|
||||
|
||||
```yml Formbricks Env for FusionAuth OIDC
|
||||
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
|
||||
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
|
||||
OIDC_ISSUER=http://localhost:9011
|
||||
OIDC_DISPLAY_NAME=FusionAuth
|
||||
OIDC_SIGNING_ALGORITHM=HS256
|
||||
```
|
||||
|
||||
|
||||
- Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
|
||||
|
||||
- Restart your Formbricks instance.
|
||||
|
||||
- You're all set! Users can now sign up & log in using their OIDC credentials.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "SAML SSO - Self-hosted"
|
||||
title: "SAML SSO"
|
||||
icon: "user-shield"
|
||||
description: "Configure SAML Single Sign-On (SSO) for secure enterprise authentication with your Formbricks instance."
|
||||
description: "How to set up SAML SSO for Formbricks"
|
||||
---
|
||||
|
||||
<Note>You require an Enterprise License along with a SAML SSO add-on to avail this feature.</Note>
|
||||
@@ -12,7 +12,7 @@ Formbricks supports SAML Single Sign-On (SSO) to enable secure, centralized auth
|
||||
|
||||
To learn more about SAML Jackson, please refer to the [BoxyHQ SAML Jackson documentation](https://boxyhq.com/docs/jackson/deploy).
|
||||
|
||||
## How SAML works in Formbricks
|
||||
## How SAML Works in Formbricks
|
||||
|
||||
SAML (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between an Identity Provider (IdP) and Formbricks. Here's how the integration works with BoxyHQ Jackson embedded into the flow:
|
||||
|
||||
@@ -37,7 +37,7 @@ SAML (Security Assertion Markup Language) is an XML-based standard for exchangin
|
||||
7. **Access Granted:**
|
||||
Formbricks logs the user in using the verified information.
|
||||
|
||||
## SAML Auth Flow Sequence Diagram
|
||||
## SAML Authentication Flow Sequence Diagram
|
||||
|
||||
Below is a sequence diagram illustrating the complete SAML authentication flow with BoxyHQ Jackson integrated:
|
||||
|
||||
@@ -67,31 +67,12 @@ sequenceDiagram
|
||||
|
||||
To configure SAML SSO in Formbricks, follow these steps:
|
||||
|
||||
<Steps>
|
||||
<Step title="Database Setup">
|
||||
Configure a dedicated database for SAML by setting the `SAML_DATABASE_URL` environment variable in your `docker-compose.yml` file (e.g., `postgres://postgres:postgres@postgres:5432/formbricks-saml`). If you're using a self-signed certificate for Postgres, include the `sslmode=disable` parameter.
|
||||
</Step>
|
||||
|
||||
<Step title="IdP Application">
|
||||
Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers))
|
||||
</Step>
|
||||
|
||||
<Step title="User Provisioning">
|
||||
Provision users in your IdP and configure access to the IdP SAML app for all your users (who need access to Formbricks).
|
||||
</Step>
|
||||
|
||||
<Step title="Metadata">
|
||||
Keep the XML metadata from your IdP handy for the next step.
|
||||
</Step>
|
||||
|
||||
<Step title="Metadata Setup">
|
||||
Create a file called `connection.xml` in your self-hosted Formbricks instance's `formbricks/saml-connection` directory and paste the XML metadata from your IdP into it. Please create the directory if it doesn't exist. Your metadata file should start with a tag like this: `<?xml version="1.0" encoding="UTF-8"?><...>` or `<md:EntityDescriptor entityID="...">`. Please remove any extra text from the metadata.
|
||||
</Step>
|
||||
|
||||
<Step title="Restart Formbricks">
|
||||
Restart Formbricks to apply the changes. You can do this by running `docker compose down` and then `docker compose up -d`.
|
||||
</Step>
|
||||
</Steps>
|
||||
1. **Database Setup:** Configure a dedicated database for SAML by setting the `SAML_DATABASE_URL` environment variable in your `docker-compose.yml` file (e.g., `postgres://postgres:postgres@postgres:5432/formbricks-saml`). If you're using a self-signed certificate for Postgres, include the `sslmode=disable` parameter.
|
||||
2. **IdP Application:** Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers))
|
||||
3. **User Provisioning:** Provision users in your IdP and configure access to the IdP SAML app for all your users (who need access to Formbricks).
|
||||
4. **Metadata:** Keep the XML metadata from your IdP handy for the next step.
|
||||
5. **Metadata Setup:** Create a file called `connection.xml` in your self-hosted Formbricks instance's `formbricks/saml-connection` directory and paste the XML metadata from your IdP into it. Please create the directory if it doesn't exist. Your metadata file should start with a tag like this: `<?xml version="1.0" encoding="UTF-8"?><...>` or `<md:EntityDescriptor entityID="...">`. Please remove any extra text from the metadata.
|
||||
6. **Restart Formbricks:** Restart Formbricks to apply the changes. You can do this by running `docker compose down` and then `docker compose up -d`.
|
||||
|
||||
<Note>
|
||||
We don't support multiple SAML connections yet. You can only have one SAML connection at a time. If you
|
||||
|
||||
@@ -33,7 +33,6 @@ These variables are present inside your machine’s 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) | |
|
||||
|
||||
@@ -33,7 +33,6 @@ 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
|
||||
@@ -76,7 +75,6 @@ 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
|
||||
@@ -97,7 +95,6 @@ environment:
|
||||
|
||||
```bash
|
||||
MAIL_FROM=noreply@yourdomain.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=smtp.sendgrid.net
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=apikey
|
||||
@@ -108,7 +105,6 @@ 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
|
||||
@@ -119,7 +115,6 @@ 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Third-party Integrations"
|
||||
title: "Overview"
|
||||
description: "Configure third-party integrations with Formbricks Cloud."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Quickstart - Link Surveys"
|
||||
title: "Quickstart"
|
||||
description: "Create your first link survey in under 5 minutes."
|
||||
icon: "rocket"
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Quickstart - Web & App Surveys"
|
||||
title: "Quickstart"
|
||||
description: "App surveys deliver 6–10x higher conversion rates compared to email surveys. If you are new to Formbricks, follow the steps in this guide to launch a survey in your web or mobile app (React Native) within 10–15 minutes."
|
||||
icon: "rocket"
|
||||
---
|
||||
|
||||
@@ -5,7 +5,8 @@ description: A Helm chart for Formbricks with PostgreSQL, Redis
|
||||
type: application
|
||||
|
||||
# Helm chart Version
|
||||
version: 0.0.0-dev
|
||||
version: 3.3.1
|
||||
appVersion: v3.3.1
|
||||
|
||||
keywords:
|
||||
- formbricks
|
||||
@@ -17,6 +18,7 @@ maintainers:
|
||||
- name: Formbricks
|
||||
email: info@formbricks.com
|
||||
|
||||
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: "16.4.16"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# formbricks
|
||||
|
||||
 
|
||||
  
|
||||
|
||||
A Helm chart for Formbricks with PostgreSQL, Redis
|
||||
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
{{- if (.Values.cronJob).enabled }}
|
||||
{{- range $name, $job := .Values.cronJob.jobs }}
|
||||
---
|
||||
{{ if $.Capabilities.APIVersions.Has "batch/v1/CronJob" -}}
|
||||
apiVersion: batch/v1
|
||||
{{- else -}}
|
||||
apiVersion: batch/v1beta1
|
||||
{{- end }}
|
||||
apiVersion: {{ if $.Capabilities.APIVersions.Has "batch/v1/CronJob" }}batch/v1{{ else }}batch/v1beta1{{ end }}
|
||||
kind: CronJob
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "formbricks.labels" $ | nindent 4 }}
|
||||
{{- if $job.additionalLabels }}
|
||||
{{ $job.additionalLabels | indent 4 }}
|
||||
{{- end }}
|
||||
{{- if $job.annotations }}
|
||||
annotations:
|
||||
{{ $job.annotations | indent 4 }}
|
||||
{{- end }}
|
||||
name: {{ $name }}
|
||||
namespace: {{ template "formbricks.namespace" $ }}
|
||||
labels:
|
||||
# Standard labels for tracking CronJobs
|
||||
{{- include "formbricks.labels" $ | nindent 4 }}
|
||||
|
||||
# Additional labels if specified
|
||||
{{- if $job.additionalLabels }}
|
||||
{{- toYaml $job.additionalLabels | indent 4 }}
|
||||
{{- end }}
|
||||
|
||||
# Additional annotations if specified
|
||||
{{- if $job.annotations }}
|
||||
annotations:
|
||||
{{- toYaml $job.annotations | indent 4 }}
|
||||
{{- end }}
|
||||
|
||||
spec:
|
||||
# Define the execution schedule for the job
|
||||
schedule: {{ $job.schedule | quote }}
|
||||
{{- if ge (int $.Capabilities.KubeVersion.Minor) 27 }}
|
||||
{{- if $job.timeZone }}
|
||||
|
||||
# Kubernetes 1.27+ supports time zones for CronJobs
|
||||
{{- if ge (int $.Capabilities.KubeVersion.Minor) 27 }}
|
||||
{{- if $job.timeZone }}
|
||||
timeZone: {{ $job.timeZone }}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- if $job.successfulJobsHistoryLimit }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
# Define job retention policies
|
||||
{{- if $job.successfulJobsHistoryLimit }}
|
||||
successfulJobsHistoryLimit: {{ $job.successfulJobsHistoryLimit }}
|
||||
{{ end }}
|
||||
{{- if $job.concurrencyPolicy }}
|
||||
concurrencyPolicy: {{ $job.concurrencyPolicy }}
|
||||
{{ end }}
|
||||
{{- if $job.failedJobsHistoryLimit }}
|
||||
{{- end }}
|
||||
{{- if $job.failedJobsHistoryLimit }}
|
||||
failedJobsHistoryLimit: {{ $job.failedJobsHistoryLimit }}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
# Define concurrency policy
|
||||
{{- if $job.concurrencyPolicy }}
|
||||
concurrencyPolicy: {{ $job.concurrencyPolicy }}
|
||||
{{- end }}
|
||||
|
||||
jobTemplate:
|
||||
spec:
|
||||
{{- with $job.activeDeadlineSeconds }}
|
||||
@@ -46,101 +55,48 @@ spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "formbricks.labels" $ | nindent 12 }}
|
||||
{{- include "formbricks.labels" $ | nindent 12 }}
|
||||
|
||||
# Additional pod-level labels
|
||||
{{- with $job.additionalPodLabels }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
# Additional annotations
|
||||
{{- with $job.additionalPodAnnotations }}
|
||||
annotations: {{ toYaml . | nindent 12 }}
|
||||
annotations: {{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
spec:
|
||||
# Define the service account if RBAC is enabled
|
||||
{{- if $.Values.rbac.enabled }}
|
||||
{{- if $.Values.rbac.serviceAccount.name }}
|
||||
serviceAccountName: {{ $.Values.rbac.serviceAccount.name }}
|
||||
{{- else }}
|
||||
serviceAccountName: {{ template "formbricks.name" $ }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
# Define the job container
|
||||
containers:
|
||||
- name: {{ $name }}
|
||||
{{- $image := required (print "Undefined image repo for container '" $name "'") $job.image.repository }}
|
||||
{{- with $job.image.tag }} {{- $image = print $image ":" . }} {{- end }}
|
||||
{{- with $job.image.digest }} {{- $image = print $image "@" . }} {{- end }}
|
||||
image: {{ $image }}
|
||||
{{- if $job.image.imagePullPolicy }}
|
||||
imagePullPolicy: {{ $job.image.imagePullPolicy }}
|
||||
{{ end }}
|
||||
- name: {{ $name }}
|
||||
image: "{{ required "Image repository is undefined" $job.image.repository }}:{{ $job.image.tag | default "latest" }}"
|
||||
imagePullPolicy: {{ $job.image.imagePullPolicy | default "IfNotPresent" }}
|
||||
|
||||
# Environment variables from values
|
||||
{{- with $job.env }}
|
||||
env:
|
||||
env:
|
||||
{{- range $key, $value := $job.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }}
|
||||
{{- else }}
|
||||
{{- toYaml $value | nindent 16 }}
|
||||
{{- end }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $job.envFrom }}
|
||||
envFrom:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
{{- if $job.command }}
|
||||
command: {{ $job.command }}
|
||||
|
||||
# Define command and arguments if specified
|
||||
{{- with $job.command }}
|
||||
command: {{- toYaml . | indent 14 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with $job.args }}
|
||||
args:
|
||||
{{- range . }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with $job.resources }}
|
||||
resources:
|
||||
{{ toYaml . | indent 14 }}
|
||||
args: {{- toYaml . | indent 14 }}
|
||||
{{- end }}
|
||||
{{- with $job.volumeMounts }}
|
||||
volumeMounts:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with $job.securityContext }}
|
||||
securityContext: {{ toYaml . | nindent 14 }}
|
||||
{{- end }}
|
||||
{{- with $job.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with $job.affinity }}
|
||||
affinity:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
{{- with $job.priorityClassName }}
|
||||
priorityClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with $job.tolerations }}
|
||||
tolerations: {{ toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $job.topologySpreadConstraints }}
|
||||
topologySpreadConstraints: {{ toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if $job.restartPolicy }}
|
||||
restartPolicy: {{ $job.restartPolicy }}
|
||||
{{ else }}
|
||||
restartPolicy: OnFailure
|
||||
{{ end }}
|
||||
{{- with $job.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{ end }}
|
||||
{{- if $job.dnsConfig }}
|
||||
dnsConfig:
|
||||
{{ toYaml $job.dnsConfig | indent 12 }}
|
||||
{{- end }}
|
||||
{{- if $job.dnsPolicy }}
|
||||
dnsPolicy: {{ $job.dnsPolicy }}
|
||||
{{- end }}
|
||||
{{- with $job.volumes }}
|
||||
volumes:
|
||||
{{ toYaml . | indent 12 }}
|
||||
{{- end }}
|
||||
|
||||
restartPolicy: {{ $job.restartPolicy | default "OnFailure" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -13,9 +13,6 @@ metadata:
|
||||
{{- if .Values.deployment.annotations }}
|
||||
{{- toYaml .Values.deployment.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.reloadOnChange }}
|
||||
reloader.stakater.com/auto: "true"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.deployment.replicas }}
|
||||
@@ -97,12 +94,8 @@ spec:
|
||||
protocol: {{ $config.protocol | default "TCP" | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if or .Values.deployment.envFrom (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }}
|
||||
{{- if .Values.deployment.envFrom }}
|
||||
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:
|
||||
@@ -127,13 +120,47 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
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
|
||||
{{- 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" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }}
|
||||
{{- else }}
|
||||
{{- toYaml $value | nindent 14 }}
|
||||
{{- end }}
|
||||
{{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | indent 10 }}
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.resources }}
|
||||
resources:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{{- 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" . }}
|
||||
|
||||
@@ -54,14 +54,16 @@ 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:
|
||||
DOCKER_CRON_ENABLED:
|
||||
value: "0"
|
||||
EMAIL_VERIFICATION_DISABLED:
|
||||
value: "1"
|
||||
PASSWORD_RESET_DISABLED:
|
||||
value: "1"
|
||||
|
||||
# Tolerations for scheduling pods on tainted nodes
|
||||
tolerations: []
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
data "aws_ssm_parameter" "slack_notification_channel" {
|
||||
name = "/prod/formbricks/slack-webhook-url"
|
||||
with_decryption = true
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_log_group" "cloudwatch_cis_benchmark" {
|
||||
name = "/aws/cis-benchmark-group"
|
||||
retention_in_days = 365
|
||||
}
|
||||
|
||||
module "notify-slack" {
|
||||
source = "terraform-aws-modules/notify-slack/aws"
|
||||
version = "6.6.0"
|
||||
|
||||
slack_channel = "kubernetes"
|
||||
slack_username = "formbricks-cloudwatch"
|
||||
slack_webhook_url = data.aws_ssm_parameter.slack_notification_channel.value
|
||||
sns_topic_name = "cloudwatch-alarms"
|
||||
create_sns_topic = true
|
||||
}
|
||||
|
||||
module "cloudwatch_cis-alarms" {
|
||||
source = "terraform-aws-modules/cloudwatch/aws//modules/cis-alarms"
|
||||
version = "5.7.1"
|
||||
log_group_name = aws_cloudwatch_log_group.cloudwatch_cis_benchmark.name
|
||||
alarm_actions = [module.notify-slack.slack_topic_arn]
|
||||
}
|
||||
@@ -10,11 +10,3 @@ 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"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ module "iam_github_oidc_role" {
|
||||
"repo:formbricks/*:*",
|
||||
]
|
||||
policies = {
|
||||
Administrator = "arn:aws:iam::aws:policy/AdministratorAccess"
|
||||
Administrator = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
|
||||
}
|
||||
|
||||
tags = local.tags
|
||||
|
||||
@@ -32,6 +32,11 @@ module "route53_zones" {
|
||||
}
|
||||
}
|
||||
|
||||
output "route53_ns_records" {
|
||||
value = module.route53_zones.route53_zone_name_servers
|
||||
}
|
||||
|
||||
|
||||
module "acm" {
|
||||
source = "terraform-aws-modules/acm/aws"
|
||||
version = "5.1.1"
|
||||
@@ -244,7 +249,7 @@ module "eks" {
|
||||
cluster_name = "${local.name}-eks"
|
||||
cluster_version = "1.32"
|
||||
|
||||
enable_cluster_creator_admin_permissions = false
|
||||
enable_cluster_creator_admin_permissions = true
|
||||
cluster_endpoint_public_access = true
|
||||
|
||||
cluster_addons = {
|
||||
@@ -266,41 +271,6 @@ 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
|
||||
@@ -603,136 +573,95 @@ resource "helm_release" "formbricks" {
|
||||
|
||||
values = [
|
||||
<<-EOT
|
||||
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:
|
||||
postgresql:
|
||||
enabled: false
|
||||
redis:
|
||||
enabled: false
|
||||
ingress:
|
||||
enabled: true
|
||||
name: formbricks
|
||||
ingressClassName: alb
|
||||
hosts:
|
||||
- host: "app.${local.domain}"
|
||||
paths:
|
||||
- path: /
|
||||
pathType: "Prefix"
|
||||
serviceName: "formbricks"
|
||||
annotations:
|
||||
eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn}
|
||||
serviceMonitor:
|
||||
enabled: true
|
||||
reloadOnChange: 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:
|
||||
last_updated_at: ${timestamp()}
|
||||
externalSecret:
|
||||
enabled: true # Enable/disable ExternalSecrets
|
||||
secretStore:
|
||||
name: aws-secrets-manager
|
||||
kind: ClusterSecretStore
|
||||
refreshInterval: "1m"
|
||||
files:
|
||||
app-env:
|
||||
dataFrom:
|
||||
key: "prod/formbricks/environment"
|
||||
app-secrets:
|
||||
dataFrom:
|
||||
key: "prod/formbricks/secrets"
|
||||
cronJob:
|
||||
enabled: true
|
||||
jobs:
|
||||
survey-status:
|
||||
schedule: "0 0 * * *"
|
||||
env:
|
||||
CRON_SECRET:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "formbricks-app-env"
|
||||
key: "CRON_SECRET"
|
||||
WEBAPP_URL:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "formbricks-app-env"
|
||||
key: "WEBAPP_URL"
|
||||
image:
|
||||
repository: curlimages/curl
|
||||
tag: latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" "$WEBAPP_URL/api/cron/survey-status"'
|
||||
weekely-summary:
|
||||
schedule: "0 8 * * 1"
|
||||
env:
|
||||
CRON_SECRET:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "formbricks-app-env"
|
||||
key: "CRON_SECRET"
|
||||
WEBAPP_URL:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "formbricks-app-env"
|
||||
key: "WEBAPP_URL"
|
||||
image:
|
||||
repository: curlimages/curl
|
||||
tag: latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" "$WEBAPP_URL/api/cron/weekly-summary"'
|
||||
ping:
|
||||
schedule: "0 9 * * *"
|
||||
env:
|
||||
CRON_SECRET:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "formbricks-app-env"
|
||||
key: "CRON_SECRET"
|
||||
WEBAPP_URL:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "formbricks-app-env"
|
||||
key: "WEBAPP_URL"
|
||||
image:
|
||||
repository: curlimages/curl
|
||||
tag: latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" "$WEBAPP_URL/api/cron/ping"'
|
||||
EOT
|
||||
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
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# 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"
|
||||
@@ -8,7 +24,10 @@ 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({
|
||||
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"
|
||||
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"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
"db:migrate:deploy": "turbo run db:migrate:deploy",
|
||||
"db:start": "turbo run db:start",
|
||||
"db:push": "turbo run db:push",
|
||||
"db:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||
"db:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"go": "pnpm db:up && turbo run go --concurrency 20",
|
||||
"go": "turbo run go --concurrency 20",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"pre-commit": "lint-staged",
|
||||
"start": "turbo run start --parallel",
|
||||
|
||||
7
packages/android/.gitignore
vendored
7
packages/android/.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -11,7 +11,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.formbricks.demo"
|
||||
minSdk = 24
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Demo"
|
||||
tools:targetApi="31">
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -12,7 +12,7 @@ android {
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
minSdk = 26
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
@@ -30,24 +30,6 @@ android {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "META-INF/library_release.kotlin_module"
|
||||
excludes += "classes.dex"
|
||||
excludes += "**.**"
|
||||
pickFirsts += "**/DataBinderMapperImpl.java"
|
||||
pickFirsts += "**/DataBinderMapperImpl.class"
|
||||
pickFirsts += "**/formbrickssdk/DataBinderMapperImpl.java"
|
||||
pickFirsts += "**/formbrickssdk/DataBinderMapperImpl.class"
|
||||
}
|
||||
}
|
||||
viewBinding {
|
||||
enable = true
|
||||
}
|
||||
dataBinding {
|
||||
enable = true
|
||||
}
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding = true
|
||||
@@ -83,6 +65,8 @@ dependencies {
|
||||
|
||||
implementation(libs.material)
|
||||
|
||||
implementation(libs.timber)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.androidx.legacy.support.v4)
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; }
|
||||
-keep class com.formbricks.formbrickssdk.Formbricks { *; }
|
||||
@@ -7,11 +7,12 @@ import androidx.annotation.Keep
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.formbricks.formbrickssdk.api.FormbricksApi
|
||||
import com.formbricks.formbrickssdk.helper.FormbricksConfig
|
||||
import com.formbricks.formbrickssdk.logger.Logger
|
||||
import com.formbricks.formbrickssdk.manager.SurveyManager
|
||||
import com.formbricks.formbrickssdk.manager.UserManager
|
||||
import com.formbricks.formbrickssdk.model.error.SDKError
|
||||
import com.formbricks.formbrickssdk.webview.FormbricksFragment
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
@Keep
|
||||
object Formbricks {
|
||||
@@ -60,6 +61,10 @@ object Formbricks {
|
||||
SurveyManager.refreshEnvironmentIfNeeded()
|
||||
UserManager.syncUserStateIfNeeded()
|
||||
|
||||
if (loggingEnabled) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
@@ -74,7 +79,7 @@ object Formbricks {
|
||||
*/
|
||||
fun setUserId(userId: String) {
|
||||
if (!isInitialized) {
|
||||
Logger.e(exception = SDKError.sdkIsNotInitialized)
|
||||
Timber.e(SDKError.sdkIsNotInitialized)
|
||||
return
|
||||
}
|
||||
UserManager.set(userId)
|
||||
@@ -91,7 +96,7 @@ object Formbricks {
|
||||
*/
|
||||
fun setAttribute(attribute: String, key: String) {
|
||||
if (!isInitialized) {
|
||||
Logger.e(exception = SDKError.sdkIsNotInitialized)
|
||||
Timber.e(SDKError.sdkIsNotInitialized)
|
||||
return
|
||||
}
|
||||
UserManager.addAttribute(attribute, key)
|
||||
@@ -108,7 +113,7 @@ object Formbricks {
|
||||
*/
|
||||
fun setAttributes(attributes: Map<String, String>) {
|
||||
if (!isInitialized) {
|
||||
Logger.e(exception = SDKError.sdkIsNotInitialized)
|
||||
Timber.e(SDKError.sdkIsNotInitialized)
|
||||
return
|
||||
}
|
||||
UserManager.setAttributes(attributes)
|
||||
@@ -125,7 +130,7 @@ object Formbricks {
|
||||
*/
|
||||
fun setLanguage(language: String) {
|
||||
if (!isInitialized) {
|
||||
Logger.e(exception = SDKError.sdkIsNotInitialized)
|
||||
Timber.e(SDKError.sdkIsNotInitialized)
|
||||
return
|
||||
}
|
||||
Formbricks.language = language
|
||||
@@ -143,12 +148,12 @@ object Formbricks {
|
||||
*/
|
||||
fun track(action: String) {
|
||||
if (!isInitialized) {
|
||||
Logger.e(exception = SDKError.sdkIsNotInitialized)
|
||||
Timber.e(SDKError.sdkIsNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isInternetAvailable()) {
|
||||
Logger.w(exception = SDKError.connectionIsNotAvailable)
|
||||
Timber.w(SDKError.connectionIsNotAvailable)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -166,7 +171,7 @@ object Formbricks {
|
||||
*/
|
||||
fun logout() {
|
||||
if (!isInitialized) {
|
||||
Logger.e(exception = SDKError.sdkIsNotInitialized)
|
||||
Timber.e(SDKError.sdkIsNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,7 +195,7 @@ object Formbricks {
|
||||
/// Assembles the survey fragment and presents it
|
||||
internal fun showSurvey(id: String) {
|
||||
if (fragmentManager == null) {
|
||||
Logger.e(exception = SDKError.fragmentManagerIsNotSet)
|
||||
Timber.e(SDKError.fragmentManagerIsNotSet)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
|
||||
import com.formbricks.formbrickssdk.model.user.UserState
|
||||
import com.formbricks.formbrickssdk.model.user.UserStateData
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
@@ -19,9 +22,9 @@ fun Date.dateString(): String {
|
||||
fun UserStateData.lastDisplayAt(): Date? {
|
||||
lastDisplayAt?.let {
|
||||
try {
|
||||
val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
|
||||
formatter.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return formatter.parse(it)
|
||||
val formatter = DateTimeFormatter.ofPattern(dateFormatPattern)
|
||||
val dateTime = LocalDateTime.parse(it, formatter)
|
||||
return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant())
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
@@ -33,9 +36,9 @@ fun UserStateData.lastDisplayAt(): Date? {
|
||||
fun UserState.expiresAt(): Date? {
|
||||
expiresAt?.let {
|
||||
try {
|
||||
val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
|
||||
formatter.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return formatter.parse(it)
|
||||
val formatter = DateTimeFormatter.ofPattern(dateFormatPattern)
|
||||
val dateTime = LocalDateTime.parse(it, formatter)
|
||||
return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant())
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
@@ -47,9 +50,9 @@ fun UserState.expiresAt(): Date? {
|
||||
fun EnvironmentDataHolder.expiresAt(): Date? {
|
||||
data?.expiresAt?.let {
|
||||
try {
|
||||
val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault())
|
||||
formatter.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return formatter.parse(it)
|
||||
val formatter = DateTimeFormatter.ofPattern(dateFormatPattern)
|
||||
val dateTime = LocalDateTime.parse(it, formatter)
|
||||
return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant())
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.formbricks.formbrickssdk.logger
|
||||
|
||||
import android.util.Log
|
||||
import com.formbricks.formbrickssdk.Formbricks
|
||||
|
||||
object Logger {
|
||||
fun d(message: String) {
|
||||
if (Formbricks.loggingEnabled) {
|
||||
Log.d("FormbricksSDK", message)
|
||||
}
|
||||
}
|
||||
|
||||
fun e(message: String? = "Exception", exception: RuntimeException? = null) {
|
||||
if (Formbricks.loggingEnabled) {
|
||||
Log.e("FormbricksSDK", message, exception)
|
||||
}
|
||||
}
|
||||
|
||||
fun w(message: String? = "Warning", exception: RuntimeException? = null) {
|
||||
if (Formbricks.loggingEnabled) {
|
||||
Log.w("FormbricksSDK", message, exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import com.formbricks.formbrickssdk.Formbricks
|
||||
import com.formbricks.formbrickssdk.api.FormbricksApi
|
||||
import com.formbricks.formbrickssdk.extensions.expiresAt
|
||||
import com.formbricks.formbrickssdk.extensions.guard
|
||||
import com.formbricks.formbrickssdk.logger.Logger
|
||||
import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder
|
||||
import com.formbricks.formbrickssdk.model.environment.Survey
|
||||
import com.formbricks.formbrickssdk.model.user.Display
|
||||
@@ -13,10 +12,12 @@ import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The SurveyManager is responsible for managing the surveys that are displayed to the user.
|
||||
@@ -57,7 +58,7 @@ object SurveyManager {
|
||||
try {
|
||||
Gson().fromJson(json, EnvironmentDataHolder::class.java)
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Unable to retrieve environment data from the local storage.")
|
||||
Timber.tag("SurveyManager").e("Unable to retrieve environment data from the local storage.")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -101,7 +102,7 @@ object SurveyManager {
|
||||
if (!force) {
|
||||
environmentDataHolder?.expiresAt()?.let {
|
||||
if (it.after(Date())) {
|
||||
Logger.d("Environment state is still valid until $it")
|
||||
Timber.tag("SurveyManager").d("Environment state is still valid until $it")
|
||||
filterSurveys()
|
||||
return
|
||||
}
|
||||
@@ -116,7 +117,7 @@ object SurveyManager {
|
||||
hasApiError = false
|
||||
} catch (e: Exception) {
|
||||
hasApiError = true
|
||||
Logger.e("Unable to refresh environment state.")
|
||||
Timber.tag("SurveyManager").e(e, "Unable to refresh environment state.")
|
||||
startErrorTimer()
|
||||
}
|
||||
}
|
||||
@@ -147,7 +148,7 @@ object SurveyManager {
|
||||
Formbricks.showSurvey(it)
|
||||
}
|
||||
|
||||
}, Date(System.currentTimeMillis() + timeout.toLong() * 1000))
|
||||
}, Date.from(Instant.now().plusSeconds(timeout.toLong())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,7 @@ object SurveyManager {
|
||||
*/
|
||||
fun postResponse(surveyId: String?) {
|
||||
val id = surveyId.guard {
|
||||
Logger.e("Survey id is mandatory to set.")
|
||||
Timber.tag("SurveyManager").e("Survey id is mandatory to set.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ object SurveyManager {
|
||||
*/
|
||||
fun onNewDisplay(surveyId: String?) {
|
||||
val id = surveyId.guard {
|
||||
Logger.e("Survey id is mandatory to set.")
|
||||
Timber.tag("SurveyManager").e("Survey id is mandatory to set.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ object SurveyManager {
|
||||
val date = expiresAt.guard { return }
|
||||
refreshTimer.schedule(object: TimerTask() {
|
||||
override fun run() {
|
||||
Logger.d("Refreshing environment state.")
|
||||
Timber.tag("SurveyManager").d("Refreshing environment state.")
|
||||
refreshEnvironmentIfNeeded()
|
||||
}
|
||||
|
||||
@@ -206,7 +207,7 @@ object SurveyManager {
|
||||
val targetDate = Date(System.currentTimeMillis() + 1000 * 60 * REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES)
|
||||
refreshTimer.schedule(object: TimerTask() {
|
||||
override fun run() {
|
||||
Logger.d("Refreshing environment state after an error")
|
||||
Timber.tag("SurveyManager").d("Refreshing environment state after an error")
|
||||
refreshEnvironmentIfNeeded()
|
||||
}
|
||||
|
||||
@@ -239,7 +240,7 @@ object SurveyManager {
|
||||
}
|
||||
|
||||
else -> {
|
||||
Logger.e("Invalid Display Option")
|
||||
Timber.tag("SurveyManager").e("Invalid Display Option")
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -256,7 +257,7 @@ object SurveyManager {
|
||||
val recontactDays = survey.recontactDays ?: defaultRecontactDays
|
||||
|
||||
if (recontactDays != null) {
|
||||
val daysBetween = TimeUnit.MILLISECONDS.toDays(Date().time - lastDisplayedAt.time)
|
||||
val daysBetween = ChronoUnit.DAYS.between(lastDisplayedAt.toInstant(), Instant.now())
|
||||
return@filter daysBetween >= recontactDays.toInt()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import com.formbricks.formbrickssdk.extensions.dateString
|
||||
import com.formbricks.formbrickssdk.extensions.expiresAt
|
||||
import com.formbricks.formbrickssdk.extensions.guard
|
||||
import com.formbricks.formbrickssdk.extensions.lastDisplayAt
|
||||
import com.formbricks.formbrickssdk.logger.Logger
|
||||
import com.formbricks.formbrickssdk.model.user.Display
|
||||
import com.formbricks.formbrickssdk.network.queue.UpdateQueue
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
@@ -140,7 +140,7 @@ object UserManager {
|
||||
SurveyManager.filterSurveys()
|
||||
startSyncTimer()
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Unable to post survey response.")
|
||||
Timber.tag("SurveyManager").e(e, "Unable to post survey response.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.formbricks.formbrickssdk.network.queue
|
||||
|
||||
import com.formbricks.formbrickssdk.logger.Logger
|
||||
import com.formbricks.formbrickssdk.manager.UserManager
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
@@ -57,11 +57,11 @@ class UpdateQueue private constructor() {
|
||||
private fun commit() {
|
||||
val currentUserId = userId
|
||||
if (currentUserId == null) {
|
||||
Logger.d("Error: User ID is not set yet")
|
||||
Timber.d("Error: User ID is not set yet")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d("UpdateQueue - commit() called on UpdateQueue with $currentUserId and $attributes")
|
||||
Timber.d("UpdateQueue - commit() called on UpdateQueue with $currentUserId and $attributes")
|
||||
UserManager.syncUser(currentUserId, attributes)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
@@ -23,14 +25,16 @@ import androidx.fragment.app.viewModels
|
||||
import com.formbricks.formbrickssdk.Formbricks
|
||||
import com.formbricks.formbrickssdk.R
|
||||
import com.formbricks.formbrickssdk.databinding.FragmentFormbricksBinding
|
||||
import com.formbricks.formbrickssdk.logger.Logger
|
||||
import com.formbricks.formbrickssdk.manager.SurveyManager
|
||||
import com.formbricks.formbrickssdk.model.javascript.FileUploadData
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.gson.JsonObject
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
@@ -54,8 +58,7 @@ class FormbricksFragment : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
}, Date(System.currentTimeMillis() + CLOSING_TIMEOUT_IN_SECONDS * 1000)
|
||||
)
|
||||
}, Date.from(Instant.now().plusSeconds(CLOSING_TIMEOUT_IN_SECONDS)))
|
||||
}
|
||||
|
||||
override fun onDisplayCreated() {
|
||||
@@ -155,7 +158,7 @@ class FormbricksFragment : BottomSheetDialogFragment() {
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
consoleMessage?.let { cm ->
|
||||
val log = "[CONSOLE:${cm.messageLevel()}] \"${cm.message()}\", source: ${cm.sourceId()} (${cm.lineNumber()})"
|
||||
Logger.d(log)
|
||||
Timber.tag("Javascript message").d(log)
|
||||
}
|
||||
return super.onConsoleMessage(consoleMessage)
|
||||
}
|
||||
@@ -226,3 +229,4 @@ class FormbricksFragment : BottomSheetDialogFragment() {
|
||||
private const val CLOSING_TIMEOUT_IN_SECONDS = 5L
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.formbricks.formbrickssdk.webview
|
||||
|
||||
import android.webkit.JavascriptInterface
|
||||
import com.formbricks.formbrickssdk.logger.Logger
|
||||
import com.formbricks.formbrickssdk.model.javascript.JsMessageData
|
||||
import com.formbricks.formbrickssdk.model.javascript.EventType
|
||||
import com.formbricks.formbrickssdk.model.javascript.FileUploadData
|
||||
import com.google.gson.JsonParseException
|
||||
import timber.log.Timber
|
||||
|
||||
class WebAppInterface(private val callback: WebAppCallback?) {
|
||||
|
||||
@@ -22,7 +22,7 @@ class WebAppInterface(private val callback: WebAppCallback?) {
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun message(data: String) {
|
||||
Logger.d(data)
|
||||
Timber.tag("WebAppInterface message").d(data)
|
||||
|
||||
try {
|
||||
val jsMessage = JsMessageData.from(data)
|
||||
@@ -34,13 +34,13 @@ class WebAppInterface(private val callback: WebAppCallback?) {
|
||||
EventType.ON_FILE_PICK -> { callback?.onFilePick(FileUploadData.from(data)) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e.message)
|
||||
Timber.tag("WebAppInterface error").e(e)
|
||||
} catch (e: JsonParseException) {
|
||||
Logger.e("Failed to parse JSON message: $data")
|
||||
Timber.tag("WebAppInterface error").e(e, "Failed to parse JSON message: $data")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.e("Invalid message format: $data")
|
||||
Timber.tag("WebAppInterface error").e(e, "Invalid message format: $data")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Unexpected error processing message: $data")
|
||||
Timber.tag("WebAppInterface error").e(e, "Unexpected error processing message: $data")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ lifecycleViewmodelKtx = "2.8.7"
|
||||
fragmentKtx = "1.8.5"
|
||||
databindingCommon = "8.8.0"
|
||||
|
||||
timber = "5.0.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
@@ -51,6 +53,7 @@ retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", ve
|
||||
retrofit-converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" }
|
||||
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp3" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" }
|
||||
|
||||
21
packages/database/docker-compose.yml
Normal file
21
packages/database/docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
mailhog:
|
||||
image: arjenz/mailhog # Copy of mailhog/MailHog to support linux/arm64
|
||||
ports:
|
||||
- 8025:8025 # web ui
|
||||
- 1025:1025 # smtp server
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
@@ -13,8 +13,10 @@
|
||||
"db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" tsx ./src/scripts/create-saml-database.ts",
|
||||
"db:create-saml-database:dev": "dotenv -e ../../.env -- tsx ./src/scripts/create-saml-database.ts",
|
||||
"db:push": "prisma db push --accept-data-loss",
|
||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||
"db:up": "docker compose up -d",
|
||||
"db:setup": "pnpm db:up && pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||
"db:start": "pnpm db:setup",
|
||||
"db:down": "docker compose down",
|
||||
"format": "prisma format",
|
||||
"generate": "prisma generate",
|
||||
"lint": "eslint ./src --fix",
|
||||
|
||||
@@ -83,7 +83,6 @@ 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;
|
||||
|
||||
@@ -49,7 +49,6 @@ 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(),
|
||||
@@ -174,7 +173,6 @@ 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,
|
||||
|
||||
@@ -36,7 +36,6 @@ interface VariableStackEntry {
|
||||
export function Survey({
|
||||
apiHost,
|
||||
environmentId,
|
||||
isPreviewMode = false,
|
||||
userId,
|
||||
contactId,
|
||||
mode,
|
||||
@@ -151,6 +150,7 @@ 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,11 +182,6 @@ 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");
|
||||
}
|
||||
@@ -211,17 +206,6 @@ 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({
|
||||
@@ -245,17 +229,7 @@ export function Survey({
|
||||
console.error("error creating display: ", err);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
apiClient,
|
||||
surveyState,
|
||||
responseQueue,
|
||||
survey.id,
|
||||
userId,
|
||||
contactId,
|
||||
onDisplayCreated,
|
||||
isPreviewMode,
|
||||
onDisplay,
|
||||
]);
|
||||
}, [apiClient, surveyState, responseQueue, survey.id, userId, contactId, onDisplayCreated]);
|
||||
|
||||
useEffect(() => {
|
||||
// call onDisplay when component is mounted
|
||||
@@ -411,32 +385,6 @@ 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);
|
||||
@@ -467,20 +415,7 @@ export function Survey({
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
apiHost,
|
||||
environmentId,
|
||||
isPreviewMode,
|
||||
surveyState,
|
||||
responseQueue,
|
||||
contactId,
|
||||
userId,
|
||||
survey,
|
||||
action,
|
||||
hiddenFieldsRecord,
|
||||
onResponseCreated,
|
||||
onResponse,
|
||||
]
|
||||
[surveyState, responseQueue, contactId, userId, survey, action, hiddenFieldsRecord, onResponseCreated]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -511,14 +446,25 @@ export function Survey({
|
||||
onChange(surveyResponseData);
|
||||
onChangeVariables(calculatedVariables);
|
||||
|
||||
onResponseCreateOrUpdate({
|
||||
data: surveyResponseData,
|
||||
ttc: responsettc,
|
||||
finished,
|
||||
variables: calculatedVariables,
|
||||
language: selectedLanguage,
|
||||
endingId,
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
if (nextQuestionId) {
|
||||
setQuestionId(nextQuestionId);
|
||||
@@ -627,7 +573,7 @@ export function Survey({
|
||||
onBack={onBack}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
onFileUpload={onFileUpload ?? onFileUploadApi}
|
||||
onFileUpload={apiHost && environmentId ? onFileUploadApi : onFileUpload!}
|
||||
isFirstQuestion={question.id === localSurvey.questions[0]?.id}
|
||||
skipPrefilled={skipPrefilled}
|
||||
prefilledQuestionValue={getQuestionPrefillData(question.id, offset)}
|
||||
|
||||
@@ -34,6 +34,12 @@ export const getShuffledRowIndices = (n: number, shuffleOption: TShuffleOption):
|
||||
shuffle(array);
|
||||
array.push(lastElement);
|
||||
}
|
||||
} else if (shuffleOption === "exceptLastTwo") {
|
||||
if (array.length >= 2) {
|
||||
const lastTwo = array.splice(array.length - 2, 2);
|
||||
shuffle(array);
|
||||
array.push(...lastTwo);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
};
|
||||
@@ -57,6 +63,20 @@ export const getShuffledChoicesIds = (
|
||||
shuffle(shuffledChoices);
|
||||
shuffledChoices.push(lastElement);
|
||||
}
|
||||
} else if (shuffleOption === "exceptLastTwo") {
|
||||
if (otherOption) {
|
||||
if (shuffledChoices.length >= 1) {
|
||||
// Keep the last element fixed (the one before "other")
|
||||
const lastElement = shuffledChoices.pop();
|
||||
shuffle(shuffledChoices);
|
||||
if (lastElement) shuffledChoices.push(lastElement);
|
||||
}
|
||||
} else if (shuffledChoices.length >= 2) {
|
||||
// No "other" option, keep last two elements fixed
|
||||
const lastTwo = shuffledChoices.splice(shuffledChoices.length - 2, 2);
|
||||
shuffle(shuffledChoices);
|
||||
shuffledChoices.push(...lastTwo);
|
||||
}
|
||||
}
|
||||
|
||||
if (otherOption) {
|
||||
|
||||
@@ -45,7 +45,6 @@ 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>;
|
||||
|
||||
@@ -570,7 +570,7 @@ export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
|
||||
|
||||
export type TSurveyConsentQuestion = z.infer<typeof ZSurveyConsentQuestion>;
|
||||
|
||||
export const ZShuffleOption = z.enum(["none", "all", "exceptLast"]);
|
||||
export const ZShuffleOption = z.enum(["none", "all", "exceptLast", "exceptLastTwo"]);
|
||||
|
||||
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
|
||||
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"dependsOn": ["@formbricks/api#build"],
|
||||
"persistent": true
|
||||
},
|
||||
"@formbricks/database#setup": {
|
||||
"dependsOn": ["db:up"]
|
||||
},
|
||||
"@formbricks/demo#go": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@formbricks/js#build"],
|
||||
@@ -122,7 +119,6 @@
|
||||
"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",
|
||||
@@ -227,10 +223,6 @@
|
||||
"db:start": {
|
||||
"cache": false
|
||||
},
|
||||
"db:up": {
|
||||
"cache": false,
|
||||
"outputs": []
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
|
||||
Reference in New Issue
Block a user