chore: Move all server action to new authorization approach (#2999)

This commit is contained in:
Piyush Gupta
2024-08-19 12:37:37 +05:30
committed by GitHub
parent 5ba2959eb4
commit ec16159497
120 changed files with 3061 additions and 2164 deletions

View File

@@ -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) {

View File

@@ -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/environment";
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;
};

View File

@@ -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/environment";
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);
});

View File

@@ -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]);
}

View File

@@ -68,9 +68,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]);
@@ -95,9 +95,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();

View File

@@ -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) {

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -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/environment";
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;
}
};
});

View File

@@ -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();

View File

@@ -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/environment";
const ZPersonDeleteAction = z.object({
personId: z.string(),
personId: ZId,
});
export const deletePersonAction = authenticatedActionClient

View File

@@ -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/environment";
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);
});

View File

@@ -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!");

View File

@@ -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/environment";
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,
});

View File

@@ -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/environment";
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;
});

View File

@@ -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();

View File

@@ -31,7 +31,6 @@ export const ActionDetailModal = ({
title: "Settings",
children: (
<ActionSettingsTab
environmentId={environmentId}
actionClass={actionClass}
actionClasses={actionClasses}
setOpen={setOpen}

View File

@@ -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);

View File

@@ -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/environment";
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);
});

View File

@@ -1,7 +0,0 @@
"use server";
import { getAirtableTables } from "@formbricks/lib/airtable/service";
export const refreshTablesAction = async (environmentId: string) => {
return await getAirtableTables(environmentId);
};

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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/environment";
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);
});

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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 () => {

View File

@@ -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/environment";
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;
}
});

View File

@@ -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.");

View File

@@ -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} />,
},
];

View File

@@ -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) {

View File

@@ -67,7 +67,6 @@ export const WebhookTable = ({
</div>
)}
<WebhookModal
environmentId={environment.id}
open={isWebhookDetailModalOpen}
setOpen={setWebhookDetailModalOpen}
webhook={activeWebhook}

View File

@@ -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/environment";
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);
});

View File

@@ -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/environment";
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);
});

View File

@@ -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 }) => {

View File

@@ -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/environment";
const ZProductDeleteAction = z.object({
productId: z.string(),
productId: ZId,
});
export const deleteProductAction = authenticatedActionClient

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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}`);

View File

@@ -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");

View File

@@ -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.");

View File

@@ -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);
}
};

View File

@@ -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/environment";
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);
});

View File

@@ -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);
});
}}
/>
)}

View File

@@ -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,
});
});
};

View File

@@ -49,7 +49,7 @@ export const NotificationSwitch = ({
!updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId];
}
await updateNotificationSettingsAction(updatedNotificationSettings);
await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings });
setIsLoading(false);
};

View File

@@ -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/environment";
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 });
});

View File

@@ -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);
}
};

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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/environment";
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);
});

View File

@@ -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");

View File

@@ -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 } from "@formbricks/types/environment";
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: ZId,
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: ZId,
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);
});

View File

@@ -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) {

View File

@@ -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}`);

View File

@@ -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");

View File

@@ -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/environment";
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);
});

View File

@@ -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(() => {

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -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/environment";
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&amp;", "?")
.replaceAll("?preview=true&;", "?")
.replaceAll("?preview=true", "");
const rawEmailHtml = await getEmailTemplateHtml(parsedInput.surveyId);
const emailHtml = rawEmailHtml
.replaceAll("?preview=true&amp;", "?")
.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);
});

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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/environment";
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);
});

View File

@@ -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");

View File

@@ -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,

View File

@@ -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);
}
};

View File

@@ -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"

View File

@@ -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);

View File

@@ -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 { ZLinkSurveyEmailData } from "@formbricks/types/email";
import { ZId } from "@formbricks/types/environment";
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 };
}
};
});

View File

@@ -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);
}, []);

View File

@@ -50,7 +50,7 @@ export const VerifyEmail = ({
}
const data = {
surveyId: survey.id,
email: email,
email: email as string,
surveyName: survey.name,
suId: singleUseId ?? "",
};

View File

@@ -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/environment";
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;
};

View File

