From 02b25138ef603ccac33dbaf9eb53979a7ffca66d Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:06:29 +0530 Subject: [PATCH] chore: API key types (#4610) Co-authored-by: pandeymangg --- apps/web/app/api/v1/auth.ts | 8 +- apps/web/app/api/v1/lib/api-key.ts | 50 ++++++++ .../app/api/v1/webhooks/[webhookId]/route.ts | 14 +-- apps/web/app/api/v1/webhooks/route.ts | 14 +-- .../cache.ts => apps/web/lib/cache/api-key.ts | 0 apps/web/lib/utils/services.ts | 2 +- .../projects/settings/api-keys/actions.ts | 4 +- .../api-keys/components/api-key-list.tsx | 2 +- .../api-keys/components/edit-api-keys.tsx | 3 +- .../settings/{ => api-keys}/lib/api-key.ts | 44 ++++++- .../settings/api-keys/types/api-keys.ts | 15 +++ packages/{types => database/zod}/api-keys.ts | 11 +- packages/lib/apiKey/auth.ts | 29 ----- packages/lib/apiKey/service.ts | 109 ------------------ packages/lib/tsconfig.json | 3 +- packages/types/contact.ts | 11 -- packages/types/displays.ts | 6 - 17 files changed, 131 insertions(+), 194 deletions(-) create mode 100644 apps/web/app/api/v1/lib/api-key.ts rename packages/lib/apiKey/cache.ts => apps/web/lib/cache/api-key.ts (100%) rename apps/web/modules/projects/settings/{ => api-keys}/lib/api-key.ts (53%) create mode 100644 apps/web/modules/projects/settings/api-keys/types/api-keys.ts rename packages/{types => database/zod}/api-keys.ts (51%) delete mode 100644 packages/lib/apiKey/auth.ts delete mode 100644 packages/lib/apiKey/service.ts delete mode 100644 packages/types/contact.ts diff --git a/apps/web/app/api/v1/auth.ts b/apps/web/app/api/v1/auth.ts index 4e29cb5785..0fe9090188 100644 --- a/apps/web/app/api/v1/auth.ts +++ b/apps/web/app/api/v1/auth.ts @@ -1,16 +1,16 @@ import { responses } from "@/app/lib/api/response"; -import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { getEnvironmentIdFromApiKey } from "./lib/api-key"; export const authenticateRequest = async (request: Request): Promise => { const apiKey = request.headers.get("x-api-key"); if (apiKey) { - const apiKeyData = await getApiKeyFromKey(apiKey); - if (apiKeyData) { + const environmentId = await getEnvironmentIdFromApiKey(apiKey); + if (environmentId) { const authentication: TAuthenticationApiKey = { type: "apiKey", - environmentId: apiKeyData.environmentId, + environmentId, }; return authentication; } diff --git a/apps/web/app/api/v1/lib/api-key.ts b/apps/web/app/api/v1/lib/api-key.ts new file mode 100644 index 0000000000..c90e80d216 --- /dev/null +++ b/apps/web/app/api/v1/lib/api-key.ts @@ -0,0 +1,50 @@ +import { apiKeyCache } from "@/lib/cache/api-key"; +import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { getHash } from "@formbricks/lib/crypto"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { ZString } from "@formbricks/types/common"; +import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { InvalidInputError } from "@formbricks/types/errors"; + +export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Promise => { + const hashedKey = getHash(apiKey); + return cache( + async () => { + validateInputs([apiKey, ZString]); + + if (!apiKey) { + throw new InvalidInputError("API key cannot be null or undefined."); + } + + try { + const apiKeyData = await prisma.apiKey.findUnique({ + where: { + hashedKey, + }, + select: { + environmentId: true, + }, + }); + + if (!apiKeyData) { + throw new ResourceNotFoundError("apiKey", apiKey); + } + + return apiKeyData.environmentId; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getEnvironmentIdFromApiKey-${apiKey}`], + { + tags: [apiKeyCache.tag.byHashedKey(hashedKey)], + } + )(); +}); diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index 30e3d51149..9502ab7483 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -1,6 +1,6 @@ +import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key"; import { responses } from "@/app/lib/api/response"; import { headers } from "next/headers"; -import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { deleteWebhook, getWebhook } from "@formbricks/lib/webhook/service"; export const GET = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => { @@ -10,8 +10,8 @@ export const GET = async (_: Request, props: { params: Promise<{ webhookId: stri if (!apiKey) { return responses.notAuthenticatedResponse(); } - const apiKeyData = await getApiKeyFromKey(apiKey); - if (!apiKeyData) { + const environmentId = await getEnvironmentIdFromApiKey(apiKey); + if (!environmentId) { return responses.notAuthenticatedResponse(); } @@ -20,7 +20,7 @@ export const GET = async (_: Request, props: { params: Promise<{ webhookId: stri if (!webhook) { return responses.notFoundResponse("Webhook", params.webhookId); } - if (webhook.environmentId !== apiKeyData.environmentId) { + if (webhook.environmentId !== environmentId) { return responses.unauthorizedResponse(); } return responses.successResponse(webhook); @@ -33,8 +33,8 @@ export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: s if (!apiKey) { return responses.notAuthenticatedResponse(); } - const apiKeyData = await getApiKeyFromKey(apiKey); - if (!apiKeyData) { + const environmentId = await getEnvironmentIdFromApiKey(apiKey); + if (!environmentId) { return responses.notAuthenticatedResponse(); } @@ -43,7 +43,7 @@ export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: s if (!webhook) { return responses.notFoundResponse("Webhook", params.webhookId); } - if (webhook.environmentId !== apiKeyData.environmentId) { + if (webhook.environmentId !== environmentId) { return responses.unauthorizedResponse(); } diff --git a/apps/web/app/api/v1/webhooks/route.ts b/apps/web/app/api/v1/webhooks/route.ts index bbee01fc73..04a3d29fd4 100644 --- a/apps/web/app/api/v1/webhooks/route.ts +++ b/apps/web/app/api/v1/webhooks/route.ts @@ -1,7 +1,7 @@ +import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { headers } from "next/headers"; -import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { createWebhook, getWebhooks } from "@formbricks/lib/webhook/service"; import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; import { ZWebhookInput } from "@formbricks/types/webhooks"; @@ -12,14 +12,14 @@ export const GET = async () => { if (!apiKey) { return responses.notAuthenticatedResponse(); } - const apiKeyData = await getApiKeyFromKey(apiKey); - if (!apiKeyData) { + const environmentId = await getEnvironmentIdFromApiKey(apiKey); + if (!environmentId) { return responses.notAuthenticatedResponse(); } // get webhooks from database try { - const webhooks = await getWebhooks(apiKeyData.environmentId); + const webhooks = await getWebhooks(environmentId); return Response.json({ data: webhooks }); } catch (error) { if (error instanceof DatabaseError) { @@ -35,8 +35,8 @@ export const POST = async (request: Request) => { if (!apiKey) { return responses.notAuthenticatedResponse(); } - const apiKeyData = await getApiKeyFromKey(apiKey); - if (!apiKeyData) { + const environmentId = await getEnvironmentIdFromApiKey(apiKey); + if (!environmentId) { return responses.notAuthenticatedResponse(); } const webhookInput = await request.json(); @@ -52,7 +52,7 @@ export const POST = async (request: Request) => { // add webhook to database try { - const webhook = await createWebhook(apiKeyData.environmentId, inputValidation.data); + const webhook = await createWebhook(environmentId, inputValidation.data); return responses.successResponse(webhook); } catch (error) { if (error instanceof InvalidInputError) { diff --git a/packages/lib/apiKey/cache.ts b/apps/web/lib/cache/api-key.ts similarity index 100% rename from packages/lib/apiKey/cache.ts rename to apps/web/lib/cache/api-key.ts diff --git a/apps/web/lib/utils/services.ts b/apps/web/lib/utils/services.ts index 80d3ab502f..a157afa30d 100644 --- a/apps/web/lib/utils/services.ts +++ b/apps/web/lib/utils/services.ts @@ -1,12 +1,12 @@ "use server"; +import { apiKeyCache } from "@/lib/cache/api-key"; import { contactCache } from "@/lib/cache/contact"; import { teamCache } from "@/lib/cache/team"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { actionClassCache } from "@formbricks/lib/actionClass/cache"; -import { apiKeyCache } from "@formbricks/lib/apiKey/cache"; import { cache } from "@formbricks/lib/cache"; import { segmentCache } from "@formbricks/lib/cache/segment"; import { environmentCache } from "@formbricks/lib/environment/cache"; diff --git a/apps/web/modules/projects/settings/api-keys/actions.ts b/apps/web/modules/projects/settings/api-keys/actions.ts index e689e6b244..66736045e1 100644 --- a/apps/web/modules/projects/settings/api-keys/actions.ts +++ b/apps/web/modules/projects/settings/api-keys/actions.ts @@ -8,10 +8,10 @@ import { getProjectIdFromApiKeyId, getProjectIdFromEnvironmentId, } from "@/lib/utils/helper"; -import { createApiKey, deleteApiKey } from "@/modules/projects/settings/lib/api-key"; +import { createApiKey, deleteApiKey } from "@/modules/projects/settings/api-keys/lib/api-key"; import { z } from "zod"; -import { ZApiKeyCreateInput } from "@formbricks/types/api-keys"; import { ZId } from "@formbricks/types/common"; +import { ZApiKeyCreateInput } from "./types/api-keys"; const ZDeleteApiKeyAction = z.object({ id: ZId, diff --git a/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx b/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx index f94cc4e364..90c8608437 100644 --- a/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx +++ b/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx @@ -1,5 +1,5 @@ +import { getApiKeys } from "@/modules/projects/settings/api-keys/lib/api-key"; import { getTranslations } from "next-intl/server"; -import { getApiKeys } from "@formbricks/lib/apiKey/service"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; import { TUserLocale } from "@formbricks/types/user"; diff --git a/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx b/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx index 2292bc5db2..e6ab2975dc 100644 --- a/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx +++ b/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx @@ -1,6 +1,7 @@ "use client"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { TApiKey } from "@/modules/projects/settings/api-keys/types/api-keys"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { FilesIcon, TrashIcon } from "lucide-react"; @@ -8,7 +9,6 @@ import { useTranslations } from "next-intl"; import { useState } from "react"; import toast from "react-hot-toast"; import { timeSince } from "@formbricks/lib/time"; -import { TApiKey } from "@formbricks/types/api-keys"; import { TUserLocale } from "@formbricks/types/user"; import { createApiKeyAction, deleteApiKeyAction } from "../actions"; import { AddApiKeyModal } from "./add-api-key-modal"; @@ -60,7 +60,6 @@ export const EditAPIKeys = ({ environmentId: environmentTypeId, apiKeyData: { label: data.label }, }); - console.log("createApiKeyResponse", createApiKeyResponse); if (createApiKeyResponse?.data) { const updatedApiKeys = [...apiKeysLocal!, createApiKeyResponse.data]; setApiKeysLocal(updatedApiKeys); diff --git a/apps/web/modules/projects/settings/lib/api-key.ts b/apps/web/modules/projects/settings/api-keys/lib/api-key.ts similarity index 53% rename from apps/web/modules/projects/settings/lib/api-key.ts rename to apps/web/modules/projects/settings/api-keys/lib/api-key.ts index 2677728a85..d341a94d21 100644 --- a/apps/web/modules/projects/settings/lib/api-key.ts +++ b/apps/web/modules/projects/settings/api-keys/lib/api-key.ts @@ -1,14 +1,48 @@ import "server-only"; -import { Prisma } from "@prisma/client"; +import { apiKeyCache } from "@/lib/cache/api-key"; +import { TApiKeyCreateInput, ZApiKeyCreateInput } from "@/modules/projects/settings/api-keys/types/api-keys"; +import { TApiKey } from "@/modules/projects/settings/api-keys/types/api-keys"; +import { ApiKey, Prisma } from "@prisma/client"; import { createHash, randomBytes } from "crypto"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; -import { apiKeyCache } from "@formbricks/lib/apiKey/cache"; +import { cache } from "@formbricks/lib/cache"; +import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; import { validateInputs } from "@formbricks/lib/utils/validate"; -import { TApiKey, TApiKeyCreateInput, ZApiKeyCreateInput } from "@formbricks/types/api-keys"; -import { ZId } from "@formbricks/types/common"; +import { ZId, ZOptionalNumber } from "@formbricks/types/common"; import { DatabaseError } from "@formbricks/types/errors"; -export const deleteApiKey = async (id: string): Promise => { +export const getApiKeys = reactCache( + async (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + + try { + const apiKeys = await prisma.apiKey.findMany({ + where: { + environmentId, + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + + return apiKeys; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } + }, + [`getApiKeys-${environmentId}-${page}`], + { + tags: [apiKeyCache.tag.byEnvironmentId(environmentId)], + } + )() +); + +export const deleteApiKey = async (id: string): Promise => { validateInputs([id, ZId]); try { diff --git a/apps/web/modules/projects/settings/api-keys/types/api-keys.ts b/apps/web/modules/projects/settings/api-keys/types/api-keys.ts new file mode 100644 index 0000000000..eb430093f0 --- /dev/null +++ b/apps/web/modules/projects/settings/api-keys/types/api-keys.ts @@ -0,0 +1,15 @@ +import { ApiKey } from "@prisma/client"; +import { z } from "zod"; +import { ZApiKey } from "@formbricks/database/zod/api-keys"; + +export const ZApiKeyCreateInput = ZApiKey.required({ + label: true, +}).pick({ + label: true, +}); + +export type TApiKeyCreateInput = z.infer; + +export interface TApiKey extends ApiKey { + apiKey?: string; +} diff --git a/packages/types/api-keys.ts b/packages/database/zod/api-keys.ts similarity index 51% rename from packages/types/api-keys.ts rename to packages/database/zod/api-keys.ts index b9fbd616fd..f63134973c 100644 --- a/packages/types/api-keys.ts +++ b/packages/database/zod/api-keys.ts @@ -1,3 +1,4 @@ +import { type ApiKey } from "@prisma/client"; import { z } from "zod"; export const ZApiKey = z.object({ @@ -7,12 +8,4 @@ export const ZApiKey = z.object({ label: z.string().nullable(), hashedKey: z.string(), environmentId: z.string().cuid2(), - apiKey: z.string().optional(), -}); - -export type TApiKey = z.infer; - -export const ZApiKeyCreateInput = z.object({ - label: z.string(), -}); -export type TApiKeyCreateInput = z.infer; +}) satisfies z.ZodType; diff --git a/packages/lib/apiKey/auth.ts b/packages/lib/apiKey/auth.ts deleted file mode 100644 index 89b525c12f..0000000000 --- a/packages/lib/apiKey/auth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import "server-only"; -import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; -import { hasUserEnvironmentAccess } from "../environment/auth"; -import { validateInputs } from "../utils/validate"; -import { apiKeyCache } from "./cache"; -import { getApiKey } from "./service"; - -export const canUserAccessApiKey = (userId: string, apiKeyId: string): Promise => - cache( - async () => { - validateInputs([userId, ZId], [apiKeyId, ZId]); - - try { - const apiKeyFromServer = await getApiKey(apiKeyId); - if (!apiKeyFromServer) return false; - - const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, apiKeyFromServer.environmentId); - if (!hasAccessToEnvironment) return false; - - return true; - } catch (error) { - throw error; - } - }, - - [`canUserAccessApiKey-${userId}-${apiKeyId}`], - { tags: [apiKeyCache.tag.byId(apiKeyId)] } - )(); diff --git a/packages/lib/apiKey/service.ts b/packages/lib/apiKey/service.ts deleted file mode 100644 index 5139ee447e..0000000000 --- a/packages/lib/apiKey/service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import "server-only"; -import { Prisma } from "@prisma/client"; -import { cache as reactCache } from "react"; -import { prisma } from "@formbricks/database"; -import { TApiKey } from "@formbricks/types/api-keys"; -import { ZOptionalNumber, ZString } from "@formbricks/types/common"; -import { ZId } from "@formbricks/types/common"; -import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; -import { cache } from "../cache"; -import { ITEMS_PER_PAGE } from "../constants"; -import { getHash } from "../crypto"; -import { validateInputs } from "../utils/validate"; -import { apiKeyCache } from "./cache"; - -export const getApiKey = reactCache( - async (apiKeyId: string): Promise => - cache( - async () => { - validateInputs([apiKeyId, ZString]); - - if (!apiKeyId) { - throw new InvalidInputError("API key cannot be null or undefined."); - } - - try { - const apiKeyData = await prisma.apiKey.findUnique({ - where: { - id: apiKeyId, - }, - }); - - return apiKeyData; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getApiKey-${apiKeyId}`], - { - tags: [apiKeyCache.tag.byId(apiKeyId)], - } - )() -); - -export const getApiKeys = reactCache( - async (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - - try { - const apiKeys = await prisma.apiKey.findMany({ - where: { - environmentId, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - - return apiKeys; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } - }, - [`getApiKeys-${environmentId}-${page}`], - { - tags: [apiKeyCache.tag.byEnvironmentId(environmentId)], - } - )() -); - -export const getApiKeyFromKey = reactCache(async (apiKey: string): Promise => { - const hashedKey = getHash(apiKey); - return cache( - async () => { - validateInputs([apiKey, ZString]); - - if (!apiKey) { - throw new InvalidInputError("API key cannot be null or undefined."); - } - - try { - const apiKeyData = await prisma.apiKey.findUnique({ - where: { - hashedKey, - }, - }); - - return apiKeyData; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getApiKeyFromKey-${apiKey}`], - { - tags: [apiKeyCache.tag.byHashedKey(hashedKey)], - } - )(); -}); diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 02bc376767..4ee33b2934 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -18,6 +18,7 @@ ".", "../types/*.d.ts", "../../apps/web/lib/cache/contact-attribute-key.ts", - "../../apps/web/modules/utils/hooks" + "../../apps/web/modules/utils/hooks", + "../../apps/web/lib/cache/api-key.ts" ] } diff --git a/packages/types/contact.ts b/packages/types/contact.ts deleted file mode 100644 index f82ec4bd0f..0000000000 --- a/packages/types/contact.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; -import { ZId } from "./common"; - -export const ZContact = z.object({ - id: ZId, - createdAt: z.date(), - updatedAt: z.date(), - environmentId: ZId, -}); - -export type TContact = z.infer; diff --git a/packages/types/displays.ts b/packages/types/displays.ts index 1ad963be02..29da1ee870 100644 --- a/packages/types/displays.ts +++ b/packages/types/displays.ts @@ -20,12 +20,6 @@ export const ZDisplayCreateInput = z.object({ export type TDisplayCreateInput = z.infer; -export const ZDisplaysWithSurveyName = ZDisplay.extend({ - surveyName: z.string(), -}); - -export type TDisplaysWithSurveyName = z.infer; - export const ZDisplayFilters = z.object({ createdAt: z .object({