mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
fix: license checks in server actions (#4274)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/lib/utils/helper";
|
||||
import { getSegment, getSurvey } from "@/lib/utils/services";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
|
||||
import { z } from "zod";
|
||||
import { createActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
|
||||
@@ -43,7 +44,7 @@ import { ZSurvey } from "@formbricks/types/surveys/types";
|
||||
const checkSurveyFollowUpsPermission = async (organizationId: string): Promise<void> => {
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization not found", organizationId);
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const isSurveyFollowUpsEnabled = await getSurveyFollowUpsPermission(organization);
|
||||
@@ -76,6 +77,10 @@ export const updateSurveyAction = authenticatedActionClient
|
||||
await checkSurveyFollowUpsPermission(organizationId);
|
||||
}
|
||||
|
||||
if (parsedInput.languages?.length) {
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
}
|
||||
|
||||
return await updateSurvey(parsedInput);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { generateObject } from "ai";
|
||||
import { z } from "zod";
|
||||
import { llmModel } from "@formbricks/lib/aiModels";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getIsAIEnabled } from "@formbricks/lib/utils/ai";
|
||||
import { ZId, ZString } from "@formbricks/types/common";
|
||||
import { ZSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getIsMultiOrgEnabled, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createOrganization, getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
@@ -70,6 +70,8 @@ export const createProductAction = authenticatedActionClient
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
const { user } = ctx;
|
||||
|
||||
const organizationId = parsedInput.organizationId;
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
@@ -83,6 +85,20 @@ export const createProductAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization);
|
||||
|
||||
if (!canDoRoleManagement) {
|
||||
throw new OperationNotAllowedError("You do not have permission to manage roles");
|
||||
}
|
||||
}
|
||||
|
||||
const product = await createProduct(parsedInput.organizationId, parsedInput.data);
|
||||
const updatedNotificationSettings = {
|
||||
...user.notificationSettings,
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromProductId } from "@/lib/utils/helper";
|
||||
import {
|
||||
getRemoveInAppBrandingPermission,
|
||||
getRemoveLinkBrandingPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZProductUpdateInput } from "@formbricks/types/product";
|
||||
|
||||
const ZUpdateProductAction = z.object({
|
||||
@@ -16,9 +22,11 @@ const ZUpdateProductAction = z.object({
|
||||
export const updateProductAction = authenticatedActionClient
|
||||
.schema(ZUpdateProductAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
schema: ZProductUpdateInput,
|
||||
@@ -34,5 +42,30 @@ export const updateProductAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
if (
|
||||
parsedInput.data.inAppSurveyBranding !== undefined ||
|
||||
parsedInput.data.linkSurveyBranding !== undefined
|
||||
) {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
if (parsedInput.data.inAppSurveyBranding !== undefined) {
|
||||
const canRemoveInAppBranding = getRemoveInAppBrandingPermission(organization);
|
||||
if (!canRemoveInAppBranding) {
|
||||
throw new OperationNotAllowedError("You are not allowed to remove in-app branding");
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedInput.data.linkSurveyBranding !== undefined) {
|
||||
const canRemoveLinkSurveyBranding = getRemoveLinkBrandingPermission(organization);
|
||||
if (!canRemoveLinkSurveyBranding) {
|
||||
throw new OperationNotAllowedError("You are not allowed to remove link survey branding");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await updateProduct(parsedInput.productId, parsedInput.data);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromInviteId } from "@/lib/utils/helper";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getIsMultiOrgEnabled, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { sendInviteMemberEmail } from "@/modules/email";
|
||||
import { OrganizationRole } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
@@ -15,7 +15,12 @@ import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"
|
||||
import { deleteInvite, getInvite, inviteUser, resendInvite } from "@formbricks/lib/invite/service";
|
||||
import { createInviteToken } from "@formbricks/lib/jwt";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import {
|
||||
deleteOrganization,
|
||||
getOrganization,
|
||||
updateOrganization,
|
||||
} from "@formbricks/lib/organization/service";
|
||||
import { ZId, ZUuid } from "@formbricks/types/common";
|
||||
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
@@ -45,30 +50,6 @@ export const updateOrganizationNameAction = authenticatedActionClient
|
||||
return await updateOrganization(parsedInput.organizationId, parsedInput.data);
|
||||
});
|
||||
|
||||
const ZUpdateOrganizationAIEnabledAction = z.object({
|
||||
organizationId: ZId,
|
||||
data: ZOrganizationUpdateInput.pick({ isAIEnabled: true }),
|
||||
});
|
||||
|
||||
export const updateOrganizationAIEnabledAction = authenticatedActionClient
|
||||
.schema(ZUpdateOrganizationAIEnabledAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
schema: ZOrganizationUpdateInput.pick({ isAIEnabled: true }),
|
||||
data: parsedInput.data,
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await updateOrganization(parsedInput.organizationId, parsedInput.data);
|
||||
});
|
||||
|
||||
const ZDeleteInviteAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
organizationId: ZId,
|
||||
@@ -152,8 +133,18 @@ export const leaveOrganizationAction = authenticatedActionClient
|
||||
throw new AuthenticationError("Not a member of this organization");
|
||||
}
|
||||
|
||||
if (membership.role === "owner") {
|
||||
throw new ValidationError("You cannot leave an organization you own");
|
||||
const { isOwner } = getAccessFlags(membership.role);
|
||||
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
if (isOwner) {
|
||||
throw new OperationNotAllowedError("You cannot leave an organization you own");
|
||||
}
|
||||
|
||||
if (!isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError(
|
||||
"You cannot leave the organization because you are the only owner and organization deletion is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
const memberships = await getMembershipsByUserId(ctx.user.id);
|
||||
@@ -264,6 +255,19 @@ export const inviteUserAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
if (parsedInput.role !== "owner") {
|
||||
const organization = await getOrganization(parsedInput.organizationId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization);
|
||||
|
||||
if (!canDoRoleManagement) {
|
||||
throw new OperationNotAllowedError("Role management is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
const invite = await inviteUser({
|
||||
organizationId: parsedInput.organizationId,
|
||||
invitee: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { updateOrganizationAIEnabledAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { updateOrganizationAIEnabledAction } from "@/modules/ee/insights/actions";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
|
||||
@@ -16,7 +16,6 @@ type DeleteOrganizationProps = {
|
||||
organization: TOrganization;
|
||||
isDeleteDisabled?: boolean;
|
||||
isUserOwner?: boolean;
|
||||
isMultiOrgEnabled: boolean;
|
||||
};
|
||||
|
||||
export const DeleteOrganization = ({
|
||||
|
||||
@@ -2,8 +2,11 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmen
|
||||
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
||||
import { OrganizationActions } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditMemberships/OrganizationActions";
|
||||
import { getMembershipsByUserId } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/lib/membership";
|
||||
import { getIsOrganizationAIReady } from "@/app/lib/utils";
|
||||
import { getIsMultiOrgEnabled, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsOrganizationAIReady,
|
||||
getRoleManagementPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
@@ -124,7 +127,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
organization={organization}
|
||||
isDeleteDisabled={isDeleteDisabled}
|
||||
isUserOwner={currentUserRole === "owner"}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
/>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use server";
|
||||
|
||||
import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper";
|
||||
@@ -108,31 +107,3 @@ export const getResponseCountAction = authenticatedActionClient
|
||||
|
||||
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
|
||||
});
|
||||
|
||||
const ZGenerateInsightsForSurveyAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const generateInsightsForSurveyAction = authenticatedActionClient
|
||||
.schema(ZGenerateInsightsForSurveyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
schema: ZGenerateInsightsForSurveyAction,
|
||||
data: parsedInput,
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
generateInsightsForSurvey(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[
|
||||
import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { getIsAIEnabled } from "@/app/lib/utils";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { generateInsightsForSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { generateInsightsForSurveyAction } from "@/modules/ee/insights/actions";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/s
|
||||
import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { getIsAIEnabled } from "@/app/lib/utils";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
|
||||
import { z } from "zod";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service";
|
||||
@@ -126,5 +127,9 @@ export const updateSurveyAction = authenticatedActionClient
|
||||
await checkSurveyFollowUpsPermission(organizationId);
|
||||
}
|
||||
|
||||
if (parsedInput.languages?.length) {
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
}
|
||||
|
||||
return await updateSurvey(parsedInput);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createDocumentAndAssignInsight } from "@/app/api/(internal)/pipeline/li
|
||||
import { sendSurveyFollowUps } from "@/app/api/(internal)/pipeline/lib/survey-follow-up";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getIsAIEnabled } from "@/app/lib/utils";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { sendResponseFinishedEmail } from "@/modules/email";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getMultiLanguagePermission,
|
||||
getSurveyFollowUpsPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { deleteSurvey, getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TSurvey, ZSurveyUpdateInput } from "@formbricks/types/surveys/types";
|
||||
@@ -100,6 +103,13 @@ export const PUT = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (surveyUpdate.languages && surveyUpdate.languages.length) {
|
||||
const isMultiLanguageEnabled = await getMultiLanguagePermission(organization);
|
||||
if (!isMultiLanguageEnabled) {
|
||||
return responses.forbiddenResponse("Multi language is not enabled for this organization");
|
||||
}
|
||||
}
|
||||
|
||||
return responses.successResponse(await updateSurvey({ ...inputValidation.data, id: params.surveyId }));
|
||||
} catch (error) {
|
||||
return handleErrorResponse(error);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
getMultiLanguagePermission,
|
||||
getSurveyFollowUpsPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
@@ -64,6 +67,13 @@ export const POST = async (request: Request): Promise<Response> => {
|
||||
}
|
||||
}
|
||||
|
||||
if (surveyData.languages && surveyData.languages.length) {
|
||||
const isMultiLanguageEnabled = await getMultiLanguagePermission(organization);
|
||||
if (!isMultiLanguageEnabled) {
|
||||
return responses.forbiddenResponse("Multi language is not enabled for this organization");
|
||||
}
|
||||
}
|
||||
|
||||
const survey = await createSurvey(environmentId, surveyData);
|
||||
return responses.successResponse(survey);
|
||||
} catch (error) {
|
||||
|
||||
@@ -113,7 +113,6 @@ export const POST = async (request: Request) => {
|
||||
}
|
||||
// Without default organization assignment
|
||||
else {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (isMultiOrgEnabled) {
|
||||
const organization = await createOrganization({ name: user.name + "'s Organization" });
|
||||
await createMembership(organization.id, user.id, { role: "owner", accepted: true });
|
||||
|
||||
@@ -1,27 +1,7 @@
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { IS_AI_CONFIGURED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { TInvite } from "@formbricks/types/invites";
|
||||
import { TOrganization, TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
|
||||
export const isInviteExpired = (invite: TInvite) => {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(invite.expiresAt);
|
||||
return now > expiresAt;
|
||||
};
|
||||
|
||||
export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBillingPlan) => {
|
||||
const { active: isEnterpriseEdition } = await getEnterpriseLicense();
|
||||
|
||||
// TODO: We'll remove the IS_FORMBRICKS_CLOUD check once we have the AI feature available for self-hosted customers
|
||||
return Boolean(
|
||||
IS_FORMBRICKS_CLOUD &&
|
||||
IS_AI_CONFIGURED &&
|
||||
isEnterpriseEdition &&
|
||||
(billingPlan === "startup" || billingPlan === "scale" || billingPlan === "enterprise")
|
||||
);
|
||||
};
|
||||
|
||||
export const getIsAIEnabled = async (organization: TOrganization) => {
|
||||
const isOrganizationAIReady = await getIsOrganizationAIReady(organization.billing.plan);
|
||||
return Boolean(isOrganizationAIReady && organization.isAIEnabled);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
InvalidInputError,
|
||||
OperationNotAllowedError,
|
||||
ResourceNotFoundError,
|
||||
UnknownError,
|
||||
} from "@formbricks/types/errors";
|
||||
@@ -16,7 +17,8 @@ export const actionClient = createSafeActionClient({
|
||||
e instanceof ResourceNotFoundError ||
|
||||
e instanceof AuthorizationError ||
|
||||
e instanceof InvalidInputError ||
|
||||
e instanceof UnknownError
|
||||
e instanceof UnknownError ||
|
||||
e instanceof OperationNotAllowedError
|
||||
) {
|
||||
return e.message;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
getProductIdFromSegmentId,
|
||||
getProductIdFromSurveyId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import {
|
||||
cloneSegment,
|
||||
createSegment,
|
||||
@@ -22,8 +24,23 @@ import {
|
||||
} from "@formbricks/lib/segment/service";
|
||||
import { loadNewSegmentInSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZSegmentCreateInput, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
|
||||
const checkAdvancedTargetingPermission = async (organizationId: string) => {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(organization);
|
||||
|
||||
if (!isAdvancedTargetingAllowed) {
|
||||
throw new OperationNotAllowedError("Advanced targeting is not allowed for this organization");
|
||||
}
|
||||
};
|
||||
|
||||
export const createSegmentAction = authenticatedActionClient
|
||||
.schema(ZSegmentCreateInput)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
@@ -35,9 +52,11 @@ export const createSegmentAction = authenticatedActionClient
|
||||
}
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -51,6 +70,8 @@ export const createSegmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAdvancedTargetingPermission(organizationId);
|
||||
|
||||
const parsedFilters = ZSegmentFilters.safeParse(parsedInput.filters);
|
||||
|
||||
if (!parsedFilters.success) {
|
||||
@@ -70,9 +91,11 @@ const ZUpdateSegmentAction = z.object({
|
||||
export const updateSegmentAction = authenticatedActionClient
|
||||
.schema(ZUpdateSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSegmentId(parsedInput.segmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
schema: ZSegmentUpdateInput,
|
||||
@@ -88,6 +111,8 @@ export const updateSegmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAdvancedTargetingPermission(organizationId);
|
||||
|
||||
const { filters } = parsedInput.data;
|
||||
if (filters) {
|
||||
const parsedFilters = ZSegmentFilters.safeParse(filters);
|
||||
@@ -117,9 +142,11 @@ export const loadNewSegmentAction = authenticatedActionClient
|
||||
throw new Error("Segment and survey are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(surveyEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(surveyEnvironmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -133,6 +160,8 @@ export const loadNewSegmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAdvancedTargetingPermission(organizationId);
|
||||
|
||||
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
|
||||
});
|
||||
|
||||
@@ -151,9 +180,11 @@ export const cloneSegmentAction = authenticatedActionClient
|
||||
throw new Error("Segment and survey are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(surveyEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(surveyEnvironmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -167,6 +198,8 @@ export const cloneSegmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAdvancedTargetingPermission(organizationId);
|
||||
|
||||
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
|
||||
});
|
||||
|
||||
@@ -177,9 +210,11 @@ const ZDeleteSegmentAction = z.object({
|
||||
export const deleteSegmentAction = authenticatedActionClient
|
||||
.schema(ZDeleteSegmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSegmentId(parsedInput.segmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -193,6 +228,8 @@ export const deleteSegmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAdvancedTargetingPermission(organizationId);
|
||||
|
||||
return await deleteSegment(parsedInput.segmentId);
|
||||
});
|
||||
|
||||
@@ -203,9 +240,11 @@ const ZResetSegmentFiltersAction = z.object({
|
||||
export const resetSegmentFiltersAction = authenticatedActionClient
|
||||
.schema(ZResetSegmentFiltersAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -219,5 +258,7 @@ export const resetSegmentFiltersAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAdvancedTargetingPermission(organizationId);
|
||||
|
||||
return await resetSegmentInSurvey(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
95
apps/web/modules/ee/insights/actions.ts
Normal file
95
apps/web/modules/ee/insights/actions.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
"use server";
|
||||
|
||||
import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getProductIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { getIsAIEnabled, getIsOrganizationAIReady } from "@/modules/ee/license-check/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
||||
|
||||
export const checkAIPermission = async (organizationId: string) => {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAIEnabled = await getIsAIEnabled(organization);
|
||||
|
||||
if (!isAIEnabled) {
|
||||
throw new OperationNotAllowedError("AI is not enabled for this organization");
|
||||
}
|
||||
};
|
||||
|
||||
const ZGenerateInsightsForSurveyAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
export const generateInsightsForSurveyAction = authenticatedActionClient
|
||||
.schema(ZGenerateInsightsForSurveyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
schema: ZGenerateInsightsForSurveyAction,
|
||||
data: parsedInput,
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
generateInsightsForSurvey(parsedInput.surveyId);
|
||||
});
|
||||
|
||||
const ZUpdateOrganizationAIEnabledAction = z.object({
|
||||
organizationId: ZId,
|
||||
data: ZOrganizationUpdateInput.pick({ isAIEnabled: true }),
|
||||
});
|
||||
|
||||
export const updateOrganizationAIEnabledAction = authenticatedActionClient
|
||||
.schema(ZUpdateOrganizationAIEnabledAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
const organizationId = parsedInput.organizationId;
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
schema: ZOrganizationUpdateInput.pick({ isAIEnabled: true }),
|
||||
data: parsedInput.data,
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isOrganizationAIReady = await getIsOrganizationAIReady(organization.billing.plan);
|
||||
|
||||
if (!isOrganizationAIReady) {
|
||||
throw new OperationNotAllowedError("AI is not ready for this organization");
|
||||
}
|
||||
|
||||
return await updateOrganization(parsedInput.organizationId, parsedInput.data);
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getProductIdFromEnvironmentId,
|
||||
getProductIdFromInsightId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { checkAIPermission } from "@/modules/ee/insights/actions";
|
||||
import {
|
||||
getDocumentsByInsightId,
|
||||
getDocumentsByInsightIdSurveyIdQuestionId,
|
||||
@@ -40,9 +41,11 @@ export const getDocumentsByInsightIdSurveyIdQuestionIdAction = authenticatedActi
|
||||
throw new Error("Insight and survey are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(surveyEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(surveyEnvironmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -56,6 +59,8 @@ export const getDocumentsByInsightIdSurveyIdQuestionIdAction = authenticatedActi
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
|
||||
return await getDocumentsByInsightIdSurveyIdQuestionId(
|
||||
parsedInput.insightId,
|
||||
parsedInput.surveyId,
|
||||
@@ -75,9 +80,10 @@ const ZGetDocumentsByInsightIdAction = z.object({
|
||||
export const getDocumentsByInsightIdAction = authenticatedActionClient
|
||||
.schema(ZGetDocumentsByInsightIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromInsightId(parsedInput.insightId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromInsightId(parsedInput.insightId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -91,6 +97,8 @@ export const getDocumentsByInsightIdAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
|
||||
return await getDocumentsByInsightId(
|
||||
parsedInput.insightId,
|
||||
parsedInput.limit,
|
||||
@@ -111,9 +119,10 @@ const ZUpdateDocumentAction = z.object({
|
||||
export const updateDocumentAction = authenticatedActionClient
|
||||
.schema(ZUpdateDocumentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromDocumentId(parsedInput.documentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromDocumentId(parsedInput.documentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -127,5 +136,7 @@ export const updateDocumentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
|
||||
return await updateDocument(parsedInput.documentId, parsedInput.data);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"use server";
|
||||
|
||||
import { insightCache } from "@/lib/cache/insight";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromInsightId,
|
||||
getProductIdFromEnvironmentId,
|
||||
getProductIdFromInsightId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { checkAIPermission } from "@/modules/ee/insights/actions";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZInsight, ZInsightFilterCriteria } from "@formbricks/types/insights";
|
||||
import { getInsights, updateInsight } from "./lib/insights";
|
||||
@@ -22,9 +25,10 @@ const ZGetEnvironmentInsightsAction = z.object({
|
||||
export const getEnvironmentInsightsAction = authenticatedActionClient
|
||||
.schema(ZGetEnvironmentInsightsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -38,6 +42,8 @@ export const getEnvironmentInsightsAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
|
||||
return await getInsights(
|
||||
parsedInput.environmentId,
|
||||
parsedInput.limit,
|
||||
@@ -54,9 +60,10 @@ const ZGetStatsAction = z.object({
|
||||
export const getStatsAction = authenticatedActionClient
|
||||
.schema(ZGetStatsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -70,6 +77,7 @@ export const getStatsAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
return await getStats(parsedInput.environmentId, parsedInput.statsFrom);
|
||||
});
|
||||
|
||||
@@ -82,29 +90,11 @@ export const updateInsightAction = authenticatedActionClient
|
||||
.schema(ZUpdateInsightAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
try {
|
||||
const insight = await cache(
|
||||
() =>
|
||||
prisma.insight.findUnique({
|
||||
where: {
|
||||
id: parsedInput.insightId,
|
||||
},
|
||||
select: {
|
||||
environmentId: true,
|
||||
},
|
||||
}),
|
||||
[`getInsight-${parsedInput.insightId}`],
|
||||
{
|
||||
tags: [insightCache.tag.byId(parsedInput.insightId)],
|
||||
}
|
||||
)();
|
||||
|
||||
if (!insight) {
|
||||
throw new Error("Insight not found");
|
||||
}
|
||||
const organizationId = await getOrganizationIdFromInsightId(parsedInput.insightId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(insight.environmentId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -112,12 +102,14 @@ export const updateInsightAction = authenticatedActionClient
|
||||
},
|
||||
{
|
||||
type: "productTeam",
|
||||
productId: await getProductIdFromEnvironmentId(insight.environmentId),
|
||||
productId: await getProductIdFromInsightId(parsedInput.insightId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkAIPermission(organizationId);
|
||||
|
||||
return await updateInsight(parsedInput.insightId, parsedInput.data);
|
||||
} catch (error) {
|
||||
console.error("Error updating insight:", {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getIsAIEnabled } from "@/app/lib/utils";
|
||||
import { Dashboard } from "@/modules/ee/insights/experience/components/dashboard";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -12,12 +12,13 @@ import { cache, revalidateTag } from "@formbricks/lib/cache";
|
||||
import {
|
||||
E2E_TESTING,
|
||||
ENTERPRISE_LICENSE_KEY,
|
||||
IS_AI_CONFIGURED,
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
PRODUCT_FEATURE_KEYS,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { hashString } from "@formbricks/lib/hashString";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TOrganization, TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
|
||||
const hashedKey = ENTERPRISE_LICENSE_KEY ? hashString(ENTERPRISE_LICENSE_KEY) : undefined;
|
||||
const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const;
|
||||
@@ -309,3 +310,22 @@ export const getIsMultiOrgEnabled = async (): Promise<boolean> => {
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.isMultiOrgEnabled;
|
||||
};
|
||||
|
||||
export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBillingPlan) => {
|
||||
// TODO: We'll remove the IS_FORMBRICKS_CLOUD check once we have the AI feature available for self-hosted customers
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return (
|
||||
IS_AI_CONFIGURED &&
|
||||
(await getEnterpriseLicense()).active &&
|
||||
(billingPlan === PRODUCT_FEATURE_KEYS.STARTUP ||
|
||||
billingPlan === PRODUCT_FEATURE_KEYS.SCALE ||
|
||||
billingPlan === PRODUCT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getIsAIEnabled = async (organization: TOrganization) => {
|
||||
return organization.isAIEnabled && (await getIsOrganizationAIReady(organization.billing.plan));
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getOrganizationIdFromProductId,
|
||||
getProductIdFromLanguageId,
|
||||
} from "@/lib/utils/helper";
|
||||
import { getMultiLanguagePermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
createLanguage,
|
||||
@@ -14,7 +15,9 @@ import {
|
||||
getSurveysUsingGivenLanguage,
|
||||
updateLanguage,
|
||||
} from "@formbricks/lib/language/service";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZLanguageInput } from "@formbricks/types/product";
|
||||
|
||||
const ZCreateLanguageAction = z.object({
|
||||
@@ -22,12 +25,28 @@ const ZCreateLanguageAction = z.object({
|
||||
languageInput: ZLanguageInput,
|
||||
});
|
||||
|
||||
export const checkMultiLanguagePermission = async (organizationId: string) => {
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
|
||||
|
||||
if (!isMultiLanguageAllowed) {
|
||||
throw new OperationNotAllowedError("Multi language is not allowed for this organization");
|
||||
}
|
||||
};
|
||||
|
||||
export const createLanguageAction = authenticatedActionClient
|
||||
.schema(ZCreateLanguageAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -42,6 +61,7 @@ export const createLanguageAction = authenticatedActionClient
|
||||
},
|
||||
],
|
||||
});
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
|
||||
return await createLanguage(parsedInput.productId, parsedInput.languageInput);
|
||||
});
|
||||
@@ -60,9 +80,11 @@ export const deleteLanguageAction = authenticatedActionClient
|
||||
throw new Error("Invalid language id");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -75,6 +97,7 @@ export const deleteLanguageAction = authenticatedActionClient
|
||||
},
|
||||
],
|
||||
});
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
|
||||
return await deleteLanguage(parsedInput.languageId, parsedInput.productId);
|
||||
});
|
||||
@@ -86,9 +109,11 @@ const ZGetSurveysUsingGivenLanguageAction = z.object({
|
||||
export const getSurveysUsingGivenLanguageAction = authenticatedActionClient
|
||||
.schema(ZGetSurveysUsingGivenLanguageAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromLanguageId(parsedInput.languageId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromLanguageId(parsedInput.languageId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -101,6 +126,7 @@ export const getSurveysUsingGivenLanguageAction = authenticatedActionClient
|
||||
},
|
||||
],
|
||||
});
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
|
||||
return await getSurveysUsingGivenLanguage(parsedInput.languageId);
|
||||
});
|
||||
@@ -114,9 +140,17 @@ const ZUpdateLanguageAction = z.object({
|
||||
export const updateLanguageAction = authenticatedActionClient
|
||||
.schema(ZUpdateLanguageAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const languageProductId = await getProductIdFromLanguageId(parsedInput.languageId);
|
||||
|
||||
if (languageProductId !== parsedInput.productId) {
|
||||
throw new Error("Invalid language id");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromLanguageId(parsedInput.languageId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -131,6 +165,7 @@ export const updateLanguageAction = authenticatedActionClient
|
||||
},
|
||||
],
|
||||
});
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
|
||||
return await updateLanguage(parsedInput.productId, parsedInput.languageId, parsedInput.languageInput);
|
||||
});
|
||||
|
||||
@@ -2,15 +2,29 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { updateMembership } from "@/modules/ee/role-management/lib/membership";
|
||||
import { z } from "zod";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { updateInvite } from "@formbricks/lib/invite/service";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { ZId, ZUuid } from "@formbricks/types/common";
|
||||
import { ValidationError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { ZInviteUpdateInput } from "@formbricks/types/invites";
|
||||
import { ZMembershipUpdateInput } from "@formbricks/types/memberships";
|
||||
|
||||
export const checkRoleManagementPermission = async (organizationId: string) => {
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isRoleManagementAllowed = await getRoleManagementPermission(organization);
|
||||
if (!isRoleManagementAllowed) {
|
||||
throw new OperationNotAllowedError("Role management is not allowed for this organization");
|
||||
}
|
||||
};
|
||||
|
||||
const ZUpdateInviteAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
organizationId: ZId,
|
||||
@@ -20,10 +34,6 @@ const ZUpdateInviteAction = z.object({
|
||||
export const updateInviteAction = authenticatedActionClient
|
||||
.schema(ZUpdateInviteAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (!IS_FORMBRICKS_CLOUD && parsedInput.data.role === "billing") {
|
||||
throw new ValidationError("Billing role is not allowed");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
@@ -37,6 +47,12 @@ export const updateInviteAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
if (!IS_FORMBRICKS_CLOUD && parsedInput.data.role === "billing") {
|
||||
throw new ValidationError("Billing role is not allowed");
|
||||
}
|
||||
|
||||
await checkRoleManagementPermission(parsedInput.organizationId);
|
||||
|
||||
return await updateInvite(parsedInput.inviteId, parsedInput.data);
|
||||
});
|
||||
|
||||
@@ -49,10 +65,6 @@ const ZUpdateMembershipAction = z.object({
|
||||
export const updateMembershipAction = authenticatedActionClient
|
||||
.schema(ZUpdateMembershipAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (!IS_FORMBRICKS_CLOUD && parsedInput.data.role === "billing") {
|
||||
throw new ValidationError("Billing role is not allowed");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
@@ -66,5 +78,11 @@ export const updateMembershipAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
if (!IS_FORMBRICKS_CLOUD && parsedInput.data.role === "billing") {
|
||||
throw new ValidationError("Billing role is not allowed");
|
||||
}
|
||||
|
||||
await checkRoleManagementPermission(parsedInput.organizationId);
|
||||
|
||||
return await updateMembership(parsedInput.userId, parsedInput.organizationId, parsedInput.data);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromProductId, getOrganizationIdFromTeamId } from "@/lib/utils/helper";
|
||||
import { checkRoleManagementPermission } from "@/modules/ee/role-management/actions";
|
||||
import {
|
||||
addTeamAccess,
|
||||
removeTeamAccess,
|
||||
@@ -38,6 +39,8 @@ export const removeAccessAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(productOrganizationId);
|
||||
|
||||
return await removeTeamAccess(parsedInput.productId, parsedInput.teamId);
|
||||
});
|
||||
|
||||
@@ -49,9 +52,10 @@ const ZAddAccessAction = z.object({
|
||||
export const addAccessAction = authenticatedActionClient
|
||||
.schema(ZAddAccessAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromProductId(parsedInput.productId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromProductId(parsedInput.productId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -60,6 +64,8 @@ export const addAccessAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
return await addTeamAccess(parsedInput.productId, parsedInput.teamIds);
|
||||
});
|
||||
|
||||
@@ -90,6 +96,8 @@ export const updateAccessPermissionAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(productOrganizationId);
|
||||
|
||||
return await updateTeamAccessPermission(
|
||||
parsedInput.productId,
|
||||
parsedInput.teamId,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromProductId, getOrganizationIdFromTeamId } from "@/lib/utils/helper";
|
||||
import { checkRoleManagementPermission } from "@/modules/ee/role-management/actions";
|
||||
import { ZTeamPermission } from "@/modules/ee/teams/product-teams/types/teams";
|
||||
import {
|
||||
addTeamMembers,
|
||||
@@ -29,9 +30,11 @@ const ZUpdateTeamNameAction = z.object({
|
||||
export const updateTeamNameAction = authenticatedActionClient
|
||||
.schema(ZUpdateTeamNameAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTeamId(parsedInput.teamId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -40,6 +43,8 @@ export const updateTeamNameAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
return await updateTeamName(parsedInput.teamId, parsedInput.name);
|
||||
});
|
||||
|
||||
@@ -50,9 +55,11 @@ const ZDeleteTeamAction = z.object({
|
||||
export const deleteTeamAction = authenticatedActionClient
|
||||
.schema(ZDeleteTeamAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTeamId(parsedInput.teamId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -61,6 +68,7 @@ export const deleteTeamAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
return await deleteTeam(parsedInput.teamId);
|
||||
});
|
||||
|
||||
@@ -73,9 +81,11 @@ const ZUpdateUserTeamRoleAction = z.object({
|
||||
export const updateUserTeamRoleAction = authenticatedActionClient
|
||||
.schema(ZUpdateUserTeamRoleAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTeamId(parsedInput.teamId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -89,6 +99,8 @@ export const updateUserTeamRoleAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
return await updateUserTeamRole(parsedInput.teamId, parsedInput.userId, parsedInput.role);
|
||||
});
|
||||
|
||||
@@ -100,16 +112,7 @@ const ZRemoveTeamMemberAction = z.object({
|
||||
export const removeTeamMemberAction = authenticatedActionClient
|
||||
.schema(ZRemoveTeamMemberAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const teamOrganizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
const membership = await getMembershipByUserIdOrganizationId(ctx.user.id, teamOrganizationId);
|
||||
|
||||
const { isOwner, isManager } = getAccessFlags(membership?.role);
|
||||
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
|
||||
if (!isOwnerOrManager && ctx.user.id === parsedInput.userId) {
|
||||
throw new Error("You can not remove yourself from the team");
|
||||
}
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -127,6 +130,18 @@ export const removeTeamMemberAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(ctx.user.id, organizationId);
|
||||
|
||||
const { isOwner, isManager } = getAccessFlags(membership?.role);
|
||||
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
|
||||
if (!isOwnerOrManager && ctx.user.id === parsedInput.userId) {
|
||||
throw new Error("You can not remove yourself from the team");
|
||||
}
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
return await removeTeamMember(parsedInput.teamId, parsedInput.userId);
|
||||
});
|
||||
|
||||
@@ -138,9 +153,10 @@ const ZAddTeamMembersAction = z.object({
|
||||
export const addTeamMembersAction = authenticatedActionClient
|
||||
.schema(ZAddTeamMembersAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTeamId(parsedInput.teamId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -154,6 +170,8 @@ export const addTeamMembersAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
return await addTeamMembers(parsedInput.teamId, parsedInput.userIds);
|
||||
});
|
||||
|
||||
@@ -184,6 +202,8 @@ export const updateTeamProductPermissionAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(productOrganizationId);
|
||||
|
||||
return await updateTeamProductPermission(
|
||||
parsedInput.teamId,
|
||||
parsedInput.productId,
|
||||
@@ -217,6 +237,8 @@ export const removeTeamProductAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(productOrganizationId);
|
||||
|
||||
return await removeTeamProduct(parsedInput.teamId, parsedInput.productId);
|
||||
});
|
||||
|
||||
@@ -228,9 +250,11 @@ const ZAddTeamProductsAction = z.object({
|
||||
export const addTeamProductsAction = authenticatedActionClient
|
||||
.schema(ZAddTeamProductsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromTeamId(parsedInput.teamId),
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -239,5 +263,7 @@ export const addTeamProductsAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
return await addTeamProducts(parsedInput.teamId, parsedInput.productIds);
|
||||
});
|
||||
|
||||
@@ -2,30 +2,10 @@
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { createTeam, getTeams } from "@/modules/ee/teams/team-list/lib/teams";
|
||||
import { checkRoleManagementPermission } from "@/modules/ee/role-management/actions";
|
||||
import { createTeam } from "@/modules/ee/teams/team-list/lib/teams";
|
||||
import { z } from "zod";
|
||||
|
||||
const ZGetTeamsAction = z.object({
|
||||
organizationId: z.string(),
|
||||
});
|
||||
|
||||
export const getTeamsAction = authenticatedActionClient
|
||||
.schema(ZGetTeamsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager", "billing", "member"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await getTeams(ctx.user.id, parsedInput.organizationId);
|
||||
});
|
||||
|
||||
const ZCreateTeamAction = z.object({
|
||||
organizationId: z.string().cuid(),
|
||||
name: z.string(),
|
||||
@@ -44,6 +24,7 @@ export const createTeamAction = authenticatedActionClient
|
||||
},
|
||||
],
|
||||
});
|
||||
await checkRoleManagementPermission(parsedInput.organizationId);
|
||||
|
||||
return await createTeam(parsedInput.organizationId, parsedInput.name);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProductIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
|
||||
import { z } from "zod";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
@@ -60,5 +61,9 @@ export const createSurveyAction = authenticatedActionClient
|
||||
await checkSurveyFollowUpsPermission(organizationId);
|
||||
}
|
||||
|
||||
if (parsedInput.surveyBody.languages?.length) {
|
||||
await checkMultiLanguagePermission(organizationId);
|
||||
}
|
||||
|
||||
return await createSurvey(parsedInput.environmentId, parsedInput.surveyBody);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user