mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-05 02:58:36 -06:00
feat: Add base path support for Formbricks (#6853)
This commit is contained in:
@@ -9,8 +9,12 @@
|
||||
WEBAPP_URL=http://localhost:3000
|
||||
|
||||
# Required for next-auth. Should be the same as WEBAPP_URL
|
||||
# If your pplication uses a custom base path, specify the route to the API endpoint in full, e.g. NEXTAUTH_URL=https://example.com/custom-route/api/auth
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Can be used to deploy the application under a sub-path of a domain. This can only be set at build time
|
||||
# BASE_PATH=
|
||||
|
||||
# Encryption keys
|
||||
# Please set both for now, we will change this in the future
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||
# but needs explicit declaration for some build systems (like Depot)
|
||||
ARG TARGETARCH
|
||||
|
||||
# Base path for the application (optional)
|
||||
ARG BASE_PATH=""
|
||||
ENV BASE_PATH=${BASE_PATH}
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ interface ProjectSettingsProps {
|
||||
organizationTeams: TOrganizationTeam[];
|
||||
isAccessControlAllowed: boolean;
|
||||
userProjectsCount: number;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
export const ProjectSettings = ({
|
||||
@@ -55,6 +56,7 @@ export const ProjectSettings = ({
|
||||
organizationTeams,
|
||||
isAccessControlAllowed = false,
|
||||
userProjectsCount,
|
||||
publicDomain,
|
||||
}: ProjectSettingsProps) => {
|
||||
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||
|
||||
@@ -231,6 +233,7 @@ export const ProjectSettings = ({
|
||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||
<div className="z-0 h-3/4 w-3/4">
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || "my Product", t)}
|
||||
styling={{ brandColor: { light: brandColor } }}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@fo
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -47,6 +48,8 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
throw new Error(t("common.organization_teams_not_found"));
|
||||
}
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header
|
||||
@@ -62,6 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
organizationTeams={organizationTeams}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
userProjectsCount={projects.length}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -15,6 +16,7 @@ interface EnvironmentLayoutProps {
|
||||
|
||||
export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLayoutProps) => {
|
||||
const t = await getTranslate();
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
// Destructure all data from props (NO database queries)
|
||||
const {
|
||||
@@ -72,6 +74,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
isDevelopment={IS_DEVELOPMENT}
|
||||
membershipRole={membership.role}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
<div id="mainContent" className="flex flex-1 flex-col overflow-hidden bg-slate-50">
|
||||
<TopControlBar
|
||||
|
||||
@@ -46,6 +46,7 @@ interface NavigationProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isDevelopment: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
export const MainNavigation = ({
|
||||
@@ -56,6 +57,7 @@ export const MainNavigation = ({
|
||||
membershipRole,
|
||||
isFormbricksCloud,
|
||||
isDevelopment,
|
||||
publicDomain,
|
||||
}: NavigationProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -286,15 +288,16 @@ export const MainNavigation = ({
|
||||
{/* Logout */}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
const loginUrl = `${publicDomain}/auth/login`;
|
||||
const route = await signOutWithAudit({
|
||||
reason: "user_initiated",
|
||||
redirectUrl: "/auth/login",
|
||||
redirectUrl: loginUrl,
|
||||
organizationId: organization.id,
|
||||
redirect: false,
|
||||
callbackUrl: "/auth/login",
|
||||
callbackUrl: loginUrl,
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
router.push(route?.url || "/auth/login"); // NOSONAR // We want to check for empty strings
|
||||
router.push(route?.url || loginUrl); // NOSONAR // We want to check for empty strings
|
||||
}}
|
||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||
{t("common.logout")}
|
||||
|
||||
@@ -44,6 +44,7 @@ export const env = createEnv({
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "fatal"]).optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
NEXTAUTH_SECRET: z.string().optional(),
|
||||
MAIL_FROM_NAME: z.string().optional(),
|
||||
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
||||
@@ -168,6 +169,7 @@ export const env = createEnv({
|
||||
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||
MAIL_FROM: process.env.MAIL_FROM,
|
||||
MAIL_FROM_NAME: process.env.MAIL_FROM_NAME,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
SENTRY_DSN: process.env.SENTRY_DSN,
|
||||
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const SignIn = ({ token }) => {
|
||||
export const SignIn = ({ token, webAppUrl }) => {
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
signIn("token", {
|
||||
token: token,
|
||||
callbackUrl: `/`,
|
||||
callbackUrl: webAppUrl,
|
||||
});
|
||||
}
|
||||
}, [token]);
|
||||
}, [token, webAppUrl]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { FormWrapper } from "@/modules/auth/components/form-wrapper";
|
||||
import { SignIn } from "@/modules/auth/verify/components/sign-in";
|
||||
@@ -9,7 +10,7 @@ export const VerifyPage = async ({ searchParams }) => {
|
||||
return token ? (
|
||||
<FormWrapper>
|
||||
<p className="text-center">{t("auth.verify.verifying")}</p>
|
||||
<SignIn token={token} />
|
||||
<SignIn token={token} webAppUrl={WEBAPP_URL} />
|
||||
</FormWrapper>
|
||||
) : (
|
||||
<p className="text-center">{t("auth.verify.no_token_provided")}</p>
|
||||
|
||||
@@ -38,6 +38,7 @@ interface ThemeStylingProps {
|
||||
isUnsplashConfigured: boolean;
|
||||
isReadOnly: boolean;
|
||||
isStorageConfigured: boolean;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
export const ThemeStyling = ({
|
||||
@@ -47,6 +48,7 @@ export const ThemeStyling = ({
|
||||
isUnsplashConfigured,
|
||||
isReadOnly,
|
||||
isStorageConfigured = true,
|
||||
publicDomain,
|
||||
}: ThemeStylingProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -199,6 +201,7 @@ export const ThemeStyling = ({
|
||||
}}
|
||||
previewType={previewSurveyType}
|
||||
setPreviewType={setPreviewSurveyType}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { IS_STORAGE_CONFIGURED, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getRemoveBrandingPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { BrandingSettingsCard } from "@/modules/ee/whitelabel/remove-branding/components/branding-settings-card";
|
||||
@@ -27,6 +28,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
|
||||
}
|
||||
|
||||
const canRemoveBranding = await getRemoveBrandingPermission(organization.billing.plan);
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
@@ -49,6 +51,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
|
||||
isUnsplashConfigured={!!UNSPLASH_ACCESS_KEY}
|
||||
isReadOnly={isReadOnly}
|
||||
isStorageConfigured={IS_STORAGE_CONFIGURED}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
|
||||
@@ -50,6 +50,7 @@ interface SurveyEditorProps {
|
||||
isStorageConfigured: boolean;
|
||||
quotas: TSurveyQuota[];
|
||||
isExternalUrlsAllowed: boolean;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
export const SurveyEditor = ({
|
||||
@@ -79,6 +80,7 @@ export const SurveyEditor = ({
|
||||
isStorageConfigured,
|
||||
quotas,
|
||||
isExternalUrlsAllowed,
|
||||
publicDomain,
|
||||
}: SurveyEditorProps) => {
|
||||
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("elements");
|
||||
const [activeElementId, setActiveElementId] = useState<string | null>(null);
|
||||
@@ -272,6 +274,7 @@ export const SurveyEditor = ({
|
||||
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
|
||||
languageCode={selectedLanguageCode}
|
||||
isSpamProtectionAllowed={isSpamProtectionAllowed}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SURVEY_BG_COLORS,
|
||||
UNSPLASH_ACCESS_KEY,
|
||||
} from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
||||
import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
|
||||
@@ -105,6 +106,7 @@ export const SurveyEditorPage = async (props) => {
|
||||
}
|
||||
|
||||
const isCxMode = searchParams.mode === "cx";
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
return (
|
||||
<SurveyEditor
|
||||
@@ -134,6 +136,7 @@ export const SurveyEditorPage = async (props) => {
|
||||
isQuotasAllowed={isQuotasAllowed}
|
||||
quotas={quotas}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,6 +70,7 @@ export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps)
|
||||
environment={environment}
|
||||
project={projectWithRequiredProps}
|
||||
isTemplatePage={false}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ type TemplateContainerWithPreviewProps = {
|
||||
environment: Pick<Environment, "id" | "appSetupCompleted">;
|
||||
userId: string;
|
||||
isTemplatePage?: boolean;
|
||||
publicDomain: string;
|
||||
};
|
||||
|
||||
export const TemplateContainerWithPreview = ({
|
||||
@@ -23,6 +24,7 @@ export const TemplateContainerWithPreview = ({
|
||||
environment,
|
||||
userId,
|
||||
isTemplatePage = true,
|
||||
publicDomain,
|
||||
}: TemplateContainerWithPreviewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const initialTemplate = customSurveyTemplate(t);
|
||||
@@ -72,6 +74,7 @@ export const TemplateContainerWithPreview = ({
|
||||
environment={environment}
|
||||
languageCode={"default"}
|
||||
isSpamProtectionAllowed={false} // setting it to false as this is a template
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getProjectWithTeamIdsByEnvironmentId } from "@/modules/survey/lib/project";
|
||||
@@ -27,7 +28,14 @@ export const SurveyTemplatesPage = async (props: SurveyTemplateProps) => {
|
||||
return redirect(`/environments/${environment.id}/surveys`);
|
||||
}
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
|
||||
return (
|
||||
<TemplateContainerWithPreview userId={session.user.id} environment={environment} project={project} />
|
||||
<TemplateContainerWithPreview
|
||||
userId={session.user.id}
|
||||
environment={environment}
|
||||
project={project}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ interface PreviewSurveyProps {
|
||||
environment: Pick<Environment, "id" | "appSetupCompleted">;
|
||||
languageCode: string;
|
||||
isSpamProtectionAllowed: boolean;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
let surveyNameTemp: string;
|
||||
@@ -38,6 +39,7 @@ export const PreviewSurvey = ({
|
||||
environment,
|
||||
languageCode,
|
||||
isSpamProtectionAllowed,
|
||||
publicDomain,
|
||||
}: PreviewSurveyProps) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||
@@ -244,6 +246,7 @@ export const PreviewSurvey = ({
|
||||
borderRadius={styling?.roundness ?? 8}
|
||||
background={styling?.cardBackgroundColor?.light}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={survey}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
@@ -273,6 +276,7 @@ export const PreviewSurvey = ({
|
||||
</div>
|
||||
<div className="z-10 w-full rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
@@ -345,6 +349,7 @@ export const PreviewSurvey = ({
|
||||
borderRadius={styling.roundness ?? 8}
|
||||
background={styling.cardBackgroundColor?.light}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={survey}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
@@ -378,6 +383,7 @@ export const PreviewSurvey = ({
|
||||
</div>
|
||||
<div className="z-0 w-full max-w-4xl rounded-lg border-transparent">
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { executeRecaptcha, loadRecaptchaScript } from "@/modules/ui/components/survey/recaptcha";
|
||||
@@ -37,7 +39,8 @@ export const SurveyInline = (props: Omit<SurveyContainerProps, "containerId">) =
|
||||
// Set loading flag immediately to prevent concurrent loads
|
||||
isLoadingScript = true;
|
||||
try {
|
||||
const response = await fetch("/js/surveys.umd.cjs");
|
||||
const scriptUrl = props.appUrl ? `${props.appUrl}/js/surveys.umd.cjs` : "/js/surveys.umd.cjs";
|
||||
const response = await fetch(scriptUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load the surveys package");
|
||||
|
||||
@@ -16,6 +16,7 @@ interface ThemeStylingPreviewSurveyProps {
|
||||
project: Project;
|
||||
previewType: TSurveyType;
|
||||
setPreviewType: (type: TSurveyType) => void;
|
||||
publicDomain: string;
|
||||
}
|
||||
|
||||
const previewParentContainerVariant: Variants = {
|
||||
@@ -50,6 +51,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
project,
|
||||
previewType,
|
||||
setPreviewType,
|
||||
publicDomain,
|
||||
}: ThemeStylingPreviewSurveyProps) => {
|
||||
const [isFullScreenPreview] = useState(false);
|
||||
const [previewPosition] = useState("relative");
|
||||
@@ -166,6 +168,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
borderRadius={project.styling.roundness ?? 8}>
|
||||
<Fragment key={surveyKey}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "app" }}
|
||||
isBrandingEnabled={project.inAppSurveyBranding}
|
||||
@@ -192,6 +195,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
key={surveyKey}
|
||||
className={`${project.logo?.url && !project.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
|
||||
<SurveyInline
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={project.linkSurveyBranding}
|
||||
|
||||
@@ -16,6 +16,7 @@ const getHostname = (url) => {
|
||||
|
||||
const nextConfig = {
|
||||
assetPrefix: process.env.ASSET_PREFIX_URL || undefined,
|
||||
basePath: process.env.BASE_PATH || undefined,
|
||||
output: "standalone",
|
||||
poweredByHeader: false,
|
||||
productionBrowserSourceMaps: true,
|
||||
@@ -403,7 +404,7 @@ const nextConfig = {
|
||||
];
|
||||
},
|
||||
env: {
|
||||
NEXTAUTH_URL: process.env.WEBAPP_URL,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL, // TODO: Remove this once we have a proper solution for the base path
|
||||
},
|
||||
};
|
||||
|
||||
@@ -441,4 +442,7 @@ const sentryOptions = {
|
||||
// Runtime Sentry reporting still depends on DSN being set via environment variables
|
||||
const exportConfig = process.env.SENTRY_AUTH_TOKEN ? withSentryConfig(nextConfig, sentryOptions) : nextConfig;
|
||||
|
||||
console.log("BASE PATH", nextConfig.basePath);
|
||||
|
||||
|
||||
export default exportConfig;
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
"AZUREAD_TENANT_ID",
|
||||
"AUTH_SSO_DEFAULT_TEAM_ID",
|
||||
"AUTH_SKIP_INVITE_FOR_SSO",
|
||||
"BASE_PATH",
|
||||
"BREVO_API_KEY",
|
||||
"BREVO_LIST_ID",
|
||||
"CRON_SECRET",
|
||||
|
||||
Reference in New Issue
Block a user