chore: Api keys to org level (#5044)

Co-authored-by: Victor Santos <victor@formbricks.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Dhruwang Jariwala
2025-04-03 18:29:42 +05:30
committed by GitHub
parent 1f039d707c
commit 1b9d91f1e8
115 changed files with 3828 additions and 1161 deletions
@@ -1,6 +1,6 @@
import { getEnvironmentIdFromApiKey } from "@/modules/api/v2/management/lib/api-key";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { getApiKeyWithPermissions } from "@/modules/organization/settings/api-keys/lib/api-key";
import { TAuthenticationApiKey } from "@formbricks/types/auth";
import { Result, err, ok } from "@formbricks/types/error-handlers";
@@ -8,27 +8,22 @@ export const authenticateRequest = async (
request: Request
): Promise<Result<TAuthenticationApiKey, ApiErrorResponseV2>> => {
const apiKey = request.headers.get("x-api-key");
if (!apiKey) return err({ type: "unauthorized" });
if (apiKey) {
const environmentIdResult = await getEnvironmentIdFromApiKey(apiKey);
if (!environmentIdResult.ok) {
return err(environmentIdResult.error);
}
const environmentId = environmentIdResult.data;
const hashedApiKey = hashApiKey(apiKey);
if (environmentId) {
const authentication: TAuthenticationApiKey = {
type: "apiKey",
environmentId,
hashedApiKey,
};
return ok(authentication);
}
return err({
type: "forbidden",
});
}
return err({
type: "unauthorized",
});
const apiKeyData = await getApiKeyWithPermissions(apiKey);
if (!apiKeyData) return err({ type: "unauthorized" });
const hashedApiKey = hashApiKey(apiKey);
const authentication: TAuthenticationApiKey = {
type: "apiKey",
environmentPermissions: apiKeyData.apiKeyEnvironments.map((env) => ({
environmentId: env.environmentId,
permission: env.permission,
})),
hashedApiKey,
apiKeyId: apiKeyData.id,
organizationId: apiKeyData.organizationId,
};
return ok(authentication);
};
@@ -1,18 +0,0 @@
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { TAuthenticationApiKey } from "@formbricks/types/auth";
import { Result, err, okVoid } from "@formbricks/types/error-handlers";
export const checkAuthorization = ({
authentication,
environmentId,
}: {
authentication: TAuthenticationApiKey;
environmentId: string;
}): Result<void, ApiErrorResponseV2> => {
if (authentication.type === "apiKey" && authentication.environmentId !== environmentId) {
return err({
type: "unauthorized",
});
}
return okVoid();
};
@@ -1,11 +1,15 @@
import { getEnvironmentIdFromApiKey } from "@/modules/api/v2/management/lib/api-key";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { describe, expect, it, vi } from "vitest";
import { err, ok } from "@formbricks/types/error-handlers";
import { prisma } from "@formbricks/database";
import { authenticateRequest } from "../authenticate-request";
vi.mock("@/modules/api/v2/management/lib/api-key", () => ({
getEnvironmentIdFromApiKey: vi.fn(),
vi.mock("@formbricks/database", () => ({
prisma: {
apiKey: {
findUnique: vi.fn(),
update: vi.fn(),
},
},
}));
vi.mock("@/modules/api/v2/management/lib/utils", () => ({
@@ -17,8 +21,32 @@ describe("authenticateRequest", () => {
const request = new Request("http://localhost", {
headers: { "x-api-key": "valid-api-key" },
});
vi.mocked(getEnvironmentIdFromApiKey).mockResolvedValue(ok("env-id"));
const mockApiKeyData = {
id: "api-key-id",
organizationId: "org-id",
createdAt: new Date(),
createdBy: "user-id",
lastUsedAt: null,
label: "Test API Key",
hashedKey: "hashed-api-key",
apiKeyEnvironments: [
{
environmentId: "env-id-1",
permission: "manage",
environment: { id: "env-id-1" },
},
{
environmentId: "env-id-2",
permission: "read",
environment: { id: "env-id-2" },
},
],
};
vi.mocked(hashApiKey).mockReturnValue("hashed-api-key");
vi.mocked(prisma.apiKey.findUnique).mockResolvedValue(mockApiKeyData);
vi.mocked(prisma.apiKey.update).mockResolvedValue(mockApiKeyData);
const result = await authenticateRequest(request);
@@ -26,37 +54,28 @@ describe("authenticateRequest", () => {
if (result.ok) {
expect(result.data).toEqual({
type: "apiKey",
environmentId: "env-id",
environmentPermissions: [
{ environmentId: "env-id-1", permission: "manage" },
{ environmentId: "env-id-2", permission: "read" },
],
hashedApiKey: "hashed-api-key",
apiKeyId: "api-key-id",
organizationId: "org-id",
});
}
});
it("should return forbidden error if environmentId is not found", async () => {
it("should return unauthorized error if apiKey is not found", async () => {
const request = new Request("http://localhost", {
headers: { "x-api-key": "invalid-api-key" },
});
vi.mocked(getEnvironmentIdFromApiKey).mockResolvedValue(err({ type: "forbidden" }));
vi.mocked(prisma.apiKey.findUnique).mockResolvedValue(null);
const result = await authenticateRequest(request);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toEqual({ type: "forbidden" });
}
});
it("should return forbidden error if environmentId is empty", async () => {
const request = new Request("http://localhost", {
headers: { "x-api-key": "invalid-api-key" },
});
vi.mocked(getEnvironmentIdFromApiKey).mockResolvedValue(ok(""));
const result = await authenticateRequest(request);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toEqual({ type: "forbidden" });
expect(result.error).toEqual({ type: "unauthorized" });
}
});
@@ -1,31 +0,0 @@
import { describe, expect, it } from "vitest";
import { TAuthenticationApiKey } from "@formbricks/types/auth";
import { checkAuthorization } from "../check-authorization";
describe("checkAuthorization", () => {
it("should return ok if authentication is valid", () => {
const authentication: TAuthenticationApiKey = {
type: "apiKey",
environmentId: "env-id",
hashedApiKey: "hashed-api-key",
};
const result = checkAuthorization({ authentication, environmentId: "env-id" });
expect(result.ok).toBe(true);
});
it("should return unauthorized error if environmentId does not match", () => {
const authentication: TAuthenticationApiKey = {
type: "apiKey",
environmentId: "env-id",
hashedApiKey: "hashed-api-key",
};
const result = checkAuthorization({ authentication, environmentId: "different-env-id" });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toEqual({ type: "unauthorized" });
}
});
});
@@ -1,44 +0,0 @@
import { apiKeyCache } from "@/lib/cache/api-key";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string) => {
const hashedKey = hashApiKey(apiKey);
return cache(
async (): Promise<Result<string, ApiErrorResponseV2>> => {
if (!apiKey) {
return err({
type: "bad_request",
details: [{ field: "apiKey", issue: "API key cannot be null or undefined." }],
});
}
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey,
},
select: {
environmentId: true,
},
});
if (!apiKeyData) {
return err({ type: "not_found", details: [{ field: "apiKey", issue: "not found" }] });
}
return ok(apiKeyData.environmentId);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "apiKey", issue: error.message }] });
}
},
[`management-api-getEnvironmentIdFromApiKey-${hashedKey}`],
{
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
}
)();
});
@@ -1,81 +0,0 @@
import { apiKey, environmentId } from "./__mocks__/api-key.mock";
import { getEnvironmentIdFromApiKey } from "@/modules/api/v2/management/lib/api-key";
import { hashApiKey } from "@/modules/api/v2/management/lib/utils";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
vi.mock("@formbricks/database", () => ({
prisma: {
apiKey: {
findUnique: vi.fn(),
},
},
}));
vi.mock("@/modules/api/v2/management/lib/utils", () => ({
hashApiKey: vi.fn((input: string) => `hashed-${input}`),
}));
describe("getEnvironmentIdFromApiKey", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("returns a bad_request error if apiKey is empty", async () => {
const result = await getEnvironmentIdFromApiKey("");
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.type).toBe("bad_request");
expect(result.error.details).toEqual([
{ field: "apiKey", issue: "API key cannot be null or undefined." },
]);
}
});
test("returns a not_found error when no apiKey record is found in the database", async () => {
vi.mocked(hashApiKey).mockImplementation((input: string) => `hashed-${input}`);
vi.mocked(prisma.apiKey.findUnique).mockResolvedValue(null);
const result = await getEnvironmentIdFromApiKey(apiKey);
expect(prisma.apiKey.findUnique).toHaveBeenCalledWith({
where: { hashedKey: `hashed-${apiKey}` },
select: { environmentId: true },
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.type).toBe("not_found");
expect(result.error.details).toEqual([{ field: "apiKey", issue: "not found" }]);
}
});
test("returns ok with environmentId when a valid apiKey record is found", async () => {
vi.mocked(hashApiKey).mockImplementation((input: string) => `hashed-${input}`);
vi.mocked(prisma.apiKey.findUnique).mockResolvedValue({ environmentId });
const result = await getEnvironmentIdFromApiKey(apiKey);
expect(prisma.apiKey.findUnique).toHaveBeenCalledWith({
where: { hashedKey: `hashed-${apiKey}` },
select: { environmentId: true },
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toBe(environmentId);
}
});
test("returns internal_server_error when an exception occurs during the database lookup", async () => {
vi.mocked(hashApiKey).mockImplementation((input: string) => `hashed-${input}`);
vi.mocked(prisma.apiKey.findUnique).mockRejectedValue(new Error("Database failure"));
const result = await getEnvironmentIdFromApiKey(apiKey);
expect(prisma.apiKey.findUnique).toHaveBeenCalledWith({
where: { hashedKey: `hashed-${apiKey}` },
select: { environmentId: true },
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.type).toBe("internal_server_error");
expect(result.error.details).toEqual([{ field: "apiKey", issue: "Database failure" }]);
}
});
});
@@ -1,13 +1,13 @@
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client";
import { checkAuthorization } from "@/modules/api/v2/management/auth/check-authorization";
import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper";
import {
deleteResponse,
getResponse,
updateResponse,
} from "@/modules/api/v2/management/responses/[responseId]/lib/response";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { z } from "zod";
import { responseIdSchema, responseUpdateSchema } from "./types/responses";
@@ -33,13 +33,10 @@ export const GET = async (request: Request, props: { params: Promise<{ responseI
return handleApiError(request, environmentIdResult.error);
}
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId: environmentIdResult.data,
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, environmentIdResult.data, "GET")) {
return handleApiError(request, {
type: "unauthorized",
});
}
const response = await getResponse(params.responseId);
@@ -73,13 +70,10 @@ export const DELETE = async (request: Request, props: { params: Promise<{ respon
return handleApiError(request, environmentIdResult.error);
}
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId: environmentIdResult.data,
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, environmentIdResult.data, "DELETE")) {
return handleApiError(request, {
type: "unauthorized",
});
}
const response = await deleteResponse(params.responseId);
@@ -115,13 +109,10 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
return handleApiError(request, environmentIdResult.error);
}
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId: environmentIdResult.data,
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, environmentIdResult.data, "PUT")) {
return handleApiError(request, {
type: "unauthorized",
});
}
const response = await updateResponse(params.responseId, body);
@@ -130,16 +130,16 @@ export const createResponse = async (
};
export const getResponses = async (
environmentId: string,
environmentIds: string[],
params: TGetResponsesFilter
): Promise<Result<ApiResponseWithMeta<Response[]>, ApiErrorResponseV2>> => {
try {
const [responses, count] = await prisma.$transaction([
prisma.response.findMany({
...getResponsesQuery(environmentId, params),
...getResponsesQuery(environmentIds, params),
}),
prisma.response.count({
where: getResponsesQuery(environmentId, params).where,
where: getResponsesQuery(environmentIds, params).where,
}),
]);
@@ -11,12 +11,12 @@ vi.mock("@/modules/api/v2/management/lib/utils", () => ({
describe("getResponsesQuery", () => {
it("adds surveyId to where clause if provided", () => {
const result = getResponsesQuery("env-id", { surveyId: "survey123" } as TGetResponsesFilter);
const result = getResponsesQuery(["env-id"], { surveyId: "survey123" } as TGetResponsesFilter);
expect(result?.where?.surveyId).toBe("survey123");
});
it("adds contactId to where clause if provided", () => {
const result = getResponsesQuery("env-id", { contactId: "contact123" } as TGetResponsesFilter);
const result = getResponsesQuery(["env-id"], { contactId: "contact123" } as TGetResponsesFilter);
expect(result?.where?.contactId).toBe("contact123");
});
@@ -24,12 +24,12 @@ describe("getResponsesQuery", () => {
vi.mocked(pickCommonFilter).mockReturnValueOnce({ someFilter: true } as any);
vi.mocked(buildCommonFilterQuery).mockReturnValueOnce({ where: { combined: true } as any });
const result = getResponsesQuery("env-id", { surveyId: "test" } as TGetResponsesFilter);
const result = getResponsesQuery(["env-id"], { surveyId: "test" } as TGetResponsesFilter);
expect(pickCommonFilter).toHaveBeenCalledWith({ surveyId: "test" });
expect(buildCommonFilterQuery).toHaveBeenCalledWith(
expect.objectContaining<Prisma.ResponseFindManyArgs>({
where: {
survey: { environmentId: "env-id" },
survey: { environmentId: { in: ["env-id"] } },
surveyId: "test",
},
}),
@@ -2,11 +2,11 @@ import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/manag
import { TGetResponsesFilter } from "@/modules/api/v2/management/responses/types/responses";
import { Prisma } from "@prisma/client";
export const getResponsesQuery = (environmentId: string, params?: TGetResponsesFilter) => {
export const getResponsesQuery = (environmentIds: string[], params?: TGetResponsesFilter) => {
let query: Prisma.ResponseFindManyArgs = {
where: {
survey: {
environmentId,
environmentId: { in: environmentIds },
},
},
};
@@ -1,9 +1,10 @@
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client";
import { checkAuthorization } from "@/modules/api/v2/management/auth/check-authorization";
import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper";
import { ZGetResponsesFilter, ZResponseInput } from "@/modules/api/v2/management/responses/types/responses";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { Response } from "@prisma/client";
import { NextRequest } from "next/server";
import { createResponse, getResponses } from "./lib/response";
@@ -23,15 +24,20 @@ export const GET = async (request: NextRequest) =>
});
}
const environmentId = authentication.environmentId;
const environmentIds = authentication.environmentPermissions.map(
(permission) => permission.environmentId
);
const res = await getResponses(environmentId, query);
const environmentResponses: Response[] = [];
const res = await getResponses(environmentIds, query);
if (res.ok) {
return responses.successResponse(res.data);
if (!res.ok) {
return handleApiError(request, res.error);
}
return handleApiError(request, res.error);
environmentResponses.push(...res.data.data);
return responses.successResponse({ data: environmentResponses });
},
});
@@ -59,13 +65,10 @@ export const POST = async (request: Request) =>
const environmentId = environmentIdResult.data;
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId,
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, environmentId, "POST")) {
return handleApiError(request, {
type: "unauthorized",
});
}
// if there is a createdAt but no updatedAt, set updatedAt to createdAt
@@ -1,12 +1,12 @@
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client";
import { checkAuthorization } from "@/modules/api/v2/management/auth/check-authorization";
import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper";
import { getContact } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/contacts";
import { getResponse } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/response";
import { getSurvey } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/surveys";
import { getContactSurveyLink } from "@/modules/ee/contacts/lib/contact-survey-link";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
@@ -43,13 +43,10 @@ export const GET = async (
const environmentId = environmentIdResult.data;
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId,
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, environmentId, "GET")) {
return handleApiError(request, {
type: "unauthorized",
});
}
const surveyResult = await getSurvey(params.surveyId);
@@ -1,7 +1,6 @@
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client";
import { checkAuthorization } from "@/modules/api/v2/management/auth/check-authorization";
import { getEnvironmentIdFromSurveyIds } from "@/modules/api/v2/management/lib/helper";
import {
deleteWebhook,
@@ -12,6 +11,7 @@ import {
webhookIdSchema,
webhookUpdateSchema,
} from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { NextRequest } from "next/server";
import { z } from "zod";
@@ -38,13 +38,11 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ webho
return handleApiError(request, webhook.error);
}
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId: webhook.ok ? webhook.data.environmentId : "",
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "GET")) {
return handleApiError(request, {
type: "unauthorized",
details: [{ field: "webhook", issue: "unauthorized" }],
});
}
return responses.successResponse(webhook);
@@ -83,14 +81,11 @@ export const PUT = async (request: NextRequest, props: { params: Promise<{ webho
return handleApiError(request, webhook.error);
}
// check webhook environment against the api key environment
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId: webhook.ok ? webhook.data.environmentId : "",
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "PUT")) {
return handleApiError(request, {
type: "unauthorized",
details: [{ field: "webhook", issue: "unauthorized" }],
});
}
// check if webhook environment matches the surveys environment
@@ -136,13 +131,11 @@ export const DELETE = async (request: NextRequest, props: { params: Promise<{ we
return handleApiError(request, webhook.error);
}
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId: webhook.ok ? webhook.data.environmentId : "",
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "DELETE")) {
return handleApiError(request, {
type: "unauthorized",
details: [{ field: "webhook", issue: "unauthorized" }],
});
}
const deletedWebhook = await deleteWebhook(params.webhookId);
@@ -13,24 +13,24 @@ describe("getWebhooksQuery", () => {
it("adds surveyIds condition when provided", () => {
const params = { surveyIds: ["survey1"] } as TGetWebhooksFilter;
const result = getWebhooksQuery(environmentId, params);
const result = getWebhooksQuery([environmentId], params);
expect(result).toBeDefined();
expect(result?.where).toMatchObject({
environmentId,
environmentId: { in: [environmentId] },
surveyIds: { hasSome: ["survey1"] },
});
});
it("calls pickCommonFilter and buildCommonFilterQuery when baseFilter is present", () => {
vi.mocked(pickCommonFilter).mockReturnValue({ someFilter: "test" } as any);
getWebhooksQuery(environmentId, { surveyIds: ["survey1"] } as TGetWebhooksFilter);
getWebhooksQuery([environmentId], { surveyIds: ["survey1"] } as TGetWebhooksFilter);
expect(pickCommonFilter).toHaveBeenCalled();
expect(buildCommonFilterQuery).toHaveBeenCalled();
});
it("buildCommonFilterQuery is not called if no baseFilter is picked", () => {
vi.mocked(pickCommonFilter).mockReturnValue(undefined as any);
getWebhooksQuery(environmentId, {} as any);
getWebhooksQuery([environmentId], {} as any);
expect(buildCommonFilterQuery).not.toHaveBeenCalled();
});
});
@@ -2,10 +2,10 @@ import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/manag
import { TGetWebhooksFilter } from "@/modules/api/v2/management/webhooks/types/webhooks";
import { Prisma } from "@prisma/client";
export const getWebhooksQuery = (environmentId: string, params?: TGetWebhooksFilter) => {
export const getWebhooksQuery = (environmentIds: string[], params?: TGetWebhooksFilter) => {
let query: Prisma.WebhookFindManyArgs = {
where: {
environmentId,
environmentId: { in: environmentIds },
},
};
@@ -9,16 +9,16 @@ import { captureTelemetry } from "@formbricks/lib/telemetry";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getWebhooks = async (
environmentId: string,
environmentIds: string[],
params: TGetWebhooksFilter
): Promise<Result<ApiResponseWithMeta<Webhook[]>, ApiErrorResponseV2>> => {
try {
const [webhooks, count] = await prisma.$transaction([
prisma.webhook.findMany({
...getWebhooksQuery(environmentId, params),
...getWebhooksQuery(environmentIds, params),
}),
prisma.webhook.count({
where: getWebhooksQuery(environmentId, params).where,
where: getWebhooksQuery(environmentIds, params).where,
}),
]);
@@ -1,10 +1,10 @@
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client";
import { checkAuthorization } from "@/modules/api/v2/management/auth/check-authorization";
import { getEnvironmentIdFromSurveyIds } from "@/modules/api/v2/management/lib/helper";
import { createWebhook, getWebhooks } from "@/modules/api/v2/management/webhooks/lib/webhook";
import { ZGetWebhooksFilter, ZWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { NextRequest } from "next/server";
export const GET = async (request: NextRequest) =>
@@ -23,9 +23,11 @@ export const GET = async (request: NextRequest) =>
});
}
const environmentId = authentication.environmentId;
const environemntIds = authentication.environmentPermissions.map(
(permission) => permission.environmentId
);
const res = await getWebhooks(environmentId, query);
const res = await getWebhooks(environemntIds, query);
if (res.ok) {
return responses.successResponse(res.data);
@@ -57,24 +59,13 @@ export const POST = async (request: NextRequest) =>
return handleApiError(request, environmentIdResult.error);
}
const environmentId = environmentIdResult.data;
if (body.environmentId !== environmentId) {
if (!hasPermission(authentication.environmentPermissions, body.environmentId, "POST")) {
return handleApiError(request, {
type: "bad_request",
details: [{ field: "environmentId", issue: "does not match the surveys environment" }],
type: "forbidden",
details: [{ field: "environmentId", issue: "does not have permission to create webhook" }],
});
}
const checkAuthorizationResult = await checkAuthorization({
authentication,
environmentId,
});
if (!checkAuthorizationResult.ok) {
return handleApiError(request, checkAuthorizationResult.error);
}
const createWebhookResult = await createWebhook(body);
if (!createWebhookResult.ok) {