chore: API key types (#4610)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2025-01-17 16:06:29 +05:30
committed by GitHub
parent 21644f5ad8
commit 02b25138ef
17 changed files with 131 additions and 194 deletions

View File

@@ -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<TAuthenticationApiKey | null> => {
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;
}

View File

@@ -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<string | null> => {
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)],
}
)();
});

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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";

View File

@@ -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,

View File

@@ -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";

View File

@@ -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);

View File

@@ -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<TApiKey | null> => {
export const getApiKeys = reactCache(
async (environmentId: string, page?: number): Promise<ApiKey[]> =>
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<ApiKey | null> => {
validateInputs([id, ZId]);
try {

View File

@@ -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<typeof ZApiKeyCreateInput>;
export interface TApiKey extends ApiKey {
apiKey?: string;
}

View File

@@ -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<typeof ZApiKey>;
export const ZApiKeyCreateInput = z.object({
label: z.string(),
});
export type TApiKeyCreateInput = z.infer<typeof ZApiKeyCreateInput>;
}) satisfies z.ZodType<ApiKey>;

View File

@@ -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<boolean> =>
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)] }
)();

View File

@@ -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<TApiKey | null> =>
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<TApiKey[]> =>
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<TApiKey | null> => {
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)],
}
)();
});

View File

@@ -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"
]
}

View File

@@ -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<typeof ZContact>;

View File

@@ -20,12 +20,6 @@ export const ZDisplayCreateInput = z.object({
export type TDisplayCreateInput = z.infer<typeof ZDisplayCreateInput>;
export const ZDisplaysWithSurveyName = ZDisplay.extend({
surveyName: z.string(),
});
export type TDisplaysWithSurveyName = z.infer<typeof ZDisplaysWithSurveyName>;
export const ZDisplayFilters = z.object({
createdAt: z
.object({