diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml
index 586ebb3edd..21bdb0a44e 100644
--- a/.github/workflows/nextjs-bundle-analysis.yml
+++ b/.github/workflows/nextjs-bundle-analysis.yml
@@ -39,6 +39,9 @@ jobs:
version: 7
run_install: true
+ - name: create .env
+ run: cp .env.example .env
+
- name: Restore next build
uses: actions/cache@v3
id: restore-build-cache
diff --git a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx
index d095f89151..701c29c1de 100644
--- a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx
@@ -1,502 +1,46 @@
-"use client";
+export const revalidate = REVALIDATION_INTERVAL;
-import FaveIcon from "@/app/favicon.ico";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuPortal,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "@/components/shared/DropdownMenu";
-import LoadingSpinner from "@/components/shared/LoadingSpinner";
-import CreateTeamModal from "@/components/team/CreateTeamModal";
-import {
- changeEnvironment,
- changeEnvironmentByProduct,
- changeEnvironmentByTeam,
-} from "@/lib/environments/changeEnvironments";
-import { useEnvironment } from "@/lib/environments/environments";
-import { formbricksLogout } from "@/lib/formbricks";
-import { useMemberships } from "@/lib/memberships";
-import { useTeam } from "@/lib/teams/teams";
-import { capitalizeFirstLetter, truncate } from "@/lib/utils";
-import formbricks from "@formbricks/js";
-import { cn } from "@formbricks/lib/cn";
-import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
-import {
- CustomersIcon,
- DashboardIcon,
- ErrorComponent,
- FilterIcon,
- FormIcon,
- Popover,
- PopoverContent,
- PopoverTrigger,
- ProfileAvatar,
- SettingsIcon,
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@formbricks/ui";
-import {
- AdjustmentsVerticalIcon,
- ArrowRightOnRectangleIcon,
- ChatBubbleBottomCenterTextIcon,
- ChevronDownIcon,
- CodeBracketIcon,
- CreditCardIcon,
- DocumentCheckIcon,
- HeartIcon,
- PaintBrushIcon,
- PlusIcon,
- UserCircleIcon,
- UsersIcon,
-} from "@heroicons/react/24/solid";
-import clsx from "clsx";
-import { MenuIcon } from "lucide-react";
+import Navigation from "@/app/(app)/environments/[environmentId]/Navigation";
+import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
+import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment";
+import { getProducts } from "@formbricks/lib/services/product";
+import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/services/team";
+import { ErrorComponent } from "@formbricks/ui";
import type { Session } from "next-auth";
-import { signOut } from "next-auth/react";
-import Image from "next/image";
-import Link from "next/link";
-import { usePathname, useRouter } from "next/navigation";
-import { useEffect, useMemo, useState } from "react";
-import AddProductModal from "./AddProductModal";
interface EnvironmentsNavbarProps {
environmentId: string;
session: Session;
}
-export default function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) {
- const router = useRouter();
- const pathname = usePathname();
+export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) {
+ const [environment, teams, team] = await Promise.all([
+ getEnvironment(environmentId),
+ getTeamsByUserId(session.user.id),
+ getTeamByEnvironmentId(environmentId),
+ ]);
- const { environment, isErrorEnvironment, isLoadingEnvironment } = useEnvironment(environmentId);
- const { memberships, isErrorMemberships, isLoadingMemberships } = useMemberships();
- const { team } = useTeam(environmentId);
-
- const [currentTeamName, setCurrentTeamName] = useState("");
- const [currentTeamId, setCurrentTeamId] = useState("");
- const [loading, setLoading] = useState(false);
- const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
- const [showAddProductModal, setShowAddProductModal] = useState(false);
- const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
-
- const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
-
- useEffect(() => {
- if (environment && environment.widgetSetupCompleted) {
- setWidgetSetupCompleted(true);
- } else {
- setWidgetSetupCompleted(false);
- }
- }, [environment]);
-
- useEffect(() => {
- if (team && team.name !== "") {
- setCurrentTeamName(team.name);
- setCurrentTeamId(team.id);
- }
- }, [team]);
-
- const navigation = useMemo(
- () => [
- {
- name: "Surveys",
- href: `/environments/${environmentId}/surveys`,
- icon: FormIcon,
- current: pathname?.includes("/surveys"),
- },
- {
- name: "People",
- href: `/environments/${environmentId}/people`,
- icon: CustomersIcon,
- current: pathname?.includes("/people"),
- },
- {
- name: "Actions & Attributes",
- href: `/environments/${environmentId}/actions`,
- icon: FilterIcon,
- current: pathname?.includes("/actions") || pathname?.includes("/attributes"),
- },
- {
- name: "Integrations",
- href: `/environments/${environmentId}/integrations`,
- icon: DashboardIcon,
- current: pathname?.includes("/integrations"),
- },
- {
- name: "Settings",
- href: `/environments/${environmentId}/settings/profile`,
- icon: SettingsIcon,
- current: pathname?.includes("/settings"),
- },
- ],
- [environmentId, pathname]
- );
-
- const dropdownnavigation = [
- {
- title: "Survey",
- links: [
- {
- icon: AdjustmentsVerticalIcon,
- label: "Product Settings",
- href: `/environments/${environmentId}/settings/product`,
- },
- {
- icon: PaintBrushIcon,
- label: "Look & Feel",
- href: `/environments/${environmentId}/settings/lookandfeel`,
- },
- ],
- },
- {
- title: "Account",
- links: [
- {
- icon: UserCircleIcon,
- label: "Profile",
- href: `/environments/${environmentId}/settings/profile`,
- },
- { icon: UsersIcon, label: "Team", href: `/environments/${environmentId}/settings/members` },
- {
- icon: CreditCardIcon,
- label: "Billing & Plan",
- href: `/environments/${environmentId}/settings/billing`,
- hidden: !IS_FORMBRICKS_CLOUD,
- },
- ],
- },
- {
- title: "Setup",
- links: [
- {
- icon: DocumentCheckIcon,
- label: "Setup checklist",
- href: `/environments/${environmentId}/settings/setup`,
- hidden: widgetSetupCompleted,
- },
- {
- icon: CodeBracketIcon,
- label: "Developer Docs",
- href: "https://formbricks.com/docs",
- target: "_blank",
- },
- {
- icon: HeartIcon,
- label: "Contribute to Formbricks",
- href: "https://github.com/formbricks/formbricks",
- target: "_blank",
- },
- ],
- },
- ];
-
- const handleEnvironmentChange = (environmentType: "production" | "development") => {
- changeEnvironment(environmentType, environment, router);
- };
-
- const handleEnvironmentChangeByProduct = (productId: string) => {
- changeEnvironmentByProduct(productId, environment, router);
- };
-
- const handleEnvironmentChangeByTeam = (teamId: string) => {
- changeEnvironmentByTeam(teamId, memberships, router);
- };
-
- if (isLoadingEnvironment || loading || isLoadingMemberships) {
- return ;
- }
-
- if (isErrorEnvironment || isErrorMemberships || !environment || !memberships) {
+ if (!team || !environment) {
return ;
}
- if (pathname?.includes("/edit")) return null;
+ const [products, environments] = await Promise.all([
+ getProducts(team.id),
+ getEnvironments(environment.productId),
+ ]);
+
+ if (!products || !environments || !teams) {
+ return ;
+ }
return (
-
+
);
}
diff --git a/apps/web/app/(app)/environments/[environmentId]/NavbarLoading.tsx b/apps/web/app/(app)/environments/[environmentId]/NavbarLoading.tsx
new file mode 100644
index 0000000000..6f9264d56e
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/NavbarLoading.tsx
@@ -0,0 +1,20 @@
+export default function NavbarLoading() {
+ return (
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx
new file mode 100644
index 0000000000..c2790c8e35
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx
@@ -0,0 +1,495 @@
+"use client";
+
+import FaveIcon from "@/app/favicon.ico";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/components/shared/DropdownMenu";
+import CreateTeamModal from "@/components/team/CreateTeamModal";
+import { formbricksLogout } from "@/lib/formbricks";
+import { capitalizeFirstLetter, truncate } from "@/lib/utils";
+import formbricks from "@formbricks/js";
+import { cn } from "@formbricks/lib/cn";
+import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
+import { TEnvironment } from "@formbricks/types/v1/environment";
+import { TProduct } from "@formbricks/types/v1/product";
+import { TTeam } from "@formbricks/types/v1/teams";
+import {
+ CustomersIcon,
+ DashboardIcon,
+ FilterIcon,
+ FormIcon,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+ ProfileAvatar,
+ SettingsIcon,
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@formbricks/ui";
+import {
+ AdjustmentsVerticalIcon,
+ ArrowRightOnRectangleIcon,
+ ChatBubbleBottomCenterTextIcon,
+ ChevronDownIcon,
+ CodeBracketIcon,
+ CreditCardIcon,
+ DocumentCheckIcon,
+ HeartIcon,
+ PaintBrushIcon,
+ PlusIcon,
+ UserCircleIcon,
+ UsersIcon,
+} from "@heroicons/react/24/solid";
+import clsx from "clsx";
+import { MenuIcon } from "lucide-react";
+import type { Session } from "next-auth";
+import { signOut } from "next-auth/react";
+import Image from "next/image";
+import Link from "next/link";
+import { usePathname, useRouter } from "next/navigation";
+import { useEffect, useMemo, useState } from "react";
+import AddProductModal from "./AddProductModal";
+
+interface NavigationProps {
+ environment: TEnvironment;
+ teams: TTeam[];
+ session: Session;
+ team: TTeam;
+ products: TProduct[];
+ environments: TEnvironment[];
+}
+
+export default function Navigation({
+ environment,
+ teams,
+ team,
+ session,
+ products,
+ environments,
+}: NavigationProps) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const [currentTeamName, setCurrentTeamName] = useState("");
+ const [currentTeamId, setCurrentTeamId] = useState("");
+ const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
+ const [showAddProductModal, setShowAddProductModal] = useState(false);
+ const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
+ const product = products.find((product) => product.id === environment.productId);
+ const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
+
+ useEffect(() => {
+ if (environment && environment.widgetSetupCompleted) {
+ setWidgetSetupCompleted(true);
+ } else {
+ setWidgetSetupCompleted(false);
+ }
+ }, [environment]);
+
+ useEffect(() => {
+ if (team && team.name !== "") {
+ setCurrentTeamName(team.name);
+ setCurrentTeamId(team.id);
+ }
+ }, [team]);
+
+ const navigation = useMemo(
+ () => [
+ {
+ name: "Surveys",
+ href: `/environments/${environment.id}/surveys`,
+ icon: FormIcon,
+ current: pathname?.includes("/surveys"),
+ },
+ {
+ name: "People",
+ href: `/environments/${environment.id}/people`,
+ icon: CustomersIcon,
+ current: pathname?.includes("/people"),
+ },
+ {
+ name: "Actions & Attributes",
+ href: `/environments/${environment.id}/actions`,
+ icon: FilterIcon,
+ current: pathname?.includes("/actions") || pathname?.includes("/attributes"),
+ },
+ {
+ name: "Integrations",
+ href: `/environments/${environment.id}/integrations`,
+ icon: DashboardIcon,
+ current: pathname?.includes("/integrations"),
+ },
+ {
+ name: "Settings",
+ href: `/environments/${environment.id}/settings/profile`,
+ icon: SettingsIcon,
+ current: pathname?.includes("/settings"),
+ },
+ ],
+ [environment.id, pathname]
+ );
+
+ const dropdownnavigation = [
+ {
+ title: "Survey",
+ links: [
+ {
+ icon: AdjustmentsVerticalIcon,
+ label: "Product Settings",
+ href: `/environments/${environment.id}/settings/product`,
+ },
+ {
+ icon: PaintBrushIcon,
+ label: "Look & Feel",
+ href: `/environments/${environment.id}/settings/lookandfeel`,
+ },
+ ],
+ },
+ {
+ title: "Account",
+ links: [
+ {
+ icon: UserCircleIcon,
+ label: "Profile",
+ href: `/environments/${environment.id}/settings/profile`,
+ },
+ { icon: UsersIcon, label: "Team", href: `/environments/${environment.id}/settings/members` },
+ {
+ icon: CreditCardIcon,
+ label: "Billing & Plan",
+ href: `/environments/${environment.id}/settings/billing`,
+ hidden: !IS_FORMBRICKS_CLOUD,
+ },
+ ],
+ },
+ {
+ title: "Setup",
+ links: [
+ {
+ icon: DocumentCheckIcon,
+ label: "Setup checklist",
+ href: `/environments/${environment.id}/settings/setup`,
+ hidden: widgetSetupCompleted,
+ },
+ {
+ icon: CodeBracketIcon,
+ label: "Developer Docs",
+ href: "https://formbricks.com/docs",
+ target: "_blank",
+ },
+ {
+ icon: HeartIcon,
+ label: "Contribute to Formbricks",
+ href: "https://github.com/formbricks/formbricks",
+ target: "_blank",
+ },
+ ],
+ },
+ ];
+
+ const handleEnvironmentChange = (environmentType: "production" | "development") => {
+ const newEnvironmentId = environments.find((e) => e.type === environmentType)?.id;
+ if (newEnvironmentId) {
+ router.push(`/environments/${newEnvironmentId}/`);
+ }
+ };
+
+ const handleEnvironmentChangeByProduct = (productId: string) => {
+ router.push(`/products/${productId}/`);
+ };
+
+ const handleEnvironmentChangeByTeam = (teamId: string) => {
+ router.push(`/teams/${teamId}/`);
+ };
+
+ if (pathname?.includes("/edit")) return null;
+
+ return (
+ <>
+ {product && (
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx
index 9a11e48e91..1ad90e1cd5 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx
@@ -20,6 +20,10 @@ export default async function ApiKeyList({
};
const product = await getProductByEnvironmentId(environmentId);
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
const environments = await getEnvironments(product.id);
const environmentTypeId = findEnvironmentByType(environments, environmentType);
const apiKeys = await getApiKeys(environmentTypeId);
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx
index 00042b4512..f1ce4391ef 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx
@@ -11,6 +11,9 @@ import { EditHighlightBorder } from "./EditHighlightBorder";
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
const product = await getProductByEnvironmentId(params.environmentId);
+ if (!product) {
+ throw new Error("Product not found");
+ }
return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx
index f82db0b540..64147ef5cb 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx
@@ -14,6 +14,10 @@ import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
export default async function SurveysList({ environmentId }: { environmentId: string }) {
const product = await getProductByEnvironmentId(environmentId);
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
const environment = await getEnvironment(environmentId);
const surveys: TSurveyWithAnalytics[] = await getSurveysWithAnalytics(environmentId);
const environments: TEnvironment[] = await getEnvironments(product.id);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx
index f580ef22c0..395ee7d6fa 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx
@@ -7,6 +7,10 @@ export default async function SurveyTemplatesPage({ params }) {
const environment = await getEnvironment(environmentId);
const product = await getProductByEnvironmentId(environmentId);
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
return (
);
diff --git a/apps/web/app/(redirects)/products/[productId]/route.ts b/apps/web/app/(redirects)/products/[productId]/route.ts
new file mode 100644
index 0000000000..1ff1bc6506
--- /dev/null
+++ b/apps/web/app/(redirects)/products/[productId]/route.ts
@@ -0,0 +1,24 @@
+import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
+import { hasTeamAccess } from "@/lib/api/apiHelper";
+import { getEnvironments } from "@formbricks/lib/services/environment";
+import { getProduct } from "@formbricks/lib/services/product";
+import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors";
+import { getServerSession } from "next-auth";
+import { notFound, redirect } from "next/navigation";
+
+export async function GET(_: Request, context: { params: { productId: string } }) {
+ const productId = context?.params?.productId;
+ if (!productId) return notFound();
+ // check auth
+ const session = await getServerSession(authOptions);
+ if (!session) throw new AuthenticationError("Not authenticated");
+ const product = await getProduct(productId);
+ if (!product) return notFound();
+ const hasAccess = await hasTeamAccess(session.user, product.teamId);
+ if (!hasAccess) throw new AuthorizationError("Unauthorized");
+ // redirect to product's production environment
+ const environments = await getEnvironments(product.id);
+ const prodEnvironment = environments.find((e) => e.type === "production");
+ if (!prodEnvironment) return notFound();
+ redirect(`/environments/${prodEnvironment.id}/`);
+}
diff --git a/apps/web/app/(redirects)/teams/[teamId]/route.ts b/apps/web/app/(redirects)/teams/[teamId]/route.ts
new file mode 100644
index 0000000000..7b67901dfe
--- /dev/null
+++ b/apps/web/app/(redirects)/teams/[teamId]/route.ts
@@ -0,0 +1,26 @@
+import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
+import { hasTeamAccess } from "@/lib/api/apiHelper";
+import { getEnvironments } from "@formbricks/lib/services/environment";
+import { getProducts } from "@formbricks/lib/services/product";
+import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors";
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import { notFound } from "next/navigation";
+
+export async function GET(_: Request, context: { params: { teamId: string } }) {
+ const teamId = context?.params?.teamId;
+ if (!teamId) return notFound();
+ // check auth
+ const session = await getServerSession(authOptions);
+ if (!session) throw new AuthenticationError("Not authenticated");
+ const hasAccess = await hasTeamAccess(session.user, teamId);
+ if (!hasAccess) throw new AuthorizationError("Unauthorized");
+ // redirect to first product's production environment
+ const products = await getProducts(teamId);
+ if (products.length === 0) return notFound();
+ const firstProduct = products[0];
+ const environments = await getEnvironments(firstProduct.id);
+ const prodEnvironment = environments.find((e) => e.type === "production");
+ if (!prodEnvironment) return notFound();
+ redirect(`/environments/${prodEnvironment.id}/`);
+}
diff --git a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts
index cd906c943d..53cafea05c 100644
--- a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts
+++ b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts
@@ -109,6 +109,10 @@ export async function POST(req: Request, { params }): Promise {
getProductByEnvironmentId(environmentId),
]);
+ if (!product) {
+ return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true);
+ }
+
// return state
const state: TJsState = {
person,
diff --git a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts
index e5889750a0..7f3a373fa4 100644
--- a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts
+++ b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts
@@ -101,6 +101,10 @@ export async function POST(req: Request, { params }): Promise {
getProductByEnvironmentId(environmentId),
]);
+ if (!product) {
+ return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true);
+ }
+
// return state
const state: TJsState = {
person,
diff --git a/apps/web/app/api/v1/js/sync/route.ts b/apps/web/app/api/v1/js/sync/route.ts
index c5321611dc..d19ad196f9 100644
--- a/apps/web/app/api/v1/js/sync/route.ts
+++ b/apps/web/app/api/v1/js/sync/route.ts
@@ -60,6 +60,10 @@ export async function POST(req: Request): Promise {
captureNewSessionTelemetry(inputValidation.data.jsVersion);
+ if (!product) {
+ return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true);
+ }
+
// return state
const state: TJsState = {
person,
@@ -87,6 +91,10 @@ export async function POST(req: Request): Promise {
getProductByEnvironmentId(environmentId),
]);
+ if (!product) {
+ return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true);
+ }
+
captureNewSessionTelemetry(inputValidation.data.jsVersion);
// return state
@@ -144,6 +152,10 @@ export async function POST(req: Request): Promise {
getProductByEnvironmentId(environmentId),
]);
+ if (!product) {
+ return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true);
+ }
+
// return state
const state: TJsState = {
person,
diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx
index 7058cf57fc..0874c50315 100644
--- a/apps/web/app/s/[surveyId]/page.tsx
+++ b/apps/web/app/s/[surveyId]/page.tsx
@@ -36,6 +36,10 @@ export default async function LinkSurveyPage({ params, searchParams }) {
// get product and person
const product = await getProductByEnvironmentId(survey.environmentId);
+ if (!product) {
+ throw new Error("Product not found");
+ }
+
const userId = searchParams.userId;
let person;
if (userId) {
diff --git a/apps/web/components/team/CreateTeamModal.tsx b/apps/web/components/team/CreateTeamModal.tsx
index 8a180b7a1c..750f5c6eeb 100644
--- a/apps/web/components/team/CreateTeamModal.tsx
+++ b/apps/web/components/team/CreateTeamModal.tsx
@@ -1,6 +1,5 @@
+import { createTeam } from "@/app/(app)/environments/[environmentId]/actions";
import Modal from "@/components/shared/Modal";
-import { changeEnvironmentByTeam } from "@/lib/environments/changeEnvironments";
-import { useMemberships } from "@/lib/memberships";
import { useProfile } from "@/lib/profile";
import { Button, Input, Label } from "@formbricks/ui";
import { PlusCircleIcon } from "@heroicons/react/24/outline";
@@ -8,7 +7,6 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
-import { createTeam } from "@/app/(app)/environments/[environmentId]/actions";
interface CreateTeamModalProps {
open: boolean;
@@ -18,7 +16,6 @@ interface CreateTeamModalProps {
export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps) {
const router = useRouter();
const { profile } = useProfile();
- const { mutateMemberships } = useMemberships();
const [loading, setLoading] = useState(false);
const { register, handleSubmit } = useForm();
@@ -26,9 +23,8 @@ export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps)
setLoading(true);
const newTeam = await createTeam(data.name, (profile as any).id);
- const newMemberships = await mutateMemberships();
- changeEnvironmentByTeam(newTeam.id, newMemberships, router);
toast.success("Team created successfully!");
+ router.push(`/teams/${newTeam.id}`);
setOpen(false);
setLoading(false);
};
diff --git a/apps/web/env.mjs b/apps/web/env.mjs
index d9dd88f2a3..e44fb676be 100644
--- a/apps/web/env.mjs
+++ b/apps/web/env.mjs
@@ -7,7 +7,7 @@ export const env = createEnv({
* Will throw if you access these variables on the client.
*/
server: {
- WEBAPP_URL: z.string().url(),
+ WEBAPP_URL: z.string().url().optional(),
DATABASE_URL: z.string().url(),
PRISMA_GENERATE_DATAPROXY: z.enum(["true", ""]).optional(),
NEXTAUTH_SECRET: z.string().min(1),
diff --git a/apps/web/lib/environments/changeEnvironments.ts b/apps/web/lib/environments/changeEnvironments.ts
deleted file mode 100644
index c4ca218dc9..0000000000
--- a/apps/web/lib/environments/changeEnvironments.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export const changeEnvironment = (environmentType: string, environment: any, router: any) => {
- const newEnvironmentId = environment.product.environments.find((e) => e.type === environmentType)?.id;
- if (newEnvironmentId) {
- router.push(`/environments/${newEnvironmentId}/`);
- }
-};
-
-export const changeEnvironmentByProduct = (productId: string, environment: any, router: any) => {
- const product = environment.availableProducts.find((p) => p.id === productId);
- const newEnvironmentId = product?.environments[0]?.id;
- if (newEnvironmentId) {
- router.push(`/environments/${newEnvironmentId}/`);
- }
-};
-
-export const changeEnvironmentByTeam = (teamId: string, memberships: any, router: any) => {
- const newTeamMembership = memberships.find((m) => m.teamId === teamId);
- const newTeamProduct = newTeamMembership?.team?.products?.[0];
-
- if (newTeamProduct) {
- const newEnvironmentId = newTeamProduct.environments.find((e) => e.type === "production")?.id;
-
- if (newEnvironmentId) {
- router.push(`/environments/${newEnvironmentId}/`);
- }
- }
-};
diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts
index 8a1f5f378d..cdd708f4db 100644
--- a/packages/lib/services/product.ts
+++ b/packages/lib/services/product.ts
@@ -1,11 +1,11 @@
-import "server-only";
import { prisma } from "@formbricks/database";
-import { z } from "zod";
-import { Prisma } from "@prisma/client";
-import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors";
-import { ZProduct } from "@formbricks/types/v1/product";
+import { DatabaseError, ValidationError } from "@formbricks/types/v1/errors";
import type { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
+import { ZProduct } from "@formbricks/types/v1/product";
+import { Prisma } from "@prisma/client";
import { cache } from "react";
+import "server-only";
+import { z } from "zod";
const selectProduct = {
id: true,
@@ -22,7 +22,26 @@ const selectProduct = {
darkOverlay: true,
};
-export const getProductByEnvironmentId = cache(async (environmentId: string): Promise => {
+export const getProducts = cache(async (teamId: string): Promise => {
+ try {
+ const products = await prisma.product.findMany({
+ where: {
+ teamId,
+ },
+ select: selectProduct,
+ });
+
+ return products;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+});
+
+export const getProductByEnvironmentId = cache(async (environmentId: string): Promise => {
if (!environmentId) {
throw new ValidationError("EnvironmentId is required");
}
@@ -39,25 +58,13 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr
select: selectProduct,
});
- if (!productPrisma) {
- throw new ResourceNotFoundError("Product for Environment", environmentId);
- }
+ return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
-
- try {
- const product = ZProduct.parse(productPrisma);
- return product;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2));
- }
- throw new ValidationError("Data validation of product failed");
- }
});
export const updateProduct = async (
@@ -91,3 +98,22 @@ export const updateProduct = async (
throw new ValidationError("Data validation of product failed");
}
};
+
+export const getProduct = cache(async (productId: string): Promise => {
+ let productPrisma;
+ try {
+ productPrisma = await prisma.product.findUnique({
+ where: {
+ id: productId,
+ },
+ select: selectProduct,
+ });
+
+ return productPrisma;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+ throw error;
+ }
+});
diff --git a/packages/lib/services/team.ts b/packages/lib/services/team.ts
index db213c2309..018d05b6ca 100644
--- a/packages/lib/services/team.ts
+++ b/packages/lib/services/team.ts
@@ -32,6 +32,29 @@ export const select = {
stripeCustomerId: true,
};
+export const getTeamsByUserId = cache(async (userId: string): Promise => {
+ try {
+ const teams = await prisma.team.findMany({
+ where: {
+ memberships: {
+ some: {
+ userId,
+ },
+ },
+ },
+ select,
+ });
+
+ return teams;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+});
+
export const getTeamByEnvironmentId = cache(async (environmentId: string): Promise => {
try {
const team = await prisma.team.findFirst({