mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
getActionCountInLast7Days,
|
||||
getActionCountInLastHour,
|
||||
} from "@formbricks/lib/services/actions";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/services/survey";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
export async function deleteActionClassAction(environmentId, actionClassId: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getSurveysByAttributeClassId } from "@formbricks/lib/services/survey";
|
||||
import { getSurveysByAttributeClassId } from "@formbricks/lib/survey/service";
|
||||
|
||||
export const GetActiveInactiveSurveysAction = async (
|
||||
attributeClassId: string
|
||||
|
||||
@@ -1,133 +1,45 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createMembership } from "@formbricks/lib/services/membership";
|
||||
import { createProduct } from "@formbricks/lib/services/product";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { deleteSurvey, getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { Team } from "@prisma/client";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
import { createProduct } from "@formbricks/lib/services/product";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
|
||||
const newTeam = await prisma.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
memberships: {
|
||||
create: {
|
||||
user: { connect: { id: ownerUserId } },
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
products: {
|
||||
create: [
|
||||
{
|
||||
name: "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: "production",
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "userId",
|
||||
description: "The internal ID of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "development",
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "userId",
|
||||
description: "The internal ID of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
memberships: true,
|
||||
products: {
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
export async function createTeamAction(teamName: string): Promise<Team> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const newTeam = await createTeam({
|
||||
name: teamName,
|
||||
});
|
||||
|
||||
const teamId = newTeam?.id;
|
||||
await createMembership(newTeam.id, session.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
if (teamId) {
|
||||
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": INTERNAL_SECRET,
|
||||
},
|
||||
});
|
||||
}
|
||||
await createProduct(newTeam.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const existingSurvey = await getSurvey(surveyId);
|
||||
|
||||
if (!existingSurvey) {
|
||||
@@ -180,6 +92,24 @@ export async function copyToOtherEnvironmentAction(
|
||||
surveyId: string,
|
||||
targetEnvironmentId: string
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorizedToAccessSourceEnvironment = await hasUserEnvironmentAccess(
|
||||
session.user.id,
|
||||
environmentId
|
||||
);
|
||||
if (!isAuthorizedToAccessSourceEnvironment) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorizedToAccessTargetEnvironment = await hasUserEnvironmentAccess(
|
||||
session.user.id,
|
||||
targetEnvironmentId
|
||||
);
|
||||
if (!isAuthorizedToAccessTargetEnvironment) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const existingSurvey = await prisma.survey.findFirst({
|
||||
where: {
|
||||
id: surveyId,
|
||||
@@ -305,12 +235,32 @@ export async function copyToOtherEnvironmentAction(
|
||||
}
|
||||
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const createProductAction = async (environmentId: string, productName: string) => {
|
||||
const productCreated = await createProduct(environmentId, productName);
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const newEnvironment = productCreated.environments[0];
|
||||
return newEnvironment;
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) throw new ResourceNotFoundError("Team from environment", environmentId);
|
||||
|
||||
const product = await createProduct(team.id, {
|
||||
name: productName,
|
||||
});
|
||||
|
||||
// get production environment
|
||||
const productionEnvironment = product.environments.find((environment) => environment.type === "production");
|
||||
if (!productionEnvironment) throw new ResourceNotFoundError("Production environment", environmentId);
|
||||
|
||||
return productionEnvironment;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import GoogleSheetWrapper from "@/app/(app)/environments/[environmentId]/integra
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { getSpreadSheets } from "@formbricks/lib/services/googleSheet";
|
||||
import { getIntegrations } from "@formbricks/lib/services/integrations";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TGoogleSheetIntegration, TGoogleSpreadsheet } from "@formbricks/types/v1/integrations";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
|
||||
@@ -4,7 +4,7 @@ import WebhookRowData from "@/app/(app)/environments/[environmentId]/integration
|
||||
import WebhookTable from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTable";
|
||||
import WebhookTableHeading from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTableHeading";
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getWebhooks } from "@formbricks/lib/services/webhook";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { updateTeamAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions";
|
||||
import { updateTeamNameAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions";
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -43,7 +43,7 @@ export default function EditTeamName({ team }: TEditTeamNameProps) {
|
||||
const handleUpdateTeamName: SubmitHandler<TEditTeamNameForm> = async (data) => {
|
||||
try {
|
||||
setIsUpdatingTeam(true);
|
||||
await updateTeamAction(team.id, data);
|
||||
await updateTeamNameAction(team.id, data.name);
|
||||
|
||||
setIsUpdatingTeam(false);
|
||||
toast.success("Team name updated successfully.");
|
||||
|
||||
@@ -20,13 +20,22 @@ import {
|
||||
import { deleteTeam, updateTeam } from "@formbricks/lib/services/team";
|
||||
import { TInviteUpdateInput } from "@formbricks/types/v1/invites";
|
||||
import { TMembershipRole, TMembershipUpdateInput } from "@formbricks/types/v1/memberships";
|
||||
import { TTeamUpdateInput } from "@formbricks/types/v1/teams";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasTeamAccess, hasTeamAuthority, hasTeamOwnership, isOwner } from "@formbricks/lib/auth";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
|
||||
export const updateTeamAction = async (teamId: string, data: TTeamUpdateInput) => {
|
||||
return await updateTeam(teamId, data);
|
||||
export const updateTeamNameAction = async (teamId: string, teamName: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
return await updateTeam(teamId, { name: teamName });
|
||||
};
|
||||
|
||||
export const updateMembershipAction = async (
|
||||
|
||||
@@ -5,7 +5,7 @@ import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import type { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { Badge } from "@formbricks/ui";
|
||||
import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
@@ -4,6 +4,7 @@ import TemplateList from "@/app/(app)/environments/[environmentId]/surveys/templ
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import type { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import type { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TSurveyInput } from "@formbricks/types/v1/surveys";
|
||||
import { TTemplate } from "@formbricks/types/v1/templates";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
@@ -28,7 +29,7 @@ export default function SurveyStarter({
|
||||
...template.preset,
|
||||
type: surveyType,
|
||||
autoComplete,
|
||||
};
|
||||
} as Partial<TSurveyInput>;
|
||||
try {
|
||||
const survey = await createSurveyAction(environmentId, augmentedTemplate);
|
||||
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service";
|
||||
import { getSurveyResponses } from "@formbricks/lib/response/service";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
|
||||
export const getAnalysisData = async (surveyId: string, environmentId: string) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import LinkSurveyShareButton from "@/app/(app)/environments/[environmentId]/surv
|
||||
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
|
||||
interface SummaryHeaderProps {
|
||||
surveyId: string;
|
||||
@@ -108,7 +108,7 @@ const SummaryHeader = ({
|
||||
value={survey.status}
|
||||
onValueChange={(value) => {
|
||||
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
|
||||
surveyMutateAction({ ...survey, status: castedValue })
|
||||
updateSurveyAction({ ...survey, status: castedValue })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
value === "inProgress"
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { validateQuestion } from "./Validation";
|
||||
import { deleteSurveyAction, surveyMutateAction } from "./actions";
|
||||
import { deleteSurveyAction, updateSurveyAction } from "./actions";
|
||||
|
||||
interface SurveyMenuBarProps {
|
||||
localSurvey: TSurveyWithAnalytics;
|
||||
@@ -143,7 +143,7 @@ export default function SurveyMenuBar({
|
||||
}
|
||||
|
||||
try {
|
||||
await surveyMutateAction({ ...strippedSurvey });
|
||||
await updateSurveyAction({ ...strippedSurvey });
|
||||
router.refresh();
|
||||
setIsMutatingSurvey(false);
|
||||
toast.success("Changes saved.");
|
||||
@@ -242,7 +242,7 @@ export default function SurveyMenuBar({
|
||||
if (!validateSurvey(localSurvey)) {
|
||||
return;
|
||||
}
|
||||
await surveyMutateAction({ ...localSurvey, status: "inProgress" });
|
||||
await updateSurveyAction({ ...localSurvey, status: "inProgress" });
|
||||
router.refresh();
|
||||
setIsMutatingSurvey(false);
|
||||
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
"use server";
|
||||
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { deleteSurvey, updateSurvey } from "@formbricks/lib/services/survey";
|
||||
import { deleteSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
export async function updateSurveyAction(survey: TSurvey): Promise<TSurvey> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, survey.id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export async function surveyMutateAction(survey: TSurvey): Promise<TSurvey> {
|
||||
return await updateSurvey(survey);
|
||||
}
|
||||
|
||||
export async function deleteSurveyAction(surveyId: string) {
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
await deleteSurvey(surveyId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
import React from "react";
|
||||
import { FORMBRICKS_ENCRYPTION_KEY, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SurveyEditor from "./SurveyEditor";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { createSurvey } from "@formbricks/lib/services/survey";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export async function createSurveyAction(environmentId: string, surveyBody: Partial<TSurvey>) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export async function createSurveyAction(environmentId: string, surveyBody: any) {
|
||||
return await createSurvey(environmentId, surveyBody);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createSurveyAction } from "./actions";
|
||||
import { customSurvey, templates } from "./templates";
|
||||
import { TSurveyInput } from "@formbricks/types/v1/surveys";
|
||||
|
||||
type TemplateList = {
|
||||
environmentId: string;
|
||||
@@ -77,7 +78,7 @@ export default function TemplateList({
|
||||
...activeTemplate.preset,
|
||||
type: surveyType,
|
||||
autoComplete,
|
||||
};
|
||||
} as Partial<TSurveyInput>;
|
||||
const survey = await createSurveyAction(environmentId, augmentedTemplate);
|
||||
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { createSurvey } from "@formbricks/lib/services/survey";
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export async function createSurveyAction(environmentId: string, surveyBody: Partial<TSurvey>) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export async function createSurveyAction(environmentId: string, surveyBody: any) {
|
||||
return await createSurvey(environmentId, surveyBody);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,11 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
if (selectedObjective) {
|
||||
try {
|
||||
setIsProfileUpdating(true);
|
||||
const updatedProfile = { ...profile, objective: selectedObjective.id };
|
||||
const updatedProfile = {
|
||||
...profile,
|
||||
objective: selectedObjective.id,
|
||||
name: profile.name ?? undefined,
|
||||
};
|
||||
await updateProfileAction(updatedProfile);
|
||||
setIsProfileUpdating(false);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getFirstEnvironmentByUserId } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Onboarding from "./components/Onboarding";
|
||||
import { getEnvironmentByUser } from "@formbricks/lib/services/environment";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
const environment = await getEnvironmentByUser(session?.user);
|
||||
if (!session) {
|
||||
throw new Error("No session found");
|
||||
}
|
||||
const environment = await getFirstEnvironmentByUserId(session?.user.id);
|
||||
const profile = await getProfile(session?.user.id!);
|
||||
const product = await getProductByEnvironmentId(environment?.id!);
|
||||
|
||||
if (!environment || !profile || !product) {
|
||||
return <ErrorComponent />;
|
||||
throw new Error("Failed to get environment, profile, or product");
|
||||
}
|
||||
|
||||
return <Onboarding session={session} environmentId={environment?.id} profile={profile} product={product} />;
|
||||
return <Onboarding session={session} environmentId={environment.id} profile={profile} product={product} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writeData } from "@formbricks/lib/services/googleSheet";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TGoogleSheetIntegration, TIntegration } from "@formbricks/types/v1/integrations";
|
||||
import { TPipelineInput } from "@formbricks/types/v1/pipelines";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { InvalidInputError } from "@formbricks/types/v1/errors";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { createDisplay } from "@formbricks/lib/services/displays";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamDetails } from "@formbricks/lib/services/teamDetails";
|
||||
import { TDisplay, ZDisplayInput } from "@formbricks/types/v1/displays";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { responses } from "@/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { sendToPipeline } from "@/lib/pipelines";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { updateResponse } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { sendToPipeline } from "@/lib/pipelines";
|
||||
import { InvalidInputError } from "@formbricks/types/v1/errors";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { createResponse } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getTeamDetails } from "@formbricks/lib/services/teamDetails";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { selectSurvey } from "@formbricks/lib/services/survey";
|
||||
import { selectSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/response/service";
|
||||
import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { handleErrorResponse } from "@/app/api/v1/auth";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TSurvey, ZSurveyInput } from "@formbricks/types/v1/surveys";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { responses } from "@/lib/api/response";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { createSurvey, getSurveys } from "@formbricks/lib/services/survey";
|
||||
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { ZSurveyInput } from "@formbricks/types/v1/surveys";
|
||||
import { DatabaseError } from "@formbricks/types/v1/errors";
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/lib/email";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { populateEnvironment } from "@/lib/populate";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { EMAIL_VERIFICATION_DISABLED, INVITE_DISABLED, SIGNUP_ENABLED } from "@formbricks/lib/constants";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { deleteInvite } from "@formbricks/lib/services/invite";
|
||||
import { createMembership } from "@formbricks/lib/services/membership";
|
||||
import { createProduct } from "@formbricks/lib/services/product";
|
||||
import { createProfile } from "@formbricks/lib/services/profile";
|
||||
import { createTeam } from "@formbricks/lib/services/team";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
INTERNAL_SECRET,
|
||||
INVITE_DISABLED,
|
||||
SIGNUP_ENABLED,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let { inviteToken, ...user } = await request.json();
|
||||
@@ -22,7 +19,6 @@ export async function POST(request: Request) {
|
||||
let inviteId;
|
||||
|
||||
try {
|
||||
let data: Prisma.UserCreateArgs;
|
||||
let invite;
|
||||
|
||||
if (inviteToken) {
|
||||
@@ -40,92 +36,35 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Invalid invite ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
data = {
|
||||
data: {
|
||||
...user,
|
||||
memberships: {
|
||||
create: {
|
||||
accepted: true,
|
||||
role: invite.role,
|
||||
team: {
|
||||
connect: {
|
||||
id: invite.teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
data: {
|
||||
...user,
|
||||
memberships: {
|
||||
create: [
|
||||
{
|
||||
accepted: true,
|
||||
role: "owner",
|
||||
team: {
|
||||
create: {
|
||||
name: `${user.name}'s Team`,
|
||||
products: {
|
||||
create: [
|
||||
{
|
||||
name: "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: "production",
|
||||
...populateEnvironment,
|
||||
},
|
||||
{
|
||||
type: "development",
|
||||
...populateEnvironment,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
// create a user and assign him to the team
|
||||
|
||||
type UserWithMemberships = Prisma.UserGetPayload<{ include: { memberships: true } }>;
|
||||
|
||||
const userData = (await prisma.user.create({
|
||||
...data,
|
||||
include: {
|
||||
memberships: true,
|
||||
},
|
||||
// TODO: This is a hack to get the correct types (casting), we should find a better way to do this
|
||||
})) as UserWithMemberships;
|
||||
|
||||
const teamId = userData.memberships[0].teamId;
|
||||
|
||||
if (teamId) {
|
||||
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": INTERNAL_SECRET,
|
||||
},
|
||||
const profile = await createProfile(user);
|
||||
await createMembership(invite.teamId, profile.id, {
|
||||
accepted: true,
|
||||
role: invite.role,
|
||||
});
|
||||
}
|
||||
|
||||
if (inviteId) {
|
||||
sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email);
|
||||
await prisma.invite.delete({ where: { id: inviteId } });
|
||||
}
|
||||
if (!EMAIL_VERIFICATION_DISABLED) {
|
||||
await sendVerificationEmail(profile);
|
||||
}
|
||||
|
||||
if (!EMAIL_VERIFICATION_DISABLED) {
|
||||
await sendVerificationEmail(userData);
|
||||
await sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email);
|
||||
await deleteInvite(inviteId);
|
||||
|
||||
return NextResponse.json(profile);
|
||||
} else {
|
||||
const team = await createTeam({
|
||||
name: `${user.name}'s Team`,
|
||||
});
|
||||
await createProduct(team.id, { name: "My Product" });
|
||||
const profile = await createProfile(user);
|
||||
await createMembership(team.id, profile.id, { role: "owner", accepted: true });
|
||||
|
||||
if (!EMAIL_VERIFICATION_DISABLED) {
|
||||
await sendVerificationEmail(profile);
|
||||
}
|
||||
return NextResponse.json(profile);
|
||||
}
|
||||
return NextResponse.json(userData);
|
||||
} catch (e) {
|
||||
if (e.code === "P2002") {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ClientLogout from "@/app/ClientLogout";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getEnvironmentByUser } from "@formbricks/lib/services/environment";
|
||||
import { getFirstEnvironmentByUserId } from "@formbricks/lib/services/environment";
|
||||
import type { Session } from "next-auth";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -18,7 +18,10 @@ export default async function Home() {
|
||||
|
||||
let environment;
|
||||
try {
|
||||
environment = await getEnvironmentByUser(session?.user);
|
||||
environment = await getFirstEnvironmentByUserId(session?.user.id);
|
||||
if (!environment) {
|
||||
throw new Error("No environment found");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("error getting environment", error);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive";
|
||||
import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/services/person";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getEmailVerificationStatus } from "./helpers";
|
||||
import { checkValidity } from "@/app/s/[surveyId]/prefilling";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cn } from "@formbricks/lib/cn";
|
||||
import SurveyNavBarName from "@/components/shared/SurveyNavBarName";
|
||||
import Link from "next/link";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
|
||||
interface SecondNavbarProps {
|
||||
tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
@@ -44,7 +44,7 @@ export default function SurveyStatusDropdown({
|
||||
disabled={isStatusChangeDisabled}
|
||||
onValueChange={(value) => {
|
||||
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
|
||||
surveyMutateAction({ ...survey, status: castedValue })
|
||||
updateSurveyAction({ ...survey, status: castedValue })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
value === "inProgress"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createTeam } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { useProfile } from "@/lib/profile";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -15,13 +14,12 @@ interface CreateTeamModalProps {
|
||||
|
||||
export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps) {
|
||||
const router = useRouter();
|
||||
const { profile } = useProfile();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit } = useForm();
|
||||
|
||||
const submitTeam = async (data) => {
|
||||
setLoading(true);
|
||||
const newTeam = await createTeam(data.name, (profile as any).id);
|
||||
const newTeam = await createTeamAction(data.name);
|
||||
|
||||
toast.success("Team created successfully!");
|
||||
router.push(`/teams/${newTeam.id}`);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { EventType } from "@prisma/client";
|
||||
export const populateEnvironment = {
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{ name: "userId", description: "The internal ID of the person", type: EventType.automatic },
|
||||
{ name: "email", description: "The email of the person", type: EventType.automatic },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getSessionUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { EnvironmentType } from "@prisma/client";
|
||||
import { populateEnvironment } from "@/lib/populate";
|
||||
import { createProduct } from "@formbricks/lib/services/product";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -93,29 +92,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// Create a new product and associate it with the current team
|
||||
const newProduct = await prisma.product.create({
|
||||
data: {
|
||||
name,
|
||||
team: {
|
||||
connect: { id: environment.product.teamId },
|
||||
},
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: EnvironmentType.production,
|
||||
...populateEnvironment,
|
||||
},
|
||||
{
|
||||
type: EnvironmentType.development,
|
||||
...populateEnvironment,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
environments: true,
|
||||
},
|
||||
});
|
||||
const newProduct = await createProduct(environment.product.teamId, { name });
|
||||
|
||||
const firstEnvironment = newProduct.environments[0];
|
||||
res.json(firstEnvironment);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import "server-only";
|
||||
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getResponse, getResponseCacheTag } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { getSurvey } from "../services/survey";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getSurvey } from "../survey/service";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { getResponse, getResponseCacheTag } from "./service";
|
||||
|
||||
export const canUserAccessResponse = async (userId: string, responseId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { z } from "zod";
|
||||
import { Prisma, EnvironmentType } from "@prisma/client";
|
||||
import type {
|
||||
TEnvironment,
|
||||
TEnvironmentCreateInput,
|
||||
TEnvironmentUpdateInput,
|
||||
} from "@formbricks/types/v1/environment";
|
||||
import {
|
||||
ZEnvironment,
|
||||
ZEnvironmentCreateInput,
|
||||
ZEnvironmentUpdateInput,
|
||||
ZId,
|
||||
} from "@formbricks/types/v1/environment";
|
||||
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors";
|
||||
import type { TEnvironment, TEnvironmentId, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment";
|
||||
import { populateEnvironment } from "../utils/createDemoProductHelpers";
|
||||
import { ZEnvironment, ZEnvironmentUpdateInput, ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { unstable_cache, revalidateTag } from "next/cache";
|
||||
import { EventType, Prisma } from "@prisma/client";
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
export const getEnvironmentCacheTag = (environmentId: string) => `environments-${environmentId}`;
|
||||
export const getEnvironmentsCacheTag = (productId: string) => `products-${productId}-environments`;
|
||||
@@ -125,86 +134,84 @@ export const updateEnvironment = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnvironmentByUser = async (user: any): Promise<TEnvironment | TEnvironmentId | null> => {
|
||||
const firstMembership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!firstMembership) {
|
||||
// create a new team and return environment
|
||||
const membership = await prisma.membership.create({
|
||||
data: {
|
||||
accepted: true,
|
||||
role: "owner",
|
||||
user: { connect: { id: user.id } },
|
||||
team: {
|
||||
create: {
|
||||
name: `${user.name}'s Team`,
|
||||
products: {
|
||||
create: {
|
||||
name: "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: EnvironmentType.production,
|
||||
...populateEnvironment,
|
||||
},
|
||||
{
|
||||
type: EnvironmentType.development,
|
||||
...populateEnvironment,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
products: {
|
||||
include: {
|
||||
environments: true,
|
||||
export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvironment | null> => {
|
||||
validateInputs([userId, ZId]);
|
||||
let environmentPrisma;
|
||||
try {
|
||||
environmentPrisma = await prisma.environment.findFirst({
|
||||
where: {
|
||||
type: "production",
|
||||
product: {
|
||||
team: {
|
||||
memberships: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
const environment = membership.team.products[0].environments[0];
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const environment = ZEnvironment.parse(environmentPrisma);
|
||||
return environment;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2));
|
||||
}
|
||||
throw new ValidationError("Data validation of environment failed");
|
||||
}
|
||||
|
||||
const firstProduct = await prisma.product.findFirst({
|
||||
where: {
|
||||
teamId: firstMembership.teamId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (firstProduct === null) {
|
||||
return null;
|
||||
}
|
||||
const firstEnvironment = await prisma.environment.findFirst({
|
||||
where: {
|
||||
productId: firstProduct.id,
|
||||
type: "production",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (firstEnvironment === null) {
|
||||
return null;
|
||||
}
|
||||
return firstEnvironment;
|
||||
};
|
||||
|
||||
export const createEnvironment = async (
|
||||
productId: string,
|
||||
environmentInput: Partial<TEnvironmentCreateInput>
|
||||
): Promise<TEnvironment> => {
|
||||
validateInputs([productId, ZId], [environmentInput, ZEnvironmentCreateInput]);
|
||||
|
||||
return await prisma.environment.create({
|
||||
data: {
|
||||
type: environmentInput.type || "development",
|
||||
product: { connect: { id: productId } },
|
||||
widgetSetupCompleted: environmentInput.widgetSetupCompleted || false,
|
||||
eventClasses: {
|
||||
create: populateEnvironment.eventClasses,
|
||||
},
|
||||
attributeClasses: {
|
||||
create: populateEnvironment.attributeClasses,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const populateEnvironment = {
|
||||
eventClasses: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
],
|
||||
attributeClasses: [
|
||||
{ name: "userId", description: "The internal ID of the person", type: EventType.automatic },
|
||||
{ name: "email", description: "The email of the person", type: EventType.automatic },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -62,6 +62,26 @@ export const getMembershipsByUserId = cache(async (userId: string): Promise<TMem
|
||||
return memberships;
|
||||
});
|
||||
|
||||
export const createMembership = async (
|
||||
teamId: string,
|
||||
userId: string,
|
||||
data: Partial<TMembership>
|
||||
): Promise<TMembership> => {
|
||||
try {
|
||||
const membership = await prisma.membership.create({
|
||||
data: {
|
||||
userId,
|
||||
teamId,
|
||||
accepted: data.accepted,
|
||||
role: data.role as TMembership["role"],
|
||||
},
|
||||
});
|
||||
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const updateMembership = async (
|
||||
userId: string,
|
||||
teamId: string,
|
||||
|
||||
@@ -9,11 +9,9 @@ import { Prisma } from "@prisma/client";
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import { cache } from "react";
|
||||
import { z } from "zod";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { EnvironmentType } from "@prisma/client";
|
||||
import { EventType } from "@prisma/client";
|
||||
import { getEnvironmentCacheTag, getEnvironmentsCacheTag } from "./environment";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { createEnvironment, getEnvironmentCacheTag, getEnvironmentsCacheTag } from "./environment";
|
||||
|
||||
export const getProductsCacheTag = (teamId: string): string => `teams-${teamId}-products`;
|
||||
const getProductCacheTag = (environmentId: string): string => `environments-${environmentId}-product`;
|
||||
@@ -35,34 +33,6 @@ const selectProduct = {
|
||||
environments: true,
|
||||
};
|
||||
|
||||
const populateEnvironment = {
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: EventType.automatic,
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{ name: "userId", description: "The internal ID of the person", type: EventType.automatic },
|
||||
{ name: "email", description: "The email of the person", type: EventType.automatic },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getProducts = async (teamId: string): Promise<TProduct[]> =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
@@ -135,6 +105,7 @@ export const updateProduct = async (
|
||||
inputProduct: Partial<TProductUpdateInput>
|
||||
): Promise<TProduct> => {
|
||||
validateInputs([productId, ZId], [inputProduct, ZProductUpdateInput.partial()]);
|
||||
const { environments, ...data } = inputProduct;
|
||||
let updatedProduct;
|
||||
try {
|
||||
updatedProduct = await prisma.product.update({
|
||||
@@ -142,7 +113,10 @@ export const updateProduct = async (
|
||||
id: productId,
|
||||
},
|
||||
data: {
|
||||
...inputProduct,
|
||||
...data,
|
||||
environments: {
|
||||
connect: environments?.map((environment) => ({ id: environment.id })) ?? [],
|
||||
},
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
@@ -210,43 +184,35 @@ export const deleteProduct = cache(async (productId: string): Promise<TProduct>
|
||||
return product;
|
||||
});
|
||||
|
||||
export const createProduct = async (environmentId: string, productName: string): Promise<TProduct> => {
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: { id: environmentId },
|
||||
select: {
|
||||
product: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Invalid environment");
|
||||
export const createProduct = async (
|
||||
teamId: string,
|
||||
productInput: Partial<TProductUpdateInput>
|
||||
): Promise<TProduct> => {
|
||||
if (!productInput.name) {
|
||||
throw new ValidationError("Product Name is required");
|
||||
}
|
||||
const { environments, ...data } = productInput;
|
||||
|
||||
const newProduct = await prisma.product.create({
|
||||
let product = await prisma.product.create({
|
||||
data: {
|
||||
name: productName,
|
||||
team: {
|
||||
connect: { id: environment.product.teamId },
|
||||
},
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: EnvironmentType.production,
|
||||
...populateEnvironment,
|
||||
},
|
||||
{
|
||||
type: EnvironmentType.development,
|
||||
...populateEnvironment,
|
||||
},
|
||||
],
|
||||
},
|
||||
...data,
|
||||
name: productInput.name,
|
||||
teamId,
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
|
||||
return newProduct;
|
||||
const devEnvironment = await createEnvironment(product.id, {
|
||||
type: "development",
|
||||
});
|
||||
|
||||
const prodEnvironment = await createEnvironment(product.id, {
|
||||
type: "production",
|
||||
});
|
||||
|
||||
product = await updateProduct(product.id, {
|
||||
environments: [devEnvironment, prodEnvironment],
|
||||
});
|
||||
|
||||
return product;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,12 @@ import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/v1/memberships";
|
||||
import { TProfile, TProfileUpdateInput, ZProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
import {
|
||||
TProfile,
|
||||
TProfileCreateInput,
|
||||
TProfileUpdateInput,
|
||||
ZProfileUpdateInput,
|
||||
} from "@formbricks/types/v1/profile";
|
||||
import { MembershipRole, Prisma } from "@prisma/client";
|
||||
import { unstable_cache, revalidateTag } from "next/cache";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
@@ -149,6 +154,19 @@ const deleteUser = async (userId: string): Promise<TProfile> => {
|
||||
return profile;
|
||||
};
|
||||
|
||||
export const createProfile = async (data: TProfileCreateInput): Promise<TProfile> => {
|
||||
validateInputs([data, ZProfileUpdateInput]);
|
||||
const profile = await prisma.user.create({
|
||||
data: data,
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
revalidateTag(getProfileByEmailCacheTag(profile.email));
|
||||
revalidateTag(getProfileCacheTag(profile.id));
|
||||
|
||||
return profile;
|
||||
};
|
||||
|
||||
// function to delete a user's profile including teams
|
||||
export const deleteProfile = async (userId: string): Promise<void> => {
|
||||
validateInputs([userId, ZId]);
|
||||
|
||||
@@ -107,7 +107,20 @@ export const getTeamByEnvironmentId = async (environmentId: string): Promise<TTe
|
||||
}
|
||||
)();
|
||||
|
||||
export const updateTeam = async (teamId: string, data: TTeamUpdateInput): Promise<TTeam> => {
|
||||
export const createTeam = async (teamInput: TTeamUpdateInput): Promise<TTeam> => {
|
||||
try {
|
||||
const team = await prisma.team.create({
|
||||
data: teamInput,
|
||||
select,
|
||||
});
|
||||
|
||||
return team;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTeam = async (teamId: string, data: Partial<TTeamUpdateInput>): Promise<TTeam> => {
|
||||
try {
|
||||
const updatedTeam = await prisma.team.update({
|
||||
where: {
|
||||
|
||||
24
packages/lib/survey/auth.ts
Normal file
24
packages/lib/survey/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getSurvey, getSurveyCacheTag } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
export const canUserAccessSurvey = async (userId: string, surveyId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([surveyId, ZId], [userId, ZId]);
|
||||
|
||||
if (!userId) return false;
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) throw new Error("Survey not found");
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
[`users-${userId}-surveys-${surveyId}`],
|
||||
{ revalidate: 30 * 60, tags: [getSurveyCacheTag(surveyId)] }
|
||||
)(); // 30 minutes
|
||||
@@ -15,22 +15,15 @@ import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { captureTelemetry } from "../telemetry";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { getDisplaysCacheTag } from "./displays";
|
||||
import { getDisplaysCacheTag } from "../services/displays";
|
||||
import { getResponsesCacheTag } from "../response/service";
|
||||
|
||||
// surveys cache key and tags
|
||||
const getSurveysCacheKey = (environmentId: string): string => `environments-${environmentId}-surveys`;
|
||||
const getSurveysCacheTag = (environmentId: string): string => `environments-${environmentId}-surveys`;
|
||||
|
||||
// survey cache key and tags
|
||||
export const getSurveyCacheKey = (surveyId: string): string => `surveys-${surveyId}`;
|
||||
export const getSurveyCacheTag = (surveyId: string): string => `surveys-${surveyId}`;
|
||||
|
||||
// survey with analytics cache key
|
||||
const getSurveysWithAnalyticsCacheKey = (environmentId: string): string =>
|
||||
`environments-${environmentId}-surveysWithAnalytics`;
|
||||
const getSurveyWithAnalyticsCacheKey = (surveyId: string): string => `surveyWithAnalytics-${surveyId}`;
|
||||
|
||||
export const selectSurvey = {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@@ -145,7 +138,7 @@ export const getSurveyWithAnalytics = async (surveyId: string): Promise<TSurveyW
|
||||
throw new ValidationError("Data validation of survey failed");
|
||||
}
|
||||
},
|
||||
[getSurveyWithAnalyticsCacheKey(surveyId)],
|
||||
[`surveyWithAnalytics-${surveyId}`],
|
||||
{
|
||||
tags: [getSurveyCacheTag(surveyId), getDisplaysCacheTag(surveyId), getResponsesCacheTag(surveyId)],
|
||||
revalidate: 60 * 30,
|
||||
@@ -204,7 +197,7 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
|
||||
throw new ValidationError("Data validation of survey failed");
|
||||
}
|
||||
},
|
||||
[getSurveyCacheKey(surveyId)],
|
||||
[`surveys-${surveyId}`],
|
||||
{
|
||||
tags: [getSurveyCacheTag(surveyId)],
|
||||
revalidate: 60 * 30,
|
||||
@@ -329,7 +322,7 @@ export const getSurveys = async (environmentId: string): Promise<TSurvey[]> => {
|
||||
throw new ValidationError("Data validation of survey failed");
|
||||
}
|
||||
},
|
||||
[getSurveysCacheKey(environmentId)],
|
||||
[`environments-${environmentId}-surveys`],
|
||||
{
|
||||
tags: [getSurveysCacheTag(environmentId)],
|
||||
revalidate: 60 * 30,
|
||||
@@ -393,7 +386,7 @@ export const getSurveysWithAnalytics = async (environmentId: string): Promise<TS
|
||||
throw new ValidationError("Data validation of survey failed");
|
||||
}
|
||||
},
|
||||
[getSurveysWithAnalyticsCacheKey(environmentId)],
|
||||
[`environments-${environmentId}-surveysWithAnalytics`],
|
||||
{
|
||||
tags: [getSurveysCacheTag(environmentId)], // TODO: add tags for displays and responses
|
||||
}
|
||||
@@ -44,4 +44,12 @@ export const ZActionClassInput = z.object({
|
||||
type: z.enum(["code", "noCode"]),
|
||||
});
|
||||
|
||||
export const ZActionClassAutomaticInput = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
type: z.enum(["automatic"]),
|
||||
});
|
||||
|
||||
export type TActionClassAutomaticInput = z.infer<typeof ZActionClassAutomaticInput>;
|
||||
|
||||
export type TActionClassInput = z.infer<typeof ZActionClassInput>;
|
||||
|
||||
@@ -22,12 +22,20 @@ export const ZAttributeClassInput = z.object({
|
||||
environmentId: z.string(),
|
||||
});
|
||||
|
||||
export const ZAttributeClassAutomaticInput = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
type: z.enum(["automatic"]),
|
||||
});
|
||||
|
||||
export const ZAttributeClassUpdateInput = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
archived: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TAttributeClassAutomaticInput = z.infer<typeof ZAttributeClassAutomaticInput>;
|
||||
|
||||
export type TAttributeClassUpdateInput = z.infer<typeof ZAttributeClassUpdateInput>;
|
||||
|
||||
export type TAttributeClassInput = z.infer<typeof ZAttributeClassInput>;
|
||||
|
||||
@@ -25,4 +25,11 @@ export const ZEnvironmentUpdateInput = z.object({
|
||||
|
||||
export const ZId = z.string().cuid2();
|
||||
|
||||
export const ZEnvironmentCreateInput = z.object({
|
||||
type: z.enum(["development", "production"]).optional(),
|
||||
widgetSetupCompleted: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TEnvironmentCreateInput = z.infer<typeof ZEnvironmentCreateInput>;
|
||||
|
||||
export type TEnvironmentUpdateInput = z.infer<typeof ZEnvironmentUpdateInput>;
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ZProduct = z.object({
|
||||
highlightBorderColor: z
|
||||
.string()
|
||||
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
|
||||
.nullish(),
|
||||
.nullable(),
|
||||
recontactDays: z.number().int(),
|
||||
formbricksSignature: z.boolean(),
|
||||
placement: z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]),
|
||||
@@ -26,7 +26,6 @@ export const ZProductUpdateInput = ZProduct.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
environments: true,
|
||||
});
|
||||
|
||||
export type TProductUpdateInput = z.infer<typeof ZProductUpdateInput>;
|
||||
|
||||
@@ -21,11 +21,21 @@ export const ZProfile = z.object({
|
||||
export type TProfile = z.infer<typeof ZProfile>;
|
||||
|
||||
export const ZProfileUpdateInput = z.object({
|
||||
name: z.string().nullable(),
|
||||
email: z.string(),
|
||||
onboardingCompleted: z.boolean(),
|
||||
role: ZRole.nullable(),
|
||||
objective: ZObjective.nullable(),
|
||||
name: z.string().nullish(),
|
||||
email: z.string().optional(),
|
||||
onboardingCompleted: z.boolean().optional(),
|
||||
role: ZRole.optional(),
|
||||
objective: ZObjective.optional(),
|
||||
});
|
||||
|
||||
export type TProfileUpdateInput = z.infer<typeof ZProfileUpdateInput>;
|
||||
|
||||
export const ZProfileCreateInput = z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string(),
|
||||
onboardingCompleted: z.boolean().optional(),
|
||||
role: ZRole.optional(),
|
||||
objective: ZObjective.optional(),
|
||||
});
|
||||
|
||||
export type TProfileCreateInput = z.infer<typeof ZProfileCreateInput>;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export const ZTeam = z.object({
|
||||
id: z.string().cuid2(),
|
||||
@@ -10,6 +9,12 @@ export const ZTeam = z.object({
|
||||
stripeCustomerId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type TTeamUpdateInput = Prisma.TeamUpdateInput;
|
||||
export const ZTeamUpdateInput = z.object({
|
||||
name: z.string(),
|
||||
plan: z.enum(["free", "pro"]).optional(),
|
||||
stripeCustomerId: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type TTeamUpdateInput = z.infer<typeof ZTeamUpdateInput>;
|
||||
|
||||
export type TTeam = z.infer<typeof ZTeam>;
|
||||
|
||||
Reference in New Issue
Block a user