mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
feat: Improve auth & permission system (#2845)
This commit is contained in:
17
packages/lib/actionClient/helper.ts
Normal file
17
packages/lib/actionClient/helper.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const getFormattedErrorMessage = (result) => {
|
||||
let message = "";
|
||||
|
||||
if (result.serverError) {
|
||||
message = result.serverError;
|
||||
} else {
|
||||
const errors = result.validationErrors;
|
||||
message = Object.keys(errors || {})
|
||||
.map((key) => {
|
||||
if (key === "_errors") return errors[key].join(", ");
|
||||
return `${key ? `${key}` : ""}${errors?.[key]?._errors.join(", ")}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
24
packages/lib/actionClient/index.ts
Normal file
24
packages/lib/actionClient/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from "next-safe-action";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { authOptions } from "../authOptions";
|
||||
|
||||
export const actionClient = createSafeActionClient({
|
||||
handleReturnedServerError(e) {
|
||||
if (e instanceof ResourceNotFoundError) {
|
||||
return e.message;
|
||||
} else if (e instanceof AuthorizationError) {
|
||||
return e.message;
|
||||
}
|
||||
|
||||
return DEFAULT_SERVER_ERROR_MESSAGE;
|
||||
},
|
||||
});
|
||||
|
||||
export const authenticatedActionClient = actionClient.use(async ({ next }) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
return next({ ctx: { user: session.user } });
|
||||
});
|
||||
110
packages/lib/actionClient/permissions.ts
Normal file
110
packages/lib/actionClient/permissions.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
export const Permissions = {
|
||||
owner: {
|
||||
product: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
organization: {
|
||||
update: true,
|
||||
},
|
||||
person: {
|
||||
delete: true,
|
||||
},
|
||||
response: {
|
||||
delete: true,
|
||||
},
|
||||
survey: {
|
||||
create: true,
|
||||
},
|
||||
},
|
||||
|
||||
admin: {
|
||||
product: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
organization: {
|
||||
update: true,
|
||||
},
|
||||
person: {
|
||||
delete: true,
|
||||
},
|
||||
response: {
|
||||
delete: true,
|
||||
},
|
||||
survey: {
|
||||
create: true,
|
||||
},
|
||||
},
|
||||
|
||||
editor: {
|
||||
product: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
organization: {
|
||||
update: false,
|
||||
},
|
||||
person: {
|
||||
delete: true,
|
||||
},
|
||||
response: {
|
||||
delete: true,
|
||||
},
|
||||
survey: {
|
||||
create: true,
|
||||
},
|
||||
},
|
||||
|
||||
developer: {
|
||||
product: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: ZProductUpdateInput.omit({
|
||||
name: true,
|
||||
}),
|
||||
delete: true,
|
||||
},
|
||||
organization: {
|
||||
update: false,
|
||||
},
|
||||
person: {
|
||||
delete: true,
|
||||
},
|
||||
response: {
|
||||
delete: true,
|
||||
},
|
||||
survey: {
|
||||
create: true,
|
||||
},
|
||||
},
|
||||
|
||||
viewer: {
|
||||
product: {
|
||||
create: false,
|
||||
read: true,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
organization: {
|
||||
update: false,
|
||||
},
|
||||
person: {
|
||||
delete: false,
|
||||
},
|
||||
response: {
|
||||
delete: false,
|
||||
},
|
||||
survey: {
|
||||
create: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
64
packages/lib/actionClient/utils.ts
Normal file
64
packages/lib/actionClient/utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { returnValidationErrors } from "next-safe-action";
|
||||
import { ZodIssue, z } from "zod";
|
||||
import { TOperation, TResource } from "@formbricks/types/actionClient";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { getMembershipRole } from "../membership/hooks/actions";
|
||||
import { Permissions } from "./permissions";
|
||||
|
||||
export const getOperationPermissions = (role: TMembershipRole, entity: TResource, operation: TOperation) => {
|
||||
const permission = Permissions[role][entity][operation];
|
||||
|
||||
if (typeof permission === "boolean" && !permission) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
return permission;
|
||||
};
|
||||
|
||||
export const getRoleBasedSchema = <T extends z.ZodRawShape>(
|
||||
schema: z.ZodObject<T>,
|
||||
role: TMembershipRole,
|
||||
entity: TResource,
|
||||
operation: TOperation
|
||||
): z.ZodObject<T> => {
|
||||
const data = getOperationPermissions(role, entity, operation);
|
||||
|
||||
return typeof data === "boolean" && data === true ? schema.strict() : data;
|
||||
};
|
||||
|
||||
export const formatErrors = (errors: ZodIssue[]) => {
|
||||
return {
|
||||
...errors.reduce((acc, error) => {
|
||||
acc[error.path.join(".")] = {
|
||||
_errors: [error.message],
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const checkAuthorization = async <T extends z.ZodRawShape>({
|
||||
schema,
|
||||
data,
|
||||
userId,
|
||||
organizationId,
|
||||
rules,
|
||||
}: {
|
||||
schema?: z.ZodObject<T>;
|
||||
data?: z.ZodObject<T>["_output"];
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
rules: [TResource, TOperation];
|
||||
}) => {
|
||||
const role = await getMembershipRole(userId, organizationId);
|
||||
if (schema) {
|
||||
const resultSchema = getRoleBasedSchema(schema, role, ...rules);
|
||||
const parsedResult = resultSchema.safeParse(data);
|
||||
if (!parsedResult.success) {
|
||||
return returnValidationErrors(resultSchema, formatErrors(parsedResult.error.issues));
|
||||
}
|
||||
} else {
|
||||
getOperationPermissions(role, ...rules);
|
||||
}
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import "server-only";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { authOptions } from "../../authOptions";
|
||||
import { getOrganizationByEnvironmentId } from "../../organization/service";
|
||||
@@ -21,11 +21,16 @@ export const getMembershipByUserIdOrganizationIdAction = async (environmentId: s
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(user.id, organization.id);
|
||||
const currentUserMembership = await getMembershipRole(user.id, organization.id);
|
||||
|
||||
if (!currentUserMembership) {
|
||||
throw new Error("Membership not found");
|
||||
return currentUserMembership;
|
||||
};
|
||||
|
||||
export const getMembershipRole = async (userId: string, organizationId: string) => {
|
||||
const membership = await getMembershipByUserIdOrganizationId(userId, organizationId);
|
||||
if (!membership) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
return currentUserMembership?.role;
|
||||
return membership.role;
|
||||
};
|
||||
|
||||
59
packages/lib/organization/utils.ts
Normal file
59
packages/lib/organization/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironment } from "../environment/service";
|
||||
import { getPerson } from "../person/service";
|
||||
import { getProduct } from "../product/service";
|
||||
import { getResponse } from "../response/service";
|
||||
import { getSurvey } from "../survey/service";
|
||||
|
||||
/**
|
||||
* GET organization ID from RESOURCE ID
|
||||
*/
|
||||
|
||||
export const getOrganizationIdFromProductId = async (productId: string) => {
|
||||
const product = await getProduct(productId);
|
||||
if (!product) {
|
||||
throw new ResourceNotFoundError("product", productId);
|
||||
}
|
||||
|
||||
return product.organizationId;
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromEnvironmentId = async (environmentId: string) => {
|
||||
const environment = await getEnvironment(environmentId);
|
||||
if (!environment) {
|
||||
throw new ResourceNotFoundError("environment", environmentId);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromProductId(environment.productId);
|
||||
return organizationId;
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromSurveyId = async (surveyId: string) => {
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("survey", surveyId);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(survey.environmentId);
|
||||
return organizationId;
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromResponseId = async (responseId: string) => {
|
||||
const response = await getResponse(responseId);
|
||||
if (!response) {
|
||||
throw new ResourceNotFoundError("response", responseId);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromSurveyId(response.surveyId);
|
||||
return organizationId;
|
||||
};
|
||||
|
||||
export const getOrganizationIdFromPersonId = async (personId: string) => {
|
||||
const person = await getPerson(personId);
|
||||
if (!person) {
|
||||
throw new ResourceNotFoundError("person", personId);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(person.environmentId);
|
||||
return organizationId;
|
||||
};
|
||||
16
packages/types/actionClient.ts
Normal file
16
packages/types/actionClient.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZResource = z.enum([
|
||||
"product",
|
||||
"organization",
|
||||
"environment",
|
||||
"membership",
|
||||
"invite",
|
||||
"response",
|
||||
"survey",
|
||||
"person",
|
||||
]);
|
||||
export type TResource = z.infer<typeof ZResource>;
|
||||
|
||||
export const ZOperation = z.enum(["create", "read", "update", "delete"]);
|
||||
export type TOperation = z.infer<typeof ZOperation>;
|
||||
@@ -1,8 +1,12 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationIdFromResponseId } from "@formbricks/lib/organization/utils";
|
||||
import { canUserAccessResponse } from "@formbricks/lib/response/auth";
|
||||
import { deleteResponse, getResponse } from "@formbricks/lib/response/service";
|
||||
import { canUserModifyResponseNote, canUserResolveResponseNote } from "@formbricks/lib/responseNote/auth";
|
||||
@@ -58,14 +62,21 @@ export const deleteTagOnResponseAction = async (responseId: string, tagId: strin
|
||||
return await deleteTagOnResponse(responseId, tagId);
|
||||
};
|
||||
|
||||
export const deleteResponseAction = async (responseId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const isAuthorized = await canUserAccessResponse(session.user!.id, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteResponseAction = z.object({
|
||||
responseId: z.string(),
|
||||
});
|
||||
|
||||
return await deleteResponse(responseId);
|
||||
};
|
||||
export const deleteResponseAction = authenticatedActionClient
|
||||
.schema(ZDeleteResponseAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromResponseId(parsedInput.responseId),
|
||||
rules: ["response", "delete"],
|
||||
});
|
||||
|
||||
return await deleteResponse(parsedInput.responseId);
|
||||
});
|
||||
|
||||
export const updateResponseNoteAction = async (responseNoteId: string, text: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -90,7 +90,7 @@ export const SingleResponseCard = ({
|
||||
if (isViewer) {
|
||||
throw new Error("You are not authorized to perform this action.");
|
||||
}
|
||||
await deleteResponseAction(response.id);
|
||||
await deleteResponseAction({ responseId: response.id });
|
||||
deleteResponse?.(response.id);
|
||||
|
||||
router.refresh();
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
import { Prisma as prismaClient } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { segmentCache } from "@formbricks/lib/segment/cache";
|
||||
import { createSegment } from "@formbricks/lib/segment/service";
|
||||
@@ -36,243 +40,241 @@ export const duplicateSurveyAction = async (environmentId: string, surveyId: str
|
||||
return duplicatedSurvey;
|
||||
};
|
||||
|
||||
export const copyToOtherEnvironmentAction = async (
|
||||
environmentId: string,
|
||||
surveyId: string,
|
||||
targetEnvironmentId: string
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCopyToOtherEnvironmentAction = z.object({
|
||||
environmentId: z.string(),
|
||||
surveyId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
});
|
||||
|
||||
const isAuthorizedToAccessSourceEnvironment = await hasUserEnvironmentAccess(
|
||||
session.user.id,
|
||||
environmentId
|
||||
);
|
||||
if (!isAuthorizedToAccessSourceEnvironment) throw new AuthorizationError("Not authorized");
|
||||
export const copyToOtherEnvironmentAction = authenticatedActionClient
|
||||
.schema(ZCopyToOtherEnvironmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
// check if user has access to source and target environments
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["survey", "create"],
|
||||
});
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.targetEnvironmentId),
|
||||
rules: ["survey", "create"],
|
||||
});
|
||||
|
||||
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,
|
||||
environmentId,
|
||||
},
|
||||
include: {
|
||||
triggers: {
|
||||
include: {
|
||||
actionClass: true,
|
||||
},
|
||||
const existingSurvey = await prisma.survey.findFirst({
|
||||
where: {
|
||||
id: parsedInput.surveyId,
|
||||
environmentId: parsedInput.environmentId,
|
||||
},
|
||||
attributeFilters: {
|
||||
include: {
|
||||
attributeClass: true,
|
||||
include: {
|
||||
triggers: {
|
||||
include: {
|
||||
actionClass: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
select: {
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
select: {
|
||||
id: true,
|
||||
attributeFilters: {
|
||||
include: {
|
||||
attributeClass: true,
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
select: {
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
segment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
let targetEnvironmentTriggers: string[] = [];
|
||||
// map the local triggers to the target environment
|
||||
for (const trigger of existingSurvey.triggers) {
|
||||
const targetEnvironmentTrigger = await prisma.actionClass.findFirst({
|
||||
where: {
|
||||
...(trigger.actionClass.type === "code"
|
||||
? { key: trigger.actionClass.key }
|
||||
: { name: trigger.actionClass.name }),
|
||||
environment: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
segment: true,
|
||||
},
|
||||
});
|
||||
if (!targetEnvironmentTrigger) {
|
||||
// if the trigger does not exist in the target environment, create it
|
||||
const newTrigger = await prisma.actionClass.create({
|
||||
data: {
|
||||
name: trigger.actionClass.name,
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
description: trigger.actionClass.description,
|
||||
type: trigger.actionClass.type,
|
||||
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
let targetEnvironmentTriggers: string[] = [];
|
||||
// map the local triggers to the target environment
|
||||
for (const trigger of existingSurvey.triggers) {
|
||||
const targetEnvironmentTrigger = await prisma.actionClass.findFirst({
|
||||
where: {
|
||||
...(trigger.actionClass.type === "code"
|
||||
? { key: trigger.actionClass.key }
|
||||
: {
|
||||
noCodeConfig: trigger.actionClass.noCodeConfig
|
||||
? structuredClone(trigger.actionClass.noCodeConfig)
|
||||
: undefined,
|
||||
}),
|
||||
: { name: trigger.actionClass.name }),
|
||||
environment: {
|
||||
id: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
});
|
||||
targetEnvironmentTriggers.push(newTrigger.id);
|
||||
} else {
|
||||
targetEnvironmentTriggers.push(targetEnvironmentTrigger.id);
|
||||
if (!targetEnvironmentTrigger) {
|
||||
// if the trigger does not exist in the target environment, create it
|
||||
const newTrigger = await prisma.actionClass.create({
|
||||
data: {
|
||||
name: trigger.actionClass.name,
|
||||
environment: {
|
||||
connect: {
|
||||
id: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
description: trigger.actionClass.description,
|
||||
type: trigger.actionClass.type,
|
||||
...(trigger.actionClass.type === "code"
|
||||
? { key: trigger.actionClass.key }
|
||||
: {
|
||||
noCodeConfig: trigger.actionClass.noCodeConfig
|
||||
? structuredClone(trigger.actionClass.noCodeConfig)
|
||||
: undefined,
|
||||
}),
|
||||
},
|
||||
});
|
||||
targetEnvironmentTriggers.push(newTrigger.id);
|
||||
} else {
|
||||
targetEnvironmentTriggers.push(targetEnvironmentTrigger.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let targetEnvironmentAttributeFilters: string[] = [];
|
||||
// map the local attributeFilters to the target env
|
||||
for (const attributeFilter of existingSurvey.attributeFilters) {
|
||||
// check if attributeClass exists in target env.
|
||||
// if not, create it
|
||||
const targetEnvironmentAttributeClass = await prisma.attributeClass.findFirst({
|
||||
where: {
|
||||
name: attributeFilter.attributeClass.name,
|
||||
environment: {
|
||||
id: targetEnvironmentId,
|
||||
let targetEnvironmentAttributeFilters: string[] = [];
|
||||
// map the local attributeFilters to the target env
|
||||
for (const attributeFilter of existingSurvey.attributeFilters) {
|
||||
// check if attributeClass exists in target env.
|
||||
// if not, create it
|
||||
const targetEnvironmentAttributeClass = await prisma.attributeClass.findFirst({
|
||||
where: {
|
||||
name: attributeFilter.attributeClass.name,
|
||||
environment: {
|
||||
id: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!targetEnvironmentAttributeClass) {
|
||||
const newAttributeClass = await prisma.attributeClass.create({
|
||||
data: {
|
||||
name: attributeFilter.attributeClass.name,
|
||||
description: attributeFilter.attributeClass.description,
|
||||
type: attributeFilter.attributeClass.type,
|
||||
environment: {
|
||||
connect: {
|
||||
id: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
targetEnvironmentAttributeFilters.push(newAttributeClass.id);
|
||||
} else {
|
||||
targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultLanguageId = existingSurvey.languages.find((l) => l.default)?.language.id;
|
||||
|
||||
// create new survey with the data of the existing survey
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: {
|
||||
...existingSurvey,
|
||||
id: undefined, // id is auto-generated
|
||||
environmentId: undefined, // environmentId is set below
|
||||
createdBy: undefined,
|
||||
segmentId: undefined,
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
thankYouCard: structuredClone(existingSurvey.thankYouCard),
|
||||
languages: {
|
||||
create: existingSurvey.languages?.map((surveyLanguage) => ({
|
||||
languageId: surveyLanguage.language.id,
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
})),
|
||||
},
|
||||
triggers: {
|
||||
create: targetEnvironmentTriggers.map((actionClassId) => ({
|
||||
actionClassId: actionClassId,
|
||||
})),
|
||||
},
|
||||
attributeFilters: {
|
||||
create: existingSurvey.attributeFilters.map((attributeFilter, idx) => ({
|
||||
attributeClassId: targetEnvironmentAttributeFilters[idx],
|
||||
condition: attributeFilter.condition,
|
||||
value: attributeFilter.value,
|
||||
})),
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
styling: existingSurvey.styling ?? prismaClient.JsonNull,
|
||||
segment: undefined,
|
||||
},
|
||||
});
|
||||
if (!targetEnvironmentAttributeClass) {
|
||||
const newAttributeClass = await prisma.attributeClass.create({
|
||||
data: {
|
||||
name: attributeFilter.attributeClass.name,
|
||||
description: attributeFilter.attributeClass.description,
|
||||
type: attributeFilter.attributeClass.type,
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId: parsedInput.environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
targetEnvironmentAttributeFilters.push(newAttributeClass.id);
|
||||
} else {
|
||||
targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id);
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultLanguageId = existingSurvey.languages.find((l) => l.default)?.language.id;
|
||||
|
||||
// create new survey with the data of the existing survey
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: {
|
||||
...existingSurvey,
|
||||
id: undefined, // id is auto-generated
|
||||
environmentId: undefined, // environmentId is set below
|
||||
createdBy: undefined,
|
||||
segmentId: undefined,
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
thankYouCard: structuredClone(existingSurvey.thankYouCard),
|
||||
languages: {
|
||||
create: existingSurvey.languages?.map((surveyLanguage) => ({
|
||||
languageId: surveyLanguage.language.id,
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
})),
|
||||
},
|
||||
triggers: {
|
||||
create: targetEnvironmentTriggers.map((actionClassId) => ({
|
||||
actionClassId: actionClassId,
|
||||
})),
|
||||
},
|
||||
attributeFilters: {
|
||||
create: existingSurvey.attributeFilters.map((attributeFilter, idx) => ({
|
||||
attributeClassId: targetEnvironmentAttributeFilters[idx],
|
||||
condition: attributeFilter.condition,
|
||||
value: attributeFilter.value,
|
||||
})),
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: session.user.id,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
styling: existingSurvey.styling ?? prismaClient.JsonNull,
|
||||
segment: undefined,
|
||||
},
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: parsedInput.targetEnvironmentId,
|
||||
});
|
||||
return newSurvey;
|
||||
});
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: targetEnvironmentId,
|
||||
});
|
||||
return newSurvey;
|
||||
};
|
||||
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
@@ -85,7 +85,12 @@ export const SurveyDropDownMenu = ({
|
||||
const copyToOtherEnvironment = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await copyToOtherEnvironmentAction(environmentId, surveyId, otherEnvironment.id);
|
||||
// await copyToOtherEnvironmentAction(environmentId, surveyId, otherEnvironment.id);
|
||||
await copyToOtherEnvironmentAction({
|
||||
environmentId,
|
||||
surveyId,
|
||||
targetEnvironmentId: otherEnvironment.id,
|
||||
});
|
||||
if (otherEnvironment.type === "production") {
|
||||
toast.success("Survey copied to production env.");
|
||||
} else if (otherEnvironment.type === "development") {
|
||||
|
||||
Reference in New Issue
Block a user