feat: renamed old frontend and fixed some minor bugs

This commit is contained in:
=
2025-01-06 14:56:49 +05:30
parent 8c902d7699
commit 54377a44d3
2577 changed files with 35240 additions and 35116 deletions
@@ -0,0 +1,37 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { CmekTable } from "./components";
export const OverviewPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<p className="mr-4 text-3xl font-semibold text-white">Key Management System</p>
<p className="text-md mb-4 text-bunker-300">
Manage keys and perform cryptographic operations.
</p>
<ProjectPermissionCan
passThrough={false}
renderGuardBanner
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Cmek}
>
<CmekTable />
</ProjectPermissionCan>
</div>
</div>
</div>
);
};
@@ -0,0 +1,169 @@
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { faCheckCircle, faCopy, faInfoCircle, faLockOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { decodeBase64 } from "tweetnacl-util";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { TCmek, useCmekDecrypt } from "@app/hooks/api/cmeks";
const formSchema = z.object({
ciphertext: z.string()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek: TCmek;
};
type FormProps = Pick<Props, "cmek">;
const DecryptForm = ({ cmek }: FormProps) => {
const cmekDecrypt = useCmekDecrypt();
const [shouldDecode, setShouldDecode] = useState(false);
const [plaintext, setPlaintext] = useState("");
const {
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const [copyCiphertext, isCopyingCiphertext, setCopyCipherText] = useTimedReset<string>({
initialState: "Copy to Clipboard"
});
const handleDecryptData = async (formData: FormData) => {
try {
const data = await cmekDecrypt.mutateAsync({ ...formData, keyId: cmek.id });
createNotification({
text: "Successfully decrypted data",
type: "success"
});
setPlaintext(
shouldDecode ? Buffer.from(decodeBase64(data.plaintext)).toString("utf8") : data.plaintext
);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to decrypt data",
type: "error"
});
}
};
useEffect(() => {
const text = cmekDecrypt.data?.plaintext;
if (!text) return;
setPlaintext(shouldDecode ? Buffer.from(decodeBase64(text)).toString("utf8") : text);
}, [shouldDecode]);
const handleCopyToClipboard = () => {
navigator.clipboard.writeText(plaintext ?? "");
setCopyCipherText("Copied to Clipboard");
};
return (
<form onSubmit={handleSubmit(handleDecryptData)}>
{plaintext ? (
<FormControl label="Decrypted Data (plaintext)">
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
isDisabled
value={plaintext}
/>
</FormControl>
) : (
<FormControl
label="Encrypted Data (ciphertext)"
errorText={errors.ciphertext?.message}
isError={Boolean(errors.ciphertext)}
>
<TextArea
{...register("ciphertext")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
)}
<Switch
id="decode-base-64"
isChecked={shouldDecode}
onCheckedChange={setShouldDecode}
containerClassName="mb-6 ml-0.5 -mt-2.5"
>
Decode Base64{" "}
<Tooltip content="Toggle this switch on if your data was originally plain text.">
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
</Tooltip>
</Switch>
<div className="flex items-center">
<Button
className={`mr-4 ${plaintext ? "w-44" : ""}`}
size="sm"
leftIcon={
// eslint-disable-next-line no-nested-ternary
plaintext ? (
isCopyingCiphertext ? (
<FontAwesomeIcon icon={faCheckCircle} />
) : (
<FontAwesomeIcon icon={faCopy} />
)
) : (
<FontAwesomeIcon icon={faLockOpen} />
)
}
onClick={plaintext ? handleCopyToClipboard : undefined}
type={plaintext ? "button" : "submit"}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{plaintext ? copyCiphertext : "Decrypt"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
{plaintext ? "Close" : "Cancel"}
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekDecryptModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
subTitle={
<>
Decrypt ciphertext using <span className="font-bold">{cmek?.name}</span>. Returns Base64
encoded plaintext.
</>
}
title="Decrypt Data"
>
<DecryptForm cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,169 @@
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle, faCopy, faInfoCircle, faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalClose,
ModalContent,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { TCmek, useCmekEncrypt } from "@app/hooks/api/cmeks";
const formSchema = z.object({
plaintext: z.string(),
isBase64Encoded: z.boolean()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek: TCmek;
};
type FormProps = Pick<Props, "cmek">;
const EncryptForm = ({ cmek }: FormProps) => {
const cmekEncrypt = useCmekEncrypt();
const {
handleSubmit,
register,
control,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
isBase64Encoded: false
}
});
const [copyCiphertext, isCopyingCiphertext, setCopyCipherText] = useTimedReset<string>({
initialState: "Copy to Clipboard"
});
const handleEncryptData = async (formData: FormData) => {
try {
await cmekEncrypt.mutateAsync({ ...formData, keyId: cmek.id });
createNotification({
text: "Successfully encrypted data",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to encrypt data",
type: "error"
});
}
};
const ciphertext = cmekEncrypt.data?.ciphertext;
const handleCopyToClipboard = () => {
navigator.clipboard.writeText(ciphertext ?? "");
setCopyCipherText("Copied to Clipboard");
};
return (
<form onSubmit={handleSubmit(handleEncryptData)}>
{ciphertext ? (
<FormControl label="Encrypted Data (Ciphertext)">
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
isDisabled
value={cmekEncrypt.data?.ciphertext}
/>
</FormControl>
) : (
<>
<FormControl
label="Data (Plaintext)"
errorText={errors.plaintext?.message}
isError={Boolean(errors.plaintext)}
>
<TextArea
{...register("plaintext")}
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
/>
</FormControl>
<Controller
control={control}
name="isBase64Encoded"
render={({ field: { onChange, value } }) => (
<Switch
id="encode-base-64"
isChecked={value}
onCheckedChange={onChange}
containerClassName="mb-6 ml-0.5 -mt-2.5"
>
Data is Base64 encoded{" "}
<Tooltip content="Toggle this switch on if your data is already Base64 encoded to avoid redundant encoding.">
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
</Tooltip>
</Switch>
)}
/>
</>
)}
<div className="flex items-center">
<Button
className={`mr-4 ${ciphertext ? "w-44" : ""}`}
size="sm"
leftIcon={
// eslint-disable-next-line no-nested-ternary
ciphertext ? (
isCopyingCiphertext ? (
<FontAwesomeIcon icon={faCheckCircle} />
) : (
<FontAwesomeIcon icon={faCopy} />
)
) : (
<FontAwesomeIcon icon={faLock} />
)
}
onClick={ciphertext ? handleCopyToClipboard : undefined}
type={ciphertext ? "button" : "submit"}
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{ciphertext ? copyCiphertext : "Encrypt"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
{ciphertext ? "Close" : "Cancel"}
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekEncryptModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Encrypt Data"
subTitle={
<>
Encrypt data using <span className="font-bold">{cmek?.name}</span>. Returns Base64
encoded ciphertext.
</>
}
>
<EncryptForm cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,151 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }),
description: z.string().max(500).optional(),
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm)
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
cmek?: TCmek | null;
};
type FormProps = Pick<Props, "cmek"> & {
onComplete: () => void;
};
const CmekForm = ({ onComplete, cmek }: FormProps) => {
const createCmek = useCreateCmek();
const updateCmek = useUpdateCmek();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const isUpdate = !!cmek;
const {
control,
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: cmek?.name,
description: cmek?.description,
encryptionAlgorithm: EncryptionAlgorithm.AES_GCM_256
}
});
const handleCreateCmek = async ({ encryptionAlgorithm, name, description }: FormData) => {
const mutation = isUpdate
? updateCmek.mutateAsync({ keyId: cmek.id, projectId, name, description })
: createCmek.mutateAsync({
projectId,
encryptionAlgorithm,
name,
description
});
try {
await mutation;
createNotification({
text: `Successfully ${isUpdate ? "updated" : "added"} key`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${isUpdate ? "update" : "add"} key`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(handleCreateCmek)}>
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-secret-key" {...register("name")} />
</FormControl>
{!isUpdate && (
<Controller
control={control}
name="encryptionAlgorithm"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Algorithm" errorText={error?.message} isError={Boolean(error)}>
<Select defaultValue={field.value} onValueChange={onChange} className="w-full">
{Object.entries(EncryptionAlgorithm)?.map(([key, value]) => (
<SelectItem value={value} key={`source-environment-${key}`}>
{key.replaceAll("_", "-")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
isError={Boolean(errors.description?.message)}
>
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
{...register("description")}
/>
</FormControl>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"} Key
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const CmekModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={`${cmek ? "Update" : "Add"} Key`}>
<CmekForm onComplete={() => onOpenChange(false)} cmek={cmek} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,457 @@
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faCancel,
faCheck,
faCheckCircle,
faCopy,
faEdit,
faEllipsis,
faInfoCircle,
faKey,
faLock,
faLockOpen,
faMagnifyingGlass,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks";
import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks";
import { CmekOrderBy, TCmek } from "@app/hooks/api/cmeks/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { CmekDecryptModal } from "./CmekDecryptModal";
import { CmekEncryptModal } from "./CmekEncryptModal";
import { CmekModal } from "./CmekModal";
import { DeleteCmekModal } from "./DeleteCmekModal";
const getStatusBadgeProps = (
isDisabled: boolean
): { variant: BadgeProps["variant"]; label: string } => {
if (isDisabled) {
return {
variant: "danger",
label: "Disabled"
};
}
return {
variant: "success",
label: "Active"
};
};
export const CmekTable = () => {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const projectId = currentWorkspace?.id ?? "";
const {
offset,
limit,
orderBy,
orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination(CmekOrderBy.Name);
const { data, isPending, isFetching } = useGetCmeksByProjectId({
projectId,
offset,
limit,
search: debouncedSearch,
orderBy,
orderDirection
});
const { keys = [], totalCount = 0 } = data ?? {};
useResetPageHelper({
totalCount,
offset,
setPage
});
const [, isCopyingCiphertext, setCopyCipherText] = useTimedReset<string>({
initialState: "",
delay: 1000
});
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKey",
"deleteKey",
"encryptData",
"decryptData"
] as const);
const handleSort = () => {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
};
const updateCmek = useUpdateCmek();
const handleDisableCmek = async ({ id: keyId, isDisabled }: TCmek) => {
try {
await updateCmek.mutateAsync({
keyId,
projectId,
isDisabled: !isDisabled
});
createNotification({
text: `Key successfully ${isDisabled ? "enabled" : "disabled"}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ?? `Failed to ${isDisabled ? "enable" : "disable"} key`;
createNotification({
text,
type: "error"
});
}
};
const cannotEditKey = permission.cannot(
ProjectPermissionCmekActions.Edit,
ProjectPermissionSub.Cmek
);
const cannotDeleteKey = permission.cannot(
ProjectPermissionCmekActions.Delete,
ProjectPermissionSub.Cmek
);
const cannotEncryptData = permission.cannot(
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionSub.Cmek
);
const cannotDecryptData = permission.cannot(
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionSub.Cmek
);
return (
<motion.div
key="kms-keys-tab"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="whitespace-nowrap text-xl font-semibold text-mineshaft-100">Keys</p>
<div className="flex w-full justify-end pr-4">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/kms"
>
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</a>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Cmek}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("upsertKey", null)}
isDisabled={!isAllowed}
>
Add Key
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search keys by name..."
/>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={handleSort}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Key ID</Th>
<Th>Algorithm</Th>
<Th>Status</Th>
<Th>Version</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={5} innerKey="project-keys" />}
{!isPending &&
keys.length > 0 &&
keys.map((cmek) => {
const { name, id, version, description, encryptionAlgorithm, isDisabled } = cmek;
const { variant, label } = getStatusBadgeProps(isDisabled);
return (
<Tr
className="group h-10 hover:bg-mineshaft-700"
key={`st-v3-${id}`}
onMouseLeave={() => {
setCopyCipherText("");
}}
>
<Td>
<div className="flex items-center gap-2">
{name}
{description && (
<Tooltip content={description}>
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
</Tooltip>
)}
</div>
</Td>
<Td>
<div>
<span> {id}</span>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group/copy duration:0 invisible relative ml-3 group-hover:visible"
onClick={() => {
navigator.clipboard.writeText(id);
setCopyCipherText("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCiphertext ? faCheck : faCopy} />
</IconButton>
</div>
</Td>
<Td className="uppercase">{encryptionAlgorithm}</Td>
<Td>
<Badge variant={variant}>{label}</Badge>
</Td>
<Td>{version}</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
variant="plain"
colorSchema="primary"
className="ml-4 p-0 data-[state=open]:text-primary-400"
ariaLabel="More options"
>
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotEncryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("encryptData", cmek)}
icon={<FontAwesomeIcon icon={faLock} />}
iconPos="left"
isDisabled={cannotEncryptData || isDisabled}
>
Encrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={
// eslint-disable-next-line no-nested-ternary
cannotDecryptData
? "Access Restricted"
: isDisabled
? "Key Disabled"
: ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("decryptData", cmek)}
icon={<FontAwesomeIcon icon={faLockOpen} />}
iconPos="left"
isDisabled={cannotDecryptData || isDisabled}
>
Decrypt Data
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("upsertKey", cmek)}
icon={<FontAwesomeIcon icon={faEdit} />}
iconPos="left"
isDisabled={cannotEditKey}
>
Edit Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handleDisableCmek(cmek)}
icon={
<FontAwesomeIcon icon={isDisabled ? faCheckCircle : faCancel} />
}
iconPos="left"
isDisabled={cannotEditKey}
>
{isDisabled ? "Enable" : "Disable"} Key
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotDeleteKey ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteKey", cmek)}
icon={<FontAwesomeIcon icon={faTrash} />}
iconPos="left"
isDisabled={cannotDeleteKey}
>
Delete Key
</DropdownMenuItem>
</div>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isPending && keys.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No keys match search filter"
: "No keys have been added to this project"
}
icon={faKey}
/>
)}
</TableContainer>
<DeleteCmekModal
isOpen={popUp.deleteKey.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteKey", isOpen)}
cmek={popUp.deleteKey.data as TCmek}
/>
<CmekModal
isOpen={popUp.upsertKey.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upsertKey", isOpen)}
cmek={popUp.upsertKey.data as TCmek | null}
/>
<CmekEncryptModal
isOpen={popUp.encryptData.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("encryptData", isOpen)}
cmek={popUp.encryptData.data as TCmek}
/>
<CmekDecryptModal
isOpen={popUp.decryptData.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("decryptData", isOpen)}
cmek={popUp.decryptData.data as TCmek}
/>
</div>
</motion.div>
);
};
@@ -0,0 +1,52 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { TCmek, useDeleteCmek } from "@app/hooks/api/cmeks";
type Props = {
cmek: TCmek;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const DeleteCmekModal = ({ isOpen, onOpenChange, cmek }: Props) => {
const deleteCmek = useDeleteCmek();
if (!cmek) return null;
const { id: keyId, projectId, name } = cmek;
const handleDeleteCmek = async () => {
try {
await deleteCmek.mutateAsync({
keyId,
projectId
});
createNotification({
text: "Key successfully deleted",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete key";
createNotification({
text,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteCmek}
/>
);
};
@@ -0,0 +1 @@
export * from "./CmekTable";
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/overview"
)({
component: OverviewPage
});
@@ -0,0 +1,40 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
export const SettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="w-full max-w-7xl px-6">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, Component }) => (
<TabPanel value={key} key={key}>
<Component />
</TabPanel>
))}
</Tabs>
</div>
</div>
);
};
@@ -0,0 +1,129 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useProjectPermission, useSubscription, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/workspace/queries";
const formSchema = z.object({
auditLogsRetentionDays: z.coerce.number().min(0)
});
type TForm = z.infer<typeof formSchema>;
export const AuditLogsRetentionSection = () => {
const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention();
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const {
control,
formState: { isSubmitting, isDirty },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
auditLogsRetentionDays:
currentWorkspace?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0
}
});
if (!currentWorkspace) return null;
const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => {
try {
if (!subscription?.auditLogs) {
handlePopUpOpen("upgradePlan", {
description: "You can only configure audit logs retention if you upgrade your plan."
});
return;
}
if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) {
handlePopUpOpen("upgradePlan", {
description:
"To update your audit logs retention period to a higher value, upgrade your plan."
});
return;
}
await updateAuditLogsRetention({
auditLogsRetentionDays,
projectSlug: currentWorkspace.slug
});
createNotification({
text: "Successfully updated audit logs retention period",
type: "success"
});
} catch {
createNotification({
text: "Failed updating audit logs retention period",
type: "error"
});
}
};
// render only for dedicated/self-hosted instances of Infisical
if (
window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")
) {
return null;
}
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
return (
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Audit Logs Retention</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
Set the number of days to keep your project audit logs.
</p>
<form onSubmit={handleSubmit(handleAuditLogsRetentionSubmit)} autoComplete="off">
<div className="max-w-xs">
<Controller
control={control}
defaultValue={0}
name="auditLogsRetentionDays"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Number of days"
>
<Input {...field} type="number" min={1} step={1} isDisabled={!isAdmin} />
</FormControl>
)}
/>
</div>
<Button
colorSchema="secondary"
type="submit"
isLoading={isSubmitting}
disabled={!isAdmin || !isDirty}
>
Save
</Button>
</form>
</div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</>
);
};
@@ -0,0 +1 @@
export { AuditLogsRetentionSection } from "./AuditLogsRetentionSection";
@@ -0,0 +1,192 @@
import { useMemo } from "react";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteProjectSection = () => {
const navigate = useNavigate();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteWorkspace",
"leaveWorkspace"
] as const);
const { currentOrg } = useOrganization();
const { hasProjectRole, membership } = useProjectPermission();
const { currentWorkspace } = useWorkspace();
const [isDeleting, setIsDeleting] = useToggle();
const [isLeaving, setIsLeaving] = useToggle();
const deleteWorkspace = useDeleteWorkspace();
const leaveProject = useLeaveProject();
const { data: members, isPending: isMembersLoading } = useGetWorkspaceUsers(
currentWorkspace?.id || ""
);
// If isNoAccessMember is true, then the user can't read the workspace members. So we need to handle this case separately.
const isNoAccessMember = hasProjectRole("no-access");
const isOnlyAdminMember = useMemo(() => {
if (!members || !membership || !hasProjectRole("admin")) return false;
const adminMembers = members.filter(
(member) => member.roles.map((r) => r.role).includes("admin") && member.id !== membership.id // exclude the current user
);
return !adminMembers.length;
}, [members, membership]);
const handleDeleteWorkspaceSubmit = async () => {
setIsDeleting.on();
try {
if (!currentWorkspace?.id) return;
await deleteWorkspace.mutateAsync({
workspaceID: currentWorkspace?.id
});
createNotification({
text: "Successfully deleted project",
type: "success"
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
handlePopUpClose("deleteWorkspace");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete project",
type: "error"
});
} finally {
setIsDeleting.off();
}
};
const handleLeaveWorkspaceSubmit = async () => {
console.log({
currentWorkspace,
currentOrg,
members,
isNoAccessMember,
membership
});
try {
setIsLeaving.on();
if (!currentWorkspace?.id || !currentOrg?.id) return;
// If there's no members, and the user has access to read members, something went wrong.
if (!members && !isNoAccessMember) return;
// If the user has elevated permissions and can read members:
if (!isNoAccessMember) {
if (!members) return;
if (members.length < 2) {
createNotification({
text: "You can't leave the project as you are the only member",
type: "error"
});
return;
}
// If the user has access to read members, and there's less than 1 admin member excluding the current user, they can't leave the project.
if (isOnlyAdminMember) {
createNotification({
text: "You can't leave a project with no admin members left. Promote another member to admin first.",
type: "error"
});
return;
}
}
// If it's actually a no-access member, then we don't really care about the members.
await leaveProject.mutateAsync({
workspaceId: currentWorkspace.id
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to leave project",
type: "error"
});
} finally {
setIsLeaving.off();
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
<div className="space-x-4">
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
{!isOnlyAdminMember && (
<Button
disabled={isMembersLoading || (members && members?.length < 2)}
isLoading={isLeaving}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("leaveWorkspace")}
>
{`Leave ${currentWorkspace?.name}`}
</Button>
)}
</div>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Delete Project"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
<LeaveProjectModal
isOpen={popUp.leaveWorkspace.isOpen}
title="Are you sure want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Leave Project"
onLeaveApproved={handleLeaveWorkspaceSubmit}
/>
</div>
);
};
@@ -0,0 +1 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
@@ -0,0 +1,13 @@
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
export const ProjectGeneralTab = () => {
return (
<div>
<ProjectOverviewChangeSection />
<AuditLogsRetentionSection />
<DeleteProjectSection />
</div>
);
};
@@ -0,0 +1 @@
export { ProjectGeneralTab } from "./ProjectGeneralTab";
@@ -0,0 +1,51 @@
import { useCallback } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
value: string;
hoverText: string;
notificationText: string;
children: React.ReactNode;
};
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
const copyToClipboard = useCallback(() => {
if (isProjectIdCopied) {
return;
}
setIsProjectIdCopied.on();
navigator.clipboard.writeText(value);
createNotification({
text: notificationText,
type: "success"
});
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isProjectIdCopied]);
return (
<Button
colorSchema="secondary"
className="group relative"
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
onClick={copyToClipboard}
>
{children}
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{hoverText}
</span>
</Button>
);
};
@@ -0,0 +1,168 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api";
import { CopyButton } from "./CopyButton";
const formSchema = z.object({
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
description: z
.string()
.trim()
.max(256, "Description too long, max length is 256 characters")
.optional()
});
type FormData = z.infer<typeof formSchema>;
export const ProjectOverviewChangeSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isPending } = useUpdateProject();
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
useEffect(() => {
if (currentWorkspace) {
reset({
name: currentWorkspace.name,
description: currentWorkspace.description ?? ""
});
}
}, [currentWorkspace]);
const onFormSubmit = async ({ name, description }: FormData) => {
try {
if (!currentWorkspace?.id) return;
await mutateAsync({
projectID: currentWorkspace.id,
newProjectName: name,
newProjectDescription: description
});
createNotification({
text: "Successfully updated project overview",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update project overview",
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="justify-betweens flex">
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
<div className="space-x-2">
<CopyButton
value={currentWorkspace?.slug || ""}
hoverText="Click to project slug"
notificationText="Copied project slug to clipboard"
>
Copy Project Slug
</CopyButton>
<CopyButton
value={currentWorkspace?.id || ""}
hoverText="Click to project ID"
notificationText="Copied project ID to clipboard"
>
Copy Project ID
</CopyButton>
</div>
</div>
<div>
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
<div className="flex w-full flex-row items-end gap-4">
<div className="w-full max-w-md">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project name"
>
<Input
placeholder="Project name"
{...field}
className="bg-mineshaft-800"
isDisabled={!isAllowed}
/>
</FormControl>
)}
control={control}
name="name"
/>
)}
</ProjectPermissionCan>
</div>
</div>
<div className="flex w-full flex-row items-end gap-4">
<div className="w-full max-w-md">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project description"
>
<TextArea
placeholder="Project description"
{...field}
rows={3}
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
isDisabled={!isAllowed}
/>
</FormControl>
)}
control={control}
name="description"
/>
)}
</ProjectPermissionCan>
</div>
</div>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isLoading={isPending}
isDisabled={isPending || !isAllowed}
>
Save
</Button>
)}
</ProjectPermissionCan>
</div>
</form>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
@@ -0,0 +1,2 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/settings"
)({
component: SettingsPage
});
@@ -1,21 +0,0 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage";
const ProjectAllowlist = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<IPAllowlistPage />
</>
);
};
export default ProjectAllowlist;
ProjectAllowlist.requireAuth = true;
@@ -1,20 +0,0 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage";
export default function ProjectIdentityDetailsPage() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<IdentityDetailsPage />
</>
);
}
ProjectIdentityDetailsPage.requireAuth = true;
-23
View File
@@ -1,23 +0,0 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { KmsPage } from "@app/views/Project/KmsPage";
const KMS = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: "KMS" })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<KmsPage />
</div>
);
};
export default KMS;
KMS.requireAuth = true;
@@ -1,20 +0,0 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage";
export default function Page() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<MemberDetailsPage />
</>
);
}
Page.requireAuth = true;
@@ -1,21 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { MembersPage } from "@app/views/Project/MembersPage";
export default function WorkspaceMemberSettings() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<MembersPage />
</>
);
}
WorkspaceMemberSettings.requireAuth = true;
@@ -1,20 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { RolePage } from "@app/views/Project/RolePage";
export default function Role() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: "Project Settings" })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<RolePage />
</>
);
}
Role.requireAuth = true;
@@ -1,22 +0,0 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage";
const ProjectSettings = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<ProjectSettingsPage />
</>
);
};
export default ProjectSettings;
ProjectSettings.requireAuth = true;
+25
View File
@@ -0,0 +1,25 @@
import { createFileRoute } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/kms/$projectId/_kms-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
}
});