diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.tsx index b1b94207a4..4823a18f26 100644 --- a/apps/web/app/(app)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/layout.tsx @@ -5,17 +5,18 @@ import { redirect } from "next/navigation"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import FormbricksClient from "../../FormbricksClient"; import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext"; -import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; export default async function EnvironmentLayout({ children, params }) { const session = await getServerSession(authOptions); if (!session) { return redirect(`/auth/login`); } - const hasAccess = await hasUserEnvironmentAccess(session.user, params.environmentId); + const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId); if (!hasAccess) { - throw new Error("User does not have access to this environment"); + throw new AuthorizationError("Not authorized"); } return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx index 1ad90e1cd5..0ab082d63c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx @@ -1,6 +1,6 @@ import EditApiKeys from "./EditApiKeys"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getApiKeys } from "@formbricks/lib/services/apiKey"; +import { getApiKeys } from "@formbricks/lib/apiKey/service"; import { getEnvironments } from "@formbricks/lib/services/environment"; export default async function ApiKeyList({ diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx index c8fd9f4670..1a4fbd04d7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx @@ -34,19 +34,29 @@ export default function EditAPIKeys({ }; const handleDeleteKey = async () => { - await deleteApiKeyAction(activeKey.id); - const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || []; - setApiKeysLocal(updatedApiKeys); - setOpenDeleteKeyModal(false); - toast.success("API Key deleted"); + try { + await deleteApiKeyAction(activeKey.id); + const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || []; + setApiKeysLocal(updatedApiKeys); + toast.success("API Key deleted"); + } catch (e) { + toast.error("Unable to delete API Key"); + } finally { + setOpenDeleteKeyModal(false); + } }; const handleAddAPIKey = async (data) => { - const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label }); - const updatedApiKeys = [...apiKeysLocal!, apiKey]; - setApiKeysLocal(updatedApiKeys); - setOpenAddAPIKeyModal(false); - toast.success("API key created"); + try { + const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label }); + const updatedApiKeys = [...apiKeysLocal!, apiKey]; + setApiKeysLocal(updatedApiKeys); + toast.success("API key created"); + } catch (e) { + toast.error("Unable to create API Key"); + } finally { + setOpenAddAPIKeyModal(false); + } }; return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts index 3220686f37..49d513804e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts @@ -1,11 +1,32 @@ "use server"; -import { deleteApiKey, createApiKey } from "@formbricks/lib/services/apiKey"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { deleteApiKey, createApiKey } from "@formbricks/lib/apiKey/service"; +import { canUserAccessApiKey } from "@formbricks/lib/apiKey/auth"; import { TApiKeyCreateInput } from "@formbricks/types/v1/apiKeys"; +import { getServerSession } from "next-auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; export async function deleteApiKeyAction(id: string) { - return await deleteApiKey(id); + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + const isAuthorized = await canUserAccessApiKey(session.user.id, id); + + if (isAuthorized) { + return await deleteApiKey(id); + } else { + throw new AuthorizationError("Not authorized"); + } } export async function createApiKeyAction(environmentId: string, apiKeyData: TApiKeyCreateInput) { - return await createApiKey(environmentId, apiKeyData); + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); + + if (isAuthorized) { + return await createApiKey(environmentId, apiKeyData); + } else { + throw new AuthorizationError("Not authorized"); + } } diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts index 4a01e3e314..300928c2f0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts @@ -3,12 +3,12 @@ import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/services/product"; import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product"; import { getServerSession } from "next-auth"; -import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; +import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; import { getEnvironment } from "@formbricks/lib/services/environment"; import { TEnvironment } from "@formbricks/types/v1/environment"; -import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; import { getMembershipByUserIdTeamId } from "@formbricks/lib/services/membership"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; export const updateProductAction = async ( environmentId: string, @@ -34,8 +34,8 @@ export const updateProductAction = async ( throw err; } - if (!hasUserEnvironmentAccess(session.user, environment.id)) { - throw new AuthenticationError("You don't have access to this environment"); + if (!hasUserEnvironmentAccess(session.user.id, environment.id)) { + throw new AuthorizationError("Not authorized"); } const updatedProduct = await updateProduct(productId, data); @@ -62,15 +62,15 @@ export const deleteProductAction = async (environmentId: string, userId: string, throw err; } - if (!hasUserEnvironmentAccess(session.user, environment.id)) { - throw new AuthenticationError("You don't have access to this environment"); + if (!hasUserEnvironmentAccess(session.user.id, environment.id)) { + throw new AuthorizationError("Not authorized"); } const team = await getTeamByEnvironmentId(environmentId); const membership = team ? await getMembershipByUserIdTeamId(userId, team.id) : null; if (membership?.role !== "admin" && membership?.role !== "owner") { - throw new AuthenticationError("You are not allowed to delete products."); + throw new AuthorizationError("You are not allowed to delete products."); } const availableProducts = team ? await getProducts(team.id) : null; diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 475bc8a268..9e3864a54d 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -1,4 +1,4 @@ -import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { GOOGLE_SHEETS_CLIENT_ID, GOOGLE_SHEETS_CLIENT_SECRET, @@ -19,11 +19,15 @@ export async function GET(req: NextRequest) { const environmentId = req.headers.get("environmentId"); const session = await getServerSession(authOptions); + if (!environmentId) { + return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 }); + } + if (!session) { return NextResponse.json({ Error: "Invalid session" }, { status: 400 }); } - const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user, environmentId); + const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId); if (!canUserAccessEnvironment) { return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); } diff --git a/apps/web/app/api/v1/auth.ts b/apps/web/app/api/v1/auth.ts index 2234d081f7..c8403b90bb 100644 --- a/apps/web/app/api/v1/auth.ts +++ b/apps/web/app/api/v1/auth.ts @@ -1,4 +1,4 @@ -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; +import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { TAuthenticationApiKey } from "@formbricks/types/v1/auth"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; import { responses } from "@/lib/api/response"; diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index ac1325f890..42375f3ed9 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -3,7 +3,7 @@ import { NextResponse } from "next/server"; import { transformErrorToDetails } from "@/lib/api/validator"; import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/services/response"; import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses"; -import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getSurvey } from "@formbricks/lib/services/survey"; import { authenticateRequest } from "@/app/api/v1/auth"; import { handleErrorResponse } from "@/app/api/v1/auth"; @@ -21,7 +21,7 @@ const canUserAccessResponse = async (authentication: any, response: TResponse): if (!survey) return false; if (authentication.type === "session") { - return await hasUserEnvironmentAccess(authentication.session.user, survey.environmentId); + return await hasUserEnvironmentAccess(authentication.session.user.id, survey.environmentId); } else if (authentication.type === "apiKey") { return survey.environmentId === authentication.environmentId; } else { diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index e500401fcd..f0d51dcba9 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -1,5 +1,5 @@ import { responses } from "@/lib/api/response"; -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; +import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { deleteWebhook, getWebhook } from "@formbricks/lib/services/webhook"; import { headers } from "next/headers"; diff --git a/apps/web/app/api/v1/webhooks/route.ts b/apps/web/app/api/v1/webhooks/route.ts index 31d2d77a88..ae1fd52d05 100644 --- a/apps/web/app/api/v1/webhooks/route.ts +++ b/apps/web/app/api/v1/webhooks/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/lib/api/response"; import { transformErrorToDetails } from "@/lib/api/validator"; +import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { DatabaseError, InvalidInputError } from "@formbricks/types/v1/errors"; -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; import { createWebhook, getWebhooks } from "@formbricks/lib/services/webhook"; import { ZWebhookInput } from "@formbricks/types/v1/webhooks"; import { headers } from "next/headers"; diff --git a/apps/web/lib/api/apiHelper.ts b/apps/web/lib/api/apiHelper.ts index 010ebe48a8..75f158f8e5 100644 --- a/apps/web/lib/api/apiHelper.ts +++ b/apps/web/lib/api/apiHelper.ts @@ -1,5 +1,6 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { prisma } from "@formbricks/database"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { createHash } from "crypto"; import { NextApiRequest, NextApiResponse } from "next"; import type { Session } from "next-auth"; @@ -22,7 +23,7 @@ export const hasEnvironmentAccess = async ( if (!user) { return false; } - const ownership = await hasUserEnvironmentAccess(user, environmentId); + const ownership = await hasUserEnvironmentAccess(user.id, environmentId); if (!ownership) { return false; } @@ -30,34 +31,6 @@ export const hasEnvironmentAccess = async ( return true; }; -export const hasUserEnvironmentAccess = async (user, environmentId) => { - const environment = await prisma.environment.findUnique({ - where: { - id: environmentId, - }, - select: { - product: { - select: { - team: { - select: { - memberships: { - select: { - userId: true, - }, - }, - }, - }, - }, - }, - }, - }); - const environmentUsers = environment?.product.team.memberships.map((member) => member.userId) || []; - if (environmentUsers.includes(user.id)) { - return true; - } - return false; -}; - export const getPlan = async (req, res) => { if (req.headers["x-api-key"]) { const apiKey = req.headers["x-api-key"].toString(); diff --git a/packages/lib/apiKey/auth.ts b/packages/lib/apiKey/auth.ts new file mode 100644 index 0000000000..58be5678cb --- /dev/null +++ b/packages/lib/apiKey/auth.ts @@ -0,0 +1,22 @@ +import { hasUserEnvironmentAccess } from "../environment/auth"; +import { getApiKey } from "./service"; +import { unstable_cache } from "next/cache"; + +export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Promise => { + return await unstable_cache( + async () => { + if (!userId) return false; + + const apiKeyFromServer = await getApiKey(apiKeyId); + if (!apiKeyFromServer) return false; + + const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, apiKeyFromServer.environmentId); + if (!hasAccessToEnvironment) return false; + + return true; + }, + + [`users-${userId}-apiKeys-${apiKeyId}`], + { revalidate: 30 * 60, tags: [`apiKeys-${apiKeyId}`] } + )(); // 30 minutes +}; diff --git a/packages/lib/services/apiKey.ts b/packages/lib/apiKey/service.ts similarity index 93% rename from packages/lib/services/apiKey.ts rename to packages/lib/apiKey/service.ts index 3794d9ca8f..89bfd03d80 100644 --- a/packages/lib/services/apiKey.ts +++ b/packages/lib/apiKey/service.ts @@ -10,21 +10,21 @@ import { cache } from "react"; import { validateInputs } from "../utils/validate"; import { ZId } from "@formbricks/types/v1/environment"; -export const getApiKey = async (apiKey: string): Promise => { - validateInputs([apiKey, z.string()]); - if (!apiKey) { +export const getApiKey = async (apiKeyId: string): Promise => { + validateInputs([apiKeyId, z.string()]); + if (!apiKeyId) { throw new InvalidInputError("API key cannot be null or undefined."); } try { const apiKeyData = await prisma.apiKey.findUnique({ where: { - hashedKey: getHash(apiKey), + id: apiKeyId, }, }); if (!apiKeyData) { - throw new ResourceNotFoundError("API Key", apiKey); + throw new ResourceNotFoundError("API Key from ID", apiKeyId); } return apiKeyData; diff --git a/packages/lib/environment/auth.ts b/packages/lib/environment/auth.ts new file mode 100644 index 0000000000..c3dc535659 --- /dev/null +++ b/packages/lib/environment/auth.ts @@ -0,0 +1,34 @@ +import { prisma } from "@formbricks/database"; +import { unstable_cache } from "next/cache"; + +export const hasUserEnvironmentAccess = async (userId: string, environmentId: string) => { + return await unstable_cache( + async () => { + if (!userId) return false; + const environment = await prisma.environment.findUnique({ + where: { + id: environmentId, + }, + select: { + product: { + select: { + team: { + select: { + memberships: { + select: { + userId: true, + }, + }, + }, + }, + }, + }, + }, + }); + const environmentUsers = environment?.product.team.memberships.map((member) => member.userId) || []; + return environmentUsers.includes(userId); + }, + [`users-${userId}-environments-${environmentId}`], + { revalidate: 30 * 60, tags: [`environments-${environmentId}`] } + )(); // 30 minutes +}; diff --git a/packages/lib/package.json b/packages/lib/package.json index a1b89465d7..94fadbfe0d 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -16,6 +16,7 @@ "@formbricks/types": "*", "@paralleldrive/cuid2": "^2.2.2", "date-fns": "^2.30.0", + "next-auth": "^4.22.3", "jsonwebtoken": "^9.0.2", "markdown-it": "^13.0.2", "nodemailer": "^6.9.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 246b561f55..7a34ad0004 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 3.12.7 turbo: specifier: latest - version: 1.10.13 + version: 1.10.3 apps/demo: dependencies: @@ -22049,64 +22049,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.13: - resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} + /turbo-darwin-64@1.10.3: + resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.13: - resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} + /turbo-darwin-arm64@1.10.3: + resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.13: - resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} + /turbo-linux-64@1.10.3: + resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.13: - resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} + /turbo-linux-arm64@1.10.3: + resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.13: - resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} + /turbo-windows-64@1.10.3: + resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.13: - resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} + /turbo-windows-arm64@1.10.3: + resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.13: - resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} + /turbo@1.10.3: + resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==} hasBin: true + requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.13 - turbo-darwin-arm64: 1.10.13 - turbo-linux-64: 1.10.13 - turbo-linux-arm64: 1.10.13 - turbo-windows-64: 1.10.13 - turbo-windows-arm64: 1.10.13 + turbo-darwin-64: 1.10.3 + turbo-darwin-arm64: 1.10.3 + turbo-linux-64: 1.10.3 + turbo-linux-arm64: 1.10.3 + turbo-windows-64: 1.10.3 + turbo-windows-arm64: 1.10.3 dev: true /tween-functions@1.2.0: