mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-03 19:40:08 -05:00
chore: introduced env variable to disable User management UI (#5526)
This commit is contained in:
committed by
GitHub
parent
da44fef89d
commit
c653996cbb
@@ -218,3 +218,6 @@ UNKEY_ROOT_KEY=
|
||||
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
|
||||
# It's used automatically by Sentry during the build for authentication when uploading source maps.
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Disable the user management from UI
|
||||
# DISABLE_USER_MANAGEMENT
|
||||
@@ -280,3 +280,5 @@ export const IS_DEVELOPMENT = env.NODE_ENV === "development";
|
||||
export const SENTRY_DSN = env.SENTRY_DSN;
|
||||
|
||||
export const PROMETHEUS_ENABLED = env.PROMETHEUS_ENABLED === "1";
|
||||
|
||||
export const DISABLE_USER_MANAGEMENT = env.DISABLE_USER_MANAGEMENT === "1";
|
||||
|
||||
@@ -110,6 +110,7 @@ export const env = createEnv({
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).optional(),
|
||||
PROMETHEUS_EXPORTER_PORT: z.string().optional(),
|
||||
PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(),
|
||||
DISABLE_USER_MANAGEMENT: z.enum(["1", "0"]).optional(),
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -206,5 +207,6 @@ export const env = createEnv({
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PROMETHEUS_ENABLED: process.env.PROMETHEUS_ENABLED,
|
||||
PROMETHEUS_EXPORTER_PORT: process.env.PROMETHEUS_EXPORTER_PORT,
|
||||
DISABLE_USER_MANAGEMENT: process.env.DISABLE_USER_MANAGEMENT,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import {
|
||||
TUpdateInviteAction,
|
||||
TUpdateMembershipAction,
|
||||
checkRoleManagementPermission,
|
||||
updateInviteAction,
|
||||
updateMembershipAction,
|
||||
} from "@/modules/ee/role-management/actions";
|
||||
import { updateInvite } from "@/modules/ee/role-management/lib/invite";
|
||||
import { updateMembership } from "@/modules/ee/role-management/lib/membership";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
|
||||
// Mock constants with getter functions to allow overriding in tests
|
||||
let mockIsFormbricksCloud = false;
|
||||
let mockDisableUserManagement = false;
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get IS_FORMBRICKS_CLOUD() {
|
||||
return mockIsFormbricksCloud;
|
||||
},
|
||||
get DISABLE_USER_MANAGEMENT() {
|
||||
return mockDisableUserManagement;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getOrganization: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/membership/service", () => ({
|
||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client-middleware", () => ({
|
||||
checkAuthorizationUpdated: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/role-management/lib/invite", () => ({
|
||||
updateInvite: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/role-management/lib/membership", () => ({
|
||||
updateMembership: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
authenticatedActionClient: {
|
||||
schema: () => ({
|
||||
action: (callback) => callback,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Role Management Actions", () => {
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockIsFormbricksCloud = false;
|
||||
mockDisableUserManagement = false;
|
||||
});
|
||||
|
||||
describe("checkRoleManagementPermission", () => {
|
||||
test("throws error if organization not found", async () => {
|
||||
vi.mocked(getOrganization).mockResolvedValue(null);
|
||||
|
||||
await expect(checkRoleManagementPermission("org-123")).rejects.toThrow("Organization not found");
|
||||
});
|
||||
|
||||
test("throws error if role management is not allowed", async () => {
|
||||
vi.mocked(getOrganization).mockResolvedValue({
|
||||
billing: { plan: "free" },
|
||||
} as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(false);
|
||||
|
||||
await expect(checkRoleManagementPermission("org-123")).rejects.toThrow(
|
||||
new OperationNotAllowedError("Role management is not allowed for this organization")
|
||||
);
|
||||
});
|
||||
|
||||
test("succeeds if role management is allowed", async () => {
|
||||
vi.mocked(getOrganization).mockResolvedValue({
|
||||
billing: { plan: "pro" },
|
||||
} as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
|
||||
await expect(checkRoleManagementPermission("org-123")).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateInviteAction", () => {
|
||||
test("throws error if user is not a member of the organization", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
updateInviteAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
inviteId: "invite-123",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateInviteAction)
|
||||
).rejects.toThrow(new AuthenticationError("User not a member of this organization"));
|
||||
});
|
||||
|
||||
test("throws error if billing role is not allowed in self-hosted", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
updateInviteAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
inviteId: "invite-123",
|
||||
organizationId: "org-123",
|
||||
data: { role: "billing" },
|
||||
},
|
||||
} as unknown as TUpdateInviteAction)
|
||||
).rejects.toThrow(new ValidationError("Billing role is not allowed"));
|
||||
});
|
||||
|
||||
test("allows billing role in cloud environment", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateInvite).mockResolvedValue({ id: "invite-123", role: "billing" } as any);
|
||||
|
||||
const result = await updateInviteAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
inviteId: "invite-123",
|
||||
organizationId: "org-123",
|
||||
data: { role: "billing" },
|
||||
},
|
||||
} as unknown as TUpdateInviteAction);
|
||||
|
||||
expect(result).toEqual({ id: "invite-123", role: "billing" });
|
||||
});
|
||||
|
||||
test("throws error if manager tries to invite a role other than member", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
updateInviteAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
inviteId: "invite-123",
|
||||
organizationId: "org-123",
|
||||
data: { role: "owner" },
|
||||
},
|
||||
} as unknown as TUpdateInviteAction)
|
||||
).rejects.toThrow(new OperationNotAllowedError("Managers can only invite members"));
|
||||
});
|
||||
|
||||
test("allows manager to invite a member", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateInvite).mockResolvedValue({ id: "invite-123", role: "member" } as any);
|
||||
|
||||
const result = await updateInviteAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
inviteId: "invite-123",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateInviteAction);
|
||||
|
||||
expect(result).toEqual({ id: "invite-123", role: "member" });
|
||||
expect(updateInvite).toHaveBeenCalledWith("invite-123", { role: "member" });
|
||||
});
|
||||
|
||||
test("successful invite update as owner", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateInvite).mockResolvedValue({ id: "invite-123", role: "member" } as any);
|
||||
|
||||
const result = await updateInviteAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
inviteId: "invite-123",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateInviteAction);
|
||||
|
||||
expect(result).toEqual({ id: "invite-123", role: "member" });
|
||||
expect(updateInvite).toHaveBeenCalledWith("invite-123", { role: "member" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateMembershipAction", () => {
|
||||
test("throws error if user is not a member of the organization", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction)
|
||||
).rejects.toThrow(new AuthenticationError("User not a member of this organization"));
|
||||
});
|
||||
|
||||
test("throws error if user management is disabled", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = true;
|
||||
|
||||
await expect(
|
||||
updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction)
|
||||
).rejects.toThrow(new OperationNotAllowedError("User management is disabled"));
|
||||
});
|
||||
|
||||
test("throws error if billing role is not allowed in self-hosted", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "billing" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction)
|
||||
).rejects.toThrow(new ValidationError("Billing role is not allowed"));
|
||||
});
|
||||
|
||||
test("allows billing role in cloud environment", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateMembership).mockResolvedValue({ id: "membership-123", role: "billing" } as any);
|
||||
|
||||
const result = await updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "billing" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction);
|
||||
|
||||
expect(result).toEqual({ id: "membership-123", role: "billing" });
|
||||
});
|
||||
|
||||
test("throws error if manager tries to assign a role other than member", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "owner" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction)
|
||||
).rejects.toThrow(new OperationNotAllowedError("Managers can only assign users to the member role"));
|
||||
});
|
||||
|
||||
test("allows manager to assign member role", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateMembership).mockResolvedValue({ id: "membership-123", role: "member" } as any);
|
||||
|
||||
const result = await updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction);
|
||||
|
||||
expect(result).toEqual({ id: "membership-123", role: "member" });
|
||||
expect(updateMembership).toHaveBeenCalledWith("user-456", "org-123", { role: "member" });
|
||||
});
|
||||
|
||||
test("successful membership update as owner", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateMembership).mockResolvedValue({ id: "membership-123", role: "member" } as any);
|
||||
|
||||
const result = await updateMembershipAction({
|
||||
ctx: { user: { id: "user-123" } },
|
||||
parsedInput: {
|
||||
userId: "user-456",
|
||||
organizationId: "org-123",
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as unknown as TUpdateMembershipAction);
|
||||
|
||||
expect(result).toEqual({ id: "membership-123", role: "member" });
|
||||
expect(updateMembership).toHaveBeenCalledWith("user-456", "org-123", { role: "member" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { DISABLE_USER_MANAGEMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
@@ -33,6 +33,8 @@ const ZUpdateInviteAction = z.object({
|
||||
data: ZInviteUpdateInput,
|
||||
});
|
||||
|
||||
export type TUpdateInviteAction = z.infer<typeof ZUpdateInviteAction>;
|
||||
|
||||
export const updateInviteAction = authenticatedActionClient
|
||||
.schema(ZUpdateInviteAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
@@ -86,6 +88,9 @@ export const updateMembershipAction = authenticatedActionClient
|
||||
if (!currentUserMembership) {
|
||||
throw new AuthenticationError("User not a member of this organization");
|
||||
}
|
||||
if (DISABLE_USER_MANAGEMENT) {
|
||||
throw new OperationNotAllowedError("User management is disabled");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Role {
|
||||
inviteId?: string;
|
||||
doesOrgHaveMoreThanOneOwner?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
export function EditMembershipRole({
|
||||
@@ -41,6 +42,7 @@ export function EditMembershipRole({
|
||||
inviteId,
|
||||
doesOrgHaveMoreThanOneOwner,
|
||||
isFormbricksCloud,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: Role) {
|
||||
const { t } = useTranslate();
|
||||
const router = useRouter();
|
||||
@@ -50,6 +52,7 @@ export function EditMembershipRole({
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
|
||||
const disableRole =
|
||||
isUserManagementDisabledFromUi ||
|
||||
memberId === userId ||
|
||||
(memberRole === "owner" && !doesOrgHaveMoreThanOneOwner) ||
|
||||
(currentUserRole === "manager" && memberRole === "owner");
|
||||
|
||||
@@ -83,6 +83,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
SAML_DATABASE_URL: "test-saml-db-url",
|
||||
NEXTAUTH_SECRET: "test-nextauth-secret",
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
DISABLE_USER_MANAGEMENT: false,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client-middleware", () => ({
|
||||
|
||||
+6
-1
@@ -11,6 +11,7 @@ interface EditMembershipsProps {
|
||||
currentUserId: string;
|
||||
role: TOrganizationRole;
|
||||
canDoRoleManagement: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
export const EditMemberships = async ({
|
||||
@@ -18,6 +19,7 @@ export const EditMemberships = async ({
|
||||
currentUserId,
|
||||
role,
|
||||
canDoRoleManagement,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: EditMembershipsProps) => {
|
||||
const members = await getMembershipByOrganizationId(organization.id);
|
||||
const invites = await getInvitesByOrganizationId(organization.id);
|
||||
@@ -34,7 +36,9 @@ export const EditMemberships = async ({
|
||||
|
||||
<div className="min-w-[80px] whitespace-nowrap">{t("common.status")}</div>
|
||||
|
||||
<div className="min-w-[125px] whitespace-nowrap">{t("common.actions")}</div>
|
||||
{!isUserManagementDisabledFromUi && (
|
||||
<div className="min-w-[125px] whitespace-nowrap">{t("common.actions")}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{role && (
|
||||
@@ -46,6 +50,7 @@ export const EditMemberships = async ({
|
||||
currentUserRole={role}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+11
-6
@@ -20,6 +20,7 @@ interface MembersInfoProps {
|
||||
currentUserId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
// Type guard to check if member is an invitee
|
||||
@@ -35,6 +36,7 @@ export const MembersInfo = ({
|
||||
currentUserId,
|
||||
canDoRoleManagement,
|
||||
isFormbricksCloud,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: MembersInfoProps) => {
|
||||
const allMembers = [...members, ...invites];
|
||||
const { t } = useTranslate();
|
||||
@@ -115,17 +117,20 @@ export const MembersInfo = ({
|
||||
inviteId={isInvitee(member) ? member.id : ""}
|
||||
doesOrgHaveMoreThanOneOwner={doesOrgHaveMoreThanOneOwner}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-[80px]">{getMembershipBadge(member)}</div>
|
||||
|
||||
<MemberActions
|
||||
organization={organization}
|
||||
member={!isInvitee(member) ? member : undefined}
|
||||
invite={isInvitee(member) ? member : undefined}
|
||||
showDeleteButton={showDeleteButton(member)}
|
||||
/>
|
||||
{!isUserManagementDisabledFromUi && (
|
||||
<MemberActions
|
||||
organization={organization}
|
||||
member={!isInvitee(member) ? member : undefined}
|
||||
invite={isInvitee(member) ? member : undefined}
|
||||
showDeleteButton={showDeleteButton(member)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+3
-1
@@ -28,6 +28,7 @@ interface OrganizationActionsProps {
|
||||
isFormbricksCloud: boolean;
|
||||
environmentId: string;
|
||||
isMultiOrgEnabled: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
export const OrganizationActions = ({
|
||||
@@ -41,6 +42,7 @@ export const OrganizationActions = ({
|
||||
isFormbricksCloud,
|
||||
environmentId,
|
||||
isMultiOrgEnabled,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: OrganizationActionsProps) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslate();
|
||||
@@ -128,7 +130,7 @@ export const OrganizationActions = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isInviteDisabled && isOwnerOrManager && (
|
||||
{!isInviteDisabled && isOwnerOrManager && !isUserManagementDisabledFromUi && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
@@ -17,6 +17,7 @@ interface MembersViewProps {
|
||||
currentUserId: string;
|
||||
environmentId: string;
|
||||
canDoRoleManagement: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
}
|
||||
|
||||
const MembersLoading = () => (
|
||||
@@ -35,6 +36,7 @@ export const MembersView = async ({
|
||||
currentUserId,
|
||||
environmentId,
|
||||
canDoRoleManagement,
|
||||
isUserManagementDisabledFromUi,
|
||||
}: MembersViewProps) => {
|
||||
const t = await getTranslate();
|
||||
|
||||
@@ -68,6 +70,7 @@ export const MembersView = async ({
|
||||
environmentId={environmentId}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
teams={teams}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -78,6 +81,7 @@ export const MembersView = async ({
|
||||
organization={organization}
|
||||
currentUserId={currentUserId}
|
||||
role={membershipRole}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { DISABLE_USER_MANAGEMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
@@ -32,6 +32,7 @@ export const TeamsPage = async (props) => {
|
||||
currentUserId={session.user.id}
|
||||
environmentId={params.environmentId}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isUserManagementDisabledFromUi={DISABLE_USER_MANAGEMENT}
|
||||
/>
|
||||
<TeamsView
|
||||
organizationId={organization.id}
|
||||
|
||||
@@ -32,6 +32,7 @@ export default defineConfig({
|
||||
"modules/environments/lib/**/*.ts",
|
||||
"modules/ui/components/post-hog-client/*.tsx",
|
||||
"modules/ee/role-management/components/*.tsx",
|
||||
"modules/ee/role-management/actions.ts",
|
||||
"modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx",
|
||||
"modules/ui/components/alert/*.tsx",
|
||||
"modules/ui/components/environmentId-base-layout/*.tsx",
|
||||
|
||||
@@ -87,7 +87,6 @@ x-environment: &environment
|
||||
# It's used for authentication when uploading source maps to Sentry, to make errors more readable.
|
||||
# SENTRY_AUTH_TOKEN:
|
||||
|
||||
|
||||
################################################### OPTIONAL (STORAGE) ###################################################
|
||||
|
||||
# Set the below to set a custom Upload Directory
|
||||
@@ -189,6 +188,9 @@ x-environment: &environment
|
||||
# DEFAULT_ORGANIZATION_ID:
|
||||
# DEFAULT_ORGANIZATION_ROLE: owner
|
||||
|
||||
# Set the below to 1 to disable the user management UI
|
||||
# DISABLE_USER_MANAGEMENT: 0
|
||||
|
||||
services:
|
||||
postgres:
|
||||
restart: always
|
||||
|
||||
@@ -68,5 +68,5 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| SURVEY_URL | Set this to change the domain of the survey. | optional | WEBAPP_URL
|
||||
| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional |
|
||||
| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional |
|
||||
|
||||
| DISABLE_USER_MANAGEMENT | Set this to hide the user management UI. | optional |
|
||||
Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Github Discussions and we’ll try our best to work out a solution with you.
|
||||
|
||||
+2
-1
@@ -196,7 +196,8 @@
|
||||
"UNSPLASH_ACCESS_KEY",
|
||||
"UNKEY_ROOT_KEY",
|
||||
"PROMETHEUS_ENABLED",
|
||||
"PROMETHEUS_EXPORTER_PORT"
|
||||
"PROMETHEUS_EXPORTER_PORT",
|
||||
"DISABLE_USER_MANAGEMENT"
|
||||
],
|
||||
"outputs": ["dist/**", ".next/**"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user