mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-05 02:58:36 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks into chore/upgrade-web-tailwind4
This commit is contained in:
@@ -3,10 +3,14 @@
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromApiKeyId } from "@/lib/utils/helper";
|
||||
import { createApiKey, deleteApiKey } from "@/modules/organization/settings/api-keys/lib/api-key";
|
||||
import {
|
||||
createApiKey,
|
||||
deleteApiKey,
|
||||
updateApiKey,
|
||||
} from "@/modules/organization/settings/api-keys/lib/api-key";
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZApiKeyCreateInput } from "./types/api-keys";
|
||||
import { ZApiKeyCreateInput, ZApiKeyUpdateInput } from "./types/api-keys";
|
||||
|
||||
const ZDeleteApiKeyAction = z.object({
|
||||
id: ZId,
|
||||
@@ -50,3 +54,25 @@ export const createApiKeyAction = authenticatedActionClient
|
||||
|
||||
return await createApiKey(parsedInput.organizationId, ctx.user.id, parsedInput.apiKeyData);
|
||||
});
|
||||
|
||||
const ZUpdateApiKeyAction = z.object({
|
||||
apiKeyId: ZId,
|
||||
apiKeyData: ZApiKeyUpdateInput,
|
||||
});
|
||||
|
||||
export const updateApiKeyAction = authenticatedActionClient
|
||||
.schema(ZUpdateApiKeyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromApiKeyId(parsedInput.apiKeyId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await updateApiKey(parsedInput.apiKeyId, parsedInput.apiKeyData);
|
||||
});
|
||||
|
||||
@@ -3,15 +3,16 @@ import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, test, vi } from "vitest";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { createApiKeyAction, deleteApiKeyAction } from "../actions";
|
||||
import { createApiKeyAction, deleteApiKeyAction, updateApiKeyAction } from "../actions";
|
||||
import { TApiKeyWithEnvironmentPermission } from "../types/api-keys";
|
||||
import { EditAPIKeys } from "./edit-api-keys";
|
||||
|
||||
// Mock the actions
|
||||
vi.mock("../actions", () => ({
|
||||
createApiKeyAction: vi.fn(),
|
||||
updateApiKeyAction: vi.fn(),
|
||||
deleteApiKeyAction: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -177,6 +178,50 @@ describe("EditAPIKeys", () => {
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_deleted");
|
||||
});
|
||||
|
||||
test("handles API key updation", async () => {
|
||||
const updatedApiKey: TApiKeyWithEnvironmentPermission = {
|
||||
id: "key1",
|
||||
label: "Updated Key",
|
||||
createdAt: new Date(),
|
||||
organizationAccess: {
|
||||
accessControl: {
|
||||
read: true,
|
||||
write: false,
|
||||
},
|
||||
},
|
||||
apiKeyEnvironments: [
|
||||
{
|
||||
environmentId: "env1",
|
||||
permission: ApiKeyPermission.read,
|
||||
},
|
||||
],
|
||||
};
|
||||
(updateApiKeyAction as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ data: updatedApiKey });
|
||||
render(<EditAPIKeys {...defaultProps} />);
|
||||
|
||||
// Open view permission modal
|
||||
const apiKeyRows = screen.getAllByTestId("api-key-row");
|
||||
|
||||
// click on the first row
|
||||
await userEvent.click(apiKeyRows[0]);
|
||||
|
||||
const labelInput = screen.getByTestId("api-key-label");
|
||||
await userEvent.clear(labelInput);
|
||||
await userEvent.type(labelInput, "Updated Key");
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: "common.update" });
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(updateApiKeyAction).toHaveBeenCalledWith({
|
||||
apiKeyId: "key1",
|
||||
apiKeyData: {
|
||||
label: "Updated Key",
|
||||
},
|
||||
});
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_updated");
|
||||
});
|
||||
|
||||
it("handles API key creation", async () => {
|
||||
const newApiKey: TApiKeyWithEnvironmentPermission = {
|
||||
id: "key3",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { ViewPermissionModal } from "@/modules/organization/settings/api-keys/components/view-permission-modal";
|
||||
import {
|
||||
TApiKeyUpdateInput,
|
||||
TApiKeyWithEnvironmentPermission,
|
||||
TOrganizationProject,
|
||||
} from "@/modules/organization/settings/api-keys/types/api-keys";
|
||||
@@ -16,7 +17,7 @@ import toast from "react-hot-toast";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TOrganizationAccess } from "@formbricks/types/api-key";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createApiKeyAction, deleteApiKeyAction } from "../actions";
|
||||
import { createApiKeyAction, deleteApiKeyAction, updateApiKeyAction } from "../actions";
|
||||
import { AddApiKeyModal } from "./add-api-key-modal";
|
||||
|
||||
interface EditAPIKeysProps {
|
||||
@@ -89,6 +90,38 @@ export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, proje
|
||||
setIsAddAPIKeyModalOpen(false);
|
||||
};
|
||||
|
||||
const handleUpdateAPIKey = async (data: TApiKeyUpdateInput) => {
|
||||
if (!activeKey) return;
|
||||
|
||||
const updateApiKeyResponse = await updateApiKeyAction({
|
||||
apiKeyId: activeKey.id,
|
||||
apiKeyData: data,
|
||||
});
|
||||
|
||||
if (updateApiKeyResponse?.data) {
|
||||
const updatedApiKeys =
|
||||
apiKeysLocal?.map((apiKey) => {
|
||||
if (apiKey.id === activeKey.id) {
|
||||
return {
|
||||
...apiKey,
|
||||
label: data.label,
|
||||
};
|
||||
}
|
||||
return apiKey;
|
||||
}) || [];
|
||||
|
||||
setApiKeysLocal(updatedApiKeys);
|
||||
toast.success(t("environments.project.api_keys.api_key_updated"));
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updateApiKeyResponse);
|
||||
toast.error(errorMessage);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setViewPermissionsOpen(false);
|
||||
};
|
||||
|
||||
const ApiKeyDisplay = ({ apiKey }) => {
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
@@ -149,6 +182,7 @@ export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, proje
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
data-testid="api-key-row"
|
||||
key={apiKey.id}>
|
||||
<div className="col-span-4 font-semibold sm:col-span-2">{apiKey.label}</div>
|
||||
<div className="col-span-4 hidden sm:col-span-5 sm:block">
|
||||
@@ -198,8 +232,10 @@ export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, proje
|
||||
<ViewPermissionModal
|
||||
open={viewPermissionsOpen}
|
||||
setOpen={setViewPermissionsOpen}
|
||||
onSubmit={handleUpdateAPIKey}
|
||||
apiKey={activeKey}
|
||||
projects={projects}
|
||||
isUpdating={isLoading}
|
||||
/>
|
||||
)}
|
||||
<DeleteDialog
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("ViewPermissionModal", () => {
|
||||
it("renders the modal with correct title", () => {
|
||||
render(<ViewPermissionModal {...defaultProps} />);
|
||||
// Check the localized text for the modal's title
|
||||
expect(screen.getByText("environments.project.api_keys.api_key")).toBeInTheDocument();
|
||||
expect(screen.getByText(mockApiKey.label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all permissions for the API key", () => {
|
||||
|
||||
@@ -2,25 +2,62 @@
|
||||
|
||||
import { getOrganizationAccessKeyDisplayName } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import {
|
||||
TApiKeyUpdateInput,
|
||||
TApiKeyWithEnvironmentPermission,
|
||||
TOrganizationProject,
|
||||
ZApiKeyUpdateInput,
|
||||
} from "@/modules/organization/settings/api-keys/types/api-keys";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DropdownMenu, DropdownMenuTrigger } from "@/modules/ui/components/dropdown-menu";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Modal } from "@/modules/ui/components/modal";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { TOrganizationAccess } from "@formbricks/types/api-key";
|
||||
|
||||
interface ViewPermissionModalProps {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
onSubmit: (data: TApiKeyUpdateInput) => Promise<void>;
|
||||
apiKey: TApiKeyWithEnvironmentPermission;
|
||||
projects: TOrganizationProject[];
|
||||
isUpdating: boolean;
|
||||
}
|
||||
|
||||
export const ViewPermissionModal = ({ open, setOpen, apiKey, projects }: ViewPermissionModalProps) => {
|
||||
export const ViewPermissionModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
onSubmit,
|
||||
apiKey,
|
||||
projects,
|
||||
isUpdating,
|
||||
}: ViewPermissionModalProps) => {
|
||||
const { register, getValues, handleSubmit, reset, watch } = useForm<TApiKeyUpdateInput>({
|
||||
defaultValues: {
|
||||
label: apiKey.label,
|
||||
},
|
||||
resolver: zodResolver(ZApiKeyUpdateInput),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reset({ label: apiKey.label });
|
||||
}, [apiKey.label, reset]);
|
||||
|
||||
const apiKeyLabel = watch("label");
|
||||
|
||||
const isSubmitDisabled = () => {
|
||||
// Check if label is empty or only whitespace or if the label is the same as the original
|
||||
if (!apiKeyLabel?.trim() || apiKeyLabel === apiKey.label) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const { t } = useTranslate();
|
||||
const organizationAccess = apiKey.organizationAccess as TOrganizationAccess;
|
||||
|
||||
@@ -34,116 +71,146 @@ export const ViewPermissionModal = ({ open, setOpen, apiKey, projects }: ViewPer
|
||||
?.environments.find((env) => env.id === environmentId)?.type;
|
||||
};
|
||||
|
||||
const updateApiKey = async () => {
|
||||
const data = getValues();
|
||||
await onSubmit(data);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} noPadding closeOnOutsideClick={true}>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-xl font-medium text-slate-700">
|
||||
{t("environments.project.api_keys.api_key")}
|
||||
</div>
|
||||
<div className="text-xl font-medium text-slate-700">{apiKey.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-col justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("environments.project.api_keys.permissions")}</Label>
|
||||
<form onSubmit={handleSubmit(updateApiKey)}>
|
||||
<div className="flex flex-col justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-6">
|
||||
<div className="space-y-2">
|
||||
{/* Permission rows */}
|
||||
{apiKey.apiKeyEnvironments?.map((permission) => {
|
||||
return (
|
||||
<div key={permission.environmentId} className="flex items-center gap-2">
|
||||
{/* Project dropdown */}
|
||||
<div className="w-1/3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-hidden">
|
||||
<span className="flex w-4/5 flex-1">
|
||||
<span className="w-full truncate text-left">
|
||||
{getProjectName(permission.environmentId)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Environment dropdown */}
|
||||
<div className="w-1/3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-hidden">
|
||||
<span className="flex w-4/5 flex-1">
|
||||
<span className="w-full truncate text-left capitalize">
|
||||
{getEnvironmentName(permission.environmentId)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Permission level dropdown */}
|
||||
<div className="w-1/3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-hidden">
|
||||
<span className="flex w-4/5 flex-1">
|
||||
<span className="w-full truncate text-left capitalize">
|
||||
{permission.permission}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Label>{t("environments.project.api_keys.api_key_label")}</Label>
|
||||
<Input
|
||||
placeholder="e.g. GitHub, PostHog, Slack"
|
||||
data-testid="api-key-label"
|
||||
{...register("label", { required: true, validate: (value) => value.trim() !== "" })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("environments.project.api_keys.organization_access")}</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-[auto_100px_100px] gap-4">
|
||||
<div></div>
|
||||
<span className="flex items-center justify-center text-sm font-medium">Read</span>
|
||||
<span className="flex items-center justify-center text-sm font-medium">Write</span>
|
||||
<Label>{t("environments.project.api_keys.permissions")}</Label>
|
||||
<div className="space-y-2">
|
||||
{/* Permission rows */}
|
||||
{apiKey.apiKeyEnvironments?.map((permission) => {
|
||||
return (
|
||||
<div key={permission.environmentId} className="flex items-center gap-2">
|
||||
{/* Project dropdown */}
|
||||
<div className="w-1/3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-hidden">
|
||||
<span className="flex w-4/5 flex-1">
|
||||
<span className="w-full truncate text-left">
|
||||
{getProjectName(permission.environmentId)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{Object.keys(organizationAccess).map((key) => (
|
||||
<Fragment key={key}>
|
||||
<div className="py-1 text-sm">{t(getOrganizationAccessKeyDisplayName(key))}</div>
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<Switch
|
||||
disabled={true}
|
||||
data-testid={`organization-access-${key}-read`}
|
||||
checked={organizationAccess[key].read}
|
||||
/>
|
||||
{/* Environment dropdown */}
|
||||
<div className="w-1/3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-hidden">
|
||||
<span className="flex w-4/5 flex-1">
|
||||
<span className="w-full truncate text-left capitalize">
|
||||
{getEnvironmentName(permission.environmentId)}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Permission level dropdown */}
|
||||
<div className="w-1/3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-hidden">
|
||||
<span className="flex w-4/5 flex-1">
|
||||
<span className="w-full truncate text-left capitalize">
|
||||
{permission.permission}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<Switch
|
||||
disabled={true}
|
||||
data-testid={`organization-access-${key}-write`}
|
||||
checked={organizationAccess[key].write}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("environments.project.api_keys.organization_access")}</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-[auto_100px_100px] gap-4">
|
||||
<div></div>
|
||||
<span className="flex items-center justify-center text-sm font-medium">Read</span>
|
||||
<span className="flex items-center justify-center text-sm font-medium">Write</span>
|
||||
|
||||
{Object.keys(organizationAccess).map((key) => (
|
||||
<Fragment key={key}>
|
||||
<div className="py-1 text-sm">{t(getOrganizationAccessKeyDisplayName(key))}</div>
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<Switch
|
||||
disabled={true}
|
||||
data-testid={`organization-access-${key}-read`}
|
||||
checked={organizationAccess[key].read}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<Switch
|
||||
disabled={true}
|
||||
data-testid={`organization-access-${key}-write`}
|
||||
checked={organizationAccess[key].write}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
reset();
|
||||
}}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitDisabled() || isUpdating} loading={isUpdating}>
|
||||
{t("common.update")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -2,6 +2,7 @@ import "server-only";
|
||||
import { apiKeyCache } from "@/lib/cache/api-key";
|
||||
import {
|
||||
TApiKeyCreateInput,
|
||||
TApiKeyUpdateInput,
|
||||
TApiKeyWithEnvironmentPermission,
|
||||
ZApiKeyCreateInput,
|
||||
} from "@/modules/organization/settings/api-keys/types/api-keys";
|
||||
@@ -193,3 +194,29 @@ export const createApiKey = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateApiKey = async (apiKeyId: string, data: TApiKeyUpdateInput): Promise<ApiKey | null> => {
|
||||
try {
|
||||
const updatedApiKey = await prisma.apiKey.update({
|
||||
where: {
|
||||
id: apiKeyId,
|
||||
},
|
||||
data: {
|
||||
label: data.label,
|
||||
},
|
||||
});
|
||||
|
||||
apiKeyCache.revalidate({
|
||||
id: updatedApiKey.id,
|
||||
hashedKey: updatedApiKey.hashedKey,
|
||||
organizationId: updatedApiKey.organizationId,
|
||||
});
|
||||
|
||||
return updatedApiKey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TApiKeyWithEnvironmentPermission } from "../types/api-keys";
|
||||
import { createApiKey, deleteApiKey, getApiKeysWithEnvironmentPermissions } from "./api-key";
|
||||
import { createApiKey, deleteApiKey, getApiKeysWithEnvironmentPermissions, updateApiKey } from "./api-key";
|
||||
|
||||
const mockApiKey: ApiKey = {
|
||||
id: "apikey123",
|
||||
@@ -39,6 +39,7 @@ vi.mock("@formbricks/database", () => ({
|
||||
findMany: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -191,4 +192,28 @@ describe("API Key Management", () => {
|
||||
await expect(createApiKey("org123", "user123", mockApiKeyData)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateApiKey", () => {
|
||||
it("updates an API key successfully", async () => {
|
||||
const updatedApiKey = { ...mockApiKey, label: "Updated API Key" };
|
||||
vi.mocked(prisma.apiKey.update).mockResolvedValueOnce(updatedApiKey);
|
||||
|
||||
const result = await updateApiKey(mockApiKey.id, { label: "Updated API Key" });
|
||||
|
||||
expect(result).toEqual(updatedApiKey);
|
||||
expect(prisma.apiKey.update).toHaveBeenCalled();
|
||||
expect(apiKeyCache.revalidate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws DatabaseError on prisma error", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||
code: "P2002",
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
|
||||
vi.mocked(prisma.apiKey.update).mockRejectedValueOnce(errToThrow);
|
||||
|
||||
await expect(updateApiKey(mockApiKey.id, { label: "Updated API Key" })).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,14 @@ export const ZApiKeyCreateInput = ZApiKey.required({
|
||||
|
||||
export type TApiKeyCreateInput = z.infer<typeof ZApiKeyCreateInput>;
|
||||
|
||||
export const ZApiKeyUpdateInput = ZApiKey.required({
|
||||
label: true,
|
||||
}).pick({
|
||||
label: true,
|
||||
});
|
||||
|
||||
export type TApiKeyUpdateInput = z.infer<typeof ZApiKeyUpdateInput>;
|
||||
|
||||
export interface TApiKey extends ApiKey {
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
@@ -784,6 +784,7 @@
|
||||
"api_key_deleted": "API-Schlüssel gelöscht",
|
||||
"api_key_label": "API-Schlüssel Label",
|
||||
"api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.",
|
||||
"api_key_updated": "API-Schlüssel aktualisiert",
|
||||
"duplicate_access": "Doppelter Projektzugriff nicht erlaubt",
|
||||
"no_api_keys_yet": "Du hast noch keine API-Schlüssel",
|
||||
"organization_access": "Organisationszugang",
|
||||
|
||||
@@ -784,6 +784,7 @@
|
||||
"api_key_deleted": "API Key deleted",
|
||||
"api_key_label": "API Key Label",
|
||||
"api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.",
|
||||
"api_key_updated": "API Key updated",
|
||||
"duplicate_access": "Duplicate project access not allowed",
|
||||
"no_api_keys_yet": "You don't have any API keys yet",
|
||||
"organization_access": "Organization Access",
|
||||
|
||||
@@ -784,6 +784,7 @@
|
||||
"api_key_deleted": "Clé API supprimée",
|
||||
"api_key_label": "Étiquette de clé API",
|
||||
"api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.",
|
||||
"api_key_updated": "Clé API mise à jour",
|
||||
"duplicate_access": "L'accès en double au projet n'est pas autorisé",
|
||||
"no_api_keys_yet": "Vous n'avez pas encore de clés API.",
|
||||
"organization_access": "Accès à l'organisation",
|
||||
|
||||
@@ -784,6 +784,7 @@
|
||||
"api_key_deleted": "Chave da API deletada",
|
||||
"api_key_label": "Rótulo da Chave API",
|
||||
"api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
|
||||
"api_key_updated": "Chave de API atualizada",
|
||||
"duplicate_access": "Acesso duplicado ao projeto não permitido",
|
||||
"no_api_keys_yet": "Você ainda não tem nenhuma chave de API",
|
||||
"organization_access": "Acesso à Organização",
|
||||
|
||||
@@ -784,6 +784,7 @@
|
||||
"api_key_deleted": "Chave API eliminada",
|
||||
"api_key_label": "Etiqueta da Chave API",
|
||||
"api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
|
||||
"api_key_updated": "Chave API atualizada",
|
||||
"duplicate_access": "Acesso duplicado ao projeto não permitido",
|
||||
"no_api_keys_yet": "Ainda não tem nenhuma chave API",
|
||||
"organization_access": "Acesso à Organização",
|
||||
|
||||
@@ -784,6 +784,7 @@
|
||||
"api_key_deleted": "API 金鑰已刪除",
|
||||
"api_key_label": "API 金鑰標籤",
|
||||
"api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。",
|
||||
"api_key_updated": "API 金鑰已更新",
|
||||
"duplicate_access": "不允許重複的 project 存取",
|
||||
"no_api_keys_yet": "您還沒有任何 API 金鑰",
|
||||
"organization_access": "組織 Access",
|
||||
|
||||
Reference in New Issue
Block a user