mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 02:55:04 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dedf552001 |
@@ -5,6 +5,7 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDisplay } from "./lib/display";
|
||||
|
||||
@@ -21,10 +22,20 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
export const POST = withV1ApiWrapper({
|
||||
handler: async ({ req, props }: THandlerParams<{ params: Promise<{ environmentId: string }> }>) => {
|
||||
const params = await props.params;
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
const inputValidation = ZDisplayCreateInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId: params.environmentId,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -38,7 +49,7 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
if (inputValidation.data.userId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(params.environmentId);
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/environment/lib/environmentState";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
|
||||
export const OPTIONS = async (): Promise<Response> => {
|
||||
return responses.successResponse(
|
||||
@@ -29,15 +30,10 @@ export const GET = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const environmentId = params.environmentId.trim();
|
||||
const idParam = params.environmentId.trim();
|
||||
|
||||
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
|
||||
// This catches all invalid formats including:
|
||||
// - null/undefined passed as string "null" or "undefined"
|
||||
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
|
||||
// - Empty or whitespace-only IDs
|
||||
// - Any other invalid CUID v1 format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
// Validate CUID format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(idParam);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -45,13 +41,23 @@ export const GET = withV1ApiWrapper({
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
"Invalid CUID format detected"
|
||||
);
|
||||
return {
|
||||
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(idParam);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", idParam),
|
||||
};
|
||||
}
|
||||
|
||||
const { environmentId } = resolved;
|
||||
|
||||
// Use optimized environment state fetcher with new caching approach
|
||||
const environmentState = await getEnvironmentState(environmentId);
|
||||
const { data } = environmentState;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { headers } from "next/headers";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
@@ -13,6 +12,7 @@ import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
@@ -66,19 +66,16 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
),
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!responseInputValidation.success) {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging
|
||||
import { MAX_FILE_UPLOAD_SIZES } from "@/lib/constants";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { getBiggerUploadFileSizePermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getSignedUrlForUpload } from "@/modules/storage/service";
|
||||
@@ -29,7 +30,16 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
export const POST = withV1ApiWrapper({
|
||||
handler: async ({ req, props }: THandlerParams<{ params: Promise<{ environmentId: string }> }>) => {
|
||||
const params = await props.params;
|
||||
const { environmentId } = params;
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
let jsonInput: TUploadPrivateFileRequest;
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displ
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDisplay } from "./lib/display";
|
||||
|
||||
@@ -25,10 +26,18 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
|
||||
export const POST = async (request: Request, context: Context): Promise<Response> => {
|
||||
const params = await context.params;
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return responses.notFoundResponse("Environment", params.environmentId);
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const jsonInput = await request.json();
|
||||
const inputValidation = ZDisplayCreateInputV2.safeParse({
|
||||
...jsonInput,
|
||||
environmentId: params.environmentId,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -40,7 +49,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
}
|
||||
|
||||
if (inputValidation.data.contactId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(params.environmentId);
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { headers } from "next/headers";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
|
||||
@@ -12,6 +11,7 @@ import { getSurvey } from "@/lib/survey/service";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -49,17 +49,14 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInputV2.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
);
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return responses.notFoundResponse("Environment", params.environmentId);
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const responseInputValidation = ZResponseInputV2.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!responseInputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { resolveClientApiIds } from "./resolve-client-id";
|
||||
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
environment: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
workspace: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("resolveClientApiIds", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("resolves an environmentId to environmentId + workspaceId", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
id: "env-123",
|
||||
workspaceId: "ws-456",
|
||||
} as any);
|
||||
|
||||
const result = await resolveClientApiIds("env-123");
|
||||
|
||||
expect(result).toEqual({
|
||||
environmentId: "env-123",
|
||||
workspaceId: "ws-456",
|
||||
});
|
||||
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "env-123" },
|
||||
select: { id: true, workspaceId: true },
|
||||
});
|
||||
expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves a workspaceId to workspaceId + production environmentId", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workspace.findUnique).mockResolvedValue({
|
||||
id: "ws-456",
|
||||
environments: [{ id: "env-prod-789" }],
|
||||
} as any);
|
||||
|
||||
const result = await resolveClientApiIds("ws-456");
|
||||
|
||||
expect(result).toEqual({
|
||||
environmentId: "env-prod-789",
|
||||
workspaceId: "ws-456",
|
||||
});
|
||||
expect(prisma.workspace.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "ws-456" },
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
where: { type: "production" },
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when neither environment nor workspace is found", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workspace.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await resolveClientApiIds("unknown-id");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when workspace exists but has no production environment", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workspace.findUnique).mockResolvedValue({
|
||||
id: "ws-456",
|
||||
environments: [],
|
||||
} as any);
|
||||
|
||||
const result = await resolveClientApiIds("ws-456");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import "server-only";
|
||||
import { prisma } from "@formbricks/database";
|
||||
|
||||
export type TResolvedClientIds = {
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a URL parameter that may be an environmentId (old SDK) or workspaceId (new SDK).
|
||||
*
|
||||
* - If the id matches an Environment, returns the environment's id and its parent workspaceId.
|
||||
* - If not, checks the Workspace table and returns the workspace's production environment id.
|
||||
* - Returns null if neither lookup succeeds.
|
||||
*/
|
||||
export const resolveClientApiIds = async (id: string): Promise<TResolvedClientIds | null> => {
|
||||
// Try as environmentId first (existing SDKs)
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, workspaceId: true },
|
||||
});
|
||||
|
||||
if (environment) {
|
||||
return { workspaceId: environment.workspaceId, environmentId: environment.id };
|
||||
}
|
||||
|
||||
// Try as workspaceId (new SDKs sending workspaceId)
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
where: { type: "production" },
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (workspace && workspace.environments[0]) {
|
||||
return { workspaceId: workspace.id, environmentId: workspace.environments[0].id };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { TJsPersonState } from "@formbricks/types/js";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { updateUser } from "./lib/update-user";
|
||||
|
||||
@@ -45,15 +46,10 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const environmentId = params.environmentId.trim();
|
||||
const idParam = params.environmentId.trim();
|
||||
|
||||
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
|
||||
// This catches all invalid formats including:
|
||||
// - null/undefined passed as string "null" or "undefined"
|
||||
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
|
||||
// - Empty or whitespace-only IDs
|
||||
// - Any other invalid CUID v1 format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
// Validate CUID format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(idParam);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -61,13 +57,22 @@ export const POST = withV1ApiWrapper({
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
"Invalid CUID format detected"
|
||||
);
|
||||
return {
|
||||
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(idParam);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", idParam),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// Basic input validation without Zod overhead
|
||||
|
||||
Reference in New Issue
Block a user