mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-05 05:11:32 -05:00
feat: adds second domain (#4989)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -80,6 +80,9 @@ S3_ENDPOINT_URL=
|
||||
# Force path style for S3 compatible storage (0 for disabled, 1 for enabled)
|
||||
S3_FORCE_PATH_STYLE=0
|
||||
|
||||
# Set this URL to add a custom domain to your survey links(default is WEBAPP_URL)
|
||||
# SURVEY_URL=https://survey.example.com
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
#####################
|
||||
|
||||
+3
-1
@@ -13,6 +13,7 @@ import {
|
||||
RESPONSES_PER_PAGE,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
@@ -47,6 +48,7 @@ const Page = async (props) => {
|
||||
});
|
||||
const shouldGenerateInsights = needsInsightsGeneration(survey);
|
||||
const locale = await findMatchingLocale();
|
||||
const surveyDomain = getSurveyDomain();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
@@ -57,8 +59,8 @@ const Page = async (props) => {
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
user={user}
|
||||
surveyDomain={surveyDomain}
|
||||
/>
|
||||
}>
|
||||
{isAIEnabled && shouldGenerateInsights && (
|
||||
|
||||
+4
-4
@@ -23,19 +23,19 @@ import { PanelInfoView } from "./shareEmbedModal/PanelInfoView";
|
||||
|
||||
interface ShareEmbedSurveyProps {
|
||||
survey: TSurvey;
|
||||
surveyDomain: string;
|
||||
open: boolean;
|
||||
modalView: "start" | "embed" | "panel";
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
webAppUrl: string;
|
||||
user: TUser;
|
||||
}
|
||||
|
||||
export const ShareEmbedSurvey = ({
|
||||
survey,
|
||||
surveyDomain,
|
||||
open,
|
||||
modalView,
|
||||
setOpen,
|
||||
webAppUrl,
|
||||
user,
|
||||
}: ShareEmbedSurveyProps) => {
|
||||
const router = useRouter();
|
||||
@@ -104,8 +104,8 @@ export const ShareEmbedSurvey = ({
|
||||
<DialogDescription className="hidden" />
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={user.locale}
|
||||
/>
|
||||
@@ -159,8 +159,8 @@ export const ShareEmbedSurvey = ({
|
||||
survey={survey}
|
||||
email={email}
|
||||
surveyUrl={surveyUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
webAppUrl={webAppUrl}
|
||||
locale={user.locale}
|
||||
/>
|
||||
) : showView === "panel" ? (
|
||||
|
||||
+4
-4
@@ -20,8 +20,8 @@ interface SurveyAnalysisCTAProps {
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
isReadOnly: boolean;
|
||||
webAppUrl: string;
|
||||
user: TUser;
|
||||
surveyDomain: string;
|
||||
}
|
||||
|
||||
interface ModalState {
|
||||
@@ -35,8 +35,8 @@ export const SurveyAnalysisCTA = ({
|
||||
survey,
|
||||
environment,
|
||||
isReadOnly,
|
||||
webAppUrl,
|
||||
user,
|
||||
surveyDomain,
|
||||
}: SurveyAnalysisCTAProps) => {
|
||||
const { t } = useTranslate();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -50,7 +50,7 @@ export const SurveyAnalysisCTA = ({
|
||||
dropdown: false,
|
||||
});
|
||||
|
||||
const surveyUrl = useMemo(() => `${webAppUrl}/s/${survey.id}`, [survey.id, webAppUrl]);
|
||||
const surveyUrl = useMemo(() => `${surveyDomain}/s/${survey.id}`, [survey.id, surveyDomain]);
|
||||
const { refreshSingleUseId } = useSingleUseId(survey);
|
||||
|
||||
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
@@ -172,9 +172,9 @@ export const SurveyAnalysisCTA = ({
|
||||
<ShareEmbedSurvey
|
||||
key={key}
|
||||
survey={survey}
|
||||
surveyDomain={surveyDomain}
|
||||
open={modalState[key as keyof ModalState]}
|
||||
setOpen={setOpen}
|
||||
webAppUrl={webAppUrl}
|
||||
user={user}
|
||||
modalView={modalView}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -20,8 +20,8 @@ interface EmbedViewProps {
|
||||
survey: any;
|
||||
email: string;
|
||||
surveyUrl: string;
|
||||
surveyDomain: string;
|
||||
setSurveyUrl: React.Dispatch<React.SetStateAction<string>>;
|
||||
webAppUrl: string;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ export const EmbedView = ({
|
||||
survey,
|
||||
email,
|
||||
surveyUrl,
|
||||
surveyDomain,
|
||||
setSurveyUrl,
|
||||
webAppUrl,
|
||||
locale,
|
||||
}: EmbedViewProps) => {
|
||||
const { t } = useTranslate();
|
||||
@@ -82,8 +82,8 @@ export const EmbedView = ({
|
||||
) : activeId === "link" ? (
|
||||
<LinkTab
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -8,13 +8,13 @@ import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
interface LinkTabProps {
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
surveyUrl: string;
|
||||
surveyDomain: string;
|
||||
setSurveyUrl: (url: string) => void;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }: LinkTabProps) => {
|
||||
export const LinkTab = ({ survey, surveyUrl, surveyDomain, setSurveyUrl, locale }: LinkTabProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
const docsLinks = [
|
||||
@@ -43,8 +43,8 @@ export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }:
|
||||
</p>
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
+6
-4
@@ -78,7 +78,7 @@ const dummySurvey = {
|
||||
} as unknown as TSurvey;
|
||||
const dummyEnvironment = { id: "env123", appSetupCompleted: true } as TEnvironment;
|
||||
const dummyUser = { id: "user123", name: "Test User" } as TUser;
|
||||
const webAppUrl = "http://example.com";
|
||||
const surveyDomain = "https://surveys.test.formbricks.com";
|
||||
|
||||
describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
afterEach(() => {
|
||||
@@ -91,7 +91,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
/>
|
||||
);
|
||||
@@ -101,7 +101,9 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(refreshSingleUseIdSpy).toHaveBeenCalled();
|
||||
expect(writeTextMock).toHaveBeenCalledWith("http://example.com/s/survey123?id=newSingleUseId");
|
||||
expect(writeTextMock).toHaveBeenCalledWith(
|
||||
"https://surveys.test.formbricks.com/s/survey123?id=newSingleUseId"
|
||||
);
|
||||
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
|
||||
});
|
||||
});
|
||||
@@ -113,7 +115,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { getPreviewEmailTemplateHtml } from "@/modules/email/components/preview-email-template";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getStyling } from "@formbricks/lib/utils/styling";
|
||||
@@ -17,7 +17,7 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
|
||||
}
|
||||
|
||||
const styling = getStyling(project, survey);
|
||||
const surveyUrl = WEBAPP_URL + "/s/" + survey.id;
|
||||
const surveyUrl = getSurveyDomain() + "/s/" + survey.id;
|
||||
const html = await getPreviewEmailTemplateHtml(survey, surveyUrl, styling, locale, t);
|
||||
const doctype =
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@ import {
|
||||
MAX_RESPONSES_FOR_INSIGHT_GENERATION,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
@@ -54,6 +55,7 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
billing: organization.billing,
|
||||
});
|
||||
const shouldGenerateInsights = needsInsightsGeneration(survey);
|
||||
const surveyDomain = getSurveyDomain();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
@@ -64,8 +66,8 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
user={user}
|
||||
surveyDomain={surveyDomain}
|
||||
/>
|
||||
}>
|
||||
{isAIEnabled && shouldGenerateInsights && (
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { AsyncParser } from "@json2csv/node";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest } from "next/server";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
let csv: string = "";
|
||||
|
||||
const { json, fields, fileName } = data;
|
||||
|
||||
const fallbackFileName = fileName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
||||
const encodedFileName = encodeURIComponent(fileName)
|
||||
.replace(/['()]/g, (match) => "%" + match.charCodeAt(0).toString(16))
|
||||
.replace(/\*/g, "%2A");
|
||||
|
||||
const parser = new AsyncParser({
|
||||
fields,
|
||||
});
|
||||
|
||||
try {
|
||||
csv = await parser.parse(json).promise();
|
||||
} catch (err) {
|
||||
logger.error({ error: err, url: request.url }, "Failed to convert to CSV");
|
||||
throw new Error("Failed to convert to CSV");
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "text/csv;charset=utf-8;");
|
||||
headers.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${fallbackFileName}"; filename*=UTF-8''${encodedFileName}`
|
||||
);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
fileResponse: csv,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest } from "next/server";
|
||||
import * as xlsx from "xlsx";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
const { json, fields, fileName } = data;
|
||||
|
||||
const fallbackFileName = fileName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
||||
const encodedFileName = encodeURIComponent(fileName)
|
||||
.replace(/['()]/g, (match) => "%" + match.charCodeAt(0).toString(16))
|
||||
.replace(/\*/g, "%2A");
|
||||
|
||||
const wb = xlsx.utils.book_new();
|
||||
const ws = xlsx.utils.json_to_sheet(json, { header: fields });
|
||||
xlsx.utils.book_append_sheet(wb, ws, "Sheet1");
|
||||
|
||||
const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" }) as Buffer;
|
||||
const base64String = buffer.toString("base64");
|
||||
|
||||
const headers = new Headers();
|
||||
|
||||
headers.set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
headers.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${fallbackFileName}"; filename*=UTF-8''${encodedFileName}`
|
||||
);
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
fileResponse: base64String,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { NextRequest } from "next/server";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { generateSurveySingleUseIds } from "@formbricks/lib/utils/singleUseSurveys";
|
||||
|
||||
@@ -36,9 +37,10 @@ export const GET = async (
|
||||
|
||||
const singleUseIds = generateSurveySingleUseIds(limit, survey.singleUse.isEncrypted);
|
||||
|
||||
const surveyDomain = getSurveyDomain();
|
||||
// map single use ids to survey links
|
||||
const surveyLinks = singleUseIds.map(
|
||||
(singleUseId) => `${process.env.WEBAPP_URL}/s/${survey.id}?suId=${singleUseId}`
|
||||
(singleUseId) => `${surveyDomain}/s/${survey.id}?suId=${singleUseId}`
|
||||
);
|
||||
|
||||
return responses.successResponse(surveyLinks);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export const fetchFile = async (
|
||||
data: { json: any; fields?: string[]; fileName?: string },
|
||||
filetype: string
|
||||
) => {
|
||||
const endpoint = filetype === "csv" ? "csv-conversion" : "excel-conversion";
|
||||
|
||||
const response = await fetch(`/api/${endpoint}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to convert to file");
|
||||
|
||||
return response.json();
|
||||
};
|
||||
+37
-16
@@ -24,8 +24,15 @@ import { ipAddress } from "@vercel/functions";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { E2E_TESTING, IS_PRODUCTION, RATE_LIMITING_DISABLED, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import {
|
||||
E2E_TESTING,
|
||||
IS_PRODUCTION,
|
||||
RATE_LIMITING_DISABLED,
|
||||
SURVEY_URL,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { isValidCallbackUrl } from "@formbricks/lib/utils/url";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
const enforceHttps = (request: NextRequest): Response | null => {
|
||||
const forwardedProto = request.headers.get("x-forwarded-proto") ?? "http";
|
||||
@@ -78,7 +85,34 @@ const applyRateLimiting = (request: NextRequest, ip: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSurveyDomain = (request: NextRequest): Response | null => {
|
||||
try {
|
||||
if (!SURVEY_URL) return null;
|
||||
|
||||
const host = request.headers.get("host") || "";
|
||||
const surveyDomain = SURVEY_URL ? new URL(SURVEY_URL).host : "";
|
||||
if (host !== surveyDomain) return null;
|
||||
|
||||
return new NextResponse(null, { status: 404 });
|
||||
} catch (error) {
|
||||
logger.error(error, "Error handling survey domain");
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
};
|
||||
|
||||
const isSurveyRoute = (request: NextRequest) => {
|
||||
return request.nextUrl.pathname.startsWith("/c/") || request.nextUrl.pathname.startsWith("/s/");
|
||||
};
|
||||
|
||||
export const middleware = async (originalRequest: NextRequest) => {
|
||||
if (isSurveyRoute(originalRequest)) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Handle survey domain routing.
|
||||
const surveyResponse = handleSurveyDomain(originalRequest);
|
||||
if (surveyResponse) return surveyResponse;
|
||||
|
||||
// Create a new Request object to override headers and add a unique request ID header
|
||||
const request = new NextRequest(originalRequest, {
|
||||
headers: new Headers(originalRequest.headers),
|
||||
@@ -88,6 +122,7 @@ export const middleware = async (originalRequest: NextRequest) => {
|
||||
request.headers.set("x-start-time", Date.now().toString());
|
||||
|
||||
// Create a new NextResponse object to forward the new request with headers
|
||||
|
||||
const nextResponseWithCustomHeader = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
@@ -132,20 +167,6 @@ export const middleware = async (originalRequest: NextRequest) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/api/auth/callback/credentials",
|
||||
"/api/(.*)/client/:path*",
|
||||
"/api/v1/js/actions",
|
||||
"/api/v1/client/storage",
|
||||
"/share/(.*)/:path",
|
||||
"/environments/:path*",
|
||||
"/setup/organization/:path*",
|
||||
"/api/auth/signout",
|
||||
"/auth/login",
|
||||
"/auth/signup",
|
||||
"/api/packages/:path*",
|
||||
"/auth/verification-requested",
|
||||
"/auth/forgot-password",
|
||||
"/api/v1/management/:path*",
|
||||
"/api/v2/management/:path*",
|
||||
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public|api/v1/og).*)", // Exclude the Open Graph image generation route from middleware
|
||||
],
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SurveyLinkDisplay } from "./components/SurveyLinkDisplay";
|
||||
|
||||
interface ShareSurveyLinkProps {
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
surveyDomain: string;
|
||||
surveyUrl: string;
|
||||
setSurveyUrl: (url: string) => void;
|
||||
locale: TUserLocale;
|
||||
@@ -23,8 +23,8 @@ interface ShareSurveyLinkProps {
|
||||
|
||||
export const ShareSurveyLink = ({
|
||||
survey,
|
||||
webAppUrl,
|
||||
surveyUrl,
|
||||
surveyDomain,
|
||||
setSurveyUrl,
|
||||
locale,
|
||||
}: ShareSurveyLinkProps) => {
|
||||
@@ -32,7 +32,7 @@ export const ShareSurveyLink = ({
|
||||
const [language, setLanguage] = useState("default");
|
||||
|
||||
const getUrl = useCallback(async () => {
|
||||
let url = `${webAppUrl}/s/${survey.id}`;
|
||||
let url = `${surveyDomain}/s/${survey.id}`;
|
||||
const queryParams: string[] = [];
|
||||
|
||||
if (survey.singleUse?.enabled) {
|
||||
@@ -58,7 +58,9 @@ export const ShareSurveyLink = ({
|
||||
}
|
||||
|
||||
setSurveyUrl(url);
|
||||
}, [survey, webAppUrl, language]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [survey, surveyDomain, language]);
|
||||
|
||||
const generateNewSingleUseLink = () => {
|
||||
getUrl();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ENCRYPTION_KEY, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { ENCRYPTION_KEY, SURVEY_URL } from "@formbricks/lib/constants";
|
||||
import * as crypto from "@formbricks/lib/crypto";
|
||||
import * as contactSurveyLink from "./contact-survey-link";
|
||||
|
||||
@@ -15,7 +15,7 @@ vi.mock("jsonwebtoken", () => ({
|
||||
// Mock constants - MUST be a literal object without using variables
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: "test-encryption-key-32-chars-long!",
|
||||
WEBAPP_URL: "https://test.formbricks.com",
|
||||
SURVEY_URL: "https://test.formbricks.com",
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/crypto", () => ({
|
||||
@@ -73,7 +73,7 @@ describe("Contact Survey Link", () => {
|
||||
// Verify the returned URL
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
data: `${WEBAPP_URL}/c/${mockToken}`,
|
||||
data: `${SURVEY_URL}/c/${mockToken}`,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("Contact Survey Link", () => {
|
||||
// Re‑mock constants to simulate missing ENCRYPTION_KEY
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: undefined,
|
||||
WEBAPP_URL: "https://test.formbricks.com",
|
||||
SURVEY_URL: "https://test.formbricks.com",
|
||||
}));
|
||||
// Re‑import the modules so they pick up the new mock
|
||||
const { getContactSurveyLink } = await import("./contact-survey-link");
|
||||
@@ -172,7 +172,7 @@ describe("Contact Survey Link", () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("@formbricks/lib/constants", () => ({
|
||||
ENCRYPTION_KEY: undefined,
|
||||
WEBAPP_URL: "https://test.formbricks.com",
|
||||
SURVEY_URL: "https://test.formbricks.com",
|
||||
}));
|
||||
const { verifyContactSurveyToken } = await import("./contact-survey-link");
|
||||
const result = verifyContactSurveyToken(mockToken);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { ENCRYPTION_KEY, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { ENCRYPTION_KEY } from "@formbricks/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
|
||||
// Creates an encrypted personalized survey link for a contact
|
||||
@@ -41,7 +42,7 @@ export const getContactSurveyLink = (
|
||||
const token = jwt.sign(payload, ENCRYPTION_KEY, tokenOptions);
|
||||
|
||||
// Return the personalized URL
|
||||
return ok(`${WEBAPP_URL}/c/${token}`);
|
||||
return ok(`${getSurveyDomain()}/c/${token}`);
|
||||
};
|
||||
|
||||
// Validates and decrypts a contact survey JWT token
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SMTP_USER,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { createInviteToken, createToken, createTokenForLinkSurvey } from "@formbricks/lib/jwt";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { logger } from "@formbricks/logger";
|
||||
@@ -270,9 +271,9 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
const t = await getTranslate();
|
||||
const getSurveyLink = (): string => {
|
||||
if (singleUseId) {
|
||||
return `${WEBAPP_URL}/s/${surveyId}?verify=${encodeURIComponent(token)}&suId=${singleUseId}`;
|
||||
return `${getSurveyDomain()}/s/${surveyId}?verify=${encodeURIComponent(token)}&suId=${singleUseId}`;
|
||||
}
|
||||
return `${WEBAPP_URL}/s/${surveyId}?verify=${encodeURIComponent(token)}`;
|
||||
return `${getSurveyDomain()}/s/${surveyId}?verify=${encodeURIComponent(token)}`;
|
||||
};
|
||||
const surveyLink = getSurveyLink();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ interface LinkSurveyWrapperProps {
|
||||
IMPRINT_URL?: string;
|
||||
PRIVACY_URL?: string;
|
||||
IS_FORMBRICKS_CLOUD: boolean;
|
||||
webAppUrl: string;
|
||||
surveyDomain: string;
|
||||
isBrandingEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const LinkSurveyWrapper = ({
|
||||
IMPRINT_URL,
|
||||
PRIVACY_URL,
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
webAppUrl,
|
||||
surveyDomain,
|
||||
isBrandingEnabled,
|
||||
}: LinkSurveyWrapperProps) => {
|
||||
//for embedded survey strip away all surrounding css
|
||||
@@ -96,7 +96,7 @@ export const LinkSurveyWrapper = ({
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
surveyUrl={webAppUrl + "/s/" + surveyId}
|
||||
surveyUrl={surveyDomain + "/s/" + surveyId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ interface LinkSurveyProps {
|
||||
emailVerificationStatus?: string;
|
||||
singleUseId?: string;
|
||||
singleUseResponse?: Pick<Response, "id" | "finished">;
|
||||
surveyDomain: string;
|
||||
webAppUrl: string;
|
||||
responseCount?: number;
|
||||
verifiedEmail?: string;
|
||||
@@ -39,6 +40,7 @@ export const LinkSurvey = ({
|
||||
emailVerificationStatus,
|
||||
singleUseId,
|
||||
singleUseResponse,
|
||||
surveyDomain,
|
||||
webAppUrl,
|
||||
responseCount,
|
||||
verifiedEmail,
|
||||
@@ -166,7 +168,7 @@ export const LinkSurvey = ({
|
||||
handleResetSurvey={handleResetSurvey}
|
||||
determineStyling={determineStyling}
|
||||
isEmbed={isEmbed}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyDomain={surveyDomain}
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
PRIVACY_URL={PRIVACY_URL}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface PinScreenProps {
|
||||
emailVerificationStatus?: string;
|
||||
singleUseId?: string;
|
||||
singleUseResponse?: Pick<Response, "id" | "finished">;
|
||||
surveyDomain: string;
|
||||
webAppUrl: string;
|
||||
IMPRINT_URL?: string;
|
||||
PRIVACY_URL?: string;
|
||||
@@ -32,6 +33,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
const {
|
||||
surveyId,
|
||||
project,
|
||||
surveyDomain,
|
||||
webAppUrl,
|
||||
emailVerificationStatus,
|
||||
singleUseId,
|
||||
@@ -118,6 +120,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
singleUseId={singleUseId}
|
||||
singleUseResponse={singleUseResponse}
|
||||
surveyDomain={surveyDomain}
|
||||
webAppUrl={webAppUrl}
|
||||
verifiedEmail={verifiedEmail}
|
||||
languageCode={languageCode}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
|
||||
import { type Response } from "@prisma/client";
|
||||
import { notFound } from "next/navigation";
|
||||
import { IMPRINT_URL, IS_FORMBRICKS_CLOUD, PRIVACY_URL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -98,11 +99,13 @@ export const renderSurvey = async ({
|
||||
const languageCode = getLanguageCode();
|
||||
const isSurveyPinProtected = Boolean(survey.pin);
|
||||
const responseCount = await getResponseCountBySurveyId(survey.id);
|
||||
const surveyDomain = getSurveyDomain();
|
||||
|
||||
if (isSurveyPinProtected) {
|
||||
return (
|
||||
<PinScreen
|
||||
surveyId={survey.id}
|
||||
surveyDomain={surveyDomain}
|
||||
project={project}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
singleUseId={singleUseId}
|
||||
@@ -125,6 +128,7 @@ export const renderSurvey = async ({
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
project={project}
|
||||
surveyDomain={surveyDomain}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
singleUseId={singleUseId}
|
||||
singleUseResponse={singleUseResponse}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSurvey } from "@/modules/survey/lib/survey";
|
||||
import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, SURVEY_URL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ vi.mock("@/modules/survey/link/lib/project", () => ({
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: vi.fn(() => false),
|
||||
WEBAPP_URL: "https://test.formbricks.com",
|
||||
SURVEY_URL: "https://surveys.test.formbricks.com",
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/styling/constants", () => ({
|
||||
@@ -170,7 +171,7 @@ describe("Metadata Utils", () => {
|
||||
const result = getSurveyOpenGraphMetadata(surveyId, surveyName);
|
||||
|
||||
expect(result).toEqual({
|
||||
metadataBase: new URL(WEBAPP_URL),
|
||||
metadataBase: new URL(SURVEY_URL),
|
||||
openGraph: {
|
||||
title: surveyName,
|
||||
description: "Thanks a lot for your time 🙏",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getSurvey } from "@/modules/survey/lib/survey";
|
||||
import { getProjectByEnvironmentId } from "@/modules/survey/link/lib/project";
|
||||
import { Metadata } from "next";
|
||||
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -72,7 +73,7 @@ export const getSurveyOpenGraphMetadata = (surveyId: string, surveyName: string)
|
||||
const ogImgURL = `/api/v1/og?brandColor=${brandColor}&name=${encodedName}`;
|
||||
|
||||
return {
|
||||
metadataBase: new URL(WEBAPP_URL),
|
||||
metadataBase: new URL(getSurveyDomain()),
|
||||
openGraph: {
|
||||
title: surveyName,
|
||||
description: "Thanks a lot for your time 🙏",
|
||||
|
||||
@@ -16,7 +16,7 @@ interface SurveyCardProps {
|
||||
survey: TSurvey;
|
||||
environmentId: string;
|
||||
isReadOnly: boolean;
|
||||
WEBAPP_URL: string;
|
||||
surveyDomain: string;
|
||||
duplicateSurvey: (survey: TSurvey) => void;
|
||||
deleteSurvey: (surveyId: string) => void;
|
||||
locale: TUserLocale;
|
||||
@@ -25,7 +25,7 @@ export const SurveyCard = ({
|
||||
survey,
|
||||
environmentId,
|
||||
isReadOnly,
|
||||
WEBAPP_URL,
|
||||
surveyDomain,
|
||||
deleteSurvey,
|
||||
duplicateSurvey,
|
||||
locale,
|
||||
@@ -102,7 +102,7 @@ export const SurveyCard = ({
|
||||
survey={survey}
|
||||
key={`surveys-${survey.id}`}
|
||||
environmentId={environmentId}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
surveyDomain={surveyDomain}
|
||||
disabled={isDraftAndReadOnly}
|
||||
refreshSingleUseId={refreshSingleUseId}
|
||||
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
|
||||
|
||||
@@ -36,7 +36,7 @@ import { CopySurveyModal } from "./copy-survey-modal";
|
||||
interface SurveyDropDownMenuProps {
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
surveyDomain: string;
|
||||
refreshSingleUseId: () => Promise<string | undefined>;
|
||||
disabled?: boolean;
|
||||
isSurveyCreationDeletionDisabled?: boolean;
|
||||
@@ -47,7 +47,7 @@ interface SurveyDropDownMenuProps {
|
||||
export const SurveyDropDownMenu = ({
|
||||
environmentId,
|
||||
survey,
|
||||
webAppUrl,
|
||||
surveyDomain,
|
||||
refreshSingleUseId,
|
||||
disabled,
|
||||
isSurveyCreationDeletionDisabled,
|
||||
@@ -61,7 +61,7 @@ export const SurveyDropDownMenu = ({
|
||||
const [isCopyFormOpen, setIsCopyFormOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey.id, webAppUrl]);
|
||||
const surveyLink = useMemo(() => surveyDomain + "/s/" + survey.id, [survey.id, surveyDomain]);
|
||||
|
||||
const handleDeleteSurvey = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
@@ -82,7 +82,7 @@ export const SurveyDropDownMenu = ({
|
||||
e.preventDefault();
|
||||
setIsDropDownOpen(false);
|
||||
const newId = await refreshSingleUseId();
|
||||
const copiedLink = copySurveyLink(surveyUrl, newId);
|
||||
const copiedLink = copySurveyLink(surveyLink, newId);
|
||||
navigator.clipboard.writeText(copiedLink);
|
||||
toast.success(t("common.copied_to_clipboard"));
|
||||
router.refresh();
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SurveyLoading } from "./survey-loading";
|
||||
interface SurveysListProps {
|
||||
environmentId: string;
|
||||
isReadOnly: boolean;
|
||||
WEBAPP_URL: string;
|
||||
surveyDomain: string;
|
||||
userId: string;
|
||||
surveysPerPage: number;
|
||||
currentProjectChannel: TProjectConfigChannel;
|
||||
@@ -37,7 +37,7 @@ export const initialFilters: TSurveyFilters = {
|
||||
export const SurveysList = ({
|
||||
environmentId,
|
||||
isReadOnly,
|
||||
WEBAPP_URL,
|
||||
surveyDomain,
|
||||
userId,
|
||||
surveysPerPage: surveysLimit,
|
||||
currentProjectChannel,
|
||||
@@ -156,7 +156,7 @@ export const SurveysList = ({
|
||||
survey={survey}
|
||||
environmentId={environmentId}
|
||||
isReadOnly={isReadOnly}
|
||||
WEBAPP_URL={WEBAPP_URL}
|
||||
surveyDomain={surveyDomain}
|
||||
duplicateSurvey={handleDuplicateSurvey}
|
||||
deleteSurvey={handleDeleteSurvey}
|
||||
locale={locale}
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("SurveyDropDownMenu", () => {
|
||||
<SurveyDropDownMenu
|
||||
environmentId="env123"
|
||||
survey={{ ...fakeSurvey, status: "completed" }}
|
||||
webAppUrl="http://webapp.test"
|
||||
surveyDomain="http://survey.test"
|
||||
refreshSingleUseId={mockRefresh}
|
||||
duplicateSurvey={mockDuplicateSurvey}
|
||||
deleteSurvey={mockDeleteSurvey}
|
||||
@@ -93,7 +93,7 @@ describe("SurveyDropDownMenu", () => {
|
||||
<SurveyDropDownMenu
|
||||
environmentId="env123"
|
||||
survey={fakeSurvey}
|
||||
webAppUrl="http://webapp.test"
|
||||
surveyDomain="http://survey.test"
|
||||
refreshSingleUseId={vi.fn()}
|
||||
duplicateSurvey={vi.fn()}
|
||||
deleteSurvey={vi.fn()}
|
||||
|
||||
@@ -11,7 +11,8 @@ import { PlusIcon } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { DEFAULT_LOCALE, SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { DEFAULT_LOCALE, SURVEYS_PER_PAGE } from "@formbricks/lib/constants";
|
||||
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||
import { getUserLocale } from "@formbricks/lib/user/service";
|
||||
import { TTemplateRole } from "@formbricks/types/templates";
|
||||
|
||||
@@ -32,6 +33,7 @@ export const SurveysPage = async ({
|
||||
params: paramsProps,
|
||||
searchParams: searchParamsProps,
|
||||
}: SurveyTemplateProps) => {
|
||||
const surveyDomain = getSurveyDomain();
|
||||
const searchParams = await searchParamsProps;
|
||||
const params = await paramsProps;
|
||||
const t = await getTranslate();
|
||||
@@ -79,7 +81,7 @@ export const SurveysPage = async ({
|
||||
<SurveysList
|
||||
environmentId={environment.id}
|
||||
isReadOnly={isReadOnly}
|
||||
WEBAPP_URL={WEBAPP_URL}
|
||||
surveyDomain={surveyDomain}
|
||||
userId={session.user.id}
|
||||
surveysPerPage={SURVEYS_PER_PAGE}
|
||||
currentProjectChannel={currentProjectChannel}
|
||||
|
||||
@@ -75,6 +75,9 @@ x-environment: &environment
|
||||
# Set the below to 0 to disable cron jobs
|
||||
# DOCKER_CRON_ENABLED: 1
|
||||
|
||||
# Set the below to your Survey Domain(default is WEBAPP_URL)
|
||||
# SURVEY_URL:
|
||||
|
||||
################################################### OPTIONAL (STORAGE) ###################################################
|
||||
|
||||
# Set the below to set a custom Upload Directory
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"self-hosting/configuration/custom-ssl",
|
||||
"self-hosting/configuration/environment-variables",
|
||||
"self-hosting/configuration/smtp",
|
||||
"self-hosting/configuration/domain-configuration",
|
||||
{
|
||||
"group": "Auth & SSO",
|
||||
"icon": "lock",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "Domain Configuration"
|
||||
description: "Configuring your domain for Formbricks."
|
||||
icon: "globe"
|
||||
---
|
||||
|
||||
Formbricks supports both single domain and second domain configurations. This guide will help you set up your domains correctly.
|
||||
|
||||
## Single Domain Setup
|
||||
|
||||
For a single domain setup, you need to configure two essential environment variables:
|
||||
|
||||
1. `WEBAPP_URL`: The base URL of your Formbricks instance
|
||||
2. `NEXTAUTH_URL`: The authentication URL (should be the same as WEBAPP_URL)
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```bash
|
||||
WEBAPP_URL=https://formbricks.example.com
|
||||
NEXTAUTH_URL=https://formbricks.example.com
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- Both URLs must be the same for authentication to work properly
|
||||
- The URLs should be the full URL including the protocol (http:// or https://)
|
||||
- Make sure your domain is properly configured in your DNS settings
|
||||
- If you're using HTTPS (recommended), ensure you have valid SSL certificates installed
|
||||
|
||||
## Second Domain Setup (Survey Domain)
|
||||
|
||||
Formbricks allows you to serve surveys from a different domain than your main application. This is useful for:
|
||||
|
||||
- Separating your admin interface from public surveys
|
||||
- Using a dedicated domain for surveys
|
||||
|
||||
### Configuration
|
||||
|
||||
To set up a second domain for surveys:
|
||||
|
||||
1. Set up DNS records for your survey domain (e.g., `surveys.example.com`)
|
||||
2. Configure your web server or reverse proxy to route requests from the survey domain to the Formbricks application
|
||||
3. Set the `SURVEY_URL` environment variable to your survey domain
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```bash
|
||||
WEBAPP_URL=https://formbricks.example.com
|
||||
NEXTAUTH_URL=https://formbricks.example.com
|
||||
SURVEY_URL=https://surveys.example.com
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- The `SURVEY_URL` must be a valid URL with protocol
|
||||
- When `SURVEY_URL` is set, all public survey links will use this domain.
|
||||
- If `SURVEY_URL` is not set, surveys will be served from the `WEBAPP_URL` domain
|
||||
- Make sure your DNS and SSL certificates are properly configured for both domains
|
||||
- The survey domain should be properly configured in your reverse proxy or web server
|
||||
- **Important**: The second domain is exclusively for serving surveys. Any non-survey paths accessed on this domain will return a 404 error. This is a security feature to ensure the survey domain only serves survey content.
|
||||
|
||||
If you have any questions or require help, feel free to reach out to us on [GitHub Discussions](https://github.com/formbricks/formbricks/discussions).
|
||||
@@ -65,5 +65,6 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
|
||||
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
|
||||
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
|
||||
| SURVEY_URL | Set this to change the domain of the survey. | optional | WEBAPP_URL |
|
||||
|
||||
Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Github Discussions and we’ll try our best to work out a solution with you.
|
||||
|
||||
@@ -8,6 +8,8 @@ export const IS_FORMBRICKS_CLOUD = env.IS_FORMBRICKS_CLOUD === "1";
|
||||
export const WEBAPP_URL =
|
||||
env.WEBAPP_URL || (env.VERCEL_URL ? `https://${env.VERCEL_URL}` : false) || "http://localhost:3000";
|
||||
|
||||
export const SURVEY_URL = env.SURVEY_URL;
|
||||
|
||||
// encryption keys
|
||||
export const FORMBRICKS_ENCRYPTION_KEY = env.FORMBRICKS_ENCRYPTION_KEY || undefined;
|
||||
export const ENCRYPTION_KEY = env.ENCRYPTION_KEY;
|
||||
|
||||
@@ -94,6 +94,7 @@ export const env = createEnv({
|
||||
SMTP_REJECT_UNAUTHORIZED_TLS: z.enum(["1", "0"]).optional(),
|
||||
STRIPE_SECRET_KEY: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||
SURVEY_URL: z.string().optional(),
|
||||
TELEMETRY_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
TERMS_URL: z
|
||||
.string()
|
||||
@@ -222,6 +223,7 @@ export const env = createEnv({
|
||||
SMTP_AUTHENTICATED: process.env.SMTP_AUTHENTICATED,
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
SURVEY_URL: process.env.SURVEY_URL,
|
||||
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED,
|
||||
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
|
||||
TERMS_URL: process.env.TERMS_URL,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import "server-only";
|
||||
import { SURVEY_URL, WEBAPP_URL } from "./constants";
|
||||
|
||||
/**
|
||||
* Returns the base URL for public surveys
|
||||
* Uses SURVEY_URL if set, otherwise falls back to WEBAPP_URL
|
||||
*/
|
||||
export const getSurveyDomain = (): string => {
|
||||
return SURVEY_URL || WEBAPP_URL;
|
||||
};
|
||||
@@ -205,6 +205,7 @@
|
||||
"STRIPE_WEBHOOK_SECRET",
|
||||
"SURVEYS_PACKAGE_MODE",
|
||||
"SURVEYS_PACKAGE_BUILD",
|
||||
"SURVEY_URL",
|
||||
"TELEMETRY_DISABLED",
|
||||
"TURNSTILE_SECRET_KEY",
|
||||
"TERMS_URL",
|
||||
|
||||
Reference in New Issue
Block a user