@@ -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}!`);
}

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -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/environment";
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 };
});

View File

@@ -79,8 +79,13 @@ export function AdvancedTargetingCard({
if (!segment) return;
try {
const clonedSegment = await cloneSegmentAction(segment.id, localSurvey.id);
setSegment(clonedSegment);
const clonedSegmentResponse = await cloneSegmentAction({
segmentId: segment.id,
surveyId: localSurvey.id,
});
if (clonedSegmentResponse?.data) {
setSegment(clonedSegmentResponse.data);
}
} catch (err: any) {
toast.error(err.message);
}
@@ -113,28 +118,24 @@ export function AdvancedTargetingCard({
};
const handleLoadNewSegment = async (surveyId: string, segmentId: string) => {
const updatedSurvey = await loadNewSegmentAction(surveyId, segmentId);
return updatedSurvey;
const updatedSurvey = await loadNewSegmentAction({ surveyId: surveyId, segmentId });
return updatedSurvey?.data as TSurvey;
};
const handleSaveAsNewSegmentUpdate = async (
environmentId: string,
segmentId: string,
data: TSegmentUpdateInput
) => {
const updatedSegment = await updateSegmentAction(environmentId, segmentId, data);
return updatedSegment;
const handleSaveAsNewSegmentUpdate = async (segmentId: string, data: TSegmentUpdateInput) => {
const updatedSegment = await updateSegmentAction({ segmentId, data });
return updatedSegment?.data as TSegment;
};
const handleSaveAsNewSegmentCreate = async (data: TSegmentCreateInput) => {
const createdSegment = await createSegmentAction(data);
return createdSegment;
return createdSegment?.data as TSegment;
};
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
try {
if (!segment) throw new Error("Invalid segment");
await updateSegmentAction(environmentId, segment.id, data);
await updateSegmentAction({ segmentId: segment.id, data });
toast.success("Segment saved successfully");
setIsSegmentEditorOpen(false);
@@ -146,7 +147,8 @@ export function AdvancedTargetingCard({
const handleResetAllFilters = async () => {
try {
return await resetSegmentFiltersAction(localSurvey.id);
const segmentResponse = await resetSegmentFiltersAction({ surveyId: localSurvey.id });
return segmentResponse?.data;
} catch (err) {
toast.error("Error resetting filters");
}

View File

@@ -73,11 +73,14 @@ export function SegmentSettings({
try {
setIsUpdatingSegment(true);
await updateSegmentAction(segment.environmentId, segment.id, {
title: segment.title,
description: segment.description ?? "",
isPrivate: segment.isPrivate,
filters: segment.filters,
await updateSegmentAction({
segmentId: segment.id,
data: {
title: segment.title,
description: segment.description ?? "",
isPrivate: segment.isPrivate,
filters: segment.filters,
},
});
setIsUpdatingSegment(false);
@@ -101,7 +104,7 @@ export function SegmentSettings({
const handleDeleteSegment = async () => {
try {
setIsDeletingSegment(true);
await deleteSegmentAction(segment.environmentId, segment.id);
await deleteSegmentAction({ segmentId: segment.id });
setIsDeletingSegment(false);
toast.success("Segment deleted successfully!");

View File

@@ -1,128 +1,148 @@
"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,
getOrganizationIdFromSegmentId,
getOrganizationIdFromSurveyId,
} from "@formbricks/lib/organization/utils";
import {
cloneSegment,
createSegment,
deleteSegment,
getSegment,
resetSegmentInSurvey,
updateSegment,
} from "@formbricks/lib/segment/service";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { loadNewSegmentInSurvey } from "@formbricks/lib/survey/service";
import { AuthorizationError } from "@formbricks/types/errors";
import type { TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
import { ZSegmentFilters } from "@formbricks/types/segment";
import { ZId } from "@formbricks/types/environment";
import { ZSegmentCreateInput, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
export const createSegmentAction = async ({
description,
environmentId,
filters,
isPrivate,
surveyId,
title,
}: TSegmentCreateInput) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
export const createSegmentAction = authenticatedActionClient
.schema(ZSegmentCreateInput)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
rules: ["segment", "create"],
});
const environmentAccess = await 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,
isPrivate,
filters,
});
return segment;
};
export const updateSegmentAction = async (
environmentId: string,
segmentId: string,
data: TSegmentUpdateInput
) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const environmentAccess = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
const { filters } = data;
if (filters) {
const parsedFilters = ZSegmentFilters.safeParse(filters);
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);
};
return await createSegment(parsedInput);
});
export const loadNewSegmentAction = async (surveyId: string, segmentId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const ZUpdateSegmentAction = z.object({
segmentId: ZId,
data: ZSegmentUpdateInput,
});
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
export const updateSegmentAction = authenticatedActionClient
.schema(ZUpdateSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
data: parsedInput.data,
schema: ZSegmentUpdateInput,
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
rules: ["segment", "update"],
});
return await loadNewSegmentInSurvey(surveyId, segmentId);
};
const { filters } = parsedInput.data;
if (filters) {
const parsedFilters = ZSegmentFilters.safeParse(filters);
export const cloneSegmentAction = async (segmentId: string, surveyId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
throw new Error(errMsg);
}
}
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
return await updateSegment(parsedInput.segmentId, parsedInput.data);
});
try {
const clonedSegment = await cloneSegment(segmentId, surveyId);
return clonedSegment;
} catch (err: any) {
throw new Error(err);
}
};
const ZLoadNewSegmentAction = z.object({
surveyId: ZId,
segmentId: ZId,
});
export const deleteSegmentAction = async (environmentId: string, segmentId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
export const loadNewSegmentAction = authenticatedActionClient
.schema(ZLoadNewSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
rules: ["survey", "update"],
});
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
rules: ["segment", "read"],
});
const foundSegment = await getSegment(segmentId);
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
});
if (!foundSegment) {
throw new Error(`Segment with id ${segmentId} not found`);
}
const ZCloneSegmentAction = z.object({
segmentId: ZId,
surveyId: ZId,
});
return await deleteSegment(segmentId);
};
export const cloneSegmentAction = authenticatedActionClient
.schema(ZCloneSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
rules: ["segment", "create"],
});
export const resetSegmentFiltersAction = async (surveyId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
rules: ["segment", "create"],
});
const environmentAccess = await canUserAccessSurvey(session.user.id, surveyId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
});
return await resetSegmentInSurvey(surveyId);
};
const ZDeleteSegmentAction = z.object({
segmentId: ZId,
});
export const deleteSegmentAction = authenticatedActionClient
.schema(ZDeleteSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
rules: ["segment", "delete"],
});
return await deleteSegment(parsedInput.segmentId);
});
const ZResetSegmentFiltersAction = z.object({
surveyId: ZId,
});
export const resetSegmentFiltersAction = authenticatedActionClient
.schema(ZResetSegmentFiltersAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
rules: ["survey", "update"],
});
return await resetSegmentInSurvey(parsedInput.surveyId);
});

View File

@@ -3,6 +3,7 @@
import { InfoIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { iso639Languages } from "@formbricks/lib/i18n/utils";
import type { TLanguage, TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
@@ -88,23 +89,32 @@ export function EditLanguage({ product, environmentId }: EditLanguageProps) {
const handleDeleteLanguage = async (languageId: string) => {
try {
const surveysUsingLanguage = await getSurveysUsingGivenLanguageAction(product.id, languageId);
const surveysUsingLanguageResponse = await getSurveysUsingGivenLanguageAction({
languageId,
});
if (surveysUsingLanguage.length > 0) {
const surveyList = surveysUsingLanguage.map((surveyName) => `${surveyName}`).join("\n");
setConfirmationModal({
isOpen: true,
languageId,
text: `You cannot remove this language since its still used in these surveys:\n\n${surveyList}\n\nPlease remove the language from these surveys in order to remove it from the product.`,
isButtonDisabled: true,
});
if (surveysUsingLanguageResponse?.data) {
if (surveysUsingLanguageResponse.data.length > 0) {
const surveyList = surveysUsingLanguageResponse.data
.map((surveyName) => `${surveyName}`)
.join("\n");
setConfirmationModal({
isOpen: true,
languageId,
text: `You cannot remove this language since its still used in these surveys:\n\n${surveyList}\n\nPlease remove the language from these surveys in order to remove it from the product.`,
isButtonDisabled: true,
});
} else {
setConfirmationModal({
isOpen: true,
languageId,
text: "Are you sure you want to delete this language? This action cannot be undone.",
isButtonDisabled: false,
});
}
} else {
setConfirmationModal({
isOpen: true,
languageId,
text: "Are you sure you want to delete this language? This action cannot be undone.",
isButtonDisabled: false,
});
const errorMessage = getFormattedErrorMessage(surveysUsingLanguageResponse);
toast.error(errorMessage);
}
} catch (err) {
toast.error("Something went wrong. Please try again later.");
@@ -113,7 +123,7 @@ export function EditLanguage({ product, environmentId }: EditLanguageProps) {
const performLanguageDeletion = async (languageId: string) => {
try {
await deleteLanguageAction(product.id, environmentId, languageId);
await deleteLanguageAction({ environmentId, languageId });
setLanguages((prev) => prev.filter((lang) => lang.id !== languageId));
toast.success("Language deleted successfully.");
// Close the modal after deletion
@@ -134,8 +144,15 @@ export function EditLanguage({ product, environmentId }: EditLanguageProps) {
await Promise.all(
languages.map((lang) => {
return lang.id === "new"
? createLanguageAction(product.id, environmentId, { code: lang.code, alias: lang.alias })
: updateLanguageAction(product.id, environmentId, lang.id, { code: lang.code, alias: lang.alias });
? createLanguageAction({
environmentId,
languageInput: { code: lang.code, alias: lang.alias },
})
: updateLanguageAction({
environmentId,
languageId: lang.id,
languageInput: { code: lang.code, alias: lang.alias },
});
})
);
toast.success("Languages updated successfully.");

View File

@@ -1,78 +1,95 @@
"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 { getEnvironment } from "@formbricks/lib/environment/service";
import {
createLanguage,
deleteLanguage,
getSurveysUsingGivenLanguage,
updateLanguage,
} from "@formbricks/lib/language/service";
import { canUserAccessProduct, verifyUserRoleAccess } from "@formbricks/lib/product/auth";
import { getProduct } from "@formbricks/lib/product/service";
import { AuthorizationError } from "@formbricks/types/errors";
import type { TLanguageInput } from "@formbricks/types/product";
import {
getOrganizationIdFromLanguageId,
getOrganizationIdFromProductId,
} from "@formbricks/lib/organization/utils";
import { ZId } from "@formbricks/types/environment";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZLanguageInput } from "@formbricks/types/product";
export const createLanguageAction = async (
productId: string,
environmentId: string,
languageInput: TLanguageInput
) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const ZCreateLanguageAction = z.object({
environmentId: ZId,
languageInput: ZLanguageInput,
});
const isAuthorized = await canUserAccessProduct(session.user?.id, productId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
export const createLanguageAction = authenticatedActionClient
.schema(ZCreateLanguageAction)
.action(async ({ ctx, parsedInput }) => {
const environment = await getEnvironment(parsedInput.environmentId);
if (!environment) {
throw new ResourceNotFoundError("Environment", parsedInput.environmentId);
}
await checkAuthorization({
data: parsedInput.languageInput,
schema: ZLanguageInput,
userId: ctx.user.id,
organizationId: await getOrganizationIdFromProductId(environment.productId),
rules: ["language", "create"],
});
const product = await getProduct(productId);
return await createLanguage(environment.productId, parsedInput.environmentId, parsedInput.languageInput);
});
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user?.id);
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
const ZDeleteLanguageAction = z.object({
environmentId: ZId,
languageId: ZId,
});
return await createLanguage(productId, environmentId, languageInput);
};
export const deleteLanguageAction = authenticatedActionClient
.schema(ZDeleteLanguageAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromLanguageId(parsedInput.languageId),
rules: ["language", "delete"],
});
export const deleteLanguageAction = async (productId: string, environmentId: string, languageId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
return await deleteLanguage(parsedInput.environmentId, parsedInput.languageId);
});
const isAuthorized = await canUserAccessProduct(session.user?.id, productId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const ZGetSurveysUsingGivenLanguageAction = z.object({
languageId: ZId,
});
const product = await getProduct(productId);
export const getSurveysUsingGivenLanguageAction = authenticatedActionClient
.schema(ZGetSurveysUsingGivenLanguageAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromLanguageId(parsedInput.languageId),
rules: ["survey", "read"],
});
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user?.id);
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
return await getSurveysUsingGivenLanguage(parsedInput.languageId);
});
return await deleteLanguage(environmentId, languageId);
};
const ZUpdateLanguageAction = z.object({
environmentId: ZId,
languageId: ZId,
languageInput: ZLanguageInput,
});
export const getSurveysUsingGivenLanguageAction = async (productId: string, languageId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
export const updateLanguageAction = authenticatedActionClient
.schema(ZUpdateLanguageAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
data: parsedInput.languageInput,
schema: ZLanguageInput,
userId: ctx.user.id,
organizationId: await getOrganizationIdFromLanguageId(parsedInput.languageId),
rules: ["language", "update"],
});
const isAuthorized = await canUserAccessProduct(session.user?.id, productId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
return await getSurveysUsingGivenLanguage(languageId);
};
export const updateLanguageAction = async (
productId: string,
environmentId: string,
languageId: string,
languageInput: TLanguageInput
) => {
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 updateLanguage(environmentId, languageId, languageInput);
};
return await updateLanguage(parsedInput.environmentId, parsedInput.languageId, parsedInput.languageInput);
});

View File

@@ -53,11 +53,11 @@ export function EditMembershipRole({
try {
if (memberAccepted && memberId) {
await updateMembershipAction(memberId, organizationId, { role });
await updateMembershipAction({ userId: memberId, organizationId, data: { role } });
}
if (inviteId) {
await updateInviteAction(inviteId, organizationId, { role });
await updateInviteAction({ inviteId: inviteId, organizationId, data: { role } });
}
} catch (error) {
toast.error("Something went wrong");
@@ -71,7 +71,7 @@ export function EditMembershipRole({
setLoading(true);
try {
if (memberId) {
await transferOwnershipAction(organizationId, memberId);
await transferOwnershipAction({ organizationId, newOwnerId: memberId });
}
setLoading(false);

View File

@@ -1,98 +1,84 @@
"use server";
import { getServerSession } from "next-auth";
import { hasOrganizationAccess, hasOrganizationAuthority, isOwner } from "@formbricks/lib/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 { isOwner } from "@formbricks/lib/auth";
import { updateInvite } from "@formbricks/lib/invite/service";
import {
getMembershipByUserIdOrganizationId,
transferOwnership,
updateMembership,
} from "@formbricks/lib/membership/service";
import { AuthenticationError, AuthorizationError, ValidationError } from "@formbricks/types/errors";
import type { TInviteUpdateInput } from "@formbricks/types/invites";
import type { TMembershipUpdateInput } from "@formbricks/types/memberships";
import type { TUser } from "@formbricks/types/user";
import { ZId } from "@formbricks/types/environment";
import { AuthorizationError, ValidationError } from "@formbricks/types/errors";
import { ZInviteUpdateInput } from "@formbricks/types/invites";
import { ZMembershipUpdateInput } from "@formbricks/types/memberships";
export const transferOwnershipAction = async (organizationId: string, newOwnerId: string) => {
const session = await getServerSession(authOptions);
const user = session?.user as TUser;
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const ZTransferOwnershipAction = z.object({
organizationId: ZId,
newOwnerId: ZId,
});
if (!user) {
throw new AuthenticationError("Not authenticated");
}
export const transferOwnershipAction = authenticatedActionClient
.schema(ZTransferOwnershipAction)
.action(async ({ ctx, parsedInput }) => {
const isUserOwner = await isOwner(ctx.user.id, parsedInput.organizationId);
if (!isUserOwner) {
throw new AuthorizationError("Not authorized");
}
const hasAccess = await hasOrganizationAccess(user.id, organizationId);
if (!hasAccess) {
throw new AuthorizationError("Not authorized");
}
if (parsedInput.newOwnerId === ctx.user.id) {
throw new ValidationError("You are already the owner of this organization");
}
const isUserOwner = await isOwner(user.id, organizationId);
if (!isUserOwner) {
throw new AuthorizationError("Not authorized");
}
const membership = await getMembershipByUserIdOrganizationId(
parsedInput.newOwnerId,
parsedInput.organizationId
);
if (!membership) {
throw new ValidationError("User is not a member of this organization");
}
if (newOwnerId === user.id) {
throw new ValidationError("You are already the owner of this organization");
}
await transferOwnership(ctx.user.id, parsedInput.newOwnerId, parsedInput.organizationId);
});
const membership = await getMembershipByUserIdOrganizationId(newOwnerId, organizationId);
if (!membership) {
throw new ValidationError("User is not a member of this organization");
}
const ZUpdateInviteAction = z.object({
inviteId: ZId,
organizationId: ZId,
data: ZInviteUpdateInput,
});
await transferOwnership(user.id, newOwnerId, organizationId);
};
export const updateInviteAction = authenticatedActionClient
.schema(ZUpdateInviteAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
data: parsedInput.data,
schema: ZInviteUpdateInput,
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
rules: ["invite", "update"],
});
export const updateInviteAction = async (
inviteId: string,
organizationId: string,
data: TInviteUpdateInput
) => {
const session = await getServerSession(authOptions);
const user = session?.user as TUser;
return await updateInvite(parsedInput.inviteId, parsedInput.data);
});
if (!user) {
throw new AuthenticationError("Not authenticated");
}
const ZUpdateMembershipAction = z.object({
userId: ZId,
organizationId: ZId,
data: ZMembershipUpdateInput,
});
if (!session) {
throw new AuthenticationError("Not authenticated");
}
export const updateMembershipAction = authenticatedActionClient
.schema(ZUpdateMembershipAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
data: parsedInput.data,
schema: ZMembershipUpdateInput,
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
rules: ["membership", "update"],
});
const isUserAuthorized = await hasOrganizationAuthority(user.id, organizationId);
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
return await updateInvite(inviteId, data);
};
export const updateMembershipAction = async (
userId: string,
organizationId: string,
data: TMembershipUpdateInput
) => {
const session = await getServerSession(authOptions);
const user = session?.user as TUser;
if (!user) {
throw new AuthenticationError("Not authenticated");
}
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasOrganizationAuthority(user.id, organizationId);
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
return await updateMembership(userId, organizationId, data);
};
return await updateMembership(parsedInput.userId, parsedInput.organizationId, parsedInput.data);
});

View File

@@ -14,6 +14,7 @@ import {
} from "@formbricks/lib/constants";
import { createInviteToken, createToken, createTokenForLinkSurvey } from "@formbricks/lib/jwt";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
import type { TResponse } from "@formbricks/types/responses";
import type { TSurvey } from "@formbricks/types/surveys/types";
import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary";
@@ -45,13 +46,6 @@ interface TEmailUser {
email: string;
}
export interface LinkSurveyEmailData {
surveyId: string;
email: string;
suId: string;
surveyName: string;
}
const getEmailSubject = (productName: string): string => {
return `${productName} User Insights - Last Week by Formbricks`;
};
@@ -205,7 +199,7 @@ export const sendEmbedSurveyPreviewEmail = async (
});
};
export const sendLinkSurveyToVerifiedEmail = async (data: LinkSurveyEmailData): Promise<void> => {
export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData): Promise<void> => {
const surveyId = data.surveyId;
const email = data.email;
const surveyName = data.surveyName;

View File

@@ -109,11 +109,8 @@ export const getActionClass = reactCache(
)()
);
export const deleteActionClass = async (
environmentId: string,
actionClassId: string
): Promise<TActionClass> => {
validateInputs([environmentId, ZId], [actionClassId, ZId]);
export const deleteActionClass = async (actionClassId: string): Promise<TActionClass> => {
validateInputs([actionClassId, ZId]);
try {
const actionClass = await prisma.actionClass.delete({
@@ -125,16 +122,17 @@ export const deleteActionClass = async (
if (actionClass === null) throw new ResourceNotFoundError("Action", actionClassId);
actionClassCache.revalidate({
environmentId,
environmentId: actionClass.environmentId,
id: actionClassId,
name: actionClass.name,
});
return actionClass;
} catch (error) {
throw new DatabaseError(
`Database error when deleting an action with id ${actionClassId} for environment ${environmentId}`
);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};

View File

@@ -2,6 +2,9 @@ import { ZProductUpdateInput } from "@formbricks/types/product";
export const Permissions = {
owner: {
environment: {
read: true,
},
product: {
create: true,
read: true,
@@ -9,20 +12,87 @@ export const Permissions = {
delete: true,
},
organization: {
read: true,
update: true,
delete: true,
},
membership: {
create: true,
update: true,
delete: true,
},
person: {
delete: true,
},
response: {
read: true,
update: true,
delete: true,
},
survey: {
create: true,
update: true,
read: true,
delete: true,
},
tag: {
create: true,
update: true,
delete: true,
},
responseNote: {
create: true,
update: true,
delete: true,
},
segment: {
create: true,
read: true,
update: true,
delete: true,
},
actionClass: {
create: true,
delete: true,
},
integration: {
create: true,
update: true,
delete: true,
},
webhook: {
create: true,
update: true,
delete: true,
},
apiKey: {
create: true,
update: true,
delete: true,
},
subscription: {
create: true,
read: true,
update: true,
delete: true,
},
invite: {
create: true,
read: true,
update: true,
delete: true,
},
language: {
create: true,
update: true,
delete: true,
},
},
admin: {
environment: {
read: true,
},
product: {
create: true,
read: true,
@@ -30,20 +100,87 @@ export const Permissions = {
delete: true,
},
organization: {
read: true,
update: true,
delete: false,
},
membership: {
create: true,
update: true,
delete: true,
},
person: {
delete: true,
},
response: {
read: true,
update: true,
delete: true,
},
survey: {
create: true,
read: true,
update: true,
delete: true,
},
tag: {
create: true,
update: true,
delete: true,
},
responseNote: {
create: true,
update: true,
delete: true,
},
segment: {
create: true,
read: true,
update: true,
delete: true,
},
actionClass: {
create: true,
delete: true,
},
integration: {
create: true,
update: true,
delete: true,
},
webhook: {
create: true,
update: true,
delete: true,
},
apiKey: {
create: true,
update: true,
delete: true,
},
subscription: {
create: true,
read: true,
update: true,
delete: true,
},
invite: {
create: true,
read: true,
update: true,
delete: true,
},
language: {
create: true,
update: true,
delete: true,
},
},
editor: {
environment: {
read: true,
},
product: {
create: false,
read: true,
@@ -51,20 +188,87 @@ export const Permissions = {
delete: true,
},
organization: {
read: true,
update: false,
delete: false,
},
membership: {
create: false,
update: false,
delete: false,
},
person: {
delete: true,
},
response: {
read: true,
update: true,
delete: true,
},
survey: {
create: true,
read: true,
update: true,
delete: true,
},
tag: {
create: true,
update: true,
delete: true,
},
responseNote: {
create: true,
update: true,
delete: true,
},
segment: {
create: true,
read: true,
update: true,
delete: true,
},
actionClass: {
create: true,
delete: true,
},
integration: {
create: true,
update: true,
delete: true,
},
webhook: {
create: true,
update: true,
delete: true,
},
apiKey: {
create: true,
update: true,
delete: true,
},
subscription: {
create: false,
read: false,
update: false,
delete: false,
},
invite: {
create: false,
read: false,
update: false,
delete: false,
},
language: {
create: false,
update: false,
delete: false,
},
},
developer: {
environment: {
read: true,
},
product: {
create: false,
read: true,
@@ -74,20 +278,87 @@ export const Permissions = {
delete: true,
},
organization: {
read: true,
update: false,
delete: false,
},
membership: {
create: false,
update: false,
delete: false,
},
person: {
delete: true,
},
response: {
read: true,
update: true,
delete: true,
},
survey: {
create: true,
read: true,
update: true,
delete: true,
},
tag: {
create: true,
update: true,
delete: true,
},
responseNote: {
create: true,
update: true,
delete: true,
},
segment: {
create: true,
read: true,
update: true,
delete: true,
},
actionClass: {
create: true,
delete: true,
},
integration: {
create: true,
update: true,
delete: true,
},
webhook: {
create: true,
update: true,
delete: true,
},
apiKey: {
create: true,
update: true,
delete: true,
},
subscription: {
create: false,
read: false,
update: false,
delete: false,
},
invite: {
create: false,
read: false,
update: false,
delete: false,
},
language: {
create: false,
update: false,
delete: false,
},
},
viewer: {
environment: {
read: true,
},
product: {
create: false,
read: true,
@@ -95,16 +366,80 @@ export const Permissions = {
delete: false,
},
organization: {
read: false,
update: false,
delete: false,
},
membership: {
create: false,
update: false,
delete: false,
},
person: {
delete: false,
},
response: {
read: true,
update: false,
delete: false,
},
survey: {
create: false,
read: true,
update: false,
delete: false,
},
tag: {
create: false,
update: false,
delete: false,
},
responseNote: {
create: false,
update: false,
delete: false,
},
segment: {
create: false,
read: true,
update: false,
delete: false,
},
actionClass: {
create: false,
delete: false,
},
integration: {
create: false,
update: true,
delete: false,
},
webhook: {
create: false,
update: false,
delete: false,
},
apiKey: {
create: false,
update: false,
delete: false,
},
subscription: {
create: false,
read: false,
update: false,
delete: false,
},
invite: {
create: false,
read: false,
update: false,
delete: false,
},
language: {
create: false,
update: false,
delete: false,
},
},
};

View File

@@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import {
TLanguage,
TLanguageInput,
@@ -23,6 +23,29 @@ const languageSelect = {
updatedAt: true,
};
export const getLanguage = async (languageId: string): Promise<TLanguage & { productId: string }> => {
try {
validateInputs([languageId, ZId]);
const language = await prisma.language.findFirst({
where: { id: languageId },
select: { ...languageSelect, productId: true },
});
if (!language) {
throw new ResourceNotFoundError("Language", languageId);
}
return language;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
};
export const createLanguage = async (
productId: string,
environmentId: string,

View File

@@ -1,22 +1,26 @@
"use server";
import "server-only";
import { getServerSession } from "next-auth";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { authOptions } from "../../authOptions";
import { z } from "zod";
import { ZId } from "@formbricks/types/environment";
import { authenticatedActionClient } from "../../actionClient";
import { checkAuthorization } from "../../actionClient/utils";
import { getOrganization } from "../service";
export const getOrganizationBillingInfoAction = async (organizationId: string) => {
const session = await getServerSession(authOptions);
const organization = await getOrganization(organizationId);
const ZGetOrganizationBillingInfoAction = z.object({
organizationId: ZId,
});
if (!session) {
throw new AuthenticationError("Not authenticated");
}
export const getOrganizationBillingInfoAction = authenticatedActionClient
.schema(ZGetOrganizationBillingInfoAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
rules: ["organization", "read"],
});
if (!organization) {
throw new ResourceNotFoundError("Organization", organizationId);
}
const organization = await getOrganization(parsedInput.organizationId);
return organization.billing;
};
return organization?.billing;
});

View File

@@ -11,16 +11,15 @@ export const useGetBillingInfo = (organizationId: string) => {
const getBillingInfo = async () => {
try {
setIsLoading(true);
const billingInfo = await getOrganizationBillingInfoAction(organizationId);
const billingInfo = await getOrganizationBillingInfoAction({ organizationId });
if (!billingInfo) {
setError("No billing info found");
if (billingInfo?.data) {
setIsLoading(false);
return;
setBillingInfo(billingInfo.data);
}
setError("No billing info found");
setIsLoading(false);
setBillingInfo(billingInfo);
} catch (err: any) {
setIsLoading(false);
setError(err.message);

View File

@@ -1,9 +1,19 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getActionClass } from "../actionClass/service";
import { getApiKey } from "../apiKey/service";
import { getAttributeClass } from "../attributeClass/service";
import { getEnvironment } from "../environment/service";
import { getIntegration } from "../integration/service";
import { getInvite } from "../invite/service";
import { getLanguage } from "../language/service";
import { getPerson } from "../person/service";
import { getProduct } from "../product/service";
import { getResponse } from "../response/service";
import { getResponseNote } from "../responseNote/service";
import { getSegment } from "../segment/service";
import { getSurvey } from "../survey/service";
import { getTag } from "../tag/service";
import { getWebhook } from "../webhook/service";
/**
* GET organization ID from RESOURCE ID
@@ -24,8 +34,7 @@ export const getOrganizationIdFromEnvironmentId = async (environmentId: string)
throw new ResourceNotFoundError("environment", environmentId);
}
const organizationId = await getOrganizationIdFromProductId(environment.productId);
return organizationId;
return await getOrganizationIdFromProductId(environment.productId);
};
export const getOrganizationIdFromSurveyId = async (surveyId: string) => {
@@ -34,8 +43,7 @@ export const getOrganizationIdFromSurveyId = async (surveyId: string) => {
throw new ResourceNotFoundError("survey", surveyId);
}
const organizationId = await getOrganizationIdFromEnvironmentId(survey.environmentId);
return organizationId;
return await getOrganizationIdFromEnvironmentId(survey.environmentId);
};
export const getOrganizationIdFromResponseId = async (responseId: string) => {
@@ -44,8 +52,7 @@ export const getOrganizationIdFromResponseId = async (responseId: string) => {
throw new ResourceNotFoundError("response", responseId);
}
const organizationId = await getOrganizationIdFromSurveyId(response.surveyId);
return organizationId;
return await getOrganizationIdFromSurveyId(response.surveyId);
};
export const getOrganizationIdFromPersonId = async (personId: string) => {
@@ -54,6 +61,95 @@ export const getOrganizationIdFromPersonId = async (personId: string) => {
throw new ResourceNotFoundError("person", personId);
}
const organizationId = await getOrganizationIdFromEnvironmentId(person.environmentId);
return organizationId;
return await getOrganizationIdFromEnvironmentId(person.environmentId);
};
export const getOrganizationIdFromTagId = async (tagId: string) => {
const tag = await getTag(tagId);
if (!tag) {
throw new ResourceNotFoundError("tag", tagId);
}
return await getOrganizationIdFromEnvironmentId(tag.environmentId);
};
export const getOrganizationIdFromResponseNoteId = async (responseNoteId: string) => {
const responseNote = await getResponseNote(responseNoteId);
if (!responseNote) {
throw new ResourceNotFoundError("responseNote", responseNoteId);
}
return await getOrganizationIdFromResponseId(responseNote.responseId);
};
export const getOrganizationIdFromAttributeClassId = async (attributeClassId: string) => {
const attributeClass = await getAttributeClass(attributeClassId);
if (!attributeClass) {
throw new ResourceNotFoundError("attributeClass", attributeClassId);
}
return await getOrganizationIdFromEnvironmentId(attributeClass.environmentId);
};
export const getOrganizationIdFromSegmentId = async (segmentId: string) => {
const segment = await getSegment(segmentId);
if (!segment) {
throw new ResourceNotFoundError("segment", segmentId);
}
return await getOrganizationIdFromEnvironmentId(segment.environmentId);
};
export const getOrganizationIdFromActionClassId = async (actionClassId: string) => {
const actionClass = await getActionClass(actionClassId);
if (!actionClass) {
throw new ResourceNotFoundError("actionClass", actionClassId);
}
return await getOrganizationIdFromEnvironmentId(actionClass.environmentId);
};
export const getOrganizationIdFromIntegrationId = async (integrationId: string) => {
const integration = await getIntegration(integrationId);
if (!integration) {
throw new ResourceNotFoundError("integration", integrationId);
}
return await getOrganizationIdFromEnvironmentId(integration.environmentId);
};
export const getOrganizationIdFromWebhookId = async (webhookId: string) => {
const webhook = await getWebhook(webhookId);
if (!webhook) {
throw new ResourceNotFoundError("webhook", webhookId);
}
return await getOrganizationIdFromEnvironmentId(webhook.environmentId);
};
export const getOrganizationIdFromApiKeyId = async (apiKeyId: string) => {
const apiKeyFromServer = await getApiKey(apiKeyId);
if (!apiKeyFromServer) {
throw new ResourceNotFoundError("apiKey", apiKeyId);
}
return await getOrganizationIdFromEnvironmentId(apiKeyFromServer.environmentId);
};
export const getOrganizationIdFromInviteId = async (inviteId: string) => {
const invite = await getInvite(inviteId);
if (!invite) {
throw new ResourceNotFoundError("invite", inviteId);
}
return invite.organizationId;
};
export const getOrganizationIdFromLanguageId = async (languageId: string) => {
const language = await getLanguage(languageId);
if (!language) {
throw new ResourceNotFoundError("language", languageId);
}
return await getOrganizationIdFromProductId(language.productId);
};

View File

@@ -70,7 +70,7 @@ export const createResponseNote = async (
};
export const getResponseNote = reactCache(
(responseNoteId: string): Promise<TResponseNote | null> =>
(responseNoteId: string): Promise<(TResponseNote & { responseId: string }) | null> =>
cache(
async () => {
try {
@@ -78,7 +78,10 @@ export const getResponseNote = reactCache(
where: {
id: responseNoteId,
},
select: responseNoteSelect,
select: {
...responseNoteSelect,
responseId: true,
},
});
return responseNote;
} catch (error) {

View File

@@ -134,11 +134,10 @@ export const createWebhook = async (
};
export const updateWebhook = async (
environmentId: string,
webhookId: string,
webhookInput: Partial<TWebhookInput>
): Promise<TWebhook> => {
validateInputs([environmentId, ZId], [webhookId, ZId], [webhookInput, ZWebhookInput]);
validateInputs([webhookId, ZId], [webhookInput, ZWebhookInput]);
try {
const updatedWebhook = await prisma.webhook.update({
where: {

View File

@@ -9,6 +9,18 @@ export const ZResource = z.enum([
"response",
"survey",
"person",
"tag",
"responseNote",
"membership",
"attributeClass",
"segment",
"actionClass",
"integration",
"webhook",
"apiKey",
"subscription",
"invite",
"language",
]);
export type TResource = z.infer<typeof ZResource>;

Some files were not shown because too many files have changed in this diff Show More