mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-14 18:54:40 -05:00
chore: Move all server action to new authorization approach (#2999)
This commit is contained in:
@@ -36,7 +36,12 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
|
||||
const handleInvite = async (data: TInviteOrganizationMemberDetails) => {
|
||||
try {
|
||||
await inviteOrganizationMemberAction(organization.id, data.email, "developer", data.inviteMessage);
|
||||
await inviteOrganizationMemberAction({
|
||||
organizationId: organization.id,
|
||||
email: data.email,
|
||||
role: "developer",
|
||||
inviteMessage: data.inviteMessage,
|
||||
});
|
||||
toast.success("Invite sent successful");
|
||||
await finishOnboarding();
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,68 +1,54 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { inviteUser } from "@formbricks/lib/invite/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
|
||||
@@ -81,34 +82,38 @@ export const TargetingCard = ({
|
||||
const handleCloneSegment = async () => {
|
||||
if (!segment) return;
|
||||
|
||||
try {
|
||||
const clonedSegment = await cloneBasicSegmentAction(segment.id, localSurvey.id);
|
||||
const cloneBasicSegmentResponse = await cloneBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
|
||||
setSegment(clonedSegment);
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
if (cloneBasicSegmentResponse?.data) {
|
||||
setSegment(cloneBasicSegmentResponse.data);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(cloneBasicSegmentResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadNewSegment = async (surveyId: string, segmentId: string) => {
|
||||
const updatedSurvey = await loadNewBasicSegmentAction(surveyId, segmentId);
|
||||
return updatedSurvey;
|
||||
const loadNewBasicSegmentResponse = await loadNewBasicSegmentAction({ surveyId, segmentId });
|
||||
return loadNewBasicSegmentResponse?.data as TSurvey;
|
||||
};
|
||||
|
||||
const handleSegmentUpdate = async (environmentId: string, segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updatedSegment = await updateBasicSegmentAction(environmentId, segmentId, data);
|
||||
return updatedSegment;
|
||||
const handleSegmentUpdate = async (segmentId: string, data: TSegmentUpdateInput) => {
|
||||
const updateBasicSegmentResponse = await updateBasicSegmentAction({ segmentId, data });
|
||||
return updateBasicSegmentResponse?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSegmentCreate = async (data: TSegmentCreateInput) => {
|
||||
const createdSegment = await createBasicSegmentAction(data);
|
||||
return createdSegment;
|
||||
return createdSegment?.data as TSegment;
|
||||
};
|
||||
|
||||
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
|
||||
try {
|
||||
if (!segment) throw new Error("Invalid segment");
|
||||
await updateBasicSegmentAction(environmentId, segment?.id, data);
|
||||
await updateBasicSegmentAction({ segmentId: segment?.id, data });
|
||||
|
||||
router.refresh();
|
||||
toast.success("Segment saved successfully");
|
||||
@@ -122,7 +127,10 @@ export const TargetingCard = ({
|
||||
|
||||
const handleResetAllFilters = async () => {
|
||||
try {
|
||||
return await resetBasicSegmentFiltersAction(localSurvey.id);
|
||||
const resetBasicSegmentFiltersResponse = await resetBasicSegmentFiltersAction({
|
||||
surveyId: localSurvey.id,
|
||||
});
|
||||
return resetBasicSegmentFiltersResponse?.data;
|
||||
} catch (err) {
|
||||
toast.error("Error resetting filters");
|
||||
}
|
||||
|
||||
@@ -123,7 +123,13 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
const fetchData = async (searchQuery: string, currentPage: number) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
|
||||
const getImagesFromUnsplashResponse = await getImagesFromUnsplashAction({
|
||||
searchQuery: searchQuery,
|
||||
page: currentPage,
|
||||
});
|
||||
if (!getImagesFromUnsplashResponse?.data) return;
|
||||
|
||||
const imagesFromUnsplash = getImagesFromUnsplashResponse.data;
|
||||
for (let i = 0; i < imagesFromUnsplash.length; i++) {
|
||||
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
|
||||
"authorName"
|
||||
@@ -163,7 +169,7 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
|
||||
try {
|
||||
handleBgChange(imageUrl, "image");
|
||||
if (downloadImageUrl) {
|
||||
await triggerDownloadUnsplashImageAction(downloadImageUrl);
|
||||
await triggerDownloadUnsplashImageAction({ downloadUrl: downloadImageUrl });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { TagIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -24,20 +25,20 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
|
||||
setLoading(true);
|
||||
|
||||
const getSurveys = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassName = await getSegmentsByAttributeClassAction(
|
||||
attributeClass.environmentId,
|
||||
attributeClass
|
||||
);
|
||||
setLoading(true);
|
||||
const segmentsWithAttributeClassNameResponse = await getSegmentsByAttributeClassAction({
|
||||
environmentId: attributeClass.environmentId,
|
||||
attributeClass,
|
||||
});
|
||||
|
||||
setActiveSurveys(segmentsWithAttributeClassName.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassName.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (segmentsWithAttributeClassNameResponse?.data) {
|
||||
setActiveSurveys(segmentsWithAttributeClassNameResponse.data.activeSurveys);
|
||||
setInactiveSurveys(segmentsWithAttributeClassNameResponse.data.inactiveSurveys);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(segmentsWithAttributeClassNameResponse);
|
||||
setError(new Error(errorMessage));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
getSurveys();
|
||||
|
||||
@@ -5,9 +5,10 @@ import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromPersonId } from "@formbricks/lib/organization/utils";
|
||||
import { deletePerson } from "@formbricks/lib/person/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
const ZPersonDeleteAction = z.object({
|
||||
personId: z.string(),
|
||||
personId: ZId,
|
||||
});
|
||||
|
||||
export const deletePersonAction = authenticatedActionClient
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { deleteSegment, getSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSegmentId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -70,11 +70,14 @@ export const BasicSegmentSettings = ({
|
||||
|
||||
try {
|
||||
setIsUpdatingSegment(true);
|
||||
await updateBasicSegmentAction(segment.environmentId, segment.id, {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
await updateBasicSegmentAction({
|
||||
segmentId: segment.id,
|
||||
data: {
|
||||
title: segment.title,
|
||||
description: segment.description ?? "",
|
||||
isPrivate: segment.isPrivate,
|
||||
filters: segment.filters,
|
||||
},
|
||||
});
|
||||
|
||||
setIsUpdatingSegment(false);
|
||||
@@ -99,7 +102,7 @@ export const BasicSegmentSettings = ({
|
||||
const handleDeleteSegment = async () => {
|
||||
try {
|
||||
setIsDeletingSegment(true);
|
||||
await deleteBasicSegmentAction(segment.environmentId, segment.id);
|
||||
await deleteBasicSegmentAction({ segmentId: segment.id });
|
||||
|
||||
setIsDeletingSegment(false);
|
||||
toast.success("Segment deleted successfully!");
|
||||
|
||||
@@ -1,68 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import { Organization } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
@@ -25,16 +26,18 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
|
||||
setLoading(true);
|
||||
|
||||
const updateState = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const activeInactiveSurveys = await getActiveInactiveSurveysAction(actionClass.id, environmentId);
|
||||
setActiveSurveys(activeInactiveSurveys.activeSurveys);
|
||||
setInactiveSurveys(activeInactiveSurveys.inactiveSurveys);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const getActiveInactiveSurveysResponse = await getActiveInactiveSurveysAction({
|
||||
actionClassId: actionClass.id,
|
||||
});
|
||||
if (getActiveInactiveSurveysResponse?.data) {
|
||||
setActiveSurveys(getActiveInactiveSurveysResponse.data.activeSurveys);
|
||||
setInactiveSurveys(getActiveInactiveSurveysResponse.data.inactiveSurveys);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(getActiveInactiveSurveysResponse);
|
||||
setError(new Error(errorMessage));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
updateState();
|
||||
|
||||
@@ -31,7 +31,6 @@ export const ActionDetailModal = ({
|
||||
title: "Settings",
|
||||
children: (
|
||||
<ActionSettingsTab
|
||||
environmentId={environmentId}
|
||||
actionClass={actionClass}
|
||||
actionClasses={actionClasses}
|
||||
setOpen={setOpen}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { CodeActionForm } from "@formbricks/ui/organisms/CodeActionForm";
|
||||
import { NoCodeActionForm } from "@formbricks/ui/organisms/NoCodeActionForm";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
actionClass: TActionClass;
|
||||
actionClasses: TActionClass[];
|
||||
setOpen: (v: boolean) => void;
|
||||
@@ -31,7 +30,6 @@ interface ActionSettingsTabProps {
|
||||
}
|
||||
|
||||
export const ActionSettingsTab = ({
|
||||
environmentId,
|
||||
actionClass,
|
||||
actionClasses,
|
||||
setOpen,
|
||||
@@ -104,7 +102,10 @@ export const ActionSettingsTab = ({
|
||||
},
|
||||
}),
|
||||
};
|
||||
await updateActionClassAction(environmentId, actionClass.id, updatedData);
|
||||
await updateActionClassAction({
|
||||
actionClassId: actionClass.id,
|
||||
updatedAction: updatedData,
|
||||
});
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
toast.success("Action updated successfully");
|
||||
@@ -118,7 +119,7 @@ export const ActionSettingsTab = ({
|
||||
const handleDeleteAction = async () => {
|
||||
try {
|
||||
setIsDeletingAction(true);
|
||||
await deleteActionClassAction(environmentId, actionClass.id);
|
||||
await deleteActionClassAction({ actionClassId: actionClass.id });
|
||||
router.refresh();
|
||||
toast.success("Action deleted successfully");
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessIntegration } from "@formbricks/lib/integration/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
|
||||
export const refreshTablesAction = async (environmentId: string) => {
|
||||
return await getAirtableTables(environmentId);
|
||||
};
|
||||
@@ -144,7 +144,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
const actionMessage = isEditMode ? "updated" : "added";
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, airtableIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: airtableIntegrationData });
|
||||
toast.success(`Integration ${actionMessage} successfully`);
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
@@ -176,7 +176,7 @@ export const AddIntegrationModal = ({
|
||||
const integrationData = structuredClone(airtableIntegrationData);
|
||||
integrationData.config.data.splice(index, 1);
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, integrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData });
|
||||
handleClose();
|
||||
router.refresh();
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(airtableIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: airtableIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export const AddIntegrationModal = ({
|
||||
// create action
|
||||
googleSheetIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction(environmentId, googleSheetIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -172,7 +172,7 @@ export const AddIntegrationModal = ({
|
||||
googleSheetIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, googleSheetIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(googleSheetIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: googleSheetIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const refreshDatabasesAction = async (environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await getNotionDatabases(environmentId);
|
||||
};
|
||||
@@ -202,7 +202,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
|
||||
await createOrUpdateIntegrationAction(environmentId, notionIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -217,7 +217,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, notionIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(notionIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: notionIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { getSlackChannels } from "@formbricks/lib/slack/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ export const AddChannelMappingModal = ({
|
||||
// create action
|
||||
slackIntegrationData.config!.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction(environmentId, slackIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success(`Integration ${selectedIntegration ? "updated" : "added"} successfully`);
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
@@ -155,7 +155,7 @@ export const AddChannelMappingModal = ({
|
||||
slackIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction(environmentId, slackIntegrationData);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success("Integration removed successfully");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ManageIntegration = ({
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
setisDeleting(true);
|
||||
await deleteIntegrationAction(slackIntegration.id);
|
||||
await deleteIntegrationAction({ integrationId: slackIntegration.id });
|
||||
setIsConnected(false);
|
||||
toast.success("Integration removed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -40,8 +40,11 @@ export const SlackWrapper = ({
|
||||
>(null);
|
||||
|
||||
const refreshChannels = async () => {
|
||||
const latestSlackChannels = await refreshChannelsAction(environment.id);
|
||||
setSlackChannels(latestSlackChannels);
|
||||
const refreshChannelsResponse = await refreshChannelsAction({ environmentId: environment.id });
|
||||
|
||||
if (refreshChannelsResponse?.data) {
|
||||
setSlackChannels(refreshChannelsResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlackAuthorization = async () => {
|
||||
|
||||
@@ -1,58 +1,77 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessWebhook } from "@formbricks/lib/webhook/auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromWebhookId,
|
||||
} from "@formbricks/lib/organization/utils";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { testEndpoint } from "@formbricks/lib/webhook/utils";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/webhooks";
|
||||
import { ZId } from "@formbricks/types/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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction(testEndpointInput);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
@@ -107,7 +107,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
|
||||
await createWebhookAction(environmentId, updatedData);
|
||||
await createWebhookAction({ environmentId, webhookInput: updatedData });
|
||||
router.refresh();
|
||||
setOpenWithStates(false);
|
||||
toast.success("Webhook added successfully.");
|
||||
|
||||
@@ -6,14 +6,13 @@ import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
|
||||
|
||||
interface WebhookModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
}
|
||||
|
||||
export const WebhookModal = ({ environmentId, open, setOpen, webhook, surveys }: WebhookModalProps) => {
|
||||
export const WebhookModal = ({ open, setOpen, webhook, surveys }: WebhookModalProps) => {
|
||||
const tabs = [
|
||||
{
|
||||
title: "Overview",
|
||||
@@ -21,14 +20,7 @@ export const WebhookModal = ({ environmentId, open, setOpen, webhook, surveys }:
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<WebhookSettingsTab
|
||||
environmentId={environmentId}
|
||||
webhook={webhook}
|
||||
surveys={surveys}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
children: <WebhookSettingsTab webhook={webhook} surveys={surveys} setOpen={setOpen} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -19,13 +19,12 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions";
|
||||
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }: ActionSettingsTabProps) => {
|
||||
export const WebhookSettingsTab = ({ webhook, surveys, setOpen }: ActionSettingsTabProps) => {
|
||||
const router = useRouter();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
@@ -48,7 +47,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction(testEndpointInput);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
@@ -113,7 +112,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
setIsUpdatingWebhook(true);
|
||||
await updateWebhookAction(environmentId, webhook.id, updatedData);
|
||||
await updateWebhookAction({ webhookId: webhook.id, webhookInput: updatedData });
|
||||
toast.success("Webhook updated successfully.");
|
||||
router.refresh();
|
||||
setIsUpdatingWebhook(false);
|
||||
@@ -232,7 +231,7 @@ export const WebhookSettingsTab = ({ environmentId, webhook, surveys, setOpen }:
|
||||
onDelete={async () => {
|
||||
setOpen(false);
|
||||
try {
|
||||
await deleteWebhookAction(webhook.id);
|
||||
await deleteWebhookAction({ id: webhook.id });
|
||||
router.refresh();
|
||||
toast.success("Webhook deleted successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -67,7 +67,6 @@ export const WebhookTable = ({
|
||||
</div>
|
||||
)}
|
||||
<WebhookModal
|
||||
environmentId={environment.id}
|
||||
open={isWebhookDetailModalOpen}
|
||||
setOpen={setWebhookDetailModalOpen}
|
||||
webhook={activeWebhook}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FilesIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||
import { TApiKey } from "@formbricks/types/api-keys";
|
||||
@@ -35,7 +36,7 @@ export const EditAPIKeys = ({
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
try {
|
||||
await deleteApiKeyAction(activeKey.id);
|
||||
await deleteApiKeyAction({ id: activeKey.id });
|
||||
const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || [];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success("API Key deleted");
|
||||
@@ -47,16 +48,20 @@ export const EditAPIKeys = ({
|
||||
};
|
||||
|
||||
const handleAddAPIKey = async (data) => {
|
||||
try {
|
||||
const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label });
|
||||
const updatedApiKeys = [...apiKeysLocal!, apiKey];
|
||||
const createApiKeyResponse = await createApiKeyAction({
|
||||
environmentId: environmentTypeId,
|
||||
apiKeyData: { label: data.label },
|
||||
});
|
||||
if (createApiKeyResponse?.data) {
|
||||
const updatedApiKeys = [...apiKeysLocal!, createApiKeyResponse.data];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success("API key created");
|
||||
} catch (e) {
|
||||
toast.error("Unable to create API Key");
|
||||
} finally {
|
||||
setOpenAddAPIKeyModal(false);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createApiKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
setOpenAddAPIKeyModal(false);
|
||||
};
|
||||
|
||||
const ApiKeyDisplay = ({ apiKey }) => {
|
||||
|
||||
@@ -4,30 +4,11 @@ import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromProductId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
const ZUpdateProductAction = z.object({
|
||||
productId: z.string(),
|
||||
data: ZProductUpdateInput,
|
||||
});
|
||||
|
||||
export const updateProductAction = authenticatedActionClient
|
||||
.schema(ZUpdateProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
schema: ZProductUpdateInput,
|
||||
data: parsedInput.data,
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
rules: ["product", "update"],
|
||||
});
|
||||
|
||||
return await updateProduct(parsedInput.productId, parsedInput.data);
|
||||
});
|
||||
import { deleteProduct, getProducts } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
|
||||
const ZProductDeleteAction = z.object({
|
||||
productId: z.string(),
|
||||
productId: ZId,
|
||||
});
|
||||
|
||||
export const deleteProductAction = authenticatedActionClient
|
||||
|
||||
@@ -4,6 +4,7 @@ import { deleteProductAction } from "@/app/(app)/environments/[environmentId]/pr
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { truncate } from "@formbricks/lib/utils/strings";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -24,19 +25,17 @@ export const DeleteProductRender = ({
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const handleDeleteProduct = async () => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const deletedProductActionResult = await deleteProductAction({ productId: product.id });
|
||||
if (deletedProductActionResult?.data) {
|
||||
toast.success("Product deleted successfully.");
|
||||
router.push("/");
|
||||
}
|
||||
setIsDeleting(false);
|
||||
} catch (err) {
|
||||
setIsDeleting(false);
|
||||
toast.error("Could not delete product.");
|
||||
setIsDeleting(true);
|
||||
const deleteProductResponse = await deleteProductAction({ productId: product.id });
|
||||
if (deleteProductResponse?.data) {
|
||||
toast.success("Product deleted successfully.");
|
||||
router.push("/");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteProductResponse);
|
||||
toast.error(errorMessage);
|
||||
setIsDeleteDialogOpen(false);
|
||||
}
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type EditProductNameProps = {
|
||||
product: TProduct;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { TProduct, ZProduct } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type EditWaitingTimeProps = {
|
||||
product: TProduct;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessProduct, verifyUserRoleAccess } from "@formbricks/lib/product/auth";
|
||||
import { getProduct, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
export const updateProductAction = async (productId: string, inputProduct: TProductUpdateInput) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const product = await getProduct(productId);
|
||||
|
||||
const { hasCreateOrUpdateAccess } = await verifyUserRoleAccess(product!.organizationId, session.user.id);
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateProduct(productId, inputProduct);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
interface EditFormbricksBrandingProps {
|
||||
type: "linkSurvey" | "inAppSurvey";
|
||||
@@ -34,7 +34,7 @@ export const EditFormbricksBranding = ({
|
||||
let inputProduct: Partial<TProductUpdateInput> = {
|
||||
[type === "linkSurvey" ? "linkSurveyBranding" : "inAppSurveyBranding"]: newBrandingState,
|
||||
};
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
await updateProductAction({ productId: product.id, data: inputProduct });
|
||||
toast.success(newBrandingState ? "Formbricks branding is shown." : "Formbricks branding is hidden.");
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
interface EditLogoProps {
|
||||
product: TProduct;
|
||||
@@ -61,7 +61,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
const updatedProduct: TProductUpdateInput = {
|
||||
logo: { url: logoUrl, bgColor: isBgColorEnabled ? logoBgColor : undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
await updateProductAction({ productId: product.id, data: updatedProduct });
|
||||
toast.success("Logo updated successfully");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update the logo");
|
||||
@@ -83,7 +83,7 @@ export const EditLogo = ({ product, environmentId, isViewer }: EditLogoProps) =>
|
||||
const updatedProduct: TProductUpdateInput = {
|
||||
logo: { url: undefined, bgColor: undefined },
|
||||
};
|
||||
await updateProductAction(product.id, updatedProduct);
|
||||
await updateProductAction({ productId: product.id, data: updatedProduct });
|
||||
toast.success("Logo removed successfully", { icon: "🗑️" });
|
||||
} catch (error) {
|
||||
toast.error("Failed to remove the logo");
|
||||
|
||||
@@ -11,7 +11,7 @@ import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@form
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
const placements = [
|
||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||
@@ -53,10 +53,13 @@ export const EditPlacementForm = ({ product }: EditPlacementProps) => {
|
||||
|
||||
const onSubmit: SubmitHandler<EditPlacementFormValues> = async (data) => {
|
||||
try {
|
||||
await updateProductAction(product.id, {
|
||||
placement: data.placement,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
placement: data.placement,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Placement updated successfully.");
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { COLOR_DEFAULTS, PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling, ZProductStyling } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
FormProvider,
|
||||
} from "@formbricks/ui/Form";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { updateProductAction } from "../../actions";
|
||||
|
||||
type ThemeStylingProps = {
|
||||
product: TProduct;
|
||||
@@ -111,8 +112,11 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
},
|
||||
};
|
||||
|
||||
await updateProductAction(product.id, {
|
||||
styling: { ...defaultStyling },
|
||||
await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
styling: { ...defaultStyling },
|
||||
},
|
||||
});
|
||||
|
||||
form.reset({ ...defaultStyling });
|
||||
@@ -122,15 +126,19 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
}, [form, product.id, router]);
|
||||
|
||||
const onSubmit: SubmitHandler<TProductStyling> = async (data) => {
|
||||
try {
|
||||
const updatedProduct = await updateProductAction(product.id, {
|
||||
const updatedProductResponse = await updateProductAction({
|
||||
productId: product.id,
|
||||
data: {
|
||||
styling: data,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
form.reset({ ...updatedProduct.styling });
|
||||
if (updatedProductResponse?.data) {
|
||||
form.reset({ ...updatedProductResponse.data.styling });
|
||||
toast.success("Styling updated successfully.");
|
||||
} catch (err) {
|
||||
toast.error("Error updating styling.");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedProductResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,50 +1,64 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessTag, verifyUserRoleAccess } from "@formbricks/lib/tag/auth";
|
||||
import { deleteTag, getTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromTagId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteTag, mergeTags, updateTagName } from "@formbricks/lib/tag/service";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AlertCircleIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TTag, TTagsCount } from "@formbricks/types/tags";
|
||||
@@ -45,16 +46,16 @@ const SingleTag: React.FC<{
|
||||
const [isMergingTags, setIsMergingTags] = useState(false);
|
||||
const [openDeleteTagDialog, setOpenDeleteTagDialog] = useState(false);
|
||||
|
||||
const confirmDeleteTag = () => {
|
||||
deleteTagAction(tagId)
|
||||
.then((response) => {
|
||||
toast.success(`${response?.name ?? "Tag"} tag deleted`);
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
});
|
||||
const confirmDeleteTag = async () => {
|
||||
const deleteTagResponse = await deleteTagAction({ tagId });
|
||||
if (deleteTagResponse?.data) {
|
||||
toast.success(`${deleteTagResponse?.data.name ?? "Tag"} tag deleted`);
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteTagResponse);
|
||||
toast.error(errorMessage ?? "Something went wrong");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -71,24 +72,25 @@ const SingleTag: React.FC<{
|
||||
)}
|
||||
defaultValue={tagName}
|
||||
onBlur={(e) => {
|
||||
updateTagNameAction(tagId, e.target.value.trim())
|
||||
.then(() => {
|
||||
updateTagNameAction({ tagId, name: e.target.value.trim() }).then((updateTagNameResponse) => {
|
||||
if (updateTagNameResponse?.data) {
|
||||
setUpdateTagError(false);
|
||||
toast.success("Tag updated");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.message.includes("Unique constraint failed on the fields")) {
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateTagNameResponse);
|
||||
if (errorMessage.includes("Unique constraint failed on the fields")) {
|
||||
toast.error("Tag already exists", {
|
||||
duration: 2000,
|
||||
icon: <AlertCircleIcon className="h-5 w-5 text-orange-500" />,
|
||||
});
|
||||
} else {
|
||||
toast.error(error?.message ?? "Something went wrong", {
|
||||
toast.error(errorMessage ?? "Something went wrong", {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
setUpdateTagError(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,18 +115,17 @@ const SingleTag: React.FC<{
|
||||
}
|
||||
onSelect={(newTagId) => {
|
||||
setIsMergingTags(true);
|
||||
mergeTagsAction(tagId, newTagId)
|
||||
.then(() => {
|
||||
mergeTagsAction({ originalTagId: tagId, newTagId }).then((mergeTagsResponse) => {
|
||||
if (mergeTagsResponse?.data) {
|
||||
toast.success("Tags merged");
|
||||
updateTagsCount();
|
||||
router.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message ?? "Something went wrong");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(mergeTagsResponse);
|
||||
toast.error(errorMessage ?? "Something went wrong");
|
||||
}
|
||||
setIsMergingTags(false);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
export const updateNotificationSettingsAction = async (notificationSettings: TUserNotificationSettings) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new AuthorizationError("Not authenticated");
|
||||
}
|
||||
const ZUpdateNotificationSettingsAction = z.object({
|
||||
notificationSettings: ZUserNotificationSettings,
|
||||
});
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings,
|
||||
export const updateNotificationSettingsAction = authenticatedActionClient
|
||||
.schema(ZUpdateNotificationSettingsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await updateUser(ctx.user.id, {
|
||||
notificationSettings: parsedInput.notificationSettings,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const NotificationSwitch = ({
|
||||
!updatedNotificationSettings[notificationType][surveyOrProductOrOrganizationId];
|
||||
}
|
||||
|
||||
await updateNotificationSettingsAction(updatedNotificationSettings);
|
||||
await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings });
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,105 +1,78 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { disableTwoFactorAuth, enableTwoFactorAuth, setupTwoFactorAuth } from "@formbricks/lib/auth/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { deleteFile } from "@formbricks/lib/storage/service";
|
||||
import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { getUser, updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserUpdateInput } from "@formbricks/types/user";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/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 });
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
@@ -43,14 +44,16 @@ export const DisableTwoFactorModal = ({ open, setOpen }: TDisableTwoFactorModalP
|
||||
const onSubmit: SubmitHandler<TDisableTwoFactorFormState> = async (data) => {
|
||||
const { code, password, backupCode } = data;
|
||||
|
||||
try {
|
||||
const { message } = await disableTwoFactorAuthAction({ code, password, backupCode });
|
||||
toast.success(message);
|
||||
const disableTwoFactorAuthResponse = await disableTwoFactorAuthAction({ code, password, backupCode });
|
||||
|
||||
if (disableTwoFactorAuthResponse?.data) {
|
||||
toast.success(disableTwoFactorAuthResponse.data.message);
|
||||
|
||||
router.refresh();
|
||||
resetState();
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(disableTwoFactorAuthResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
try {
|
||||
if (imageUrl) {
|
||||
// If avatar image already exists, then remove it before update action
|
||||
await removeAvatarAction(environmentId);
|
||||
await removeAvatarAction({ environmentId });
|
||||
}
|
||||
const { url, error } = await handleFileUpload(file, environmentId);
|
||||
|
||||
@@ -70,7 +70,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAvatarAction(url);
|
||||
await updateAvatarAction({ avatarUrl: url });
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast.error("Avatar update failed. Please try again.");
|
||||
@@ -84,7 +84,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await removeAvatarAction(environmentId);
|
||||
await removeAvatarAction({ environmentId });
|
||||
} catch (err) {
|
||||
toast.error("Avatar update failed. Please try again.");
|
||||
} finally {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import { OTPInput } from "@formbricks/ui/OTPInput";
|
||||
@@ -46,16 +47,17 @@ const ConfirmPasswordForm = ({
|
||||
const { control, handleSubmit, setError } = useForm<TConfirmPasswordFormState>();
|
||||
|
||||
const onSubmit: SubmitHandler<TConfirmPasswordFormState> = async (data) => {
|
||||
try {
|
||||
const { backupCodes, dataUri, secret } = await setupTwoFactorAuthAction(data.password);
|
||||
const setupTwoFactorAuthResponse = await setupTwoFactorAuthAction({ password: data.password });
|
||||
|
||||
if (setupTwoFactorAuthResponse?.data) {
|
||||
const { backupCodes, dataUri, secret } = setupTwoFactorAuthResponse.data;
|
||||
setBackupCodes(backupCodes);
|
||||
setDataUri(dataUri);
|
||||
setSecret(secret);
|
||||
|
||||
setCurrentStep("scanQRCode");
|
||||
} catch (err) {
|
||||
setError("password", { message: err.message });
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(setupTwoFactorAuthResponse);
|
||||
setError("password", { message: errorMessage });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,12 +156,14 @@ const EnterCode = ({ setCurrentStep, setOpen, refreshData }: TEnableCodeProps) =
|
||||
|
||||
const onSubmit: SubmitHandler<TEnterCodeFormState> = async (data) => {
|
||||
try {
|
||||
const { message } = await enableTwoFactorAuthAction(data.code);
|
||||
toast.success(message);
|
||||
setCurrentStep("backupCodes");
|
||||
const enableTwoFactorAuthResponse = await enableTwoFactorAuthAction({ code: data.code });
|
||||
if (enableTwoFactorAuthResponse?.data) {
|
||||
toast.success(enableTwoFactorAuthResponse.data.message);
|
||||
setCurrentStep("backupCodes");
|
||||
|
||||
// refresh data to update the UI
|
||||
refreshData();
|
||||
// refresh data to update the UI
|
||||
refreshData();
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
}
|
||||
|
||||
@@ -1,84 +1,83 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { createCustomerPortalSession } from "@formbricks/ee/billing/lib/create-customer-portal-session";
|
||||
import { createSubscription } from "@formbricks/ee/billing/lib/create-subscription";
|
||||
import { isSubscriptionCancelled } from "@formbricks/ee/billing/lib/is-subscription-cancelled";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -57,26 +57,39 @@ export const PricingTable = ({
|
||||
|
||||
useEffect(() => {
|
||||
const checkSubscriptionStatus = async () => {
|
||||
const isCancelled = await isSubscriptionCancelledAction(organization.id);
|
||||
if (isCancelled) {
|
||||
setCancellingOn(isCancelled.date);
|
||||
const isSubscriptionCancelledResponse = await isSubscriptionCancelledAction({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
if (isSubscriptionCancelledResponse?.data) {
|
||||
setCancellingOn(isSubscriptionCancelledResponse.data.date);
|
||||
}
|
||||
};
|
||||
checkSubscriptionStatus();
|
||||
}, [organization.id]);
|
||||
|
||||
const openCustomerPortal = async () => {
|
||||
const sessionUrl = await manageSubscriptionAction(organization.id, environmentId);
|
||||
router.push(sessionUrl);
|
||||
const manageSubscriptionResponse = await manageSubscriptionAction({
|
||||
organizationId: organization.id,
|
||||
environmentId,
|
||||
});
|
||||
if (manageSubscriptionResponse?.data) {
|
||||
router.push(manageSubscriptionResponse.data);
|
||||
}
|
||||
};
|
||||
|
||||
const upgradePlan = async (priceLookupKey) => {
|
||||
try {
|
||||
const { status, newPlan, url } = await upgradePlanAction(
|
||||
organization.id,
|
||||
const upgradePlanResponse = await upgradePlanAction({
|
||||
organizationId: organization.id,
|
||||
environmentId,
|
||||
priceLookupKey
|
||||
);
|
||||
priceLookupKey,
|
||||
});
|
||||
|
||||
if (!upgradePlanResponse?.data) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
|
||||
const { status, newPlan, url } = upgradePlanResponse.data;
|
||||
|
||||
if (status != 200) {
|
||||
throw new Error("Something went wrong");
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { sendInviteMemberEmail } from "@formbricks/email";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { INVITE_DISABLED } from "@formbricks/lib/constants";
|
||||
import { deleteInvite, getInvite, inviteUser, resendInvite } from "@formbricks/lib/invite/service";
|
||||
import { createInviteToken } from "@formbricks/lib/jwt";
|
||||
@@ -16,20 +13,15 @@ import {
|
||||
getMembershipByUserIdOrganizationId,
|
||||
getMembershipsByUserId,
|
||||
} from "@formbricks/lib/membership/service";
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import {
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
OperationNotAllowedError,
|
||||
ValidationError,
|
||||
} from "@formbricks/types/errors";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { getOrganizationIdFromInviteId } from "@formbricks/lib/organization/utils";
|
||||
import { ZId } 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);
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DeleteOrganization = ({
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
await deleteOrganizationAction(organization.id);
|
||||
await deleteOrganizationAction({ organizationId: organization.id });
|
||||
toast.success("Organization deleted successfully.");
|
||||
router.push("/");
|
||||
} catch (err) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SendHorizonalIcon, ShareIcon, TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { TInvite } from "@formbricks/types/invites";
|
||||
import { TMember } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
@@ -39,14 +40,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
if (!member && invite) {
|
||||
// This is an invite
|
||||
|
||||
await deleteInviteAction(invite?.id, organization.id);
|
||||
await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id });
|
||||
toast.success("Invite deleted successfully");
|
||||
}
|
||||
|
||||
if (member && !invite) {
|
||||
// This is a member
|
||||
|
||||
await deleteMembershipAction(member.userId, organization.id);
|
||||
await deleteMembershipAction({ userId: member.userId, organizationId: organization.id });
|
||||
toast.success("Member deleted successfully");
|
||||
}
|
||||
|
||||
@@ -74,9 +75,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
const handleShareInvite = async () => {
|
||||
try {
|
||||
if (!invite) return;
|
||||
const { inviteToken } = await createInviteTokenAction(invite.id);
|
||||
setShareInviteToken(inviteToken);
|
||||
setShowShareInviteModal(true);
|
||||
const createInviteTokenResponse = await createInviteTokenAction({ inviteId: invite.id });
|
||||
if (createInviteTokenResponse?.data) {
|
||||
setShareInviteToken(createInviteTokenResponse.data.inviteToken);
|
||||
setShowShareInviteModal(true);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createInviteTokenResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`Error: ${err.message}`);
|
||||
}
|
||||
@@ -86,7 +92,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
try {
|
||||
if (!invite) return;
|
||||
|
||||
await resendInviteAction(invite.id, organization.id);
|
||||
await resendInviteAction({ inviteId: invite.id, organizationId: organization.id });
|
||||
toast.success("Invitation sent once more.");
|
||||
} catch (err) {
|
||||
toast.error(`Error: ${err.message}`);
|
||||
|
||||
@@ -47,7 +47,7 @@ export const OrganizationActions = ({
|
||||
const handleLeaveOrganization = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await leaveOrganizationAction(organization.id);
|
||||
await leaveOrganizationAction({ organizationId: organization.id });
|
||||
toast.success("You left the organization successfully");
|
||||
router.refresh();
|
||||
setLoading(false);
|
||||
@@ -62,7 +62,7 @@ export const OrganizationActions = ({
|
||||
try {
|
||||
await Promise.all(
|
||||
data.map(async ({ name, email, role }) => {
|
||||
await inviteUserAction(organization.id, email, name, role);
|
||||
await inviteUserAction({ organizationId: organization.id, email, name, role });
|
||||
})
|
||||
);
|
||||
toast.success("Member invited successfully");
|
||||
|
||||
@@ -1,56 +1,72 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
|
||||
import { getResponseCountBySurveyId, getResponses, getSurveySummary } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -48,23 +48,29 @@ export const SurveyAnalysisNavigation = ({
|
||||
latestFiltersRef.current = filters;
|
||||
|
||||
const getResponseCount = () => {
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey);
|
||||
return getResponseCountAction(survey.id);
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction({ sharingKey });
|
||||
return getResponseCountAction({ surveyId: survey.id });
|
||||
};
|
||||
|
||||
const fetchResponseCount = async () => {
|
||||
const count = await getResponseCount();
|
||||
setTotalResponseCount(count);
|
||||
const responseCount = count?.data ?? 0;
|
||||
setTotalResponseCount(responseCount);
|
||||
};
|
||||
|
||||
const getFilteredResponseCount = () => {
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey, latestFiltersRef.current);
|
||||
return getResponseCountAction(survey.id, latestFiltersRef.current);
|
||||
if (isSharingPage)
|
||||
return getResponseCountBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
return getResponseCountAction({ surveyId: survey.id, filterCriteria: latestFiltersRef.current });
|
||||
};
|
||||
|
||||
const fetchFilteredResponseCount = async () => {
|
||||
const count = await getFilteredResponseCount();
|
||||
setFilteredResponseCount(count);
|
||||
const responseCount = count?.data ?? 0;
|
||||
setFilteredResponseCount(responseCount);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { canUserAccessTagOnResponse } from "@formbricks/lib/tagOnResponse/auth";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const createTagAction = async (environmentId: string, tagName: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createTag(environmentId, tagName);
|
||||
};
|
||||
|
||||
export const createTagToResponeAction = async (responseId: string, tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await addTagToRespone(responseId, tagId);
|
||||
};
|
||||
|
||||
export const deleteTagOnResponseAction = async (responseId: string, tagId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessTagOnResponse(session.user.id, tagId, responseId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteTagOnResponse(responseId, tagId);
|
||||
};
|
||||
@@ -69,19 +69,21 @@ export const ResponsePage = ({
|
||||
let newResponses: TResponse[] = [];
|
||||
|
||||
if (isSharingPage) {
|
||||
newResponses = await getResponsesBySurveySharingKeyAction(
|
||||
sharingKey,
|
||||
responsesPerPage,
|
||||
(newPage - 1) * responsesPerPage,
|
||||
filters
|
||||
);
|
||||
const getResponsesActionResponse = await getResponsesBySurveySharingKeyAction({
|
||||
sharingKey: sharingKey,
|
||||
limit: responsesPerPage,
|
||||
offset: (newPage - 1) * responsesPerPage,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
newResponses = getResponsesActionResponse?.data || [];
|
||||
} else {
|
||||
newResponses = await getResponsesAction(
|
||||
const getResponsesActionResponse = await getResponsesAction({
|
||||
surveyId,
|
||||
responsesPerPage,
|
||||
(newPage - 1) * responsesPerPage,
|
||||
filters
|
||||
);
|
||||
limit: responsesPerPage,
|
||||
offset: (newPage - 1) * responsesPerPage,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
newResponses = getResponsesActionResponse?.data || [];
|
||||
}
|
||||
|
||||
if (newResponses.length === 0 || newResponses.length < responsesPerPage) {
|
||||
@@ -113,9 +115,17 @@ export const ResponsePage = ({
|
||||
let responseCount = 0;
|
||||
|
||||
if (isSharingPage) {
|
||||
responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
|
||||
const responseCountActionResponse = await getResponseCountBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
responseCount = responseCountActionResponse?.data || 0;
|
||||
} else {
|
||||
responseCount = await getResponseCountAction(surveyId, filters);
|
||||
const responseCountActionResponse = await getResponseCountAction({
|
||||
surveyId,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
responseCount = responseCountActionResponse?.data || 0;
|
||||
}
|
||||
|
||||
setResponseCount(responseCount);
|
||||
@@ -131,9 +141,23 @@ export const ResponsePage = ({
|
||||
let responses: TResponse[] = [];
|
||||
|
||||
if (isSharingPage) {
|
||||
responses = await getResponsesBySurveySharingKeyAction(sharingKey, responsesPerPage, 0, filters);
|
||||
const getResponsesActionResponse = await getResponsesBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
limit: responsesPerPage,
|
||||
offset: 0,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
|
||||
responses = getResponsesActionResponse?.data || [];
|
||||
} else {
|
||||
responses = await getResponsesAction(surveyId, responsesPerPage, 0, filters);
|
||||
const getResponsesActionResponse = await getResponsesAction({
|
||||
surveyId,
|
||||
limit: responsesPerPage,
|
||||
offset: 0,
|
||||
filterCriteria: filters,
|
||||
});
|
||||
|
||||
responses = getResponsesActionResponse?.data || [];
|
||||
}
|
||||
|
||||
if (responses.length < responsesPerPage) {
|
||||
|
||||
@@ -2,106 +2,141 @@
|
||||
|
||||
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@formbricks/email";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZId } from "@formbricks/types/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&", "?")
|
||||
.replaceAll("?preview=true&;", "?")
|
||||
.replaceAll("?preview=true", "");
|
||||
const rawEmailHtml = await getEmailTemplateHtml(parsedInput.surveyId);
|
||||
const emailHtml = rawEmailHtml
|
||||
.replaceAll("?preview=true&", "?")
|
||||
.replaceAll("?preview=true&;", "?")
|
||||
.replaceAll("?preview=true", "");
|
||||
|
||||
return await sendEmbedSurveyPreviewEmail(
|
||||
user.email,
|
||||
"Formbricks Email Survey Preview",
|
||||
emailHtml,
|
||||
survey.environmentId
|
||||
);
|
||||
};
|
||||
return await sendEmbedSurveyPreviewEmail(
|
||||
ctx.user.email,
|
||||
"Formbricks Email Survey Preview",
|
||||
emailHtml,
|
||||
survey.environmentId
|
||||
);
|
||||
});
|
||||
|
||||
export const generateResultShareUrlAction = async (surveyId: string): Promise<string> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZGenerateResultShareUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
export const generateResultShareUrlAction = authenticatedActionClient
|
||||
.schema(ZGenerateResultShareUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "update"],
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey?.id) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
const resultShareKey = customAlphabet(
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
20
|
||||
)();
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
await updateSurvey({ ...survey, resultShareKey });
|
||||
const resultShareKey = customAlphabet(
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
20
|
||||
)();
|
||||
|
||||
return resultShareKey;
|
||||
};
|
||||
await updateSurvey({ ...survey, resultShareKey });
|
||||
|
||||
export const getResultShareUrlAction = async (surveyId: string): Promise<string | null> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return resultShareKey;
|
||||
});
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZGetResultShareUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey?.id) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
export const getResultShareUrlAction = authenticatedActionClient
|
||||
.schema(ZGetResultShareUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "read"],
|
||||
});
|
||||
|
||||
return survey.resultShareKey;
|
||||
};
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
export const deleteResultShareUrlAction = async (surveyId: string): Promise<void> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
return survey.resultShareKey;
|
||||
});
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
const ZDeleteResultShareUrlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey?.id) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
export const deleteResultShareUrlAction = authenticatedActionClient
|
||||
.schema(ZDeleteResultShareUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["response", "update"],
|
||||
});
|
||||
|
||||
await updateSurvey({ ...survey, resultShareKey: null });
|
||||
};
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "update"],
|
||||
});
|
||||
|
||||
export const getEmailHtmlAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
return await updateSurvey({ ...survey, resultShareKey: null });
|
||||
});
|
||||
|
||||
return await getEmailTemplateHtml(surveyId);
|
||||
};
|
||||
const ZGetEmailHtmlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const getEmailHtmlAction = authenticatedActionClient
|
||||
.schema(ZGetEmailHtmlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
rules: ["survey", "read"],
|
||||
});
|
||||
|
||||
return await getEmailTemplateHtml(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
@@ -79,22 +79,40 @@ export const SummaryPage = ({
|
||||
latestFiltersRef.current = filters;
|
||||
|
||||
const getResponseCount = () => {
|
||||
if (isSharingPage) return getResponseCountBySurveySharingKeyAction(sharingKey, latestFiltersRef.current);
|
||||
return getResponseCountAction(surveyId, latestFiltersRef.current);
|
||||
if (isSharingPage)
|
||||
return getResponseCountBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
return getResponseCountAction({
|
||||
surveyId,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
const getSummary = () => {
|
||||
if (isSharingPage) return getSummaryBySurveySharingKeyAction(sharingKey, latestFiltersRef.current);
|
||||
return getSurveySummaryAction(surveyId, latestFiltersRef.current);
|
||||
if (isSharingPage)
|
||||
return getSummaryBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
|
||||
return getSurveySummaryAction({
|
||||
surveyId,
|
||||
filterCriteria: latestFiltersRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
const handleInitialData = async () => {
|
||||
try {
|
||||
const updatedResponseCount = await getResponseCount();
|
||||
const updatedResponseCountData = await getResponseCount();
|
||||
const updatedSurveySummary = await getSummary();
|
||||
|
||||
setResponseCount(updatedResponseCount);
|
||||
setSurveySummary(updatedSurveySummary);
|
||||
const responseCount = updatedResponseCountData?.data ?? 0;
|
||||
const surveySummary = updatedSurveySummary?.data ?? initialSurveySummary;
|
||||
|
||||
setResponseCount(responseCount);
|
||||
setSurveySummary(surveySummary);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const emailHtml = await getEmailHtmlAction(surveyId);
|
||||
setEmailHtmlPreview(emailHtml);
|
||||
const emailHtml = await getEmailHtmlAction({ surveyId });
|
||||
setEmailHtmlPreview(emailHtml?.data || "");
|
||||
};
|
||||
|
||||
getData();
|
||||
@@ -37,7 +37,7 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
|
||||
|
||||
const sendPreviewEmail = async () => {
|
||||
try {
|
||||
await sendEmbedSurveyPreviewEmailAction(surveyId);
|
||||
await sendEmbedSurveyPreviewEmailAction({ surveyId });
|
||||
toast.success("Email sent!");
|
||||
} catch (err) {
|
||||
if (err instanceof AuthenticationError) {
|
||||
|
||||
@@ -1,53 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { getOrganizationIdFromSurveyId } from "@formbricks/lib/organization/utils";
|
||||
import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ZId } from "@formbricks/types/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);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ArrowDownToLineIcon, ChevronDown, ChevronUp, DownloadIcon } from "lucid
|
||||
import { useParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Calendar } from "@formbricks/ui/Calendar";
|
||||
@@ -172,13 +173,20 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
const handleDowndloadResponses = async (filter: FilterDownload, filetype: "csv" | "xlsx") => {
|
||||
try {
|
||||
const responseFilters = filter === FilterDownload.ALL ? {} : filters;
|
||||
const fileUrl = await getResponsesDownloadUrlAction(survey.id, filetype, responseFilters);
|
||||
if (fileUrl) {
|
||||
const responsesDownloadUrlResponse = await getResponsesDownloadUrlAction({
|
||||
surveyId: survey.id,
|
||||
format: filetype,
|
||||
filterCriteria: responseFilters,
|
||||
});
|
||||
if (responsesDownloadUrlResponse?.data) {
|
||||
const link = document.createElement("a");
|
||||
link.href = fileUrl;
|
||||
link.href = responsesDownloadUrlResponse.data;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(responsesDownloadUrlResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Error downloading responses");
|
||||
|
||||
@@ -42,10 +42,16 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
// Fetch the initial data for the filter and load it into the state
|
||||
const handleInitialData = async () => {
|
||||
if (isOpen) {
|
||||
const { attributes, meta, environmentTags, hiddenFields } = isSharingPage
|
||||
? await getSurveyFilterDataBySurveySharingKeyAction(sharingKey, survey.environmentId)
|
||||
: await getSurveyFilterDataAction(survey.id, survey.environmentId);
|
||||
const surveyFilterData = isSharingPage
|
||||
? await getSurveyFilterDataBySurveySharingKeyAction({
|
||||
sharingKey,
|
||||
environmentId: survey.environmentId,
|
||||
})
|
||||
: await getSurveyFilterDataAction({ surveyId: survey.id });
|
||||
|
||||
if (!surveyFilterData?.data) return;
|
||||
|
||||
const { attributes, meta, environmentTags, hiddenFields } = surveyFilterData.data;
|
||||
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
|
||||
survey,
|
||||
environmentTags,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { CopyIcon, DownloadIcon, GlobeIcon, LinkIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -29,27 +30,33 @@ export const ResultsShareButton = ({ survey, webAppUrl }: ResultsShareButtonProp
|
||||
const [surveyUrl, setSurveyUrl] = useState("");
|
||||
|
||||
const handlePublish = async () => {
|
||||
const key = await generateResultShareUrlAction(survey.id);
|
||||
setSurveyUrl(webAppUrl + "/share/" + key);
|
||||
setShowPublishModal(true);
|
||||
const resultShareKeyResponse = await generateResultShareUrlAction({ surveyId: survey.id });
|
||||
if (resultShareKeyResponse?.data) {
|
||||
setSurveyUrl(webAppUrl + "/share/" + resultShareKeyResponse.data);
|
||||
setShowPublishModal(true);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(resultShareKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnpublish = () => {
|
||||
deleteResultShareUrlAction(survey.id)
|
||||
.then(() => {
|
||||
deleteResultShareUrlAction({ surveyId: survey.id }).then((deleteResultShareUrlResponse) => {
|
||||
if (deleteResultShareUrlResponse?.data) {
|
||||
toast.success("Results unpublished successfully.");
|
||||
setShowPublishModal(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteResultShareUrlResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSharingKey = async () => {
|
||||
const sharingKey = await getResultShareUrlAction(survey.id);
|
||||
if (sharingKey) {
|
||||
setSurveyUrl(webAppUrl + "/share/" + sharingKey);
|
||||
const resultShareUrlResponse = await getResultShareUrlAction({ surveyId: survey.id });
|
||||
if (resultShareUrlResponse?.data) {
|
||||
setSurveyUrl(webAppUrl + "/share/" + resultShareUrlResponse.data);
|
||||
setShowPublishModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SurveyStatusDropdown = ({
|
||||
disabled={isStatusChangeDisabled}
|
||||
onValueChange={(value) => {
|
||||
const castedValue = value as TSurvey["status"];
|
||||
updateSurveyAction({ ...survey, status: castedValue })
|
||||
updateSurveyAction({ survey: { ...survey, status: castedValue } })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
value === "inProgress"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
|
||||
import { LinkSurvey } from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -53,12 +54,15 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
const [survey, setSurvey] = useState<TSurvey>();
|
||||
|
||||
const _validateSurveyPinAsync = useCallback(async (surveyId: string, pin: string) => {
|
||||
const response = await validateSurveyPinAction(surveyId, pin);
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else if (response.survey) {
|
||||
setSurvey(response.survey);
|
||||
const response = await validateSurveyPinAction({ surveyId, pin });
|
||||
|
||||
if (response?.data) {
|
||||
setSurvey(response.data.survey);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(response) as TSurveyPinValidationResponseError;
|
||||
setError(errorMessage);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export const VerifyEmail = ({
|
||||
}
|
||||
const data = {
|
||||
surveyId: survey.id,
|
||||
email: email,
|
||||
email: email as string,
|
||||
surveyName: survey.name,
|
||||
suId: singleUseId ?? "",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
for (const email of emails) {
|
||||
try {
|
||||
if (!email) continue;
|
||||
await inviteOrganizationMemberAction(email, organizationId);
|
||||
await inviteOrganizationMemberAction({ email, organizationId });
|
||||
if (IS_SMTP_CONFIGURED) {
|
||||
toast.success(`Invitation sent to ${email}!`);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
"use server";
|
||||
|
||||
import { Organization } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { gethasNoOrganizations } from "@formbricks/lib/instance/service";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
|
||||
export const createOrganizationAction = async (organizationName: string): Promise<Organization> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrganizationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: organizationName,
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, session.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
};
|
||||
|
||||
@@ -33,8 +33,10 @@ export const CreateOrganization = () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const organizationName = data.name.trim();
|
||||
const organization = await createOrganizationAction(organizationName);
|
||||
router.push(`/setup/organization/${organization.id}/invite`);
|
||||
const createOrganizationResponse = await createOrganizationAction({ organizationName });
|
||||
if (createOrganizationResponse?.data) {
|
||||
router.push(`/setup/organization/${createOrganizationResponse.data.id}/invite`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Some error occurred while creating organization");
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { actionClient } from "@formbricks/lib/actionClient";
|
||||
import {
|
||||
getResponseCountBySurveyId,
|
||||
getResponseFilteringValues,
|
||||
@@ -8,54 +10,75 @@ import {
|
||||
} from "@formbricks/lib/response/service";
|
||||
import { getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { ZId } from "@formbricks/types/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 };
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 it’s 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 it’s 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.");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user