fix: license checks in server actions (#4274)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2024-11-25 14:57:46 +05:30
committed by GitHub
parent 44980d21a9
commit eb2621f72a
31 changed files with 452 additions and 184 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ type DeleteOrganizationProps = {
organization: TOrganization;
isDeleteDisabled?: boolean;
isUserOwner?: boolean;
isMultiOrgEnabled: boolean;
};
export const DeleteOrganization = ({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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:", {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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