mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-03 10:09:16 -06:00
Rewrite Api Key Settings to React Server Components (#654)
* moved apikey settings to server component * rename ZApiKeyData to ZApiKeyCreateInput * Make smaller improvements --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
c6686209be
commit
47a8fd6b62
@@ -1,19 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { useProduct } from "@/lib/products/products";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import EditApiKeys from "./EditApiKeys";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getApiKeys } from "@formbricks/lib/services/apiKey";
|
||||
import { getEnvironments } from "@formbricks/lib/services/environment";
|
||||
|
||||
export default function ApiKeyList({
|
||||
export default async function ApiKeyList({
|
||||
environmentId,
|
||||
environmentType,
|
||||
}: {
|
||||
environmentId: string;
|
||||
environmentType: string;
|
||||
}) {
|
||||
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
|
||||
|
||||
const findEnvironmentByType = (environments, targetType) => {
|
||||
for (const environment of environments) {
|
||||
if (environment.type === targetType) {
|
||||
@@ -23,15 +19,12 @@ export default function ApiKeyList({
|
||||
return null;
|
||||
};
|
||||
|
||||
if (isLoadingProduct) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
const environments = await getEnvironments(product.id);
|
||||
const environmentTypeId = findEnvironmentByType(environments, environmentType);
|
||||
const apiKeys = await getApiKeys(environmentTypeId);
|
||||
|
||||
if (isErrorProduct) {
|
||||
<ErrorComponent />;
|
||||
}
|
||||
|
||||
const environmentTypeId = findEnvironmentByType(product?.environments, environmentType);
|
||||
|
||||
return <EditApiKeys environmentTypeId={environmentTypeId} environmentType={environmentType} />;
|
||||
return (
|
||||
<EditApiKeys environmentTypeId={environmentTypeId} environmentType={environmentType} apiKeys={apiKeys} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { createApiKey, deleteApiKey, useApiKeys } from "@/lib/apiKeys";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { Button, ErrorComponent } from "@formbricks/ui";
|
||||
import { TApiKey } from "@formbricks/types/v1/apiKeys";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import AddAPIKeyModal from "./AddApiKeyModal";
|
||||
import { createApiKeyAction, deleteApiKeyAction } from "./actions";
|
||||
|
||||
export default function EditAPIKeys({
|
||||
environmentTypeId,
|
||||
environmentType,
|
||||
apiKeys,
|
||||
}: {
|
||||
environmentTypeId: string;
|
||||
environmentType: string;
|
||||
apiKeys: TApiKey[];
|
||||
}) {
|
||||
const { apiKeys, mutateApiKeys, isLoadingApiKeys, isErrorApiKeys } = useApiKeys(environmentTypeId);
|
||||
|
||||
const [isAddAPIKeyModalOpen, setOpenAddAPIKeyModal] = useState(false);
|
||||
const [isDeleteKeyModalOpen, setOpenDeleteKeyModal] = useState(false);
|
||||
|
||||
const [apiKeysLocal, setApiKeysLocal] = useState<TApiKey[]>(apiKeys);
|
||||
const [activeKey, setActiveKey] = useState({} as any);
|
||||
|
||||
const handleOpenDeleteKeyModal = (e, apiKey) => {
|
||||
@@ -32,26 +32,21 @@ export default function EditAPIKeys({
|
||||
};
|
||||
|
||||
const handleDeleteKey = async () => {
|
||||
await deleteApiKey(environmentTypeId, activeKey);
|
||||
mutateApiKeys();
|
||||
await deleteApiKeyAction(activeKey.id);
|
||||
const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || [];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
setOpenDeleteKeyModal(false);
|
||||
toast.success("API Key deleted");
|
||||
};
|
||||
|
||||
const handleAddAPIKey = async (data) => {
|
||||
const apiKey = await createApiKey(environmentTypeId, { label: data.label });
|
||||
mutateApiKeys([...JSON.parse(JSON.stringify(apiKeys)), apiKey], false);
|
||||
const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label });
|
||||
const updatedApiKeys = [...apiKeysLocal!, apiKey];
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
setOpenAddAPIKeyModal(false);
|
||||
toast.success("API key created");
|
||||
};
|
||||
|
||||
if (isLoadingApiKeys) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (isErrorApiKeys) {
|
||||
<ErrorComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
@@ -72,19 +67,22 @@ export default function EditAPIKeys({
|
||||
<div className=""></div>
|
||||
</div>
|
||||
<div className="grid-cols-9">
|
||||
{apiKeys.length === 0 ? (
|
||||
{apiKeysLocal && apiKeysLocal.length === 0 ? (
|
||||
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400 ">
|
||||
You don't have any API keys yet
|
||||
</div>
|
||||
) : (
|
||||
apiKeys.map((apiKey) => (
|
||||
apiKeysLocal &&
|
||||
apiKeysLocal.map((apiKey) => (
|
||||
<div
|
||||
className="grid h-12 w-full grid-cols-9 content-center rounded-lg px-6 text-left text-sm text-slate-900"
|
||||
key={apiKey.hashedKey}>
|
||||
<div className="col-span-2 font-semibold">{apiKey.label}</div>
|
||||
<div className="col-span-2">{apiKey.apiKey || <span className="italic">secret</span>}</div>
|
||||
<div className="col-span-2">{apiKey.lastUsed && timeSince(apiKey.lastUsed)}</div>
|
||||
<div className="col-span-2">{timeSince(apiKey.createdAt)}</div>
|
||||
<div className="col-span-2">
|
||||
{apiKey.lastUsedAt && timeSince(apiKey.lastUsedAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-2">{timeSince(apiKey.createdAt.toString())}</div>
|
||||
<div className="col-span-1 text-center">
|
||||
<button onClick={(e) => handleOpenDeleteKeyModal(e, apiKey)}>
|
||||
<TrashIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { deleteApiKey, createApiKey } from "@formbricks/lib/services/apiKey";
|
||||
import { TApiKeyCreateInput } from "@formbricks/types/v1/apiKeys";
|
||||
|
||||
export async function deleteApiKeyAction(id: string) {
|
||||
return await deleteApiKey(id);
|
||||
}
|
||||
export async function createApiKeyAction(environmentId: string, apiKeyData: TApiKeyCreateInput) {
|
||||
return await createApiKey(environmentId, apiKeyData);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
function LoadingCard({ title, description }) {
|
||||
return (
|
||||
<div className="my-4 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-100 px-6 py-5 text-left text-slate-900">
|
||||
<h3 className="text-lg font-medium leading-6">{title}</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{description}</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="rounded-lg px-6 py-5 hover:bg-slate-100">
|
||||
<div className="flex justify-end">
|
||||
<div className="mt-4 h-6 w-28 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="mt-6 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-9 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-2">Label</div>
|
||||
<div className="col-span-2">API Key</div>
|
||||
<div className="col-span-2">Last used</div>
|
||||
<div className="col-span-2">Created at</div>
|
||||
</div>
|
||||
<div className="px-6">
|
||||
<div className="my-4 h-6 w-full animate-pulse rounded-full bg-gray-200"></div>
|
||||
<div className="my-4 h-6 w-full animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function Loading() {
|
||||
const cards = [
|
||||
{
|
||||
title: "Development Env Keys",
|
||||
description: "Add and remove API keys for your Development environment.",
|
||||
},
|
||||
{
|
||||
title: "Production Env Keys",
|
||||
description: "Add and remove API keys for your Production environment.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="my-4 text-2xl font-medium leading-6 text-slate-800">API Keys</h2>
|
||||
{cards.map((card, index) => (
|
||||
<LoadingCard key={index} {...card} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import ApiKeyList from "./ApiKeyList";
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TApiKey } from "@formbricks/types/v1/apiKeys";
|
||||
import { TApiKey, TApiKeyCreateInput } from "@formbricks/types/v1/apiKeys";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { getHash } from "../crypto";
|
||||
import { createHash, randomBytes } from "crypto";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/errors";
|
||||
import { cache } from "react";
|
||||
|
||||
export const getApiKey = async (apiKey: string): Promise<TApiKey | null> => {
|
||||
if (!apiKey) {
|
||||
@@ -30,6 +34,47 @@ export const getApiKey = async (apiKey: string): Promise<TApiKey | null> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getApiKeys = cache(async (environmentId: string): Promise<TApiKey[]> => {
|
||||
try {
|
||||
const apiKeys = await prisma.apiKey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
},
|
||||
});
|
||||
|
||||
return apiKeys;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
|
||||
|
||||
export async function createApiKey(environmentId: string, apiKeyData: TApiKeyCreateInput): Promise<TApiKey> {
|
||||
try {
|
||||
const key = randomBytes(16).toString("hex");
|
||||
const hashedKey = hashApiKey(key);
|
||||
|
||||
const result = await prisma.apiKey.create({
|
||||
data: {
|
||||
...apiKeyData,
|
||||
hashedKey,
|
||||
environment: { connect: { id: environmentId } },
|
||||
},
|
||||
});
|
||||
|
||||
return { ...result, apiKey: key };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null> => {
|
||||
if (!apiKey) {
|
||||
throw new InvalidInputError("API key cannot be null or undefined.");
|
||||
@@ -51,3 +96,19 @@ export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null>
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteApiKey = async (id: string): Promise<void> => {
|
||||
try {
|
||||
await prisma.apiKey.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,12 @@ 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>;
|
||||
|
||||
Reference in New Issue
Block a user