mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 19:28:35 +00:00
feat(secret-sharing): secret requests
This commit is contained in:
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { ShareSecretSection } from "./components";
|
||||
import { ShareSecretSection } from "./ShareSecretSection";
|
||||
|
||||
export const SecretSharingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
|
||||
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
enum SecretSharingPageTabs {
|
||||
ShareSecret = "share-secret",
|
||||
RequestSecret = "request-secret"
|
||||
}
|
||||
|
||||
export const ShareSecretSection = () => {
|
||||
const deleteSharedSecret = useDeleteSharedSecret();
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createSharedSecret",
|
||||
"deleteSharedSecretConfirmation",
|
||||
"createSecretRequest",
|
||||
"deleteSecretRequestConfirmation",
|
||||
"revealSecretRequestValue"
|
||||
] as const);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { selectedTab } = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SecretSharing.id
|
||||
});
|
||||
|
||||
const updateSelectedTab = (tab: string) => {
|
||||
navigate({
|
||||
to: ROUTE_PATHS.Organization.SecretSharing.path,
|
||||
search: (prev) => ({ ...prev, selectedTab: tab as SecretSharingPageTabs })
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>Secret Sharing</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
|
||||
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
<Tab value={SecretSharingPageTabs.ShareSecret}>Share Secrets</Tab>
|
||||
<Tab value={SecretSharingPageTabs.RequestSecret}>Request Secrets</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
|
||||
<ShareSecretTab
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
|
||||
<RequestSecretTab
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { RequestSecretForm } from "./RequestSecretForm";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["createSecretRequest"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["createSecretRequest"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const AddSecretRequestModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.createSecretRequest?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("createSecretRequest", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Request a Secret"
|
||||
subTitle="Securely request one off secrets from your team or people outside your organization."
|
||||
>
|
||||
<RequestSecretForm />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy, faRedo } 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, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { SecretSharingAccessType, useCreateSecretRequest } from "@app/hooks/api/secretSharing";
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
accessType: z
|
||||
.nativeEnum(SecretSharingAccessType)
|
||||
.default(SecretSharingAccessType.Anyone)
|
||||
.optional(),
|
||||
expiresIn: z.string()
|
||||
});
|
||||
|
||||
const expiresInOptions = [
|
||||
{ label: "5 min", value: 5 * 60 * 1000 },
|
||||
{ label: "30 min", value: 30 * 60 * 1000 },
|
||||
{ label: "1 hour", value: 60 * 60 * 1000 },
|
||||
{ label: "1 day", value: 24 * 60 * 60 * 1000 },
|
||||
{ label: "7 days", value: 7 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "14 days", value: 14 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "30 days", value: 30 * 24 * 60 * 60 * 1000 }
|
||||
];
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const RequestSecretForm = () => {
|
||||
const [secretLink, setSecretLink] = useState("");
|
||||
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
const { mutateAsync: createSecretRequest } = useCreateSecretRequest();
|
||||
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ name, accessType, expiresIn }: FormData) => {
|
||||
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||
|
||||
try {
|
||||
const { id } = await createSecretRequest({
|
||||
name,
|
||||
accessType,
|
||||
expiresAt
|
||||
});
|
||||
|
||||
const link = `${window.location.origin}/secret-request/secret/${id}`;
|
||||
|
||||
setSecretLink(link);
|
||||
reset();
|
||||
|
||||
navigator.clipboard.writeText(link);
|
||||
setCopyTextSecret("secret");
|
||||
|
||||
createNotification({
|
||||
text: "Shared secret link copied to clipboard.",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to create a shared secret.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasSecretLink = Boolean(secretLink);
|
||||
|
||||
return !hasSecretLink ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name (Optional)" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="API Key" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue="3600000"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expiresInOptions.map(({ label, value: expiresInValue }) => (
|
||||
<SelectItem value={String(expiresInValue || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessType"
|
||||
defaultValue={SecretSharingAccessType.Organization}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="Select who is able to input the secret"
|
||||
label="General Access"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
|
||||
<SelectItem value={SecretSharingAccessType.Organization}>
|
||||
People within your organization
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create Request Link
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div className="mr-2 flex items-center justify-end rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{secretLink}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(secretLink);
|
||||
setCopyTextSecret("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => setSecretLink("")}
|
||||
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
||||
>
|
||||
Request Another Secret
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useDeleteSecretRequest } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddSecretRequestModal } from "./AddSecretRequestModal";
|
||||
import { RequestedSecretsTable } from "./RequestedSecretsTable";
|
||||
import { RevealSecretValueModal } from "./RevealSecretValueModal";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
|
||||
>,
|
||||
data?: any
|
||||
) => void;
|
||||
popUp: UsePopUpState<
|
||||
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
|
||||
>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
|
||||
>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
handlePopUpClose: (
|
||||
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>
|
||||
) => void;
|
||||
};
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const RequestSecretTab = ({
|
||||
handlePopUpOpen,
|
||||
popUp,
|
||||
handlePopUpToggle,
|
||||
handlePopUpClose
|
||||
}: Props) => {
|
||||
const { mutateAsync: deleteSecretRequest } = useDeleteSecretRequest();
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
await deleteSecretRequest({
|
||||
secretRequestId: popUp.deleteSecretRequestConfirmation.data?.id
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted secret request",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteSecretRequestConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete shared secret",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Secret Requests</p>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createSecretRequest");
|
||||
}}
|
||||
>
|
||||
Request Secret
|
||||
</Button>
|
||||
</div>
|
||||
<RequestedSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddSecretRequestModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<RevealSecretValueModal
|
||||
isOpen={popUp.revealSecretRequestValue.isOpen}
|
||||
popUp={popUp}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("revealSecretRequestValue", isOpen)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretRequestConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteSecretRequestConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} secret request?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretRequestConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteSecretRequestConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteSecretRequestConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-extra-boolean-cast */
|
||||
import { faCopy, faEye, faSpinner, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { TSharedSecret, useRevealSecretRequestValue } from "@app/hooks/api/secretSharing";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
export const RequestedSecretsRow = ({
|
||||
row,
|
||||
handlePopUpOpen
|
||||
}: {
|
||||
row: TSharedSecret;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>,
|
||||
data: unknown
|
||||
) => void;
|
||||
}) => {
|
||||
const { mutateAsync: revealSecretValue, isPending } = useRevealSecretRequestValue();
|
||||
|
||||
let isExpired = false;
|
||||
if (row.expiresAt !== null && new Date(row.expiresAt) < new Date()) {
|
||||
isExpired = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr key={row.id}>
|
||||
<Td>{row.name ? `${row.name}` : "-"}</Td>
|
||||
<Td>
|
||||
{isExpired && !row.encryptedSecret ? (
|
||||
<Badge variant="danger">Expired</Badge>
|
||||
) : (
|
||||
<Badge variant={row.encryptedSecret ? "success" : "primary"}>
|
||||
{row.encryptedSecret ? "Secret Provided" : "Pending Secret"}
|
||||
</Badge>
|
||||
)}
|
||||
</Td>
|
||||
<Td>{`${format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}`}</Td>
|
||||
<Td>{row.expiresAt ? format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a") : "-"}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip
|
||||
content={
|
||||
row.encryptedSecret
|
||||
? "Reveal shared secret"
|
||||
: "Secret value must be provided before it can be viewed."
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
isDisabled={!row.encryptedSecret}
|
||||
className={row.encryptedSecret ? "" : "opacity-50"}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const secretRequest = await revealSecretValue({
|
||||
id: row.id
|
||||
});
|
||||
|
||||
console.log("revealSecretRequestValue", {
|
||||
secretValue: secretRequest.secretValue,
|
||||
secretRequestName: secretRequest.name
|
||||
});
|
||||
|
||||
handlePopUpOpen("revealSecretRequestValue", {
|
||||
secretValue: secretRequest.secretValue,
|
||||
secretRequestName: secretRequest.name
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
ariaLabel="reveal"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={isPending ? "animate-spin" : ""}
|
||||
icon={!isPending ? faEye : faSpinner}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<IconButton
|
||||
isDisabled={Boolean(row.encryptedSecret) || isExpired}
|
||||
className={Boolean(row.encryptedSecret) || isExpired ? "opacity-50" : ""}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
navigator.clipboard.writeText(
|
||||
`${window.location.origin}/secret-request/secret/${row.id}`
|
||||
);
|
||||
|
||||
createNotification({
|
||||
text: "Shared secret link copied to clipboard.",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
ariaLabel="copy link"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
|
||||
<Tooltip content="Delete Secret Request">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteSecretRequestConfirmation", {
|
||||
name: "delete",
|
||||
id: row.id
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
ariaLabel="delete"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { useState } from "react";
|
||||
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useGetSecretRequests } from "@app/hooks/api/secretSharing";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { RequestedSecretsRow } from "./RequestedSecretsRow";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>,
|
||||
data: unknown
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const RequestedSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const { isPending, data } = useGetSecretRequests({
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage
|
||||
});
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Created At</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th aria-label="button" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={7} innerKey="shared-secrets" />}
|
||||
{!isPending &&
|
||||
data?.secrets?.map((row) => (
|
||||
<RequestedSecretsRow key={row.id} row={row} handlePopUpOpen={handlePopUpOpen} />
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending &&
|
||||
data?.secrets &&
|
||||
data?.totalCount >= perPage &&
|
||||
data?.totalCount !== undefined && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !data?.secrets?.length && (
|
||||
<EmptyState title="No secrets shared yet" icon={faKey} />
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, IconButton, Modal, ModalClose, ModalContent, Tooltip } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
popUp: UsePopUpState<["revealSecretRequestValue"]>;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
secretValue: string;
|
||||
secretRequestName?: string;
|
||||
};
|
||||
|
||||
const Content = ({ secretValue, secretRequestName }: ContentProps) => {
|
||||
const [isSecretValueCopied, setIsSecretValueCopied] = useToggle(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{secretRequestName && (
|
||||
<p className="mb-8 text-sm text-mineshaft-200">
|
||||
Shared secret value for <strong>{secretRequestName}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{secretValue}</p>
|
||||
<Tooltip content="Click to copy">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(secretValue);
|
||||
setIsSecretValueCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecretValueCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex w-full items-center justify-between gap-2">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="primary">Close</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RevealSecretValueModal = ({ isOpen, onOpenChange, popUp }: Props) => {
|
||||
const data = popUp.revealSecretRequestValue.data as {
|
||||
secretValue: string;
|
||||
secretRequestName?: string;
|
||||
};
|
||||
|
||||
const title = data?.secretRequestName
|
||||
? `Shared secret value for secret request ${data.secretRequestName}`
|
||||
: "Shared secret value";
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent title={title}>
|
||||
<Content secretRequestName={data?.secretRequestName} secretValue={data?.secretValue} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+23
-15
@@ -1,27 +1,40 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
import { useDeleteSharedSecret } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddShareSecretModal } from "./AddShareSecretModal";
|
||||
import { ShareSecretsTable } from "./ShareSecretsTable";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
|
||||
data?: any
|
||||
) => void;
|
||||
popUp: UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["deleteSharedSecretConfirmation"]>) => void;
|
||||
};
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const ShareSecretSection = () => {
|
||||
const deleteSharedSecret = useDeleteSharedSecret();
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createSharedSecret",
|
||||
"deleteSharedSecretConfirmation"
|
||||
] as const);
|
||||
export const ShareSecretTab = ({
|
||||
handlePopUpOpen,
|
||||
popUp,
|
||||
handlePopUpToggle,
|
||||
handlePopUpClose
|
||||
}: Props) => {
|
||||
const deleteSecretShare = useDeleteSharedSecret();
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteSharedSecret.mutateAsync({
|
||||
deleteSecretShare.mutateAsync({
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
createNotification({
|
||||
@@ -41,11 +54,6 @@ export const ShareSecretSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<Helmet>
|
||||
<title>Secret Sharing</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p>
|
||||
<Button
|
||||
@@ -1,13 +1,24 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSharingPage } from "./SecretSharingPage";
|
||||
|
||||
const SecretSharingQueryParams = z.object({
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
|
||||
)({
|
||||
component: SecretSharingPage,
|
||||
|
||||
validateSearch: zodValidator(SecretSharingQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
@@ -16,7 +27,7 @@ export const Route = createFileRoute(
|
||||
link: linkOptions({ to: "/" })
|
||||
},
|
||||
{
|
||||
label: "secret sharing"
|
||||
label: "Secret Sharing"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user