mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 01:52:22 +00:00
improvement: finish address changes
This commit is contained in:
@@ -86,13 +86,15 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
icon,
|
||||
as: Item = "button",
|
||||
iconPos = "left",
|
||||
isDisabled = false,
|
||||
...props
|
||||
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => (
|
||||
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T> & { isDisabled?: boolean }) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||
className
|
||||
className,
|
||||
isDisabled ? "pointer-events-none opacity-50" : ""
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const Pagination = ({
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-end bg-mineshaft-800 py-3 px-4 text-white",
|
||||
"flex w-full items-center justify-end border-t border-mineshaft-600 bg-mineshaft-800 py-3 px-4 text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disable
|
||||
isChecked?: boolean;
|
||||
isRequired?: boolean;
|
||||
isDisabled?: boolean;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export const Switch = ({
|
||||
@@ -17,9 +18,10 @@ export const Switch = ({
|
||||
isChecked,
|
||||
isDisabled,
|
||||
isRequired,
|
||||
containerClassName,
|
||||
...props
|
||||
}: SwitchProps): JSX.Element => (
|
||||
<div className="flex items-center font-inter text-bunker-300">
|
||||
<div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
|
||||
<label className="text-sm" htmlFor={id}>
|
||||
{children}
|
||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||
|
||||
@@ -27,35 +27,40 @@ export const Tooltip = ({
|
||||
isDisabled,
|
||||
position = "top",
|
||||
...props
|
||||
}: TooltipProps) => (
|
||||
<TooltipPrimitive.Root
|
||||
delayDuration={50}
|
||||
open={isOpen}
|
||||
defaultOpen={defaultOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
|
||||
}: TooltipProps) =>
|
||||
// just render children if tooltip content is empty
|
||||
content ? (
|
||||
<TooltipPrimitive.Root
|
||||
delayDuration={50}
|
||||
open={isOpen}
|
||||
defaultOpen={defaultOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
|
||||
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
||||
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
||||
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
||||
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
|
||||
`,
|
||||
isDisabled && "!hidden",
|
||||
center && "text-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Root>
|
||||
);
|
||||
isDisabled && "!hidden",
|
||||
center && "text-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Root>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
export const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
|
||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||
export { ProjectPermissionActions, ProjectPermissionSub } from "./types";
|
||||
export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionSub
|
||||
} from "./types";
|
||||
|
||||
@@ -7,6 +7,15 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCmekActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Encrypt = "encrypt",
|
||||
Decrypt = "decrypt"
|
||||
}
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
@@ -55,7 +64,8 @@ export enum ProjectPermissionSub {
|
||||
CertificateTemplates = "certificate-templates",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
Cmek = "cmek"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -97,6 +107,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||
export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionProvider,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { cmekKeys } from "@app/hooks/api/cmeks/queries";
|
||||
import {
|
||||
TCmekDecrypt,
|
||||
TCmekDecryptResponse,
|
||||
TCmekEncrypt,
|
||||
TCmekEncryptResponse,
|
||||
TCreateCmek,
|
||||
TDeleteCmek,
|
||||
TUpdateCmek
|
||||
} from "@app/hooks/api/cmeks/types";
|
||||
|
||||
export const useCreateCmek = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (payload: TCreateCmek) => {
|
||||
const { data } = await apiRequest.post("/api/v1/kms/keys", payload);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCmek = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ keyId, name, description, isDisabled }: TUpdateCmek) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/kms/keys/${keyId}`, {
|
||||
name,
|
||||
description,
|
||||
isDisabled
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCmek = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ keyId }: TDeleteCmek) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/kms/keys/${keyId}`);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCmekEncrypt = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ keyId, plaintext, isBase64Encoded }: TCmekEncrypt) => {
|
||||
const { data } = await apiRequest.post<TCmekEncryptResponse>(
|
||||
`/api/v1/kms/keys/${keyId}/encrypt`,
|
||||
{
|
||||
plaintext: isBase64Encoded ? plaintext : encodeBase64(Buffer.from(plaintext))
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCmekDecrypt = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
|
||||
const { data } = await apiRequest.post<TCmekDecryptResponse>(
|
||||
`/api/v1/kms/keys/${keyId}/decrypt`,
|
||||
{
|
||||
ciphertext
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { CmekOrderBy, TListProjectCmeksDTO, TProjectCmeksList } from "@app/hooks/api/cmeks/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
export const cmekKeys = {
|
||||
all: ["cmek"] as const,
|
||||
lists: () => [...cmekKeys.all, "list"] as const,
|
||||
getCmeksByProjectId: ({ projectId, ...filters }: TListProjectCmeksDTO) =>
|
||||
[...cmekKeys.lists(), projectId, filters] as const
|
||||
};
|
||||
|
||||
export const useGetCmeksByProjectId = (
|
||||
{
|
||||
projectId,
|
||||
offset = 0,
|
||||
limit = 100,
|
||||
orderBy = CmekOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search = ""
|
||||
}: TListProjectCmeksDTO,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TProjectCmeksList,
|
||||
unknown,
|
||||
TProjectCmeksList,
|
||||
ReturnType<typeof cmekKeys.getCmeksByProjectId>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: cmekKeys.getCmeksByProjectId({
|
||||
projectId,
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TProjectCmeksList>("/api/v1/kms/keys", {
|
||||
params: { projectId, offset, limit, search, orderBy, orderDirection }
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(projectId) && (options?.enabled ?? true),
|
||||
keepPreviousData: true,
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
export type TCmek = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
encryptionAlgorithm: EncryptionAlgorithm;
|
||||
projectId: string;
|
||||
isDisabled: boolean;
|
||||
isReserved: boolean;
|
||||
orgId: string;
|
||||
version: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type ProjectRef = { projectId: string };
|
||||
type KeyRef = { keyId: string };
|
||||
|
||||
export type TCreateCmek = Pick<TCmek, "name" | "description" | "encryptionAlgorithm"> & ProjectRef;
|
||||
export type TUpdateCmek = KeyRef &
|
||||
Partial<Pick<TCmek, "name" | "description" | "isDisabled">> &
|
||||
ProjectRef;
|
||||
export type TDeleteCmek = KeyRef & ProjectRef;
|
||||
|
||||
export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
|
||||
export type TCmekDecrypt = KeyRef & { ciphertext: string };
|
||||
|
||||
export type TProjectCmeksList = {
|
||||
keys: TCmek[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export type TListProjectCmeksDTO = {
|
||||
projectId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: CmekOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TCmekEncryptResponse = {
|
||||
ciphertext: string;
|
||||
};
|
||||
|
||||
export type TCmekDecryptResponse = {
|
||||
plaintext: string;
|
||||
};
|
||||
|
||||
export enum CmekOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export enum EncryptionAlgorithm {
|
||||
AES_GCM_256 = "aes-256-gcm",
|
||||
AES_GCM_128 = "aes-128-gcm"
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import { AddExternalKmsType, KmsType } from "./types";
|
||||
export const useAddExternalKms = (orgId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ slug, description, provider }: AddExternalKmsType) => {
|
||||
mutationFn: async ({ name, description, provider }: AddExternalKmsType) => {
|
||||
const { data } = await apiRequest.post("/api/v1/external-kms", {
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
provider
|
||||
});
|
||||
@@ -28,14 +28,14 @@ export const useUpdateExternalKms = (orgId: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
kmsId,
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
provider
|
||||
}: {
|
||||
kmsId: string;
|
||||
} & AddExternalKmsType) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/external-kms/${kmsId}`, {
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
provider
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export const useGetActiveProjectKms = (projectId: string) => {
|
||||
} = await apiRequest.get<{
|
||||
secretManagerKmsKey: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
isExternal: string;
|
||||
};
|
||||
}>(`/api/v1/workspace/${projectId}/kms`);
|
||||
|
||||
@@ -5,7 +5,7 @@ export type Kms = {
|
||||
id: string;
|
||||
description: string;
|
||||
orgId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
external: {
|
||||
id: string;
|
||||
status: string;
|
||||
@@ -21,7 +21,7 @@ export type KmsListEntry = {
|
||||
isDisabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
externalKms: {
|
||||
provider: string;
|
||||
status: string;
|
||||
@@ -88,7 +88,7 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
|
||||
]);
|
||||
|
||||
export const AddExternalKmsSchema = z.object({
|
||||
slug: z
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
|
||||
@@ -122,6 +122,6 @@ export type TOrgIdentitiesList = {
|
||||
};
|
||||
|
||||
export enum OrgIdentityOrderBy {
|
||||
Name = "name",
|
||||
Role = "role"
|
||||
Name = "name"
|
||||
// Role = "role"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { useDebounce } from "./useDebounce";
|
||||
export { useLeaveConfirm } from "./useLeaveConfirm";
|
||||
export { usePagination } from "./usePagination";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
export { useSyntaxHighlight } from "./useSyntaxHighlight";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useDebounce } from "@app/hooks/useDebounce";
|
||||
|
||||
export const usePagination = <T extends string>(initialOrderBy: T) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(20);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebounce(search);
|
||||
|
||||
const offset = (page - 1) * perPage;
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit: perPage,
|
||||
page,
|
||||
setPage,
|
||||
perPage,
|
||||
setPerPage,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
debouncedSearch,
|
||||
search,
|
||||
setSearch,
|
||||
orderBy,
|
||||
setOrderBy
|
||||
};
|
||||
};
|
||||
@@ -630,6 +630,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/kms`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?.id}/kms`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Key Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
@@ -942,7 +952,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.slug}
|
||||
{kms.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -1101,7 +1101,7 @@ const OrganizationPage = () => {
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.slug}
|
||||
{kms.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
+27
-18
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowDown,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { usePagination } from "@app/hooks";
|
||||
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
|
||||
@@ -50,28 +50,35 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const INIT_PER_PAGE = 20;
|
||||
|
||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState(OrgIdentityOrderBy.Name);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebounce(search);
|
||||
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
search,
|
||||
debouncedSearch,
|
||||
setPage,
|
||||
setSearch,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage
|
||||
} = usePagination<OrgIdentityOrderBy>(OrgIdentityOrderBy.Name);
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||
|
||||
const offset = (page - 1) * perPage;
|
||||
const { data, isLoading, isFetching } = useGetIdentityMembershipOrgs(
|
||||
{
|
||||
organizationId,
|
||||
offset,
|
||||
limit: perPage,
|
||||
limit,
|
||||
orderDirection,
|
||||
orderBy,
|
||||
search: debouncedSearch
|
||||
@@ -79,10 +86,11 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { totalCount = 0 } = data ?? {};
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (data && data.totalCount < offset) setPage(1);
|
||||
}, [data?.totalCount]);
|
||||
if (totalCount <= offset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
const { data: roles } = useGetOrgRoles(organizationId);
|
||||
|
||||
@@ -155,7 +163,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<Th>Role</Th>
|
||||
{/* <Th>
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
<IconButton
|
||||
@@ -174,7 +183,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
</Th> */}
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -277,9 +286,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && data && data.totalCount > 0 && (
|
||||
{!isLoading && data && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { CaTab, CertificatesTab,PkiAlertsTab } from "./components";
|
||||
import { CaTab, CertificatesTab, PkiAlertsTab } from "./components";
|
||||
|
||||
enum TabSections {
|
||||
Ca = "certificate-authorities",
|
||||
@@ -36,5 +36,5 @@ export const CertificatesPage = withProjectPermission(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Certificates }
|
||||
);
|
||||
|
||||
@@ -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,158 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
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";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.toLowerCase()
|
||||
.max(32)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Name must be in slug format"
|
||||
}),
|
||||
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,427 @@
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faCancel,
|
||||
faCheckCircle,
|
||||
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 } 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, isLoading, isFetching } = useGetCmeksByProjectId({
|
||||
projectId,
|
||||
offset,
|
||||
limit,
|
||||
search: debouncedSearch,
|
||||
orderBy,
|
||||
orderDirection
|
||||
});
|
||||
|
||||
const { keys = [], totalCount = 0 } = data ?? {};
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (totalCount <= offset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
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">
|
||||
<Link href="https://infisical.com/docs/documentation/platform/kms/infisical-kms">
|
||||
<span className="w-max cursor-pointer 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>
|
||||
</Link>
|
||||
</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 className="w-1/3">
|
||||
<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>Algorithm</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Version</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="project-keys" />}
|
||||
{!isLoading &&
|
||||
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}`}>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
{name}
|
||||
{description && (
|
||||
<Tooltip content={description}>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
className=" text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</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>
|
||||
{!isLoading && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && 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,24 @@
|
||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { CmekTable } from "./components";
|
||||
|
||||
export const KmsPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<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 py-6 px-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>
|
||||
<CmekTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: ProjectPermissionCmekActions.Read,
|
||||
subject: ProjectPermissionSub.Cmek
|
||||
}
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
faArrowDown,
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { usePagination } from "@app/hooks";
|
||||
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||
@@ -56,7 +56,7 @@ import { IdentityModal } from "./components/IdentityModal";
|
||||
import { IdentityRoleForm } from "./components/IdentityRoleForm";
|
||||
|
||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||
const INIT_PER_PAGE = 20;
|
||||
|
||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||
@@ -67,27 +67,43 @@ export const IdentityTab = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState(ProjectIdentityOrderBy.Name);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebounce(search);
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
search,
|
||||
debouncedSearch,
|
||||
setPage,
|
||||
setSearch,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage
|
||||
} = usePagination(ProjectIdentityOrderBy.Name);
|
||||
|
||||
const workspaceId = currentWorkspace?.id ?? "";
|
||||
|
||||
const offset = (page - 1) * perPage;
|
||||
const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships(
|
||||
{
|
||||
workspaceId: currentWorkspace?.id || "",
|
||||
offset,
|
||||
limit: perPage,
|
||||
limit,
|
||||
orderDirection,
|
||||
orderBy,
|
||||
search: debouncedSearch
|
||||
},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { totalCount = 0 } = data ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (totalCount <= offset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -122,11 +138,6 @@ export const IdentityTab = withProjectPermission(
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (data && data.totalCount < offset) setPage(1);
|
||||
}, [data?.totalCount]);
|
||||
|
||||
const handleSort = (column: ProjectIdentityOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
setOrderDirection((prev) =>
|
||||
@@ -369,9 +380,9 @@ export const IdentityTab = withProjectPermission(
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && data && data.totalCount > 0 && (
|
||||
{!isLoading && data && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
|
||||
+46
-4
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
TPermissionCondition,
|
||||
@@ -15,6 +19,15 @@ const GeneralPolicyActionSchema = z.object({
|
||||
create: z.boolean().optional()
|
||||
});
|
||||
|
||||
const CmekPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
edit: z.boolean().optional(),
|
||||
delete: z.boolean().optional(),
|
||||
create: z.boolean().optional(),
|
||||
encrypt: z.boolean().optional(),
|
||||
decrypt: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretFolderPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional()
|
||||
});
|
||||
@@ -88,7 +101,8 @@ export const formSchema = z.object({
|
||||
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([])
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([])
|
||||
})
|
||||
.partial()
|
||||
.optional()
|
||||
@@ -119,7 +133,7 @@ const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition
|
||||
return formConditions;
|
||||
};
|
||||
|
||||
// convert role permission to form compatiable data structure
|
||||
// convert role permission to form compatible data structure
|
||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||
|
||||
@@ -198,6 +212,23 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||
} else if (subject === ProjectPermissionSub.Cmek) {
|
||||
const canRead = action.includes(ProjectPermissionCmekActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionCmekActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
|
||||
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
|
||||
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead) formVal[subject]![0].read = true;
|
||||
if (canEdit) formVal[subject]![0].edit = true;
|
||||
if (canCreate) formVal[subject]![0].create = true;
|
||||
if (canDelete) formVal[subject]![0].delete = true;
|
||||
if (canEncrypt) formVal[subject]![0].encrypt = true;
|
||||
if (canDecrypt) formVal[subject]![0].decrypt = true;
|
||||
}
|
||||
});
|
||||
return formVal;
|
||||
@@ -277,8 +308,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
title: "Secret Folders",
|
||||
actions: [{ label: "Read Only", value: "read" }]
|
||||
},
|
||||
[ProjectPermissionSub.Kms]: {
|
||||
[ProjectPermissionSub.Cmek]: {
|
||||
title: "KMS",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" },
|
||||
{ label: "Encrypt", value: "encrypt" },
|
||||
{ label: "Decrypt", value: "decrypt" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.Kms]: {
|
||||
title: "Project KMS Configuration",
|
||||
actions: [{ label: "Modify", value: "edit" }]
|
||||
},
|
||||
[ProjectPermissionSub.Integrations]: {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useGetImportedSecretsSingleEnv,
|
||||
useGetSecretApprovalPolicyOfABoard,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
useGetWsTags
|
||||
} from "@app/hooks/api";
|
||||
import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
|
||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { DynamicSecretListView } from "@app/views/SecretMainPage/components/DynamicSecretListView";
|
||||
import { FolderListView } from "@app/views/SecretMainPage/components/FolderListView";
|
||||
@@ -47,7 +48,6 @@ const LOADER_TEXT = [
|
||||
"Getting secret import links..."
|
||||
];
|
||||
|
||||
const INIT_PER_PAGE = 20;
|
||||
export const SecretMainPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||
@@ -55,11 +55,18 @@ export const SecretMainPage = () => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [orderDirection, setOrderDirection] = useState<OrderByDirection>(OrderByDirection.ASC);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||
const paginationOffset = (page - 1) * perPage;
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
setPage,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage,
|
||||
orderBy
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
|
||||
|
||||
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
||||
const isRollbackMode = Boolean(snapshotId);
|
||||
@@ -129,8 +136,9 @@ export const SecretMainPage = () => {
|
||||
environment,
|
||||
projectId: workspaceId,
|
||||
secretPath,
|
||||
offset: paginationOffset,
|
||||
limit: perPage,
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
search: debouncedSearchFilter,
|
||||
orderDirection,
|
||||
includeImports: canReadSecret && filter.include.import,
|
||||
@@ -152,6 +160,11 @@ export const SecretMainPage = () => {
|
||||
totalCount = 0
|
||||
} = data ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (totalCount <= offset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
// fetch imported secrets to show user the overriden ones
|
||||
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
|
||||
projectId: workspaceId,
|
||||
@@ -253,11 +266,6 @@ export const SecretMainPage = () => {
|
||||
handlePopUpClose("snapshots");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (totalCount < paginationOffset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
useEffect(() => {
|
||||
// restore filters for path if set
|
||||
const restore = filterHistory.get(secretPath);
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import { useDebounce, usePagination, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretV3,
|
||||
@@ -95,7 +95,6 @@ enum RowType {
|
||||
type Filter = {
|
||||
[key in RowType]: boolean;
|
||||
};
|
||||
const INIT_PER_PAGE = 20;
|
||||
|
||||
const DEFAULT_FILTER_STATE = {
|
||||
[RowType.Folder]: true,
|
||||
@@ -111,7 +110,6 @@ export const SecretOverviewPage = () => {
|
||||
// coz when overflow the table goes to the right
|
||||
const parentTableRef = useRef<HTMLTableElement>(null);
|
||||
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
||||
const [orderDirection, setOrderDirection] = useState<OrderByDirection>(OrderByDirection.ASC);
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -142,8 +140,17 @@ export const SecretOverviewPage = () => {
|
||||
[EntryType.SECRET]: {}
|
||||
});
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
setPage,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage,
|
||||
orderBy
|
||||
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name);
|
||||
|
||||
const toggleSelectedEntry = useCallback(
|
||||
(type: EntryType, key: string) => {
|
||||
@@ -208,21 +215,19 @@ export const SecretOverviewPage = () => {
|
||||
environments: userAvailableEnvs.map(({ slug }) => slug)
|
||||
});
|
||||
|
||||
const paginationOffset = (page - 1) * perPage;
|
||||
|
||||
const { isLoading: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
||||
{
|
||||
projectId: workspaceId,
|
||||
environments: visibleEnvs.map((env) => env.slug),
|
||||
secretPath,
|
||||
orderDirection,
|
||||
orderBy: DashboardSecretsOrderBy.Name,
|
||||
orderBy,
|
||||
includeFolders: filter.folder,
|
||||
includeDynamicSecrets: filter.dynamic,
|
||||
includeSecrets: filter.secret,
|
||||
search: debouncedSearchFilter,
|
||||
limit: perPage,
|
||||
offset: paginationOffset
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
{ enabled: isProjectV3 }
|
||||
);
|
||||
@@ -231,12 +236,17 @@ export const SecretOverviewPage = () => {
|
||||
secrets,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
totalCount = 0,
|
||||
totalFolderCount,
|
||||
totalSecretCount,
|
||||
totalDynamicSecretCount
|
||||
totalDynamicSecretCount,
|
||||
totalCount = 0
|
||||
} = overview ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (totalCount <= offset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
|
||||
|
||||
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||
@@ -530,11 +540,6 @@ export const SecretOverviewPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (totalCount < paginationOffset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
const handleToggleRowType = useCallback(
|
||||
(rowType: RowType) =>
|
||||
setFilter((state) => {
|
||||
|
||||
+5
-5
@@ -61,7 +61,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||
} = useForm<AddExternalKmsType>({
|
||||
resolver: zodResolver(AddExternalKmsSchema),
|
||||
defaultValues: {
|
||||
slug: kms?.slug,
|
||||
name: kms?.name,
|
||||
description: kms?.description ?? "",
|
||||
provider: {
|
||||
type: ExternalKmsProvider.AWS,
|
||||
@@ -89,12 +89,12 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||
const selectedAwsAuthType = watch("provider.inputs.credential.type");
|
||||
|
||||
const handleAddAwsKms = async (data: AddExternalKmsType) => {
|
||||
const { slug, description, provider } = data;
|
||||
const { name, description, provider } = data;
|
||||
try {
|
||||
if (kms) {
|
||||
await updateAwsExternalKms({
|
||||
kmsId: kms.id,
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
provider
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||
});
|
||||
} else {
|
||||
await addAwsExternalKms({
|
||||
slug,
|
||||
name,
|
||||
description,
|
||||
provider
|
||||
});
|
||||
@@ -126,7 +126,7 @@ export const AwsKmsForm = ({ onCompleted, onCancel, kms }: Props) => {
|
||||
<form onSubmit={handleSubmit(handleAddAwsKms)} autoComplete="off">
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Alias" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Input placeholder="" {...field} />
|
||||
|
||||
+2
-2
@@ -122,7 +122,7 @@ export const OrgEncryptionTab = withPermission(
|
||||
)}
|
||||
<div className="ml-2">{kms.externalKms.provider.toUpperCase()}</div>
|
||||
</Td>
|
||||
<Td>{kms.slug}</Td>
|
||||
<Td>{kms.name}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
@@ -170,7 +170,7 @@ export const OrgEncryptionTab = withPermission(
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("removeExternalKms", {
|
||||
slug: kms.slug,
|
||||
name: kms.name,
|
||||
kmsId: kms.id,
|
||||
provider: kms.externalKms.provider
|
||||
});
|
||||
|
||||
+1
-1
@@ -307,7 +307,7 @@ export const EncryptionTab = () => {
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.slug}
|
||||
{kms.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
Reference in New Issue
Block a user