mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 19:21:15 -05:00
Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor
This commit is contained in:
@@ -70,6 +70,8 @@ S3_BUCKET_NAME=
|
||||
# Configure a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
|
||||
# e.g., https://gateway.storjshare.io
|
||||
S3_ENDPOINT_URL=
|
||||
# Force path style for S3 compatible storage (0 for disabled, 1 for enabled)
|
||||
S3_FORCE_PATH_STYLE=0
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
|
||||
@@ -128,6 +128,7 @@ const AppPage = ({}) => {
|
||||
}}>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and
|
||||
try again.
|
||||
|
||||
@@ -39,7 +39,7 @@ We currently have the following Management API methods exposed and below is thei
|
||||
- [Me API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#79e08365-641d-4b2d-aea2-9a855e0438ec) - Retrieve Account Information
|
||||
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#cffc27a6-dafb-428f-8ea7-5165bedb911e) - List and Delete People
|
||||
- [Response API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#e544ec0d-8b30-4e33-8d35-2441cb40d676) - List, List by Survey, Update, and Delete Responses
|
||||
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, and Delete Surveys
|
||||
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, generate multiple suId and Delete Surveys
|
||||
- [Webhook API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - List, Create, and Delete Webhooks
|
||||
|
||||
## How to Generate an API key
|
||||
|
||||
@@ -36,7 +36,12 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
|
||||
const handleInvite = async (data: TInviteOrganizationMemberDetails) => {
|
||||
try {
|
||||
await inviteOrganizationMemberAction(organization.id, data.email, "developer", data.inviteMessage);
|
||||
await inviteOrganizationMemberAction({
|
||||
organizationId: organization.id,
|
||||
email: data.email,
|
||||
role: "developer",
|
||||
inviteMessage: data.inviteMessage,
|
||||
});
|
||||
toast.success("Invite sent successful");
|
||||
await finishOnboarding();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,68 +1,54 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { inviteUser } from "@formbricks/lib/invite/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { ZMembershipRole } from "@formbricks/types/memberships";
|
||||
|
||||
export const inviteOrganizationMemberAction = async (
|
||||
organizationId: string,
|
||||
email: string,
|
||||
role: TMembershipRole,
|
||||
inviteMessage: string
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ZInviteOrganizationMemberAction = z.object({
|
||||
organizationId: ZId,
|
||||
email: z.string(),
|
||||
role: ZMembershipRole,
|
||||
inviteMessage: z.string(),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
.schema(ZInviteOrganizationMemberAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["membership", "create"],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const invite = await inviteUser({
|
||||
organizationId: parsedInput.organizationId,
|
||||
invitee: {
|
||||
email: parsedInput.email,
|
||||
name: "",
|
||||
role: parsedInput.role,
|
||||
},
|
||||
});
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
parsedInput.inviteMessage
|
||||
);
|
||||
}
|
||||
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId,
|
||||
invitee: {
|
||||
email,
|
||||
name: "",
|
||||
role,
|
||||
},
|
||||
return invite;
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
email,
|
||||
user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
inviteMessage
|
||||
);
|
||||
}
|
||||
|
||||
return invite;
|
||||
};
|
||||
|
||||
@@ -1,208 +1,206 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { createActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { actionClient, authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromProductId,
|
||||
getOrganizationIdFromSegmentId,
|
||||
getOrganizationIdFromSurveyId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { getProduct } from "@formbricks/lib/product/service";
|
||||
import {
|
||||
cloneSegment,
|
||||
createSegment,
|
||||
deleteSegment,
|
||||
getSegment,
|
||||
resetSegmentInSurvey,
|
||||
updateSegment,
|
||||
} from "@formbricks/lib/segment/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import {
|
||||
deleteSurvey,
|
||||
getSurvey,
|
||||
loadNewSegmentInSurvey,
|
||||
updateSurvey,
|
||||
} from "@formbricks/lib/survey/service";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TBaseFilters, TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { loadNewSegmentInSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZBaseFilters, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
import { ZSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const surveyMutateAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
|
||||
export const updateSurveyAction = async (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");
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(survey.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(survey!.environmentId, session.user.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const refetchProductAction = async (productId: string): Promise<TProduct | null> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const product = await getProduct(productId);
|
||||
return product;
|
||||
};
|
||||
|
||||
export const createBasicSegmentAction = async ({
|
||||
description,
|
||||
environmentId,
|
||||
filters,
|
||||
isPrivate,
|
||||
surveyId,
|
||||
title,
|
||||
}: {
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
isPrivate: boolean;
|
||||
filters: TBaseFilters;
|
||||
}) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const segment = await createSegment({
|
||||
environmentId,
|
||||
surveyId,
|
||||
title,
|
||||
description: description || "",
|
||||
isPrivate,
|
||||
filters,
|
||||
export const updateSurveyAction = authenticatedActionClient
|
||||
.schema(ZSurvey)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.id),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
return await updateSurvey(parsedInput);
|
||||
});
|
||||
surveyCache.revalidate({ id: surveyId });
|
||||
|
||||
return segment;
|
||||
};
|
||||
const ZRefetchProductAction = z.object({
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = async (
|
||||
environmentId: string,
|
||||
segmentId: string,
|
||||
data: TSegmentUpdateInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const refetchProductAction = authenticatedActionClient
|
||||
.schema(ZRefetchProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "read"],
|
||||
});
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
return await getProduct(parsedInput.productId);
|
||||
});
|
||||
|
||||
const { filters } = data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
const ZCreateBasicSegmentAction = z.object({
|
||||
description: z.string().optional(),
|
||||
environmentId: ZId,
|
||||
filters: ZBaseFilters,
|
||||
isPrivate: z.boolean(),
|
||||
surveyId: ZId,
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export const createBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZCreateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["segment", "create"],
|
||||
});
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(parsedInput.filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(segmentId, data);
|
||||
};
|
||||
const segment = await createSegment({
|
||||
environmentId: parsedInput.environmentId,
|
||||
surveyId: parsedInput.surveyId,
|
||||
title: parsedInput.title,
|
||||
description: parsedInput.description || "",
|
||||
isPrivate: parsedInput.isPrivate,
|
||||
filters: parsedInput.filters,
|
||||
});
|
||||
surveyCache.revalidate({ id: parsedInput.surveyId });
|
||||
|
||||
export const loadNewBasicSegmentAction = async (surveyId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await loadNewSegmentInSurvey(surveyId, segmentId);
|
||||
};
|
||||
|
||||
export const cloneBasicSegmentAction = async (segmentId: string, surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneSegment(segmentId, surveyId);
|
||||
return clonedSegment;
|
||||
} catch (err: any) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const foundSegment = await getSegment(segmentId);
|
||||
|
||||
if (!foundSegment) {
|
||||
throw new Error(`Segment with id ${segmentId} not found`);
|
||||
}
|
||||
|
||||
return await deleteSegment(segmentId);
|
||||
};
|
||||
|
||||
export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await resetSegmentInSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const getImagesFromUnsplashAction = async (searchQuery: string, page: number = 1) => {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
const baseUrl = "https://api.unsplash.com/search/photos";
|
||||
const params = new URLSearchParams({
|
||||
query: searchQuery,
|
||||
client_id: UNSPLASH_ACCESS_KEY,
|
||||
orientation: "landscape",
|
||||
per_page: "9",
|
||||
page: page.toString(),
|
||||
return segment;
|
||||
});
|
||||
|
||||
try {
|
||||
const ZUpdateBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
data: ZSegmentUpdateInput,
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZUpdateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
const { filters } = parsedInput.data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(parsedInput.segmentId, parsedInput.data);
|
||||
});
|
||||
|
||||
const ZLoadNewBasicSegmentAction = z.object({
|
||||
surveyId: ZId,
|
||||
segmentId: ZId,
|
||||
});
|
||||
|
||||
export const loadNewBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZLoadNewBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.surveyId),
|
||||
rules: ["segment", "read"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
|
||||
});
|
||||
|
||||
const ZCloneBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const cloneBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZCloneBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "create"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZResetBasicSegmentFiltersAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const resetBasicSegmentFiltersAction = authenticatedActionClient
|
||||
.schema(ZResetBasicSegmentFiltersAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
return await resetSegmentInSurvey(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZGetImagesFromUnsplashAction = z.object({
|
||||
searchQuery: z.string(),
|
||||
page: z.number().optional(),
|
||||
});
|
||||
|
||||
export const getImagesFromUnsplashAction = actionClient
|
||||
.schema(ZGetImagesFromUnsplashAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
const baseUrl = "https://api.unsplash.com/search/photos";
|
||||
const params = new URLSearchParams({
|
||||
query: parsedInput.searchQuery,
|
||||
client_id: UNSPLASH_ACCESS_KEY,
|
||||
orientation: "landscape",
|
||||
per_page: "9",
|
||||
page: (parsedInput.page || 1).toString(),
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}?${params}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -227,14 +225,16 @@ export const getImagesFromUnsplashAction = async (searchQuery: string, page: num
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Error getting images from Unsplash");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) => {
|
||||
try {
|
||||
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
const ZTriggerDownloadUnsplashImageAction = z.object({
|
||||
downloadUrl: z.string(),
|
||||
});
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = actionClient
|
||||
.schema(ZTriggerDownloadUnsplashImageAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const response = await fetch(`${parsedInput.downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -245,20 +245,20 @@ export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) =>
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new Error("Error downloading image from Unsplash");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const createActionClassAction = async (environmentId: string, action: TActionClassInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateActionClassAction = z.object({
|
||||
action: ZActionClassInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, action.environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createActionClassAction = authenticatedActionClient
|
||||
.schema(ZCreateActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.action.environmentId),
|
||||
rules: ["actionClass", "create"],
|
||||
});
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createActionClass(action.environmentId, action);
|
||||
};
|
||||
return await createActionClass(parsedInput.action.environmentId, parsedInput.action);
|
||||
});
|
||||
|
||||
@@ -135,10 +135,14 @@ export const CreateNewActionTab = ({
|
||||
};
|
||||
}
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(
|
||||
environmentId,
|
||||
updatedAction as TActionClassInput
|
||||
);
|
||||
// const newActionClass: TActionClass =
|
||||
const createActionClassResposne = await createActionClassAction({
|
||||
action: updatedAction as TActionClassInput,
|
||||
});
|
||||
|
||||
if (!createActionClassResposne?.data) return;
|
||||
|
||||
const newActionClass = createActionClassResposne.data;
|
||||
if (setActionClasses) {
|
||||
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
|
||||
}
|
||||
|
||||
@@ -213,8 +213,6 @@ export const EditorCardMenu = ({
|
||||
|
||||
<DropdownMenuSubContent className="ml-4 border border-slate-200">
|
||||
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
|
||||
if (type === card.type) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { validateId } from "@formbricks/types/surveys/validation";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -36,9 +37,26 @@ export const HiddenFieldsCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const updateSurvey = (data: TSurveyHiddenFields) => {
|
||||
const updateSurvey = (data: TSurveyHiddenFields, currentFieldId?: string) => {
|
||||
const questions = [...localSurvey.questions];
|
||||
|
||||
// Remove recall info from question headlines
|
||||
if (currentFieldId) {
|
||||
questions.forEach((question) => {
|
||||
for (const [languageCode, headline] of Object.entries(question.headline)) {
|
||||
if (headline.includes(`recall:${currentFieldId}`)) {
|
||||
const recallInfo = extractRecallInfo(headline);
|
||||
if (recallInfo) {
|
||||
question.headline[languageCode] = headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
questions,
|
||||
hiddenFields: {
|
||||
...localSurvey.hiddenFields,
|
||||
...data,
|
||||
@@ -93,10 +111,13 @@ export const HiddenFieldsCard = ({
|
||||
<Tag
|
||||
key={fieldId}
|
||||
onDelete={() => {
|
||||
updateSurvey({
|
||||
enabled: true,
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||
});
|
||||
updateSurvey(
|
||||
{
|
||||
enabled: true,
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||
},
|
||||
fieldId
|
||||
);
|
||||
}}
|
||||
tagId={fieldId}
|
||||
tagName={fieldId}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { getDefaultEndingCard } from "@formbricks/lib/templates";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
@@ -213,15 +213,14 @@ export const QuestionsView = ({
|
||||
const activeQuestionIdTemp = activeQuestionId ?? localSurvey.questions[0].id;
|
||||
let updatedSurvey: TSurvey = { ...localSurvey };
|
||||
|
||||
// check if we are recalling from this question
|
||||
// check if we are recalling from this question for every language
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
if (question.headline[selectedLanguageCode].includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(getLocalizedValue(question.headline, selectedLanguageCode));
|
||||
if (recallInfo) {
|
||||
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replace(
|
||||
recallInfo,
|
||||
""
|
||||
);
|
||||
for (const [languageCode, headline] of Object.entries(question.headline)) {
|
||||
if (headline.includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(headline);
|
||||
if (recallInfo) {
|
||||
question.headline[languageCode] = headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -440,6 +439,13 @@ export const QuestionsView = ({
|
||||
activeQuestionId={activeQuestionId}
|
||||
/>
|
||||
|
||||
{/* <SurveyVariablesCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
/> */}
|
||||
|
||||
<MultiLanguageCard
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
|
||||
@@ -72,9 +72,9 @@ export const SurveyEditor = ({
|
||||
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
|
||||
|
||||
const fetchLatestProduct = useCallback(async () => {
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
}
|
||||
}, [localProduct.id]);
|
||||
|
||||
@@ -99,9 +99,9 @@ export const SurveyEditor = ({
|
||||
const listener = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
const fetchLatestProduct = async () => {
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
const refetchProductResponse = await refetchProductAction({ productId: localProduct.id });
|
||||
if (refetchProductResponse?.data) {
|
||||
setLocalProduct(refetchProductResponse.data);
|
||||
}
|
||||
};
|
||||
fetchLatestProduct();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { createSegmentAction } from "@formbricks/ee/advanced-targeting/lib/actions";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -132,7 +133,7 @@ export const SurveyMenuBar = ({
|
||||
title: localSurvey.id,
|
||||
});
|
||||
|
||||
return newSegment;
|
||||
return newSegment?.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -224,12 +225,16 @@ export const SurveyMenuBar = ({
|
||||
});
|
||||
|
||||
const segment = await handleSegmentUpdate();
|
||||
const updatedSurvey = await updateSurveyAction({ ...localSurvey, segment });
|
||||
const updatedSurveyResponse = await updateSurveyAction({ ...localSurvey, segment });
|
||||
|
||||
setIsSurveySaving(false);
|
||||
setLocalSurvey(updatedSurvey);
|
||||
|
||||
toast.success("Changes saved.");
|
||||
if (updatedSurveyResponse?.data) {
|
||||
setLocalSurvey(updatedSurveyResponse.data);
|
||||
toast.success("Changes saved.");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { SurveyVariablesCardItem } from "./SurveyVariablesCardItem";
|
||||
|
||||
interface SurveyVariablesCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
}
|
||||
|
||||
const variablesCardId = `fb-variables-${Date.now()}`;
|
||||
|
||||
export const SurveyVariablesCard = ({
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
activeQuestionId,
|
||||
setActiveQuestionId,
|
||||
}: SurveyVariablesCardProps) => {
|
||||
const open = activeQuestionId === variablesCardId;
|
||||
|
||||
const setOpenState = (state: boolean) => {
|
||||
if (state) {
|
||||
setActiveQuestionId(variablesCardId);
|
||||
} else {
|
||||
setActiveQuestionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
|
||||
<div
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
|
||||
)}>
|
||||
<p>🪣</p>
|
||||
</div>
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpenState}
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-300 ease-in-out">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="flex cursor-pointer justify-between p-4 hover:bg-slate-50">
|
||||
<div>
|
||||
<div className="inline-flex">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Variables</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
{localSurvey.variables.length > 0 ? (
|
||||
localSurvey.variables.map((variable) => (
|
||||
<SurveyVariablesCardItem
|
||||
key={variable.id}
|
||||
mode="edit"
|
||||
variable={variable}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">No variables yet. Add the first one below.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SurveyVariablesCardItem mode="create" localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
interface SurveyVariablesCardItemProps {
|
||||
variable?: TSurveyVariable;
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
|
||||
mode: "create" | "edit";
|
||||
}
|
||||
|
||||
export const SurveyVariablesCardItem = ({
|
||||
variable,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
mode,
|
||||
}: SurveyVariablesCardItemProps) => {
|
||||
const form = useForm<TSurveyVariable>({
|
||||
defaultValues: variable ?? {
|
||||
id: createId(),
|
||||
name: "",
|
||||
type: "number",
|
||||
value: 0,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const { errors } = form.formState;
|
||||
const isNameError = !!errors.name?.message;
|
||||
const variableType = form.watch("type");
|
||||
|
||||
const editSurveyVariable = useCallback(
|
||||
(data: TSurveyVariable) => {
|
||||
setLocalSurvey((prevSurvey) => {
|
||||
const updatedVariables = prevSurvey.variables.map((v) => (v.id === data.id ? data : v));
|
||||
return { ...prevSurvey, variables: updatedVariables };
|
||||
});
|
||||
},
|
||||
[setLocalSurvey]
|
||||
);
|
||||
|
||||
const createSurveyVariable = (data: TSurveyVariable) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
variables: [...localSurvey.variables, data],
|
||||
});
|
||||
|
||||
form.reset({
|
||||
id: createId(),
|
||||
name: "",
|
||||
type: "number",
|
||||
value: 0,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "create") {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = form.watch(() => form.handleSubmit(editSurveyVariable)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, mode, editSurveyVariable]);
|
||||
|
||||
const onVaribleDelete = (variable: TSurveyVariable) => {
|
||||
const questions = [...localSurvey.questions];
|
||||
|
||||
// find if this variable is used in any question's recall and remove it for every language
|
||||
|
||||
questions.forEach((question) => {
|
||||
for (const [languageCode, headline] of Object.entries(question.headline)) {
|
||||
if (headline.includes(`recall:${variable.id}`)) {
|
||||
const recallInfo = extractRecallInfo(headline);
|
||||
if (recallInfo) {
|
||||
question.headline[languageCode] = headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setLocalSurvey((prevSurvey) => {
|
||||
const updatedVariables = prevSurvey.variables.filter((v) => v.id !== variable.id);
|
||||
return { ...prevSurvey, variables: updatedVariables, questions };
|
||||
});
|
||||
};
|
||||
|
||||
if (mode === "edit" && !variable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
className="mt-5"
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
if (mode === "create") {
|
||||
createSurveyVariable(data);
|
||||
} else {
|
||||
editSurveyVariable(data);
|
||||
}
|
||||
})}>
|
||||
{mode === "create" && <Label htmlFor="headline">Add variable</Label>}
|
||||
|
||||
<div className="mt-2 flex w-full items-center gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
rules={{
|
||||
pattern: {
|
||||
value: /^[a-z0-9_]+$/,
|
||||
message: "Only lower case letters, numbers, and underscores are allowed.",
|
||||
},
|
||||
validate: (value) => {
|
||||
// if the variable name is already taken
|
||||
if (
|
||||
mode === "create" &&
|
||||
localSurvey.variables.find((variable) => variable.name === value)
|
||||
) {
|
||||
return "Variable name is already taken, please choose another.";
|
||||
}
|
||||
|
||||
if (mode === "edit" && variable && variable.name !== value) {
|
||||
if (localSurvey.variables.find((variable) => variable.name === value)) {
|
||||
return "Variable name is already taken, please choose another.";
|
||||
}
|
||||
}
|
||||
|
||||
// if it does not start with a letter
|
||||
if (!/^[a-z]/.test(value)) {
|
||||
return "Variable name must start with a letter.";
|
||||
}
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
isInvalid={isNameError}
|
||||
type="text"
|
||||
placeholder="Field name e.g, score, price"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue("value", value === "number" ? 0 : "");
|
||||
field.onChange(value);
|
||||
}}>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue placeholder="Select type" className="text-sm" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"number"}>Number</SelectItem>
|
||||
<SelectItem value={"text"}>Text</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="text-slate-600">=</p>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(variableType === "number" ? Number(e.target.value) : e.target.value);
|
||||
}}
|
||||
placeholder="Initial value"
|
||||
type={variableType === "number" ? "number" : "text"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{mode === "create" && (
|
||||
<Button variant="secondary" type="submit" className="h-10 whitespace-nowrap">
|
||||
Add variable
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{mode === "edit" && variable && (
|
||||
<Button
|
||||
variant="minimal"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
onClick={() => onVaribleDelete(variable)}>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isNameError && <p className="mt-1 text-sm text-red-500">{errors.name?.message}</p>}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
|
||||
@@ -81,34 +82,38 @@ export const TargetingCard = ({
|
||||
const handleCloneSegment = async () => {
|
||||
if (!segment) return;
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneBasicSegmentAction(segment.id, localSurvey.id);
|
||||
const cloneBasicSegmentResponse = await cloneBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
|
||||
setSegment(clonedSegment);
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
if (cloneBasicSegmentResponse?.data) {
|
||||
setSegment(cloneBasicSegmentResponse.data);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(cloneBasicSegmentResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadNewSegment = async (surveyId: string, segmentId: string) => {
|
||||
const updatedSurvey = await loadNewBasicSegmentAction(surveyId, segmentId);
|
||||
return updatedSurvey;
|
||||
const loadNewBasicSegmentResponse = await loadNewBasicSegmentAction({ surveyId, segmentId });
|
||||
return loadNewBasicSegmentResponse?.data as TSurvey;
|
||||
};
|
||||
|
||||
const handleSegmentUpdate = async (environmentId: string, segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updatedSegment = await updateBasicSegmentAction(environmentId, segmentId, data);
|
||||
return updatedSegment;
|
||||
const handleSegmentUpdate = async (segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updateBasicSegmentResponse = await updateBasicSegmentAction({ segmentId, data });
|
||||
return updateBasicSegmentResponse?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSegmentCreate = async (data: TSegmentCreateInput) => {
|
||||
const createdSegment = await createBasicSegmentAction(data);
|
||||
return createdSegment;
|
||||
return createdSegment?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
|
||||
try {
|
||||
if (!segment) throw new Error("Invalid segment");
|
||||
await updateBasicSegmentAction(environmentId, segment?.id, data);
|
||||
await updateBasicSegmentAction({ segmentId: segment?.id, data });
|
||||
|
||||
router.refresh();
|
||||
toast.success("Segment saved successfully");
|
||||
@@ -122,7 +127,10 @@ export const TargetingCard = ({
|
||||
|
||||
const handleResetAllFilters = async () => {
|
||||
try {
|
||||
return await resetBasicSegmentFiltersAction(localSurvey.id);
|
||||
const resetBasicSegmentFiltersResponse = await resetBasicSegmentFiltersAction({
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
return resetBasicSegmentFiltersResponse?.data;
|
||||
} catch (err) {
|
||||
toast.error("Error resetting filters");
|
||||
}
|
||||
|
||||
@@ -123,7 +123,13 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
const fetchData = async (searchQuery: string, currentPage: number) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
|
||||
const getImagesFromUnsplashResponse = await getImagesFromUnsplashAction({
|
||||
searchQuery: searchQuery,
|
||||
page: currentPage,
|
||||
});
|
||||
if (!getImagesFromUnsplashResponse?.data) return;
|
||||
|
||||
const imagesFromUnsplash = getImagesFromUnsplashResponse.data;
|
||||
for (let i = 0; i < imagesFromUnsplash.length; i++) {
|
||||
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
|
||||
"authorName"
|
||||
@@ -163,7 +169,7 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
try {
|
||||
handleBgChange(imageUrl, "image");
|
||||
if (downloadImageUrl) {
|
||||
await triggerDownloadUnsplashImageAction(downloadImageUrl);
|
||||
await triggerDownloadUnsplashImageAction({ downloadUrl: downloadImageUrl });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
|
||||
@@ -37,4 +37,5 @@ export const minimalSurvey: TSurvey = {
|
||||
languages: [],
|
||||
showLanguageSwitch: false,
|
||||
isVerifyEmailEnabled: false,
|
||||
variables: [],
|
||||
};
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { canUserAccessAttributeClass } from "@formbricks/lib/attributeClass/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import {
|
||||
getOrganizationIdFromAttributeClassId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
export const getSegmentsByAttributeClassAction = async (
|
||||
environmentId: string,
|
||||
attributeClass: TAttributeClass
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetSegmentsByAttributeClassAction = z.object({
|
||||
environmentId: ZId,
|
||||
attributeClass: ZAttributeClass,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessAttributeClass(session.user.id, attributeClass.id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const segments = await getSegmentsByAttributeClassName(environmentId, attributeClass.name);
|
||||
export const getSegmentsByAttributeClassAction = authenticatedActionClient
|
||||
.schema(ZGetSegmentsByAttributeClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromAttributeClassId(parsedInput.attributeClass.id),
|
||||
rules: ["attributeClass", "read"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["environment", "read"],
|
||||
});
|
||||
|
||||
const segments = await getSegmentsByAttributeClassName(
|
||||
parsedInput.environmentId,
|
||||
parsedInput.attributeClass.name
|
||||
);
|
||||
|
||||
// segments is an array of segments, each segment has a survey array with objects with properties: id, name and status.
|
||||
// We need the name of the surveys only and we need to filter out the surveys that are both in progress and not in progress.
|
||||
@@ -34,8 +51,4 @@ export const getSegmentsByAttributeClassAction = async (
|
||||
.flat();
|
||||
|
||||
return { activeSurveys, inactiveSurveys };
|
||||
} catch (err) {
|
||||
console.error(`Error getting segments by attribute class: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { TagIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -24,20 +25,20 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
|
||||
setLoading(true);
|
||||
|
||||
const getSurveys = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassName = await getSegmentsByAttributeClassAction(
|
||||
attributeClass.environmentId,
|
||||
attributeClass
|
||||
);
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassNameResponse = await getSegmentsByAttributeClassAction({
|
||||
environmentId: attributeClass.environmentId,
|
||||
attributeClass,
|
||||
});
|
||||
|
||||
setActiveSurveys(segmentsWithAttributeClassName.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassName.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (segmentsWithAttributeClassNameResponse?.data) {
|
||||
setActiveSurveys(segmentsWithAttributeClassNameResponse.data.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassNameResponse.data.inactiveSurveys);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(segmentsWithAttributeClassNameResponse);
|
||||
setError(new Error(errorMessage));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
getSurveys();
|
||||
|
||||
@@ -5,9 +5,10 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromPersonId } from "@formbricks/lib/organization/utils";
|
||||
import { deletePerson } from "@formbricks/lib/person/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
const ZPersonDeleteAction = z.object({
|
||||
personId: z.string(),
|
||||
personId: ZId,
|
||||
});
|
||||
|
||||
export const deletePersonAction = authenticatedActionClient
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { deleteSegment, getSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSegmentId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
|
||||
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
});
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
export const deleteBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZDeleteBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "delete"],
|
||||
});
|
||||
|
||||
const foundSegment = await getSegment(segmentId);
|
||||
return await deleteSegment(parsedInput.segmentId);
|
||||
});
|
||||
|
||||
if (!foundSegment) {
|
||||
throw new Error(`Segment with id ${segmentId} not found`);
|
||||
}
|
||||
const ZUpdateBasicSegmentAction = z.object({
|
||||
segmentId: ZId,
|
||||
data: ZSegmentUpdateInput,
|
||||
});
|
||||
|
||||
return await deleteSegment(segmentId);
|
||||
};
|
||||
export const updateBasicSegmentAction = authenticatedActionClient
|
||||
.schema(ZUpdateBasicSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
rules: ["segment", "update"],
|
||||
});
|
||||
|
||||
export const updateBasicSegmentAction = async (
|
||||
environmentId: string,
|
||||
segmentId: string,
|
||||
data: TSegmentUpdateInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const { filters } = parsedInput.data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const { filters } = data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await updateSegment(segmentId, data);
|
||||
};
|
||||
return await updateSegment(parsedInput.segmentId, parsedInput.data);
|
||||
});
|
||||
|
||||
@@ -70,11 +70,14 @@ export const BasicSegmentSettings = ({
|
||||
|
||||
try {
|
||||
setIsUpdatingSegment(true);
|
||||
await updateBasicSegmentAction(segment.environmentId, segment.id, {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
await updateBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
data: {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
},
|
||||
});
|
||||
|
||||
setIsUpdatingSegment(false);
|
||||
@@ -99,7 +102,7 @@ export const BasicSegmentSettings = ({
|
||||
const handleDeleteSegment = async () => {
|
||||
try {
|
||||
setIsDeletingSegment(true);
|
||||
await deleteBasicSegmentAction(segment.environmentId, segment.id);
|
||||
await deleteBasicSegmentAction({ segmentId: segment.id });
|
||||
|
||||
setIsDeletingSegment(false);
|
||||
toast.success("Segment deleted successfully!");
|
||||
|
||||
@@ -1,68 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import { Organization } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled)
|
||||
throw new OperationNotAllowedError(
|
||||
"Creating Multiple organization is restricted on your instance of Formbricks"
|
||||
);
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) throw new Error("User not found");
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrganizationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled)
|
||||
throw new OperationNotAllowedError(
|
||||
"Creating Multiple organization is restricted on your instance of Formbricks"
|
||||
);
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: organizationName,
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const product = await createProduct(newOrganization.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...ctx.user.notificationSettings,
|
||||
alert: {
|
||||
...ctx.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...ctx.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: Array.from(
|
||||
new Set([...(ctx.user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id])
|
||||
),
|
||||
};
|
||||
|
||||
await updateUser(ctx.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, session.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const product = await createProduct(newOrganization.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...user.notificationSettings,
|
||||
alert: {
|
||||
...user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: Array.from(
|
||||
new Set([...(user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id])
|
||||
),
|
||||
};
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
};
|
||||
|
||||
const ZCreateProductAction = z.object({
|
||||
organizationId: z.string(),
|
||||
organizationId: ZId,
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,76 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { canUserUpdateActionClass, verifyUserRoleAccess } from "@formbricks/lib/actionClass/auth";
|
||||
import { deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { z } from "zod";
|
||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromActionClassId } from "@formbricks/lib/organization/utils";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const deleteActionClassAction = async (environmentId, actionClassId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteActionClassAction = z.object({
|
||||
actionClassId: ZId,
|
||||
});
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
export const deleteActionClassAction = authenticatedActionClient
|
||||
.schema(ZDeleteActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromActionClassId(parsedInput.actionClassId),
|
||||
rules: ["actionClass", "delete"],
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
await deleteActionClass(parsedInput.actionClassId);
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const ZUpdateActionClassAction = z.object({
|
||||
actionClassId: ZId,
|
||||
updatedAction: ZActionClassInput,
|
||||
});
|
||||
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
export const updateActionClassAction = authenticatedActionClient
|
||||
.schema(ZUpdateActionClassAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const actionClass = await getActionClass(parsedInput.actionClassId);
|
||||
if (actionClass === null) {
|
||||
throw new ResourceNotFoundError("ActionClass", parsedInput.actionClassId);
|
||||
}
|
||||
|
||||
await deleteActionClass(environmentId, actionClassId);
|
||||
};
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromActionClassId(parsedInput.actionClassId),
|
||||
rules: ["actionClass", "update"],
|
||||
});
|
||||
|
||||
export const updateActionClassAction = async (
|
||||
environmentId: string,
|
||||
actionClassId: string,
|
||||
updatedAction: Partial<TActionClassInput>
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return await updateActionClass(
|
||||
actionClass.environmentId,
|
||||
parsedInput.actionClassId,
|
||||
parsedInput.updatedAction
|
||||
);
|
||||
});
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
const ZGetActiveInactiveSurveysAction = z.object({
|
||||
actionClassId: ZId,
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
export const getActiveInactiveSurveysAction = authenticatedActionClient
|
||||
.schema(ZGetActiveInactiveSurveysAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromActionClassId(parsedInput.actionClassId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateActionClass(environmentId, actionClassId, updatedAction);
|
||||
};
|
||||
|
||||
export const getActiveInactiveSurveysAction = async (
|
||||
actionClassId: string,
|
||||
environmentId: string
|
||||
): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserUpdateActionClass(session.user.id, actionClassId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const surveys = await getSurveysByActionClassId(actionClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name),
|
||||
};
|
||||
return response;
|
||||
};
|
||||
const surveys = await getSurveysByActionClassId(parsedInput.actionClassId);
|
||||
const response = {
|
||||
activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name),
|
||||
};
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
@@ -25,16 +26,18 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
|
||||
setLoading(true);
|
||||
|
||||
const updateState = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const activeInactiveSurveys = await getActiveInactiveSurveysAction(actionClass.id, environmentId);
|
||||
setActiveSurveys(activeInactiveSurveys.activeSurveys);
|
||||
setInactiveSurveys(activeInactiveSurveys.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const getActiveInactiveSurveysResponse = await getActiveInactiveSurveysAction({
|
||||
actionClassId: actionClass.id,
|
||||
});
|
||||
if (getActiveInactiveSurveysResponse?.data) {
|
||||
setActiveSurveys(getActiveInactiveSurveysResponse.data.activeSurveys);
|
||||
setInactiveSurveys(getActiveInactiveSurveysResponse.data.inactiveSurveys);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(getActiveInactiveSurveysResponse);
|
||||
setError(new Error(errorMessage));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
updateState();
|
||||
|
||||
@@ -31,7 +31,6 @@ export const ActionDetailModal = ({
|
||||
title: "Settings",
|
||||
children: (
|
||||
<ActionSettingsTab
|
||||
environmentId={environmentId}
|
||||
actionClass={actionClass}
|
||||
actionClasses={actionClasses}
|
||||
setOpen={setOpen}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { CodeActionForm } from "@formbricks/ui/organisms/CodeActionForm";
|
||||
import { NoCodeActionForm } from "@formbricks/ui/organisms/NoCodeActionForm";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
actionClass: TActionClass;
|
||||
actionClasses: TActionClass[];
|
||||
setOpen: (v: boolean) => void;
|
||||
@@ -31,7 +30,6 @@ interface ActionSettingsTabProps {
|
||||
}
|
||||
|
||||
export const ActionSettingsTab = ({
|
||||
environmentId,
|
||||
actionClass,
|
||||
actionClasses,
|
||||
setOpen,
|
||||
@@ -104,7 +102,10 @@ export const ActionSettingsTab = ({
|
||||
},
|
||||
}),
|
||||
};
|
||||
await updateActionClassAction(environmentId, actionClass.id, updatedData);
|
||||
await updateActionClassAction({
|
||||
actionClassId: actionClass.id,
|
||||
updatedAction: updatedData,
|
||||
});
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
toast.success("Action updated successfully");
|
||||
@@ -118,7 +119,7 @@ export const ActionSettingsTab = ({
|
||||
const handleDeleteAction = async () => {
|
||||
try {
|
||||
setIsDeletingAction(true);
|
||||
await deleteActionClassAction(environmentId, actionClass.id);
|
||||
await deleteActionClassAction({ actionClassId: actionClass.id });
|
||||
router.refresh();
|
||||
toast.success("Action deleted successfully");
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessIntegration } from "@formbricks/lib/integration/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZIntegrationInput } from "@formbricks/types/integration";
|
||||
|
||||
export const createOrUpdateIntegrationAction = async (
|
||||
environmentId: string,
|
||||
integrationData: TIntegrationInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authenticated");
|
||||
const ZCreateOrUpdateIntegrationAction = z.object({
|
||||
environmentId: ZId,
|
||||
integrationData: ZIntegrationInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createOrUpdateIntegrationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrUpdateIntegrationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["integration", "create"],
|
||||
});
|
||||
|
||||
return await createOrUpdateIntegration(environmentId, integrationData);
|
||||
};
|
||||
return await createOrUpdateIntegration(parsedInput.environmentId, parsedInput.integrationData);
|
||||
});
|
||||
|
||||
export const deleteIntegrationAction = async (integrationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteIntegrationAction = z.object({
|
||||
integrationId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessIntegration(session.user.id, integrationId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteIntegrationAction = authenticatedActionClient
|
||||
.schema(ZDeleteIntegrationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.integrationId),
|
||||
rules: ["integration", "delete"],
|
||||
});
|
||||
|
||||
return await deleteIntegration(integrationId);
|
||||
};
|
||||
return await deleteIntegration(parsedInput.integrationId);
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
|
||||
export const refreshTablesAction = async (environmentId: string) => {
|
||||
return await getAirtableTables(environmentId);
|
||||
};
|
||||
@@ -144,7 +144,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
const actionMessage = isEditMode ? "updated" : "added";
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, airtableIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: airtableIntegrationData });
|
||||
toast.success(`Integration ${actionMessage} successfully`);
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
@@ -176,7 +176,7 @@ export const AddIntegrationModal = ({
|
||||
const integrationData = structuredClone(airtableIntegrationData);
|
||||
integrationData.config.data.splice(index, 1);
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, integrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData });
|
||||
handleClose();
|
||||
router.refresh();
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(airtableIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: airtableIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export const AddIntegrationModal = ({
|
||||
// create action
|
||||
googleSheetIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction(environmentId, googleSheetIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -172,7 +172,7 @@ export const AddIntegrationModal = ({
|
||||
googleSheetIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, googleSheetIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(googleSheetIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: googleSheetIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const refreshDatabasesAction = async (environmentId: string) => {
|
||||
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");
|
||||
|
||||
return await getNotionDatabases(environmentId);
|
||||
};
|
||||
@@ -202,7 +202,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, notionIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -217,7 +217,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, notionIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(notionIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: notionIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { getSlackChannels } from "@formbricks/lib/slack/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
export const refreshChannelsAction = async (environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZRefreshChannelsAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const refreshChannelsAction = authenticatedActionClient
|
||||
.schema(ZRefreshChannelsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["integration", "update"],
|
||||
});
|
||||
|
||||
return await getSlackChannels(environmentId);
|
||||
};
|
||||
return await getSlackChannels(parsedInput.environmentId);
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ export const AddChannelMappingModal = ({
|
||||
// create action
|
||||
slackIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction(environmentId, slackIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -155,7 +155,7 @@ export const AddChannelMappingModal = ({
|
||||
slackIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, slackIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(slackIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: slackIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -40,8 +40,11 @@ export const SlackWrapper = ({
|
||||
>(null);
|
||||
|
||||
const refreshChannels = async () => {
|
||||
const latestSlackChannels = await refreshChannelsAction(environment.id);
|
||||
setSlackChannels(latestSlackChannels);
|
||||
const refreshChannelsResponse = await refreshChannelsAction({ environmentId: environment.id });
|
||||
|
||||
if (refreshChannelsResponse?.data) {
|
||||
setSlackChannels(refreshChannelsResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlackAuthorization = async () => {
|
||||
|
||||
@@ -1,58 +1,77 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessWebhook } from "@formbricks/lib/webhook/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromWebhookId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { testEndpoint } from "@formbricks/lib/webhook/utils";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/webhooks";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZWebhookInput } from "@formbricks/types/webhooks";
|
||||
|
||||
export const createWebhookAction = async (
|
||||
environmentId: string,
|
||||
webhookInput: TWebhookInput
|
||||
): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateWebhookAction = z.object({
|
||||
environmentId: ZId,
|
||||
webhookInput: ZWebhookInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const createWebhookAction = authenticatedActionClient
|
||||
.schema(ZCreateWebhookAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["webhook", "create"],
|
||||
});
|
||||
|
||||
return await createWebhook(environmentId, webhookInput);
|
||||
};
|
||||
return await createWebhook(parsedInput.environmentId, parsedInput.webhookInput);
|
||||
});
|
||||
|
||||
export const deleteWebhookAction = async (id: string): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteWebhookAction = z.object({
|
||||
id: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessWebhook(session.user.id, id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteWebhookAction = authenticatedActionClient
|
||||
.schema(ZDeleteWebhookAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromWebhookId(parsedInput.id),
|
||||
rules: ["webhook", "delete"],
|
||||
});
|
||||
|
||||
return await deleteWebhook(id);
|
||||
};
|
||||
return await deleteWebhook(parsedInput.id);
|
||||
});
|
||||
|
||||
export const updateWebhookAction = async (
|
||||
environmentId: string,
|
||||
webhookId: string,
|
||||
webhookInput: Partial<TWebhookInput>
|
||||
): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZUpdateWebhookAction = z.object({
|
||||
webhookId: ZId,
|
||||
webhookInput: ZWebhookInput,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessWebhook(session.user.id, webhookId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const updateWebhookAction = authenticatedActionClient
|
||||
.schema(ZUpdateWebhookAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromWebhookId(parsedInput.webhookId),
|
||||
rules: ["webhook", "update"],
|
||||
});
|
||||
|
||||
return await updateWebhook(environmentId, webhookId, webhookInput);
|
||||
};
|
||||
return await updateWebhook(parsedInput.webhookId, parsedInput.webhookInput);
|
||||
});
|
||||
|
||||
export const testEndpointAction = async (url: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZTestEndpointAction = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
const res = await testEndpoint(url);
|
||||
export const testEndpointAction = authenticatedActionClient
|
||||
.schema(ZTestEndpointAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const res = await testEndpoint(parsedInput.url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw res.error;
|
||||
}
|
||||
};
|
||||
if (!res.ok) {
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction(testEndpointInput);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
@@ -107,7 +107,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
|
||||
await createWebhookAction(environmentId, updatedData);
|
||||
await createWebhookAction({ environmentId, webhookInput: updatedData });
|
||||
router.refresh();
|
||||
setOpenWithStates(false);
|
||||
toast.success("Webhook added successfully.");
|
||||
|
||||
@@ -6,14 +6,13 @@ import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
|
||||
|
||||
interface WebhookModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
}
|
||||
|
||||
export const WebhookModal = ({ environmentId, open, setOpen, webhook, surveys }: WebhookModalProps) => {
|
||||
export const WebhookModal = ({ open, setOpen, webhook, surveys }: WebhookModalProps) => {
|
||||
const tabs = [
|
||||
{
|
||||
title: "Overview",
|
||||
@@ -21,14 +20,7 @@ export const WebhookModal = ({ environmentId, open, setOpen, webhook, surveys }:
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<WebhookSettingsTab
|
||||
environmentId={environmentId}
|
||||
webhook={webhook}
|
||||
surveys={surveys}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
children: <WebhookSettingsTab webhook={webhook} surveys={surveys} setOpen={setOpen} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -19,13 +19,12 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }: ActionSettingsTabProps) => {
|
||||
export const WebhookSettingsTab = ({ webhook, surveys, setOpen }: ActionSettingsTabProps) => {
|
||||
const router = useRouter();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
@@ -48,7 +47,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction(testEndpointInput);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
@@ -113,7 +112,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
setIsUpdatingWebhook(true);
|
||||
await updateWebhookAction(environmentId, webhook.id, updatedData);
|
||||
await updateWebhookAction({ webhookId: webhook.id, webhookInput: updatedData });
|
||||
toast.success("Webhook updated successfully.");
|
||||
router.refresh();
|
||||
setIsUpdatingWebhook(false);
|
||||
@@ -232,7 +231,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
onDelete={async () => {
|
||||
setOpen(false);
|
||||
try {
|
||||
await deleteWebhookAction(webhook.id);
|
||||
await deleteWebhookAction({ id: webhook.id });
|
||||
router.refresh();
|
||||
toast.success("Webhook deleted successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -67,7 +67,6 @@ export const WebhookTable = ({
|
||||
</div>
|
||||
)}
|
||||
<WebhookModal
|
||||
environmentId={environment.id}
|
||||
open={isWebhookDetailModalOpen}
|
||||
setOpen={setWebhookDetailModalOpen}
|
||||
webhook={activeWebhook}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
const ZUpdateProductAction = z.object({
|
||||
productId: ZId,
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
export const updateProductAction = authenticatedActionClient
|
||||
.schema(ZUpdateProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
schema: ZProductUpdateInput,
|
||||
data: parsedInput.data,
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "update"],
|
||||
});
|
||||
|
||||
return await updateProduct(parsedInput.productId, parsedInput.data);
|
||||
});
|
||||
@@ -1,28 +1,45 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { canUserAccessApiKey } from "@formbricks/lib/apiKey/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { createApiKey, deleteApiKey } from "@formbricks/lib/apiKey/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { TApiKeyCreateInput } from "@formbricks/types/api-keys";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import {
|
||||
getOrganizationIdFromApiKeyId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { ZApiKeyCreateInput } from "@formbricks/types/api-keys";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
export const deleteApiKeyAction = async (id: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteApiKeyAction = z.object({
|
||||
id: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessApiKey(session.user.id, id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteApiKeyAction = authenticatedActionClient
|
||||
.schema(ZDeleteApiKeyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromApiKeyId(parsedInput.id),
|
||||
rules: ["apiKey", "delete"],
|
||||
});
|
||||
|
||||
return await deleteApiKey(id);
|
||||
};
|
||||
export const createApiKeyAction = async (environmentId: string, apiKeyData: TApiKeyCreateInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return await deleteApiKey(parsedInput.id);
|
||||
});
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateApiKeyAction = z.object({
|
||||
environmentId: ZId,
|
||||
apiKeyData: ZApiKeyCreateInput,
|
||||
});
|
||||
|
||||
return await createApiKey(environmentId, apiKeyData);
|
||||
};
|
||||
export const createApiKeyAction = authenticatedActionClient
|
||||
.schema(ZCreateApiKeyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["apiKey", "create"],
|
||||
});
|
||||
|
||||
return await createApiKey(parsedInput.environmentId, parsedInput.apiKeyData);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FilesIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TApiKey } from "@formbricks/types/api-keys";
|
||||
@@ -35,7 +36,7 @@ export const EditAPIKeys = ({
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
try {
|
||||
await deleteApiKeyAction(activeKey.id);
|
||||
await deleteApiKeyAction({ id: activeKey.id });
|
||||
const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || [];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success("API Key deleted");
|
||||
@@ -47,16 +48,20 @@ export const EditAPIKeys = ({
|
||||
};
|
||||
|
||||
const handleAddAPIKey = async (data) => {
|
||||
try {
|
||||
const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label });
|
||||
const updatedApiKeys = [...apiKeysLocal!, apiKey];
|
||||
const createApiKeyResponse = await createApiKeyAction({
|
||||
environmentId: environmentTypeId,
|
||||
apiKeyData: { label: data.label },
|
||||
});
|
||||
if (createApiKeyResponse?.data) {
|
||||
const updatedApiKeys = [...apiKeysLocal!, createApiKeyResponse.data];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success("API key created");
|
||||
} catch (e) {
|
||||
toast.error("Unable to create API Key");
|
||||
} finally {
|
||||
setOpenAddAPIKeyModal(false);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createApiKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
setOpenAddAPIKeyModal(false);
|
||||
};
|
||||
|
||||
const ApiKeyDisplay = ({ apiKey }) => {
|
||||
|
||||
@@ -4,30 +4,11 @@ import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
const ZUpdateProductAction = z.object({
|
||||
productId: z.string(),
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
export const updateProductAction = authenticatedActionClient
|
||||
.schema(ZUpdateProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
schema: ZProductUpdateInput,
|
||||
data: parsedInput.data,
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "update"],
|
||||
});
|
||||
|
||||
return await updateProduct(parsedInput.productId, parsedInput.data);
|
||||
});
|
||||
import { deleteProduct, getProducts } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
const ZProductDeleteAction = z.object({
|
||||
productId: z.string(),
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const deleteProductAction = authenticatedActionClient
|
||||
|
||||
@@ -4,6 +4,7 @@ import { deleteProductAction } from "@/app/(app)/environments/[environmentId]/pr
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { truncate } from "@formbricks/lib/utils/strings";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -24,19 +25,17 @@ export const DeleteProductRender = ({
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const handleDeleteProduct = async () => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const deletedProductActionResult = await deleteProductAction({ productId: product.id });
|
||||
if (deletedProductActionResult?.data) {
|
||||
toast.success("Product deleted successfully.");
|
||||
router.push("/");
|
||||
}
|
||||
setIsDeleting(false);
|
||||
} catch (err) {
|
||||
setIsDeleting(false);
|
||||
toast.error("Could not delete product.");
|
||||
setIsDeleting(true);
|
||||
const deleteProductResponse = await deleteProductAction({ productId: product.id });
|
||||
if (deleteProductResponse?.data) {
|
||||
toast.success("Product deleted successfully.");
|
||||
router.push("/");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteProductResponse);
|
||||
toast.error(errorMessage);
|
||||
setIsDeleteDialogOpen(false);
|
||||
}
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type EditProductNameProps = {
|
||||
product: TProduct;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type EditWaitingTimeProps = {
|
||||
product: TProduct;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessProduct, verifyUserRoleAccess } from "@formbricks/lib/product/auth";
|
||||
import { getProduct, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
export const updateProductAction = async (productId: string, inputProduct: TProductUpdateInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const product = await getProduct(productId);
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateProduct(productId, inputProduct);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
interface EditFormbricksBrandingProps {
|
||||
type: "linkSurvey" | "inAppSurvey";
|
||||
@@ -34,7 +34,7 @@ export const EditFormbricksBranding = ({
|
||||
let inputProduct: Partial<TProductUpdateInput> = {
|
||||
[type === "linkSurvey" ? "linkSurveyBranding" : "inAppSurveyBranding"]: newBrandingState,
|
||||
};
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
await updateProductAction({ productId: product.id, data: inputProduct });
|
||||
toast.success(newBrandingState ? "Formbricks branding is shown." : "Formbricks branding is hidden.");
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
interface EditLogoProps {
|
||||
product: TProduct;
|
||||
@@ -61,7 +61,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
const updatedProduct: TProductUpdateInput = {
|
||||
logo: { url: logoUrl, bgColor: isBgColorEnabled ? logoBgColor : undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
await updateProductAction({ productId: product.id, data: updatedProduct });
|
||||
toast.success("Logo updated successfully");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update the logo");
|
||||
@@ -83,7 +83,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
const updatedProduct: TProductUpdateInput = {
|
||||
logo: { url: undefined, bgColor: undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
await updateProductAction({ productId: product.id, data: updatedProduct });
|
||||
toast.success("Logo removed successfully", { icon: "🗑️" });
|
||||
} catch (error) {
|
||||
toast.error("Failed to remove the logo");
|
||||
|
||||
@@ -11,7 +11,7 @@ import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@form
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
const placements = [
|
||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||
@@ -53,10 +53,13 @@ export const EditPlacementForm = ({ product }: EditPlacementProps) => {
|
||||
|
||||
const onSubmit: SubmitHandler<EditPlacementFormValues> = async (data) => {
|
||||
try {
|
||||
await updateProductAction(product.id, {
|
||||
placement: data.placement,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
placement: data.placement,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Placement updated successfully.");
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { COLOR_DEFAULTS, PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling, ZProductStyling } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
FormProvider,
|
||||
} from "@formbricks/ui/Form";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type ThemeStylingProps = {
|
||||
product: TProduct;
|
||||
@@ -111,8 +112,11 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
},
|
||||
};
|
||||
|
||||
await updateProductAction(product.id, {
|
||||
styling: { ...defaultStyling },
|
||||
await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
styling: { ...defaultStyling },
|
||||
},
|
||||
});
|
||||
|
||||
form.reset({ ...defaultStyling });
|
||||
@@ -122,15 +126,19 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
}, [form, product.id, router]);
|
||||
|
||||
const onSubmit: SubmitHandler<TProductStyling> = async (data) => {
|
||||
try {
|
||||
const updatedProduct = await updateProductAction(product.id, {
|
||||
const updatedProductResponse = await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
styling: data,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
form.reset({ ...updatedProduct.styling });
|
||||
if (updatedProductResponse?.data) {
|
||||
form.reset({ ...updatedProductResponse.data.styling });
|
||||
toast.success("Styling updated successfully.");
|
||||
} catch (err) {
|
||||
toast.error("Error updating styling.");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedProductResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,50 +1,64 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessTag, verifyUserRoleAccess } from "@formbricks/lib/tag/auth";
|
||||
import { deleteTag, getTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromTagId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
|
||||
export const deleteTagAction = async (tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteTagAction = z.object({
|
||||
tagId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessTag(session.user.id, tagId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const deleteTagAction = authenticatedActionClient
|
||||
.schema(ZDeleteTagAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.tagId),
|
||||
rules: ["tag", "delete"],
|
||||
});
|
||||
|
||||
const tag = await getTag(tagId);
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(tag!.environmentId, session.user!.id);
|
||||
if (!hasDeleteAccess) throw new AuthorizationError("Not authorized");
|
||||
return await deleteTag(parsedInput.tagId);
|
||||
});
|
||||
|
||||
return await deleteTag(tagId);
|
||||
};
|
||||
const ZUpdateTagNameAction = z.object({
|
||||
tagId: ZId,
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const updateTagNameAction = async (tagId: string, name: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const updateTagNameAction = authenticatedActionClient
|
||||
.schema(ZUpdateTagNameAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.tagId),
|
||||
rules: ["tag", "update"],
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessTag(session.user.id, tagId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
return await updateTagName(parsedInput.tagId, parsedInput.name);
|
||||
});
|
||||
|
||||
const tag = await getTag(tagId);
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(tag!.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZMergeTagsAction = z.object({
|
||||
originalTagId: ZId,
|
||||
newTagId: ZId,
|
||||
});
|
||||
|
||||
return await updateTagName(tagId, name);
|
||||
};
|
||||
export const mergeTagsAction = authenticatedActionClient
|
||||
.schema(ZMergeTagsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.originalTagId),
|
||||
rules: ["tag", "update"],
|
||||
});
|
||||
|
||||
export const mergeTagsAction = async (originalTagId: string, newTagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTagId(parsedInput.newTagId),
|
||||
rules: ["tag", "update"],
|
||||
});
|
||||
|
||||
const isAuthorizedForOld = await canUserAccessTag(session.user.id, originalTagId);
|
||||
const isAuthorizedForNew = await canUserAccessTag(session.user.id, newTagId);
|
||||
if (!isAuthorizedForOld || !isAuthorizedForNew) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const tag = await getTag(originalTagId);
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(tag!.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await mergeTags(originalTagId, newTagId);
|
||||
};
|
||||
return await mergeTags(parsedInput.originalTagId, parsedInput.newTagId);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AlertCircleIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TTag, TTagsCount } from "@formbricks/types/tags";
|
||||
@@ -45,16 +46,16 @@ const SingleTag: React.FC<{
|
||||
const [isMergingTags, setIsMergingTags] = useState(false);
|
||||
const [openDeleteTagDialog, setOpenDeleteTagDialog] = useState(false);
|
||||
|
||||
const confirmDeleteTag = () => {
|
||||
deleteTagAction(tagId)
|
||||
.then((response) => {
|
||||
toast.success(`${response?.name ?? "Tag"} tag deleted`);
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
});
|
||||
const confirmDeleteTag = async () => {
|
||||
const deleteTagResponse = await deleteTagAction({ tagId });
|
||||
if (deleteTagResponse?.data) {
|
||||
toast.success(`${deleteTagResponse?.data.name ?? "Tag"} tag deleted`);
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteTagResponse);
|
||||
toast.error(errorMessage ?? "Something went wrong");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -71,24 +72,25 @@ const SingleTag: React.FC<{
|
||||
)}
|
||||
defaultValue={tagName}
|
||||
onBlur={(e) => {
|
||||
updateTagNameAction(tagId, e.target.value.trim())
|
||||
.then(() => {
|
||||
updateTagNameAction({ tagId, name: e.target.value.trim() }).then((updateTagNameResponse) => {
|
||||
if (updateTagNameResponse?.data) {
|
||||
setUpdateTagError(false);
|
||||
toast.success("Tag updated");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.message.includes("Unique constraint failed on the fields")) {
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateTagNameResponse);
|
||||
if (errorMessage.includes("Unique constraint failed on the fields")) {
|
||||
toast.error("Tag already exists", {
|
||||
duration: 2000,
|
||||
icon: <AlertCircleIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
} else {
|
||||
toast.error(error?.message ?? "Something went wrong", {
|
||||
toast.error(errorMessage ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
setUpdateTagError(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,18 +115,17 @@ const SingleTag: React.FC<{
|
||||
}
|
||||
onSelect={(newTagId) => {
|
||||
setIsMergingTags(true);
|
||||
mergeTagsAction(tagId, newTagId)
|
||||
.then(() => {
|
||||
mergeTagsAction({ originalTagId: tagId, newTagId }).then((mergeTagsResponse) => {
|
||||
if (mergeTagsResponse?.data) {
|
||||
toast.success("Tags merged");
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(mergeTagsResponse);
|
||||
toast.error(errorMessage ?? "Something went wrong");
|
||||
}
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export const updateNotificationSettingsAction = async (notificationSettings: TUserNotificationSettings) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new AuthorizationError("Not authenticated");
|
||||
}
|
||||
const ZUpdateNotificationSettingsAction = z.object({
|
||||
notificationSettings: ZUserNotificationSettings,
|
||||
});
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings,
|
||||
export const updateNotificationSettingsAction = authenticatedActionClient
|
||||
.schema(ZUpdateNotificationSettingsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await updateUser(ctx.user.id, {
|
||||
notificationSettings: parsedInput.notificationSettings,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const NotificationSwitch = ({
|
||||
!updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId];
|
||||
}
|
||||
|
||||
await updateNotificationSettingsAction(updatedNotificationSettings);
|
||||
await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings });
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,105 +1,78 @@
|
||||
"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 { disableTwoFactorAuth, enableTwoFactorAuth, setupTwoFactorAuth } from "@formbricks/lib/auth/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteFile } from "@formbricks/lib/storage/service";
|
||||
import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserUpdateInput } from "@formbricks/types/user";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZUserUpdateInput } from "@formbricks/types/user";
|
||||
|
||||
export const updateUserAction = async (data: Partial<TUserUpdateInput>) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
export const updateUserAction = authenticatedActionClient
|
||||
.schema(ZUserUpdateInput.partial())
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await updateUser(ctx.user.id, parsedInput);
|
||||
});
|
||||
|
||||
return await updateUser(session.user.id, data);
|
||||
};
|
||||
const ZSetupTwoFactorAuthAction = z.object({
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export const setupTwoFactorAuthAction = async (password: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
export const setupTwoFactorAuthAction = authenticatedActionClient
|
||||
.schema(ZSetupTwoFactorAuthAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await setupTwoFactorAuth(ctx.user.id, parsedInput.password);
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
const ZEnableTwoFactorAuthAction = z.object({
|
||||
code: z.string(),
|
||||
});
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
export const enableTwoFactorAuthAction = authenticatedActionClient
|
||||
.schema(ZEnableTwoFactorAuthAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await enableTwoFactorAuth(ctx.user.id, parsedInput.code);
|
||||
});
|
||||
|
||||
return await setupTwoFactorAuth(session.user.id, password);
|
||||
};
|
||||
const ZDisableTwoFactorAuthAction = z.object({
|
||||
code: z.string(),
|
||||
password: z.string(),
|
||||
backupCode: z.string().optional(),
|
||||
});
|
||||
|
||||
export const enableTwoFactorAuthAction = async (code: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
export const disableTwoFactorAuthAction = authenticatedActionClient
|
||||
.schema(ZDisableTwoFactorAuthAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await disableTwoFactorAuth(ctx.user.id, parsedInput);
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
const ZUpdateAvatarAction = z.object({
|
||||
avatarUrl: z.string(),
|
||||
});
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
export const updateAvatarAction = authenticatedActionClient
|
||||
.schema(ZUpdateAvatarAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
return await updateUser(ctx.user.id, { imageUrl: parsedInput.avatarUrl });
|
||||
});
|
||||
|
||||
return await enableTwoFactorAuth(session.user.id, code);
|
||||
};
|
||||
const ZRemoveAvatarAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
type TDisableTwoFactorAuthParams = {
|
||||
code: string;
|
||||
password: string;
|
||||
backupCode?: string;
|
||||
};
|
||||
export const removeAvatarAction = authenticatedActionClient
|
||||
.schema(ZRemoveAvatarAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["environment", "read"],
|
||||
});
|
||||
|
||||
export const disableTwoFactorAuthAction = async (params: TDisableTwoFactorAuthParams) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return await disableTwoFactorAuth(session.user.id, params);
|
||||
};
|
||||
|
||||
export const updateAvatarAction = async (avatarUrl: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return await updateUser(session.user.id, { imageUrl: avatarUrl });
|
||||
};
|
||||
|
||||
export const removeAvatarAction = async (environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
if (!session.user.id) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isUserAuthorized) {
|
||||
throw new Error("Not Authorized");
|
||||
}
|
||||
|
||||
try {
|
||||
const imageUrl = user.imageUrl;
|
||||
const imageUrl = ctx.user.imageUrl;
|
||||
if (!imageUrl) {
|
||||
throw new Error("Image not found");
|
||||
}
|
||||
@@ -109,12 +82,9 @@ export const removeAvatarAction = async (environmentId: string) => {
|
||||
throw new Error("Invalid filename");
|
||||
}
|
||||
|
||||
const deletionResult = await deleteFile(environmentId, "public", fileName);
|
||||
const deletionResult = await deleteFile(parsedInput.environmentId, "public", fileName);
|
||||
if (!deletionResult.success) {
|
||||
throw new Error("Deletion failed");
|
||||
}
|
||||
return await updateUser(session.user.id, { imageUrl: null });
|
||||
} catch (error) {
|
||||
throw new Error(`${"Deletion failed"}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
return await updateUser(ctx.user.id, { imageUrl: null });
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
@@ -43,14 +44,16 @@ export const DisableTwoFactorModal = ({ open, setOpen }: TDisableTwoFactorModalP
|
||||
const onSubmit: SubmitHandler<TDisableTwoFactorFormState> = async (data) => {
|
||||
const { code, password, backupCode } = data;
|
||||
|
||||
try {
|
||||
const { message } = await disableTwoFactorAuthAction({ code, password, backupCode });
|
||||
toast.success(message);
|
||||
const disableTwoFactorAuthResponse = await disableTwoFactorAuthAction({ code, password, backupCode });
|
||||
|
||||
if (disableTwoFactorAuthResponse?.data) {
|
||||
toast.success(disableTwoFactorAuthResponse.data.message);
|
||||
|
||||
router.refresh();
|
||||
resetState();
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(disableTwoFactorAuthResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
try {
|
||||
if (imageUrl) {
|
||||
// If avatar image already exists, then remove it before update action
|
||||
await removeAvatarAction(environmentId);
|
||||
await removeAvatarAction({ environmentId });
|
||||
}
|
||||
const { url, error } = await handleFileUpload(file, environmentId);
|
||||
|
||||
@@ -70,7 +70,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAvatarAction(url);
|
||||
await updateAvatarAction({ avatarUrl: url });
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast.error("Avatar update failed. Please try again.");
|
||||
@@ -84,7 +84,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await removeAvatarAction(environmentId);
|
||||
await removeAvatarAction({ environmentId });
|
||||
} catch (err) {
|
||||
toast.error("Avatar update failed. Please try again.");
|
||||
} finally {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import { OTPInput } from "@formbricks/ui/OTPInput";
|
||||
@@ -46,16 +47,17 @@ const ConfirmPasswordForm = ({
|
||||
const { control, handleSubmit, setError } = useForm<TConfirmPasswordFormState>();
|
||||
|
||||
const onSubmit: SubmitHandler<TConfirmPasswordFormState> = async (data) => {
|
||||
try {
|
||||
const { backupCodes, dataUri, secret } = await setupTwoFactorAuthAction(data.password);
|
||||
const setupTwoFactorAuthResponse = await setupTwoFactorAuthAction({ password: data.password });
|
||||
|
||||
if (setupTwoFactorAuthResponse?.data) {
|
||||
const { backupCodes, dataUri, secret } = setupTwoFactorAuthResponse.data;
|
||||
setBackupCodes(backupCodes);
|
||||
setDataUri(dataUri);
|
||||
setSecret(secret);
|
||||
|
||||
setCurrentStep("scanQRCode");
|
||||
} catch (err) {
|
||||
setError("password", { message: err.message });
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(setupTwoFactorAuthResponse);
|
||||
setError("password", { message: errorMessage });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,12 +156,14 @@ const EnterCode = ({ setCurrentStep, setOpen, refreshData }: TEnableCodeProps) =
|
||||
|
||||
const onSubmit: SubmitHandler<TEnterCodeFormState> = async (data) => {
|
||||
try {
|
||||
const { message } = await enableTwoFactorAuthAction(data.code);
|
||||
toast.success(message);
|
||||
setCurrentStep("backupCodes");
|
||||
const enableTwoFactorAuthResponse = await enableTwoFactorAuthAction({ code: data.code });
|
||||
if (enableTwoFactorAuthResponse?.data) {
|
||||
toast.success(enableTwoFactorAuthResponse.data.message);
|
||||
setCurrentStep("backupCodes");
|
||||
|
||||
// refresh data to update the UI
|
||||
refreshData();
|
||||
// refresh data to update the UI
|
||||
refreshData();
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
}
|
||||
|
||||
@@ -1,84 +1,83 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { createCustomerPortalSession } from "@formbricks/ee/billing/lib/create-customer-portal-session";
|
||||
import { createSubscription } from "@formbricks/ee/billing/lib/create-subscription";
|
||||
import { isSubscriptionCancelled } from "@formbricks/ee/billing/lib/is-subscription-cancelled";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const upgradePlanAction = async (
|
||||
organizationId: string,
|
||||
environmentId: string,
|
||||
priceLookupKey: STRIPE_PRICE_LOOKUP_KEYS
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZUpgradePlanAction = z.object({
|
||||
organizationId: ZId,
|
||||
environmentId: ZId,
|
||||
priceLookupKey: z.nativeEnum(STRIPE_PRICE_LOOKUP_KEYS),
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessOrganization(session.user.id, organizationId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const upgradePlanAction = authenticatedActionClient
|
||||
.schema(ZUpgradePlanAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["subscription", "create"],
|
||||
});
|
||||
const organization = await getOrganization(parsedInput.organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", parsedInput.organizationId);
|
||||
}
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", organizationId);
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organizationId);
|
||||
|
||||
if (membership?.role === "owner" || membership?.role === "admin") {
|
||||
const subscriptionSession = await createSubscription(organizationId, environmentId, priceLookupKey);
|
||||
|
||||
return subscriptionSession;
|
||||
} else {
|
||||
throw new AuthorizationError("Only organization owner or admin can upgrade plan");
|
||||
}
|
||||
};
|
||||
|
||||
export const manageSubscriptionAction = async (organizationId: string, environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessOrganization(session.user.id, organizationId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", organizationId);
|
||||
}
|
||||
|
||||
if (!organization.billing.stripeCustomerId) {
|
||||
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organizationId);
|
||||
|
||||
if (membership?.role === "owner" || membership?.role === "admin") {
|
||||
const sessionUrl = await createCustomerPortalSession(
|
||||
organization.billing.stripeCustomerId,
|
||||
`${WEBAPP_URL}/environments/${environmentId}/settings/billing`
|
||||
return await createSubscription(
|
||||
parsedInput.organizationId,
|
||||
parsedInput.environmentId,
|
||||
parsedInput.priceLookupKey
|
||||
);
|
||||
return sessionUrl;
|
||||
} else {
|
||||
throw new AuthorizationError("Only organization owner or admin can upgrade plan");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const isSubscriptionCancelledAction = async (organizationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZManageSubscriptionAction = z.object({
|
||||
organizationId: ZId,
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessOrganization(session.user.id, organizationId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const manageSubscriptionAction = authenticatedActionClient
|
||||
.schema(ZManageSubscriptionAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["subscription", "read"],
|
||||
});
|
||||
const organization = await getOrganization(parsedInput.organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", parsedInput.organizationId);
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organizationId);
|
||||
if (membership?.role === "owner" || membership?.role === "admin") {
|
||||
return await isSubscriptionCancelled(organizationId);
|
||||
} else {
|
||||
throw new AuthorizationError("Only organization owner or admin can upgrade plan");
|
||||
}
|
||||
};
|
||||
if (!organization.billing.stripeCustomerId) {
|
||||
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
|
||||
}
|
||||
|
||||
return await createCustomerPortalSession(
|
||||
organization.billing.stripeCustomerId,
|
||||
`${WEBAPP_URL}/environments/${parsedInput.environmentId}/settings/billing`
|
||||
);
|
||||
});
|
||||
|
||||
const ZIsSubscriptionCancelledAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const isSubscriptionCancelledAction = authenticatedActionClient
|
||||
.schema(ZIsSubscriptionCancelledAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["subscription", "read"],
|
||||
});
|
||||
|
||||
return await isSubscriptionCancelled(parsedInput.organizationId);
|
||||
});
|
||||
|
||||
@@ -57,26 +57,39 @@ export const PricingTable = ({
|
||||
|
||||
useEffect(() => {
|
||||
const checkSubscriptionStatus = async () => {
|
||||
const isCancelled = await isSubscriptionCancelledAction(organization.id);
|
||||
if (isCancelled) {
|
||||
setCancellingOn(isCancelled.date);
|
||||
const isSubscriptionCancelledResponse = await isSubscriptionCancelledAction({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
if (isSubscriptionCancelledResponse?.data) {
|
||||
setCancellingOn(isSubscriptionCancelledResponse.data.date);
|
||||
}
|
||||
};
|
||||
checkSubscriptionStatus();
|
||||
}, [organization.id]);
|
||||
|
||||
const openCustomerPortal = async () => {
|
||||
const sessionUrl = await manageSubscriptionAction(organization.id, environmentId);
|
||||
router.push(sessionUrl);
|
||||
const manageSubscriptionResponse = await manageSubscriptionAction({
|
||||
organizationId: organization.id,
|
||||
environmentId,
|
||||
});
|
||||
if (manageSubscriptionResponse?.data) {
|
||||
router.push(manageSubscriptionResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const upgradePlan = async (priceLookupKey) => {
|
||||
try {
|
||||
const { status, newPlan, url } = await upgradePlanAction(
|
||||
organization.id,
|
||||
const upgradePlanResponse = await upgradePlanAction({
|
||||
organizationId: organization.id,
|
||||
environmentId,
|
||||
priceLookupKey
|
||||
);
|
||||
priceLookupKey,
|
||||
});
|
||||
|
||||
if (!upgradePlanResponse?.data) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
|
||||
const { status, newPlan, url } = upgradePlanResponse.data;
|
||||
|
||||
if (status != 200) {
|
||||
throw new Error("Something went wrong");
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { deleteInvite, getInvite, inviteUser, resendInvite } from "@formbricks/lib/invite/service";
|
||||
import { createInviteToken } from "@formbricks/lib/jwt";
|
||||
@@ -16,20 +13,15 @@ import {
|
||||
getMembershipByUserIdOrganizationId,
|
||||
getMembershipsByUserId,
|
||||
} from "@formbricks/lib/membership/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import {
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
OperationNotAllowedError,
|
||||
ValidationError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { getOrganizationIdFromInviteId } from "@formbricks/lib/organization/utils";
|
||||
import { ZId, ZUuid } from "@formbricks/types/common";
|
||||
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { ZMembershipRole } from "@formbricks/types/memberships";
|
||||
import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
||||
|
||||
const ZUpdateOrganizationNameAction = z.object({
|
||||
organizationId: z.string(),
|
||||
organizationId: ZId,
|
||||
data: ZOrganizationUpdateInput.pick({ name: true }),
|
||||
});
|
||||
|
||||
@@ -46,177 +38,189 @@ export const updateOrganizationNameAction = authenticatedActionClient
|
||||
return await updateOrganization(parsedInput.organizationId, parsedInput.data);
|
||||
});
|
||||
|
||||
export const deleteInviteAction = async (inviteId: string, organizationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ZDeleteInviteAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
return await deleteInvite(inviteId);
|
||||
};
|
||||
|
||||
export const deleteMembershipAction = async (userId: string, organizationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const { hasDeleteMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasDeleteMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
if (userId === session.user.id) {
|
||||
throw new AuthenticationError("You cannot delete yourself from the organization");
|
||||
}
|
||||
|
||||
return await deleteMembership(userId, organizationId);
|
||||
};
|
||||
|
||||
export const leaveOrganizationAction = async (organizationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organizationId);
|
||||
|
||||
if (!membership) {
|
||||
throw new AuthenticationError("Not a member of this organization");
|
||||
}
|
||||
|
||||
if (membership.role === "owner") {
|
||||
throw new ValidationError("You cannot leave a organization you own");
|
||||
}
|
||||
|
||||
const memberships = await getMembershipsByUserId(session.user.id);
|
||||
if (!memberships || memberships?.length <= 1) {
|
||||
throw new ValidationError("You cannot leave the only organization you are a member of");
|
||||
}
|
||||
|
||||
await deleteMembership(session.user.id, organizationId);
|
||||
};
|
||||
|
||||
export const createInviteTokenAction = async (inviteId: string) => {
|
||||
const invite = await getInvite(inviteId);
|
||||
if (!invite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
const inviteToken = createInviteToken(inviteId, invite.email, {
|
||||
expiresIn: "7d",
|
||||
export const deleteInviteAction = authenticatedActionClient
|
||||
.schema(ZDeleteInviteAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["invite", "delete"],
|
||||
});
|
||||
return await deleteInvite(parsedInput.inviteId);
|
||||
});
|
||||
|
||||
return { inviteToken: encodeURIComponent(inviteToken) };
|
||||
};
|
||||
const ZDeleteMembershipAction = z.object({
|
||||
userId: ZId,
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const resendInviteAction = async (inviteId: string, organizationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
export const deleteMembershipAction = authenticatedActionClient
|
||||
.schema(ZDeleteMembershipAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["membership", "delete"],
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
const invite = await getInvite(inviteId);
|
||||
|
||||
const updatedInvite = await resendInvite(inviteId);
|
||||
await sendInviteMemberEmail(
|
||||
inviteId,
|
||||
updatedInvite.email,
|
||||
invite?.creator.name ?? "",
|
||||
updatedInvite.name ?? ""
|
||||
);
|
||||
};
|
||||
|
||||
export const inviteUserAction = async (
|
||||
organizationId: string,
|
||||
email: string,
|
||||
name: string,
|
||||
role: TMembershipRole
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
|
||||
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId,
|
||||
invitee: {
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
},
|
||||
if (parsedInput.userId === ctx.user.id) {
|
||||
throw new AuthenticationError("You cannot delete yourself from the organization");
|
||||
}
|
||||
return await deleteMembership(parsedInput.userId, parsedInput.organizationId);
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(invite.id, email, user.name ?? "", name ?? "", false);
|
||||
}
|
||||
const ZLeaveOrganizationAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
return invite;
|
||||
};
|
||||
export const leaveOrganizationAction = authenticatedActionClient
|
||||
.schema(ZLeaveOrganizationAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["organization", "read"],
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = async (organizationId: string) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled");
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
const membership = await getMembershipByUserIdOrganizationId(ctx.user.id, parsedInput.organizationId);
|
||||
|
||||
const { hasDeleteAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!membership) {
|
||||
throw new AuthenticationError("Not a member of this organization");
|
||||
}
|
||||
|
||||
if (!hasDeleteAccess) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
if (membership.role === "owner") {
|
||||
throw new ValidationError("You cannot leave a organization you own");
|
||||
}
|
||||
|
||||
return await deleteOrganization(organizationId);
|
||||
};
|
||||
const memberships = await getMembershipsByUserId(ctx.user.id);
|
||||
if (!memberships || memberships?.length <= 1) {
|
||||
throw new ValidationError("You cannot leave the only organization you are a member of");
|
||||
}
|
||||
|
||||
return await deleteMembership(ctx.user.id, parsedInput.organizationId);
|
||||
});
|
||||
|
||||
const ZCreateInviteTokenAction = z.object({
|
||||
inviteId: z.string(),
|
||||
});
|
||||
|
||||
export const createInviteTokenAction = authenticatedActionClient
|
||||
.schema(ZCreateInviteTokenAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromInviteId(parsedInput.inviteId),
|
||||
rules: ["invite", "create"],
|
||||
});
|
||||
|
||||
const invite = await getInvite(parsedInput.inviteId);
|
||||
if (!invite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
const inviteToken = createInviteToken(parsedInput.inviteId, invite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
return { inviteToken: encodeURIComponent(inviteToken) };
|
||||
});
|
||||
|
||||
const ZResendInviteAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const resendInviteAction = authenticatedActionClient
|
||||
.schema(ZResendInviteAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["invite", "update"],
|
||||
});
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromInviteId(parsedInput.inviteId),
|
||||
rules: ["invite", "update"],
|
||||
});
|
||||
|
||||
const invite = await getInvite(parsedInput.inviteId);
|
||||
|
||||
const updatedInvite = await resendInvite(parsedInput.inviteId);
|
||||
await sendInviteMemberEmail(
|
||||
parsedInput.inviteId,
|
||||
updatedInvite.email,
|
||||
invite?.creator.name ?? "",
|
||||
updatedInvite.name ?? ""
|
||||
);
|
||||
});
|
||||
|
||||
const ZInviteUserAction = z.object({
|
||||
organizationId: ZId,
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
role: ZMembershipRole,
|
||||
});
|
||||
|
||||
export const inviteUserAction = authenticatedActionClient
|
||||
.schema(ZInviteUserAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["invite", "create"],
|
||||
});
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId: parsedInput.organizationId,
|
||||
invitee: {
|
||||
email: parsedInput.email,
|
||||
name: parsedInput.name,
|
||||
role: parsedInput.role,
|
||||
},
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
parsedInput.name ?? "",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return invite;
|
||||
});
|
||||
|
||||
const ZDeleteOrganizationAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = authenticatedActionClient
|
||||
.schema(ZDeleteOrganizationAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled");
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["organization", "delete"],
|
||||
});
|
||||
|
||||
return await deleteOrganization(parsedInput.organizationId);
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DeleteOrganization = ({
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
await deleteOrganizationAction(organization.id);
|
||||
await deleteOrganizationAction({ organizationId: organization.id });
|
||||
toast.success("Organization deleted successfully.");
|
||||
router.push("/");
|
||||
} catch (err) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SendHorizonalIcon, ShareIcon, TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { TInvite } from "@formbricks/types/invites";
|
||||
import { TMember } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
@@ -39,14 +40,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
if (!member && invite) {
|
||||
// This is an invite
|
||||
|
||||
await deleteInviteAction(invite?.id, organization.id);
|
||||
await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id });
|
||||
toast.success("Invite deleted successfully");
|
||||
}
|
||||
|
||||
if (member && !invite) {
|
||||
// This is a member
|
||||
|
||||
await deleteMembershipAction(member.userId, organization.id);
|
||||
await deleteMembershipAction({ userId: member.userId, organizationId: organization.id });
|
||||
toast.success("Member deleted successfully");
|
||||
}
|
||||
|
||||
@@ -74,9 +75,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
const handleShareInvite = async () => {
|
||||
try {
|
||||
if (!invite) return;
|
||||
const { inviteToken } = await createInviteTokenAction(invite.id);
|
||||
setShareInviteToken(inviteToken);
|
||||
setShowShareInviteModal(true);
|
||||
const createInviteTokenResponse = await createInviteTokenAction({ inviteId: invite.id });
|
||||
if (createInviteTokenResponse?.data) {
|
||||
setShareInviteToken(createInviteTokenResponse.data.inviteToken);
|
||||
setShowShareInviteModal(true);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createInviteTokenResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`Error: ${err.message}`);
|
||||
}
|
||||
@@ -86,7 +92,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
try {
|
||||
if (!invite) return;
|
||||
|
||||
await resendInviteAction(invite.id, organization.id);
|
||||
await resendInviteAction({ inviteId: invite.id, organizationId: organization.id });
|
||||
toast.success("Invitation sent once more.");
|
||||
} catch (err) {
|
||||
toast.error(`Error: ${err.message}`);
|
||||
|
||||
@@ -47,7 +47,7 @@ export const OrganizationActions = ({
|
||||
const handleLeaveOrganization = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await leaveOrganizationAction(organization.id);
|
||||
await leaveOrganizationAction({ organizationId: organization.id });
|
||||
toast.success("You left the organization successfully");
|
||||
router.refresh();
|
||||
setLoading(false);
|
||||
@@ -62,7 +62,7 @@ export const OrganizationActions = ({
|
||||
try {
|
||||
await Promise.all(
|
||||
data.map(async ({ name, email, role }) => {
|
||||
await inviteUserAction(organization.id, email, name, role);
|
||||
await inviteUserAction({ organizationId: organization.id, email, name, role });
|
||||
})
|
||||
);
|
||||
toast.success("Member invited successfully");
|
||||
|
||||
@@ -1,56 +1,72 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
|
||||
import { getResponseCountBySurveyId, getResponses, getSurveySummary } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
|
||||
export const revalidateSurveyIdPath = async (environmentId: string, surveyId: string) => {
|
||||
revalidatePath(`/environments/${environmentId}/surveys/${surveyId}`);
|
||||
};
|
||||
|
||||
export const getResponsesAction = async (
|
||||
surveyId: string,
|
||||
limit: number = 10,
|
||||
offset: number = 0,
|
||||
filterCriteria?: TResponseFilterCriteria
|
||||
): Promise<TResponse[]> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResponsesAction = z.object({
|
||||
surveyId: ZId,
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
filterCriteria: ZResponseFilterCriteria.optional(),
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const getResponsesAction = authenticatedActionClient
|
||||
.schema(ZGetResponsesAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "read"],
|
||||
});
|
||||
|
||||
const responses = await getResponses(surveyId, limit, offset, filterCriteria);
|
||||
return responses;
|
||||
};
|
||||
return getResponses(
|
||||
parsedInput.surveyId,
|
||||
parsedInput.limit,
|
||||
parsedInput.offset,
|
||||
parsedInput.filterCriteria
|
||||
);
|
||||
});
|
||||
|
||||
export const getSurveySummaryAction = async (
|
||||
surveyId: string,
|
||||
filterCriteria?: TResponseFilterCriteria
|
||||
): Promise<TSurveySummary> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetSurveySummaryAction = z.object({
|
||||
surveyId: ZId,
|
||||
filterCriteria: ZResponseFilterCriteria.optional(),
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const getSurveySummaryAction = authenticatedActionClient
|
||||
.schema(ZGetSurveySummaryAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "read"],
|
||||
});
|
||||
|
||||
return await getSurveySummary(surveyId, filterCriteria);
|
||||
};
|
||||
return getSurveySummary(parsedInput.surveyId, parsedInput.filterCriteria);
|
||||
});
|
||||
|
||||
export const getResponseCountAction = async (
|
||||
surveyId: string,
|
||||
filters?: TResponseFilterCriteria
|
||||
): Promise<number> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResponseCountAction = z.object({
|
||||
surveyId: ZId,
|
||||
filterCriteria: ZResponseFilterCriteria.optional(),
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const getResponseCountAction = authenticatedActionClient
|
||||
.schema(ZGetResponseCountAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "read"],
|
||||
});
|
||||
|
||||
return await getResponseCountBySurveyId(surveyId, filters);
|
||||
};
|
||||
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
|
||||
});
|
||||
|
||||
@@ -48,23 +48,29 @@ export const SurveyAnalysisNavigation = ({
|
||||
latestFiltersRef.current = filters;
|
||||
|
||||
const getResponseCount = () => {
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey);
|
||||
return getResponseCountAction(survey.id);
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction({ sharingKey });
|
||||
return getResponseCountAction({ surveyId: survey.id });
|
||||
};
|
||||
|
||||
const fetchResponseCount = async () => {
|
||||
const count = await getResponseCount();
|
||||
setTotalResponseCount(count);
|
||||
const responseCount = count?.data ?? 0;
|
||||
setTotalResponseCount(responseCount);
|
||||
};
|
||||
|
||||
const getFilteredResponseCount = () => {
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey, latestFiltersRef.current);
|
||||
return getResponseCountAction(survey.id, latestFiltersRef.current);
|
||||
if (isSharingPage)
|
||||
return getResponseCountBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
return getResponseCountAction({ surveyId: survey.id, filterCriteria: latestFiltersRef.current });
|
||||
};
|
||||
|
||||
const fetchFilteredResponseCount = async () => {
|
||||
const count = await getFilteredResponseCount();
|
||||
setFilteredResponseCount(count);
|
||||
const responseCount = count?.data ?? 0;
|
||||
setFilteredResponseCount(responseCount);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { canUserAccessTagOnResponse } from "@formbricks/lib/tagOnResponse/auth";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const createTagAction = async (environmentId: string, tagName: string) => {
|
||||
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");
|
||||
|
||||
return await createTag(environmentId, tagName);
|
||||
};
|
||||
|
||||
export const createTagToResponeAction = async (responseId: string, tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await addTagToRespone(responseId, tagId);
|
||||
};
|
||||
|
||||
export const deleteTagOnResponseAction = async (responseId: string, tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteTagOnResponse(responseId, tagId);
|
||||
};
|
||||
@@ -69,19 +69,21 @@ export const ResponsePage = ({
|
||||
let newResponses: TResponse[] = [];
|
||||
|
||||
if (isSharingPage) {
|
||||
newResponses = await getResponsesBySurveySharingKeyAction(
|
||||
sharingKey,
|
||||
responsesPerPage,
|
||||
(newPage - 1) * responsesPerPage,
|
||||
filters
|
||||
);
|
||||
const getResponsesActionResponse = await getResponsesBySurveySharingKeyAction({
|
||||
sharingKey: sharingKey,
|
||||
limit: responsesPerPage,
|
||||
offset: (newPage - 1) * responsesPerPage,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
newResponses = getResponsesActionResponse?.data || [];
|
||||
} else {
|
||||
newResponses = await getResponsesAction(
|
||||
const getResponsesActionResponse = await getResponsesAction({
|
||||
surveyId,
|
||||
responsesPerPage,
|
||||
(newPage - 1) * responsesPerPage,
|
||||
filters
|
||||
);
|
||||
limit: responsesPerPage,
|
||||
offset: (newPage - 1) * responsesPerPage,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
newResponses = getResponsesActionResponse?.data || [];
|
||||
}
|
||||
|
||||
if (newResponses.length === 0 || newResponses.length < responsesPerPage) {
|
||||
@@ -113,9 +115,17 @@ export const ResponsePage = ({
|
||||
let responseCount = 0;
|
||||
|
||||
if (isSharingPage) {
|
||||
responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
|
||||
const responseCountActionResponse = await getResponseCountBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
responseCount = responseCountActionResponse?.data || 0;
|
||||
} else {
|
||||
responseCount = await getResponseCountAction(surveyId, filters);
|
||||
const responseCountActionResponse = await getResponseCountAction({
|
||||
surveyId,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
responseCount = responseCountActionResponse?.data || 0;
|
||||
}
|
||||
|
||||
setResponseCount(responseCount);
|
||||
@@ -131,9 +141,23 @@ export const ResponsePage = ({
|
||||
let responses: TResponse[] = [];
|
||||
|
||||
if (isSharingPage) {
|
||||
responses = await getResponsesBySurveySharingKeyAction(sharingKey, responsesPerPage, 0, filters);
|
||||
const getResponsesActionResponse = await getResponsesBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
limit: responsesPerPage,
|
||||
offset: 0,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
|
||||
responses = getResponsesActionResponse?.data || [];
|
||||
} else {
|
||||
responses = await getResponsesAction(surveyId, responsesPerPage, 0, filters);
|
||||
const getResponsesActionResponse = await getResponsesAction({
|
||||
surveyId,
|
||||
limit: responsesPerPage,
|
||||
offset: 0,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
|
||||
responses = getResponsesActionResponse?.data || [];
|
||||
}
|
||||
|
||||
if (responses.length < responsesPerPage) {
|
||||
|
||||
@@ -2,106 +2,141 @@
|
||||
|
||||
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@formbricks/email";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const sendEmbedSurveyPreviewEmailAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
.schema(ZSendEmbedSurveyPreviewEmailAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
const isUserAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isUserAuthorized) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
const rawEmailHtml = await getEmailTemplateHtml(surveyId);
|
||||
const emailHtml = rawEmailHtml
|
||||
.replaceAll("?preview=true&", "?")
|
||||
.replaceAll("?preview=true&;", "?")
|
||||
.replaceAll("?preview=true", "");
|
||||
const rawEmailHtml = await getEmailTemplateHtml(parsedInput.surveyId);
|
||||
const emailHtml = rawEmailHtml
|
||||
.replaceAll("?preview=true&", "?")
|
||||
.replaceAll("?preview=true&;", "?")
|
||||
.replaceAll("?preview=true", "");
|
||||
|
||||
return await sendEmbedSurveyPreviewEmail(
|
||||
user.email,
|
||||
"Formbricks Email Survey Preview",
|
||||
emailHtml,
|
||||
survey.environmentId
|
||||
);
|
||||
};
|
||||
return await sendEmbedSurveyPreviewEmail(
|
||||
ctx.user.email,
|
||||
"Formbricks Email Survey Preview",
|
||||
emailHtml,
|
||||
survey.environmentId
|
||||
);
|
||||
});
|
||||
|
||||
export const generateResultShareUrlAction = async (surveyId: string): Promise<string> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGenerateResultShareUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
export const generateResultShareUrlAction = authenticatedActionClient
|
||||
.schema(ZGenerateResultShareUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "update"],
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey?.id) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
const resultShareKey = customAlphabet(
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
20
|
||||
)();
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
await updateSurvey({ ...survey, resultShareKey });
|
||||
const resultShareKey = customAlphabet(
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
20
|
||||
)();
|
||||
|
||||
return resultShareKey;
|
||||
};
|
||||
await updateSurvey({ ...survey, resultShareKey });
|
||||
|
||||
export const getResultShareUrlAction = async (surveyId: string): Promise<string | null> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return resultShareKey;
|
||||
});
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResultShareUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey?.id) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
export const getResultShareUrlAction = authenticatedActionClient
|
||||
.schema(ZGetResultShareUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "read"],
|
||||
});
|
||||
|
||||
return survey.resultShareKey;
|
||||
};
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
export const deleteResultShareUrlAction = async (surveyId: string): Promise<void> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return survey.resultShareKey;
|
||||
});
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteResultShareUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey?.id) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
export const deleteResultShareUrlAction = authenticatedActionClient
|
||||
.schema(ZDeleteResultShareUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "update"],
|
||||
});
|
||||
|
||||
await updateSurvey({ ...survey, resultShareKey: null });
|
||||
};
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
export const getEmailHtmlAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
return await updateSurvey({ ...survey, resultShareKey: null });
|
||||
});
|
||||
|
||||
return await getEmailTemplateHtml(surveyId);
|
||||
};
|
||||
const ZGetEmailHtmlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const getEmailHtmlAction = authenticatedActionClient
|
||||
.schema(ZGetEmailHtmlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
return await getEmailTemplateHtml(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
@@ -79,22 +79,40 @@ export const SummaryPage = ({
|
||||
latestFiltersRef.current = filters;
|
||||
|
||||
const getResponseCount = () => {
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey, latestFiltersRef.current);
|
||||
return getResponseCountAction(surveyId, latestFiltersRef.current);
|
||||
if (isSharingPage)
|
||||
return getResponseCountBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
return getResponseCountAction({
|
||||
surveyId,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
const getSummary = () => {
|
||||
if (isSharingPage) return getSummaryBySurveySharingKeyAction(sharingKey, latestFiltersRef.current);
|
||||
return getSurveySummaryAction(surveyId, latestFiltersRef.current);
|
||||
if (isSharingPage)
|
||||
return getSummaryBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
|
||||
return getSurveySummaryAction({
|
||||
surveyId,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
const handleInitialData = async () => {
|
||||
try {
|
||||
const updatedResponseCount = await getResponseCount();
|
||||
const updatedResponseCountData = await getResponseCount();
|
||||
const updatedSurveySummary = await getSummary();
|
||||
|
||||
setResponseCount(updatedResponseCount);
|
||||
setSurveySummary(updatedSurveySummary);
|
||||
const responseCount = updatedResponseCountData?.data ?? 0;
|
||||
const surveySummary = updatedSurveySummary?.data ?? initialSurveySummary;
|
||||
|
||||
setResponseCount(responseCount);
|
||||
setSurveySummary(surveySummary);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const emailHtml = await getEmailHtmlAction(surveyId);
|
||||
setEmailHtmlPreview(emailHtml);
|
||||
const emailHtml = await getEmailHtmlAction({ surveyId });
|
||||
setEmailHtmlPreview(emailHtml?.data || "");
|
||||
};
|
||||
|
||||
getData();
|
||||
@@ -37,7 +37,7 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
|
||||
|
||||
const sendPreviewEmail = async () => {
|
||||
try {
|
||||
await sendEmbedSurveyPreviewEmailAction(surveyId);
|
||||
await sendEmbedSurveyPreviewEmailAction({ surveyId });
|
||||
toast.success("Email sent!");
|
||||
} catch (err) {
|
||||
if (err instanceof AuthenticationError) {
|
||||
|
||||
@@ -1,53 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
|
||||
import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { ZSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const getResponsesDownloadUrlAction = async (
|
||||
surveyId: string,
|
||||
format: "csv" | "xlsx",
|
||||
filterCritera: TResponseFilterCriteria
|
||||
): Promise<string> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResponsesDownloadUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
format: z.union([z.literal("csv"), z.literal("xlsx")]),
|
||||
filterCriteria: ZResponseFilterCriteria,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const getResponsesDownloadUrlAction = authenticatedActionClient
|
||||
.schema(ZGetResponsesDownloadUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "read"],
|
||||
});
|
||||
|
||||
return getResponseDownloadUrl(surveyId, format, filterCritera);
|
||||
};
|
||||
return getResponseDownloadUrl(parsedInput.surveyId, parsedInput.format, parsedInput.filterCriteria);
|
||||
});
|
||||
|
||||
export const getSurveyFilterDataAction = async (surveyId: string, environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGetSurveyFilterDataAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
export const getSurveyFilterDataAction = authenticatedActionClient
|
||||
.schema(ZGetSurveyFilterDataAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
|
||||
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
|
||||
getTagsByEnvironmentId(environmentId),
|
||||
getResponseFilteringValues(surveyId),
|
||||
]);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
return { environmentTags: tags, attributes, meta, hiddenFields };
|
||||
};
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
export const updateSurveyAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
|
||||
getTagsByEnvironmentId(survey.environmentId),
|
||||
getResponseFilteringValues(parsedInput.surveyId),
|
||||
]);
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, survey.id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
return { environmentTags: tags, attributes, meta, hiddenFields };
|
||||
});
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(survey.environmentId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZUpdateSurveyAction = z.object({
|
||||
survey: ZSurvey,
|
||||
});
|
||||
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
export const updateSurveyAction = authenticatedActionClient
|
||||
.schema(ZUpdateSurveyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.survey.id),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
return await updateSurvey(parsedInput.survey);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ArrowDownToLineIcon, ChevronDown, ChevronUp, DownloadIcon } from "lucid
|
||||
import { useParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Calendar } from "@formbricks/ui/Calendar";
|
||||
@@ -172,13 +173,20 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
const handleDowndloadResponses = async (filter: FilterDownload, filetype: "csv" | "xlsx") => {
|
||||
try {
|
||||
const responseFilters = filter === FilterDownload.ALL ? {} : filters;
|
||||
const fileUrl = await getResponsesDownloadUrlAction(survey.id, filetype, responseFilters);
|
||||
if (fileUrl) {
|
||||
const responsesDownloadUrlResponse = await getResponsesDownloadUrlAction({
|
||||
surveyId: survey.id,
|
||||
format: filetype,
|
||||
filterCriteria: responseFilters,
|
||||
});
|
||||
if (responsesDownloadUrlResponse?.data) {
|
||||
const link = document.createElement("a");
|
||||
link.href = fileUrl;
|
||||
link.href = responsesDownloadUrlResponse.data;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(responsesDownloadUrlResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Error downloading responses");
|
||||
|
||||
@@ -42,10 +42,16 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
// Fetch the initial data for the filter and load it into the state
|
||||
const handleInitialData = async () => {
|
||||
if (isOpen) {
|
||||
const { attributes, meta, environmentTags, hiddenFields } = isSharingPage
|
||||
? await getSurveyFilterDataBySurveySharingKeyAction(sharingKey, survey.environmentId)
|
||||
: await getSurveyFilterDataAction(survey.id, survey.environmentId);
|
||||
const surveyFilterData = isSharingPage
|
||||
? await getSurveyFilterDataBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
environmentId: survey.environmentId,
|
||||
})
|
||||
: await getSurveyFilterDataAction({ surveyId: survey.id });
|
||||
|
||||
if (!surveyFilterData?.data) return;
|
||||
|
||||
const { attributes, meta, environmentTags, hiddenFields } = surveyFilterData.data;
|
||||
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
|
||||
survey,
|
||||
environmentTags,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { CopyIcon, DownloadIcon, GlobeIcon, LinkIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -29,27 +30,33 @@ export const ResultsShareButton = ({ survey, webAppUrl }: ResultsShareButtonProp
|
||||
const [surveyUrl, setSurveyUrl] = useState("");
|
||||
|
||||
const handlePublish = async () => {
|
||||
const key = await generateResultShareUrlAction(survey.id);
|
||||
setSurveyUrl(webAppUrl + "/share/" + key);
|
||||
setShowPublishModal(true);
|
||||
const resultShareKeyResponse = await generateResultShareUrlAction({ surveyId: survey.id });
|
||||
if (resultShareKeyResponse?.data) {
|
||||
setSurveyUrl(webAppUrl + "/share/" + resultShareKeyResponse.data);
|
||||
setShowPublishModal(true);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(resultShareKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnpublish = () => {
|
||||
deleteResultShareUrlAction(survey.id)
|
||||
.then(() => {
|
||||
deleteResultShareUrlAction({ surveyId: survey.id }).then((deleteResultShareUrlResponse) => {
|
||||
if (deleteResultShareUrlResponse?.data) {
|
||||
toast.success("Results unpublished successfully.");
|
||||
setShowPublishModal(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteResultShareUrlResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSharingKey = async () => {
|
||||
const sharingKey = await getResultShareUrlAction(survey.id);
|
||||
if (sharingKey) {
|
||||
setSurveyUrl(webAppUrl + "/share/" + sharingKey);
|
||||
const resultShareUrlResponse = await getResultShareUrlAction({ surveyId: survey.id });
|
||||
if (resultShareUrlResponse?.data) {
|
||||
setSurveyUrl(webAppUrl + "/share/" + resultShareUrlResponse.data);
|
||||
setShowPublishModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SurveyStatusDropdown = ({
|
||||
disabled={isStatusChangeDisabled}
|
||||
onValueChange={(value) => {
|
||||
const castedValue = value as TSurvey["status"];
|
||||
updateSurveyAction({ ...survey, status: castedValue })
|
||||
updateSurveyAction({ survey: { ...survey, status: castedValue } })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
value === "inProgress"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { convertResponseValue } from "@formbricks/lib/responses";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TWeeklyEmailResponseData,
|
||||
TWeeklySummaryEnvironmentData,
|
||||
TWeeklySummaryNotificationDataSurvey,
|
||||
TWeeklySummaryNotificationResponse,
|
||||
@@ -23,7 +25,11 @@ export const getNotificationResponse = (
|
||||
const surveys: TWeeklySummaryNotificationDataSurvey[] = [];
|
||||
// iterate through the surveys and calculate the overall insights
|
||||
for (const survey of environment.surveys) {
|
||||
const parsedSurvey = replaceHeadlineRecall(survey, "default", environment.attributeClasses);
|
||||
const parsedSurvey = replaceHeadlineRecall(
|
||||
survey as unknown as TSurvey,
|
||||
"default",
|
||||
environment.attributeClasses
|
||||
) as TSurvey & { responses: TWeeklyEmailResponseData[] };
|
||||
const surveyData: TWeeklySummaryNotificationDataSurvey = {
|
||||
id: parsedSurvey.id,
|
||||
name: parsedSurvey.name,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { capturePosthogEnvironmentEvent } from "@formbricks/lib/posthogServer";
|
||||
import { createResponse } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export const DELETE = async (
|
||||
if (actionClass.type === "automatic") {
|
||||
return responses.badRequestResponse("Automatic action classes cannot be deleted");
|
||||
}
|
||||
const deletedActionClass = await deleteActionClass(authentication.environmentId, params.actionClassId);
|
||||
const deletedActionClass = await deleteActionClass(params.actionClassId);
|
||||
return responses.successResponse(deletedActionClass);
|
||||
} catch (error) {
|
||||
return handleErrorResponse(error);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { NextRequest } from "next/server";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { generateSurveySingleUseIds } from "@formbricks/lib/utils/singleUseSurveys";
|
||||
|
||||
export const GET = async (
|
||||
request: NextRequest,
|
||||
{ params }: { params: { surveyId: string } }
|
||||
): Promise<Response> => {
|
||||
try {
|
||||
const authentication = await authenticateRequest(request);
|
||||
if (!authentication) return responses.notAuthenticatedResponse();
|
||||
const survey = await getSurvey(params.surveyId);
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", params.surveyId);
|
||||
}
|
||||
if (survey.environmentId !== authentication.environmentId) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
if (!survey.singleUse || !survey.singleUse.enabled) {
|
||||
return responses.badRequestResponse("Single use links are not enabled for this survey");
|
||||
}
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const limit = searchParams.get("limit") ? Number(searchParams.get("limit")) : 10;
|
||||
|
||||
if (limit < 1) {
|
||||
return responses.badRequestResponse("Limit cannot be less than 1");
|
||||
}
|
||||
|
||||
if (limit > 5000) {
|
||||
return responses.badRequestResponse("Limit cannot be more than 5000");
|
||||
}
|
||||
|
||||
const singleUseIds = generateSurveySingleUseIds(limit, survey.singleUse.isEncrypted);
|
||||
|
||||
// map single use ids to survey links
|
||||
const surveyLinks = singleUseIds.map(
|
||||
(singleUseId) => `${process.env.WEBAPP_URL}/s/${survey.id}?suId=${singleUseId}`
|
||||
);
|
||||
|
||||
return responses.successResponse(surveyLinks);
|
||||
} catch (error) {
|
||||
return handleErrorResponse(error);
|
||||
}
|
||||
};
|
||||
@@ -1,39 +1,44 @@
|
||||
"use server";
|
||||
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { LinkSurveyEmailData, sendLinkSurveyToVerifiedEmail } from "@formbricks/email";
|
||||
import { z } from "zod";
|
||||
import { sendLinkSurveyToVerifiedEmail } from "@formbricks/email";
|
||||
import { actionClient } from "@formbricks/lib/actionClient";
|
||||
import { verifyTokenForLinkSurvey } from "@formbricks/lib/jwt";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
|
||||
interface TSurveyPinValidationResponse {
|
||||
error?: TSurveyPinValidationResponseError;
|
||||
survey?: TSurvey;
|
||||
}
|
||||
export const sendLinkSurveyEmailAction = actionClient
|
||||
.schema(ZLinkSurveyEmailData)
|
||||
.action(async ({ parsedInput }) => {
|
||||
return await sendLinkSurveyToVerifiedEmail(parsedInput);
|
||||
});
|
||||
|
||||
export const sendLinkSurveyEmailAction = async (data: LinkSurveyEmailData) => {
|
||||
return await sendLinkSurveyToVerifiedEmail(data);
|
||||
};
|
||||
export const verifyTokenAction = async (token: string, surveyId: string): Promise<boolean> => {
|
||||
return await verifyTokenForLinkSurvey(token, surveyId);
|
||||
};
|
||||
const ZVerifyTokenAction = z.object({
|
||||
surveyId: ZId,
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export const validateSurveyPinAction = async (
|
||||
surveyId: string,
|
||||
pin: string
|
||||
): Promise<TSurveyPinValidationResponse> => {
|
||||
try {
|
||||
const survey = await getSurvey(surveyId);
|
||||
export const verifyTokenAction = actionClient.schema(ZVerifyTokenAction).action(async ({ parsedInput }) => {
|
||||
return await verifyTokenForLinkSurvey(parsedInput.token, parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZValidateSurveyPinAction = z.object({
|
||||
surveyId: ZId,
|
||||
pin: z.string(),
|
||||
});
|
||||
|
||||
export const validateSurveyPinAction = actionClient
|
||||
.schema(ZValidateSurveyPinAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) return { error: TSurveyPinValidationResponseError.NOT_FOUND };
|
||||
|
||||
const originalPin = survey.pin?.toString();
|
||||
|
||||
if (!originalPin) return { survey };
|
||||
|
||||
if (originalPin !== pin) return { error: TSurveyPinValidationResponseError.INCORRECT_PIN };
|
||||
if (originalPin !== parsedInput.pin) return { error: TSurveyPinValidationResponseError.INCORRECT_PIN };
|
||||
|
||||
return { survey };
|
||||
} catch (error) {
|
||||
return { error: TSurveyPinValidationResponseError.INTERNAL_SERVER_ERROR };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
|
||||
import { LinkSurvey } from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -53,12 +54,15 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
const [survey, setSurvey] = useState<TSurvey>();
|
||||
|
||||
const _validateSurveyPinAsync = useCallback(async (surveyId: string, pin: string) => {
|
||||
const response = await validateSurveyPinAction(surveyId, pin);
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else if (response.survey) {
|
||||
setSurvey(response.survey);
|
||||
const response = await validateSurveyPinAction({ surveyId, pin });
|
||||
|
||||
if (response?.data) {
|
||||
setSurvey(response.data.survey);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(response) as TSurveyPinValidationResponseError;
|
||||
setError(errorMessage);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export const VerifyEmail = ({
|
||||
}
|
||||
const data = {
|
||||
surveyId: survey.id,
|
||||
email: email,
|
||||
email: email as string,
|
||||
surveyName: survey.name,
|
||||
suId: singleUseId ?? "",
|
||||
};
|
||||
|
||||
@@ -104,9 +104,12 @@ export const transformAnswer = (
|
||||
): string | number | string[] => {
|
||||
switch (question.type) {
|
||||
case TSurveyQuestionTypeEnum.OpenText:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle: {
|
||||
return answer;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Consent:
|
||||
case TSurveyQuestionTypeEnum.CTA: {
|
||||
if (answer === "dismissed") return "";
|
||||
return answer;
|
||||
}
|
||||
|
||||
@@ -143,6 +146,6 @@ export const transformAnswer = (
|
||||
}
|
||||
|
||||
default:
|
||||
return "dismissed";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service"
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseBySingleUseId, getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { getEmailVerificationDetails } from "./lib/helpers";
|
||||
|
||||
|
||||
@@ -1,56 +1,53 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { inviteUser } from "@formbricks/lib/invite/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
|
||||
export const inviteOrganizationMemberAction = async (email: string, organizationId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const ZInviteOrganizationMemberAction = z.object({
|
||||
email: z.string(),
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
.schema(ZInviteOrganizationMemberAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
rules: ["membership", "create"],
|
||||
});
|
||||
|
||||
const organizations = await getOrganizationsByUserId(session.user.id);
|
||||
const organizations = await getOrganizationsByUserId(ctx.user.id);
|
||||
|
||||
if (INVITE_DISABLED) {
|
||||
throw new AuthenticationError("Invite disabled");
|
||||
}
|
||||
const invite = await inviteUser({
|
||||
organizationId: organizations[0].id,
|
||||
invitee: {
|
||||
email: parsedInput.email,
|
||||
name: "",
|
||||
role: "admin",
|
||||
},
|
||||
});
|
||||
|
||||
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateMembersAccess) {
|
||||
throw new AuthenticationError("Not authorized");
|
||||
}
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
"",
|
||||
false // is onboarding invite
|
||||
);
|
||||
}
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId: organizations[0].id,
|
||||
invitee: {
|
||||
email,
|
||||
name: "",
|
||||
role: "admin",
|
||||
},
|
||||
return invite;
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
email,
|
||||
user.name ?? "",
|
||||
"",
|
||||
false // is onboarding invite
|
||||
);
|
||||
}
|
||||
|
||||
return invite;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
for (const email of emails) {
|
||||
try {
|
||||
if (!email) continue;
|
||||
await inviteOrganizationMemberAction(email, organizationId);
|
||||
await inviteOrganizationMemberAction({ email, organizationId });
|
||||
if (IS_SMTP_CONFIGURED) {
|
||||
toast.success(`Invitation sent to ${email}!`);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
"use server";
|
||||
|
||||
import { Organization } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { gethasNoOrganizations } from "@formbricks/lib/instance/service";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
|
||||
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrganizationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: organizationName,
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, session.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
};
|
||||
|
||||
@@ -33,8 +33,10 @@ export const CreateOrganization = () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const organizationName = data.name.trim();
|
||||
const organization = await createOrganizationAction(organizationName);
|
||||
router.push(`/setup/organization/${organization.id}/invite`);
|
||||
const createOrganizationResponse = await createOrganizationAction({ organizationName });
|
||||
if (createOrganizationResponse?.data) {
|
||||
router.push(`/setup/organization/${createOrganizationResponse.data.id}/invite`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Some error occurred while creating organization");
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { actionClient } from "@formbricks/lib/actionClient";
|
||||
import {
|
||||
getResponseCountBySurveyId,
|
||||
getResponseFilteringValues,
|
||||
@@ -8,54 +10,75 @@ import {
|
||||
} from "@formbricks/lib/response/service";
|
||||
import { getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
|
||||
export const getResponsesBySurveySharingKeyAction = async (
|
||||
sharingKey: string,
|
||||
limit: number = 10,
|
||||
offset: number = 0,
|
||||
filterCriteria?: TResponseFilterCriteria
|
||||
): Promise<TResponse[]> => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResponsesBySurveySharingKeyAction = z.object({
|
||||
sharingKey: z.string(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
filterCriteria: ZResponseFilterCriteria.optional(),
|
||||
});
|
||||
|
||||
const responses = await getResponses(surveyId, limit, offset, filterCriteria);
|
||||
return responses;
|
||||
};
|
||||
export const getResponsesBySurveySharingKeyAction = actionClient
|
||||
.schema(ZGetResponsesBySurveySharingKeyAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(parsedInput.sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export const getSummaryBySurveySharingKeyAction = async (
|
||||
sharingKey: string,
|
||||
filterCriteria?: TResponseFilterCriteria
|
||||
): Promise<TSurveySummary> => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
const responses = await getResponses(
|
||||
surveyId,
|
||||
parsedInput.limit,
|
||||
parsedInput.offset,
|
||||
parsedInput.filterCriteria
|
||||
);
|
||||
return responses;
|
||||
});
|
||||
|
||||
return await getSurveySummary(surveyId, filterCriteria);
|
||||
};
|
||||
const ZGetSummaryBySurveySharingKeyAction = z.object({
|
||||
sharingKey: z.string(),
|
||||
filterCriteria: ZResponseFilterCriteria.optional(),
|
||||
});
|
||||
|
||||
export const getResponseCountBySurveySharingKeyAction = async (
|
||||
sharingKey: string,
|
||||
filterCriteria?: TResponseFilterCriteria
|
||||
): Promise<number> => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
export const getSummaryBySurveySharingKeyAction = actionClient
|
||||
.schema(ZGetSummaryBySurveySharingKeyAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(parsedInput.sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await getResponseCountBySurveyId(surveyId, filterCriteria);
|
||||
};
|
||||
return await getSurveySummary(surveyId, parsedInput.filterCriteria);
|
||||
});
|
||||
|
||||
export const getSurveyFilterDataBySurveySharingKeyAction = async (
|
||||
sharingKey: string,
|
||||
environmentId: string
|
||||
) => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResponseCountBySurveySharingKeyAction = z.object({
|
||||
sharingKey: z.string(),
|
||||
filterCriteria: ZResponseFilterCriteria.optional(),
|
||||
});
|
||||
|
||||
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
|
||||
getTagsByEnvironmentId(environmentId),
|
||||
getResponseFilteringValues(surveyId),
|
||||
]);
|
||||
export const getResponseCountBySurveySharingKeyAction = actionClient
|
||||
.schema(ZGetResponseCountBySurveySharingKeyAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(parsedInput.sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return { environmentTags: tags, attributes, meta, hiddenFields };
|
||||
};
|
||||
return await getResponseCountBySurveyId(surveyId, parsedInput.filterCriteria);
|
||||
});
|
||||
|
||||
const ZGetSurveyFilterDataBySurveySharingKeyAction = z.object({
|
||||
sharingKey: z.string(),
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
export const getSurveyFilterDataBySurveySharingKeyAction = actionClient
|
||||
.schema(ZGetSurveyFilterDataBySurveySharingKeyAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(parsedInput.sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
|
||||
getTagsByEnvironmentId(parsedInput.environmentId),
|
||||
getResponseFilteringValues(surveyId),
|
||||
]);
|
||||
|
||||
return { environmentTags: tags, attributes, meta, hiddenFields };
|
||||
});
|
||||
|
||||
@@ -84,6 +84,9 @@ x-environment: &environment
|
||||
# Set a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
|
||||
# S3_ENDPOINT_URL=
|
||||
|
||||
# Force path style for S3 compatible storage (0 for disabled, 1 for enabled)
|
||||
S3_FORCE_PATH_STYLE: 0
|
||||
|
||||
############################################# OPTIONAL (OAUTH CONFIGURATION) #############################################
|
||||
|
||||
# Set the below from GitHub if you want to enable GitHub OAuth
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/* eslint-disable no-console -- logging is allowed in migration scripts */
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { type TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function runMigration(): Promise<void> {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const startTime = Date.now();
|
||||
console.log("Starting data migration...");
|
||||
|
||||
// Get all surveys with status not in draft and questions containing cta or consent
|
||||
const relevantSurveys = await tx.survey.findMany({
|
||||
where: {
|
||||
status: {
|
||||
notIn: ["draft"],
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
questions: {
|
||||
array_contains: [{ type: "cta" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
questions: {
|
||||
array_contains: [{ type: "consent" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
questions: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Process each survey
|
||||
const migrationPromises = relevantSurveys.map(async (survey) => {
|
||||
const ctaOrConsentQuestionIds = survey.questions
|
||||
.filter(
|
||||
(ques: TSurveyQuestion) =>
|
||||
ques.type === TSurveyQuestionTypeEnum.CTA || ques.type === TSurveyQuestionTypeEnum.Consent
|
||||
)
|
||||
.map((ques: TSurveyQuestion) => ques.id);
|
||||
|
||||
const responses = await tx.response.findMany({
|
||||
where: { surveyId: survey.id },
|
||||
select: { id: true, data: true },
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
responses.map(async (response) => {
|
||||
const updatedData = { ...response.data };
|
||||
|
||||
ctaOrConsentQuestionIds.forEach((questionId: string) => {
|
||||
if (updatedData[questionId] && updatedData[questionId] === "dismissed") {
|
||||
updatedData[questionId] = "";
|
||||
}
|
||||
});
|
||||
|
||||
return tx.response.update({
|
||||
where: { id: response.id },
|
||||
data: { data: updatedData },
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(migrationPromises);
|
||||
|
||||
const endTime = Date.now();
|
||||
console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toString()}s`);
|
||||
},
|
||||
{
|
||||
timeout: 180000, // 3 minutes
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function handleError(error: unknown): void {
|
||||
console.error("An error occurred during migration:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function handleDisconnectError(): void {
|
||||
console.error("Failed to disconnect Prisma client");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
runMigration()
|
||||
.catch(handleError)
|
||||
.finally(() => {
|
||||
prisma.$disconnect().catch(handleDisconnectError);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type TSurveyQuestions,
|
||||
type TSurveySingleUse,
|
||||
type TSurveyStyling,
|
||||
type TSurveyVariables,
|
||||
type TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { type TUserNotificationSettings } from "@formbricks/types/user";
|
||||
@@ -34,6 +35,7 @@ declare global {
|
||||
export type SurveyQuestions = TSurveyQuestions;
|
||||
export type SurveyEnding = TSurveyEnding;
|
||||
export type SurveyHiddenFields = TSurveyHiddenFields;
|
||||
export type SurveyVariables = TSurveyVariables;
|
||||
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
||||
export type SurveyStyling = TSurveyStyling;
|
||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user