mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
3 Commits
configurab
...
v3.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aeb92eb4f | ||
|
|
00dfa629b5 | ||
|
|
3ca471b6a2 |
@@ -211,5 +211,5 @@ UNKEY_ROOT_KEY=
|
||||
# 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=1
|
||||
# Configure the minimum role for user management from UI(owner, manager, disabled)
|
||||
# USER_MANAGEMENT_MINIMUM_ROLE="manager"
|
||||
@@ -28,7 +28,7 @@ export const useSurveyQRCode = (surveyUrl: string) => {
|
||||
} catch (error) {
|
||||
toast.error(t("environments.surveys.summary.failed_to_generate_qr_code"));
|
||||
}
|
||||
}, [surveyUrl]);
|
||||
}, [surveyUrl, t]);
|
||||
|
||||
const downloadQRCode = () => {
|
||||
try {
|
||||
|
||||
@@ -282,4 +282,4 @@ 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";
|
||||
export const USER_MANAGEMENT_MINIMUM_ROLE = env.USER_MANAGEMENT_MINIMUM_ROLE ?? "manager";
|
||||
|
||||
@@ -104,7 +104,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(),
|
||||
USER_MANAGEMENT_MINIMUM_ROLE: z.enum(["owner", "manager", "disabled"]).optional(),
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -199,6 +199,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,
|
||||
USER_MANAGEMENT_MINIMUM_ROLE: process.env.USER_MANAGEMENT_MINIMUM_ROLE,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,3 +13,21 @@ export const getAccessFlags = (role?: TOrganizationRole) => {
|
||||
isMember,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUserManagementAccess = (
|
||||
role: TOrganizationRole,
|
||||
minimumRole: "owner" | "manager" | "disabled"
|
||||
): boolean => {
|
||||
// If minimum role is "disabled", no one has access
|
||||
if (minimumRole === "disabled") {
|
||||
return false;
|
||||
}
|
||||
if (minimumRole === "owner") {
|
||||
return role === "owner";
|
||||
}
|
||||
|
||||
if (minimumRole === "manager") {
|
||||
return role === "owner" || role === "manager";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
export const useIntervalWhenFocused = (
|
||||
callback: () => void,
|
||||
@@ -8,7 +8,7 @@ export const useIntervalWhenFocused = (
|
||||
) => {
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleFocus = () => {
|
||||
const handleFocus = useCallback(() => {
|
||||
if (isActive) {
|
||||
if (shouldExecuteImmediately) {
|
||||
// Execute the callback immediately when the tab comes into focus
|
||||
@@ -20,7 +20,7 @@ export const useIntervalWhenFocused = (
|
||||
callback();
|
||||
}, intervalDuration);
|
||||
}
|
||||
};
|
||||
}, [isActive, intervalDuration, callback, shouldExecuteImmediately]);
|
||||
|
||||
const handleBlur = () => {
|
||||
// Clear the interval when the tab loses focus
|
||||
@@ -46,7 +46,7 @@ export const useIntervalWhenFocused = (
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
};
|
||||
}, [isActive, intervalDuration]);
|
||||
}, [isActive, intervalDuration, handleFocus]);
|
||||
};
|
||||
|
||||
export default useIntervalWhenFocused;
|
||||
|
||||
@@ -12,13 +12,12 @@ import {
|
||||
isClientSideApiRoute,
|
||||
isForgotPasswordRoute,
|
||||
isLoginRoute,
|
||||
isManagementApiRoute,
|
||||
isShareUrlRoute,
|
||||
isSignupRoute,
|
||||
isSyncWithUserIdentificationEndpoint,
|
||||
isVerifyEmailRoute,
|
||||
} from "@/app/middleware/endpoint-validator";
|
||||
import { E2E_TESTING, IS_PRODUCTION, RATE_LIMITING_DISABLED, SURVEY_URL, WEBAPP_URL } from "@/lib/constants";
|
||||
import { IS_PRODUCTION, RATE_LIMITING_DISABLED, SURVEY_URL, WEBAPP_URL } from "@/lib/constants";
|
||||
import { isValidCallbackUrl } from "@/lib/utils/url";
|
||||
import { logApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
@@ -28,24 +27,6 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
const enforceHttps = (request: NextRequest): Response | null => {
|
||||
const forwardedProto = request.headers.get("x-forwarded-proto") ?? "http";
|
||||
if (IS_PRODUCTION && !E2E_TESTING && forwardedProto !== "https") {
|
||||
const apiError: ApiErrorResponseV2 = {
|
||||
type: "forbidden",
|
||||
details: [
|
||||
{
|
||||
field: "",
|
||||
issue: "Only HTTPS connections are allowed on the management endpoints.",
|
||||
},
|
||||
],
|
||||
};
|
||||
logApiError(request, apiError);
|
||||
return NextResponse.json(apiError, { status: 403 });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleAuth = async (request: NextRequest): Promise<Response | null> => {
|
||||
const token = await getToken({ req: request as any });
|
||||
|
||||
@@ -132,12 +113,6 @@ export const middleware = async (originalRequest: NextRequest) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Enforce HTTPS for management endpoints
|
||||
if (isManagementApiRoute(request.nextUrl.pathname)) {
|
||||
const httpsResponse = enforceHttps(request);
|
||||
if (httpsResponse) return httpsResponse;
|
||||
}
|
||||
|
||||
// Handle authentication
|
||||
const authResponse = await handleAuth(request);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
@@ -15,14 +15,14 @@ import { AuthenticationError, OperationNotAllowedError, ValidationError } from "
|
||||
|
||||
// Mock constants with getter functions to allow overriding in tests
|
||||
let mockIsFormbricksCloud = false;
|
||||
let mockDisableUserManagement = false;
|
||||
let mockUserManagementMinimumRole = "owner";
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get IS_FORMBRICKS_CLOUD() {
|
||||
return mockIsFormbricksCloud;
|
||||
},
|
||||
get DISABLE_USER_MANAGEMENT() {
|
||||
return mockDisableUserManagement;
|
||||
get USER_MANAGEMENT_MINIMUM_ROLE() {
|
||||
return mockUserManagementMinimumRole;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("Role Management Actions", () => {
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockIsFormbricksCloud = false;
|
||||
mockDisableUserManagement = false;
|
||||
mockUserManagementMinimumRole = "owner";
|
||||
});
|
||||
|
||||
describe("checkRoleManagementPermission", () => {
|
||||
@@ -220,7 +220,7 @@ describe("Role Management Actions", () => {
|
||||
|
||||
test("throws error if user management is disabled", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = true;
|
||||
mockUserManagementMinimumRole = "disabled";
|
||||
|
||||
await expect(
|
||||
updateMembershipAction({
|
||||
@@ -231,12 +231,12 @@ describe("Role Management Actions", () => {
|
||||
data: { role: "member" },
|
||||
},
|
||||
} as any)
|
||||
).rejects.toThrow(new OperationNotAllowedError("User management is disabled"));
|
||||
).rejects.toThrow(new OperationNotAllowedError("User management is not allowed for your role"));
|
||||
});
|
||||
|
||||
test("throws error if billing role is not allowed in self-hosted", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
mockUserManagementMinimumRole = "owner";
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
@@ -253,7 +253,7 @@ describe("Role Management Actions", () => {
|
||||
|
||||
test("allows billing role in cloud environment", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
mockUserManagementMinimumRole = "owner";
|
||||
mockIsFormbricksCloud = true;
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
@@ -274,7 +274,7 @@ describe("Role Management Actions", () => {
|
||||
|
||||
test("throws error if manager tries to assign a role other than member", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
mockUserManagementMinimumRole = "manager";
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
@@ -291,7 +291,7 @@ describe("Role Management Actions", () => {
|
||||
|
||||
test("allows manager to assign member role", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "manager" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
mockUserManagementMinimumRole = "manager";
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
@@ -312,7 +312,7 @@ describe("Role Management Actions", () => {
|
||||
|
||||
test("successful membership update as owner", async () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({ role: "owner" } as any);
|
||||
mockDisableUserManagement = false;
|
||||
mockUserManagementMinimumRole = "owner";
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue({ billing: { plan: "pro" } } as any);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { DISABLE_USER_MANAGEMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getUserManagementAccess } from "@/lib/membership/utils";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
@@ -87,8 +88,13 @@ 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");
|
||||
const hasUserManagementAccess = getUserManagementAccess(
|
||||
currentUserMembership.role,
|
||||
USER_MANAGEMENT_MINIMUM_ROLE
|
||||
);
|
||||
|
||||
if (!hasUserManagementAccess) {
|
||||
throw new OperationNotAllowedError("User management is not allowed for your role");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { TInviteUpdateInput } from "@/modules/ee/role-management/types/invites";
|
||||
import { Session } from "next-auth";
|
||||
import { TMembership, TMembershipUpdateInput } from "@formbricks/types/memberships";
|
||||
import { TOrganization, TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
// Common mock IDs
|
||||
export const mockOrganizationId = "cblt7dwr7d0hvdifl4iw6d5x";
|
||||
export const mockUserId = "wl43gybf3pxmqqx3fcmsk8eb";
|
||||
export const mockInviteId = "dc0b6ea6-bb65-4a22-88e1-847df2e85af4";
|
||||
export const mockTargetUserId = "vevt9qm7sqmh44e3za6a2vzd";
|
||||
|
||||
// Mock user
|
||||
export const mockUser: TUser = {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
identityProvider: "email",
|
||||
twoFactorEnabled: false,
|
||||
objective: null,
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
},
|
||||
locale: "en-US",
|
||||
imageUrl: null,
|
||||
role: null,
|
||||
lastLoginAt: new Date(),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
// Mock session
|
||||
export const mockSession: Session = {
|
||||
user: {
|
||||
id: mockUserId,
|
||||
},
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
};
|
||||
|
||||
// Mock organizations
|
||||
export const createMockOrganization = (plan: TOrganizationBillingPlan): TOrganization => ({
|
||||
id: mockOrganizationId,
|
||||
name: "Test Organization",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isAIEnabled: false,
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
plan,
|
||||
period: "monthly",
|
||||
periodStart: new Date(),
|
||||
limits: {
|
||||
projects: plan === "free" ? 3 : null,
|
||||
monthly: {
|
||||
responses: plan === "free" ? 1500 : null,
|
||||
miu: plan === "free" ? 2000 : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const mockOrganizationFree = createMockOrganization("free");
|
||||
export const mockOrganizationStartup = createMockOrganization("startup");
|
||||
export const mockOrganizationScale = createMockOrganization("scale");
|
||||
|
||||
// Mock membership data
|
||||
export const createMockMembership = (role: TMembership["role"]): TMembership => ({
|
||||
userId: mockUserId,
|
||||
organizationId: mockOrganizationId,
|
||||
role,
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
export const mockMembershipMember = createMockMembership("member");
|
||||
export const mockMembershipManager = createMockMembership("manager");
|
||||
export const mockMembershipOwner = createMockMembership("owner");
|
||||
|
||||
// Mock data payloads
|
||||
export const mockInviteDataMember: TInviteUpdateInput = { role: "member" };
|
||||
export const mockInviteDataOwner: TInviteUpdateInput = { role: "owner" };
|
||||
export const mockInviteDataBilling: TInviteUpdateInput = { role: "billing" };
|
||||
|
||||
export const mockMembershipUpdateMember: TMembershipUpdateInput = { role: "member" };
|
||||
export const mockMembershipUpdateOwner: TMembershipUpdateInput = { role: "owner" };
|
||||
export const mockMembershipUpdateBilling: TMembershipUpdateInput = { role: "billing" };
|
||||
|
||||
// Mock input objects for actions
|
||||
export const mockUpdateInviteInput = {
|
||||
inviteId: mockInviteId,
|
||||
organizationId: mockOrganizationId,
|
||||
data: mockInviteDataMember,
|
||||
};
|
||||
|
||||
export const mockUpdateMembershipInput = {
|
||||
userId: mockTargetUserId,
|
||||
organizationId: mockOrganizationId,
|
||||
data: mockMembershipUpdateMember,
|
||||
};
|
||||
|
||||
// Mock responses
|
||||
export const mockUpdatedMembership: TMembership = {
|
||||
userId: mockTargetUserId,
|
||||
organizationId: mockOrganizationId,
|
||||
role: "member",
|
||||
accepted: true,
|
||||
};
|
||||
@@ -1,257 +0,0 @@
|
||||
import {
|
||||
mockInviteDataBilling,
|
||||
mockInviteDataOwner,
|
||||
mockMembershipManager,
|
||||
mockMembershipMember,
|
||||
mockMembershipUpdateBilling,
|
||||
mockMembershipUpdateOwner,
|
||||
mockOrganizationFree,
|
||||
mockOrganizationId,
|
||||
mockOrganizationScale,
|
||||
mockOrganizationStartup,
|
||||
mockSession,
|
||||
mockUpdateInviteInput,
|
||||
mockUpdateMembershipInput,
|
||||
mockUpdatedMembership,
|
||||
mockUser,
|
||||
} from "./__mocks__/actions.mock";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import "@/lib/utils/action-client-middleware";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { updateInvite } from "@/modules/ee/role-management/lib/invite";
|
||||
import { updateMembership } from "@/modules/ee/role-management/lib/membership";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { checkRoleManagementPermission } from "../actions";
|
||||
import { updateInviteAction, updateMembershipAction } from "../actions";
|
||||
|
||||
// Mock all external dependencies
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/role-management/lib/invite", () => ({
|
||||
updateInvite: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/role-management/lib/membership", () => ({
|
||||
updateMembership: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/membership/service", () => ({
|
||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getOrganization: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client-middleware", () => ({
|
||||
checkAuthorizationUpdated: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock constants without importing the actual module
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
IS_MULTI_ORG_ENABLED: true,
|
||||
ENCRYPTION_KEY: "test-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key",
|
||||
GITHUB_ID: "test-github-id",
|
||||
GITHUB_SECRET: "test-github-secret",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azure-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure-client-secret",
|
||||
AZUREAD_TENANT_ID: "test-azure-tenant-id",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-algorithm",
|
||||
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", () => ({
|
||||
checkAuthorizationUpdated: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/errors", () => ({
|
||||
OperationNotAllowedError: vi.fn(),
|
||||
ValidationError: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("role-management/actions.ts", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("checkRoleManagementPermission", () => {
|
||||
test("throws error when organization not found", async () => {
|
||||
vi.mocked(getOrganization).mockResolvedValue(null);
|
||||
|
||||
await expect(checkRoleManagementPermission(mockOrganizationId)).rejects.toThrow(
|
||||
"Organization not found"
|
||||
);
|
||||
|
||||
expect(getOrganization).toHaveBeenCalledWith(mockOrganizationId);
|
||||
});
|
||||
|
||||
test("throws error when role management is not allowed", async () => {
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganizationFree);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(false);
|
||||
|
||||
await expect(checkRoleManagementPermission(mockOrganizationId)).rejects.toThrow(
|
||||
new OperationNotAllowedError("Role management is not allowed for this organization")
|
||||
);
|
||||
|
||||
expect(getRoleManagementPermission).toHaveBeenCalledWith("free");
|
||||
|
||||
expect(getOrganization).toHaveBeenCalledWith(mockOrganizationId);
|
||||
});
|
||||
|
||||
test("succeeds when role management is allowed", async () => {
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganizationStartup);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
|
||||
await expect(checkRoleManagementPermission(mockOrganizationId)).resolves.toBeUndefined();
|
||||
await expect(getRoleManagementPermission).toHaveBeenCalledWith("startup");
|
||||
expect(getOrganization).toHaveBeenCalledWith(mockOrganizationId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateInviteAction", () => {
|
||||
test("throws error when user is not a member of the organization", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
|
||||
|
||||
expect(await updateInviteAction(mockUpdateInviteInput)).toStrictEqual({
|
||||
serverError: "User not a member of this organization",
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error when billing role is not allowed in self-hosted", async () => {
|
||||
const inputWithBillingRole = {
|
||||
...mockUpdateInviteInput,
|
||||
data: mockInviteDataBilling,
|
||||
};
|
||||
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipMember);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
expect(await updateInviteAction(inputWithBillingRole)).toStrictEqual({
|
||||
serverError: "Something went wrong while executing the operation.",
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error when manager tries to assign non-member role", async () => {
|
||||
const inputWithOwnerRole = {
|
||||
...mockUpdateInviteInput,
|
||||
data: mockInviteDataOwner,
|
||||
};
|
||||
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
expect(await updateInviteAction(inputWithOwnerRole)).toStrictEqual({
|
||||
serverError: "Managers can only invite members",
|
||||
});
|
||||
});
|
||||
|
||||
test("successfully updates invite", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganizationScale);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateInvite).mockResolvedValue(true);
|
||||
|
||||
const result = await updateInviteAction(mockUpdateInviteInput);
|
||||
|
||||
expect(result).toEqual({ data: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateMembershipAction", () => {
|
||||
test("throws error when user is not a member of the organization", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
|
||||
|
||||
expect(await updateMembershipAction(mockUpdateMembershipInput)).toStrictEqual({
|
||||
serverError: "User not a member of this organization",
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error when billing role is not allowed in self-hosted", async () => {
|
||||
const inputWithBillingRole = {
|
||||
...mockUpdateMembershipInput,
|
||||
data: mockMembershipUpdateBilling,
|
||||
};
|
||||
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipMember);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
expect(await updateMembershipAction(inputWithBillingRole)).toStrictEqual({
|
||||
serverError: "Something went wrong while executing the operation.",
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error when manager tries to assign non-member role", async () => {
|
||||
const inputWithOwnerRole = {
|
||||
...mockUpdateMembershipInput,
|
||||
data: mockMembershipUpdateOwner,
|
||||
};
|
||||
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
|
||||
expect(await updateMembershipAction(inputWithOwnerRole)).toStrictEqual({
|
||||
serverError: "Managers can only assign users to the member role",
|
||||
});
|
||||
});
|
||||
|
||||
test("successfully updates membership", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembershipManager);
|
||||
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(true);
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganizationScale);
|
||||
vi.mocked(getRoleManagementPermission).mockResolvedValue(true);
|
||||
vi.mocked(updateMembership).mockResolvedValue(mockUpdatedMembership);
|
||||
|
||||
const result = await updateMembershipAction(mockUpdateMembershipInput);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: mockUpdatedMembership,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ vi.mock(
|
||||
);
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
DISABLE_USER_MANAGEMENT: 0,
|
||||
USER_MANAGEMENT_MINIMUM_ROLE: "owner",
|
||||
IS_FORMBRICKS_CLOUD: 1,
|
||||
ENCRYPTION_KEY: "test-key",
|
||||
ENTERPRISE_LICENSE_KEY: "test-enterprise-key",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { DISABLE_USER_MANAGEMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants";
|
||||
import { getUserManagementAccess } from "@/lib/membership/utils";
|
||||
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";
|
||||
@@ -15,6 +16,10 @@ export const TeamsPage = async (props) => {
|
||||
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
|
||||
const hasUserManagementAccess = getUserManagementAccess(
|
||||
currentUserMembership?.role,
|
||||
USER_MANAGEMENT_MINIMUM_ROLE
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
@@ -32,7 +37,7 @@ export const TeamsPage = async (props) => {
|
||||
currentUserId={session.user.id}
|
||||
environmentId={params.environmentId}
|
||||
canDoRoleManagement={canDoRoleManagement}
|
||||
isUserManagementDisabledFromUi={DISABLE_USER_MANAGEMENT}
|
||||
isUserManagementDisabledFromUi={!hasUserManagementAccess}
|
||||
/>
|
||||
<TeamsView
|
||||
organizationId={organization.id}
|
||||
|
||||
@@ -26,11 +26,6 @@ const nextConfig = {
|
||||
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
|
||||
"/api/auth/**/*": ["../../node_modules/jose/**/*"],
|
||||
},
|
||||
i18n: {
|
||||
locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW", "pt-PT"],
|
||||
localeDetection: false,
|
||||
defaultLocale: "en-US",
|
||||
},
|
||||
experimental: {},
|
||||
transpilePackages: ["@formbricks/database"],
|
||||
images: {
|
||||
|
||||
@@ -187,8 +187,8 @@ x-environment: &environment
|
||||
# (Role Management is an Enterprise feature)
|
||||
# AUTH_SSO_DEFAULT_TEAM_ID=
|
||||
|
||||
# Set the below to 1 to disable the user management UI
|
||||
# DISABLE_USER_MANAGEMENT: 0
|
||||
# Configure the minimum role for user management from UI(owner, manager, disabled)
|
||||
# USER_MANAGEMENT_MINIMUM_ROLE="manager"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
||||
@@ -8,67 +8,67 @@ icon: "code"
|
||||
|
||||
These variables are present inside your machine's docker-compose file. Restart the docker containers if you change any variables for them to take effect.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| WEBAPP_URL | Base URL of the site. | required | http://localhost:3000 |
|
||||
| NEXTAUTH_URL | Location of the auth server. This should normally be the same as WEBAPP_URL | required | http://localhost:3000 |
|
||||
| DATABASE_URL | Database URL with credentials. | required | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) |
|
||||
| ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) |
|
||||
| CRON_SECRET | API Secret for running cron jobs. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) |
|
||||
| LOG_LEVEL | Minimum log level (debug, info, warn, error, fatal) | optional | info |
|
||||
| UPLOADS_DIR | Local directory for storing uploads. | optional | ./uploads |
|
||||
| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_BUCKET_NAME | S3 bucket name for data storage. Formbricks enables S3 storage when this is set. | optional (required if S3 is enabled) | |
|
||||
| S3_ENDPOINT_URL | Endpoint for S3. | optional | (resolved by the AWS SDK) |
|
||||
| SAML_DATABASE_URL | Database URL for SAML. | optional | postgres://postgres:@localhost:5432/formbricks-saml |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
| IMPRINT_ADDRESS | Address for imprint. | optional | |
|
||||
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to 1. | optional | |
|
||||
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to 1. | optional | |
|
||||
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to 1. | optional | |
|
||||
| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_AUTHENTICATED | If set to 0, the server will not require SMTP_USER and SMTP_PASSWORD(default is 1) | optional | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to 1 else to 0. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_REJECT_UNAUTHORIZED_TLS | If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs. | optional | 1 |
|
||||
| TURNSTILE_SITE_KEY | Site key for Turnstile. | optional | |
|
||||
| TURNSTILE_SECRET_KEY | Secret key for Turnstile. | optional | |
|
||||
| RECAPTCHA_SITE_KEY | Site key for survey responses recaptcha bot protection | optional | |
|
||||
| RECAPTCHA_SECRET_KEY | Secret key for recaptcha bot protection. | optional | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
|
||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
|
||||
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 |
|
||||
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
|
||||
| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | |
|
||||
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
|
||||
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
|
||||
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
|
||||
| DEFAULT_TEAM_ID | Default team ID for new users. | optional | |
|
||||
| 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 |
|
||||
| Variable | Description | Required | Default |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| WEBAPP_URL | Base URL of the site. | required | http://localhost:3000 |
|
||||
| NEXTAUTH_URL | Location of the auth server. This should normally be the same as WEBAPP_URL | required | http://localhost:3000 |
|
||||
| DATABASE_URL | Database URL with credentials. | required | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) |
|
||||
| ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) |
|
||||
| CRON_SECRET | API Secret for running cron jobs. | required | (Generated by the user, must not exceed 32 bytes, `openssl rand -hex 32`) |
|
||||
| LOG_LEVEL | Minimum log level (debug, info, warn, error, fatal) | optional | info |
|
||||
| UPLOADS_DIR | Local directory for storing uploads. | optional | ./uploads |
|
||||
| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_BUCKET_NAME | S3 bucket name for data storage. Formbricks enables S3 storage when this is set. | optional (required if S3 is enabled) | |
|
||||
| S3_ENDPOINT_URL | Endpoint for S3. | optional | (resolved by the AWS SDK) |
|
||||
| SAML_DATABASE_URL | Database URL for SAML. | optional | postgres://postgres:@localhost:5432/formbricks-saml |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
| IMPRINT_ADDRESS | Address for imprint. | optional | |
|
||||
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to 1. | optional | |
|
||||
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to 1. | optional | |
|
||||
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to 1. | optional | |
|
||||
| RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | |
|
||||
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | |
|
||||
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_AUTHENTICATED | If set to 0, the server will not require SMTP_USER and SMTP_PASSWORD(default is 1) | optional | |
|
||||
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to 1 else to 0. | optional (required if email services are to be enabled) | |
|
||||
| SMTP_REJECT_UNAUTHORIZED_TLS | If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs. | optional | 1 |
|
||||
| TURNSTILE_SITE_KEY | Site key for Turnstile. | optional | |
|
||||
| TURNSTILE_SECRET_KEY | Secret key for Turnstile. | optional | |
|
||||
| RECAPTCHA_SITE_KEY | Site key for survey responses recaptcha bot protection | optional | |
|
||||
| RECAPTCHA_SECRET_KEY | Secret key for recaptcha bot protection. | optional | |
|
||||
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
|
||||
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
|
||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
|
||||
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 |
|
||||
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
|
||||
| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | |
|
||||
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
|
||||
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
|
||||
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
|
||||
| DEFAULT_TEAM_ID | Default team ID for new users. | optional | |
|
||||
| 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 |
|
||||
| USER_MANAGEMENT_MINIMUM_ROLE | Set this to control which roles can access user management features. Accepted values: "owner", "manager", "disabled" | optional | manager |
|
||||
|
||||
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.
|
||||
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,10 +2,13 @@ import Pino, { type Logger, type LoggerOptions, stdSerializers } from "pino";
|
||||
import { type TLogLevel, ZLogLevel } from "../types/logger";
|
||||
|
||||
const IS_PRODUCTION = !process.env.NODE_ENV || process.env.NODE_ENV === "production";
|
||||
const IS_BUILD = process.env.NEXT_PHASE === "phase-production-build";
|
||||
|
||||
const getLogLevel = (): TLogLevel => {
|
||||
let logLevel: TLogLevel = "info";
|
||||
|
||||
if (IS_PRODUCTION) logLevel = "warn";
|
||||
if (IS_BUILD) logLevel = "error"; // Only show errors during build
|
||||
|
||||
const envLogLevel = process.env.LOG_LEVEL;
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
"UNKEY_ROOT_KEY",
|
||||
"PROMETHEUS_ENABLED",
|
||||
"PROMETHEUS_EXPORTER_PORT",
|
||||
"DISABLE_USER_MANAGEMENT"
|
||||
"USER_MANAGEMENT_MINIMUM_ROLE"
|
||||
],
|
||||
"outputs": ["dist/**", ".next/**"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user