feat(secret-sharing): secret requests

This commit is contained in:
Daniel Hougaard
2025-03-03 21:14:40 +04:00
parent 2aa6fdf983
commit 7aaf0f4ed3
40 changed files with 1915 additions and 49 deletions
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
</>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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"
}
]
})
@@ -0,0 +1,184 @@
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { addSeconds, formatISO } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { SessionStorageKeys } from "@app/const";
import { ROUTE_PATHS } from "@app/const/routes";
import { useGetSecretRequestById } from "@app/hooks/api/secretSharing";
import { SecretRequestErrorContainer } from "./components/SecretErrorContainer";
import { SecretRequestContainer } from "./components/SecretRequestContainer";
import { SecretRequestSuccessContainer } from "./components/SecretRequestSuccessContainer";
import { SecretValueAlreadySharedContainer } from "./components/SecretValueAlreadySharedContainer";
export const ViewSecretRequestByIDPage = () => {
const id = useParams({
from: ROUTE_PATHS.Public.ViewSecretRequestByIDPage.id,
select: (el) => el.secretRequestId
});
const [step, setStep] = useState<"set-value" | "success">("set-value");
const {
data: secretRequest,
error,
isPending
} = useGetSecretRequestById({
secretRequestId: id
});
const navigate = useNavigate();
const statusCode = ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode;
const message = ((error as AxiosError)?.response?.data as { message: string })?.message;
const isUnauthorized = statusCode === 401;
const isForbidden = statusCode === 403;
const isInvalidCredential = message === "Invalid credentials";
useEffect(() => {
if (isUnauthorized && !isInvalidCredential) {
// persist current URL in session storage so that we can come back to this after successful login
sessionStorage.setItem(
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL,
JSON.stringify({
expiry: formatISO(addSeconds(new Date(), 60)),
data: window.location.href
})
);
createNotification({
type: "info",
text: "Login is required in order to access the shared secret."
});
navigate({
to: "/login"
});
}
if (isForbidden) {
createNotification({
type: "error",
text: "You do not have access to this shared secret."
});
}
}, [error]);
return (
<>
<Helmet>
<title>Securely Share Secrets | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="" />
<meta name="og:description" content="" />
</Helmet>
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div />
<div className="mx-auto w-full max-w-xl px-4 py-4 md:px-0">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<img
src="/images/gradientLogo.svg"
height={90}
width={120}
alt="Infisical logo"
className="cursor-pointer"
/>
</a>
</div>
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
{step === "set-value" ? "Secret Request" : "Secret request shared"}
</h1>
<p className="text-sm text-mineshaft-300">
Secret requested by {secretRequest?.requester.username} from the{" "}
{secretRequest?.requester.organizationName} organization
</p>
<p className="text-md mt-2">
Powered by{" "}
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical &rarr;
</a>
</p>
</div>
{!isPending && (
<>
{!error &&
secretRequest &&
step === "set-value" &&
!secretRequest.isSecretValueSet && (
<SecretRequestContainer
onSuccess={() => setStep("success")}
secretRequestId={id}
/>
)}
{secretRequest?.isSecretValueSet && <SecretValueAlreadySharedContainer />}
{error && !isInvalidCredential && !isUnauthorized && <SecretRequestErrorContainer />}
{step === "success" && (
<SecretRequestSuccessContainer
requesterUsername={secretRequest!.requester.username}
/>
)}
</>
)}
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
</p>
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs,
and certificates across your team and infrastructure.
</p>
<div className="mt-4 cursor-pointer sm:mt-0">
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 px-3 py-2 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</a>
</div>
</div>
</div>
</div>
<div className="w-full bg-mineshaft-600 p-2">
<p className="text-center text-sm text-mineshaft-300">
Made with ❤️ by{" "}
<a className="text-primary" href="https://infisical.com">
Infisical
</a>
<br />
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
</p>
</div>
</div>
</>
);
};
@@ -0,0 +1,13 @@
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const SecretRequestErrorContainer = () => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center">
<FontAwesomeIcon icon={faKey} size="2x" />
<p className="mt-4">The secret request you are looking is missing or has expired.</p>
</div>
</div>
);
};
@@ -0,0 +1,80 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faArrowRight } 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, TextArea } from "@app/components/v2";
import { useSetSecretRequestValue } from "@app/hooks/api";
const formSchema = z.object({
secretValue: z.string().min(1)
});
type FormData = z.infer<typeof formSchema>;
type Props = {
onSuccess: () => void;
secretRequestId: string;
};
export const SecretRequestContainer = ({ onSuccess, secretRequestId }: Props) => {
const form = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const { mutateAsync: setSecretValue, isPending } = useSetSecretRequestValue();
const onSubmit = async (data: FormData) => {
await setSecretValue({
id: secretRequestId,
secretValue: data.secretValue
});
createNotification({
title: "Secret request value shared",
text: "The secret request value has been shared",
type: "success"
});
onSuccess();
};
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex items-center justify-between rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
<div className="w-full">
<Controller
control={form.control}
name="secretValue"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
isRequired
label="Secret Value"
>
<TextArea {...field} rows={10} reSize="none" />
</FormControl>
)}
/>
<Button
isLoading={isPending}
isDisabled={isPending}
colorSchema="secondary"
className="w-full"
type="submit"
>
Share Secret
<FontAwesomeIcon className="ml-2" icon={faArrowRight} />
</Button>
</div>
</div>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,23 @@
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type Props = {
requesterUsername: string;
};
export const SecretRequestSuccessContainer = ({ requesterUsername }: Props) => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center">
<div className="mx-auto w-min rounded-md border border-mineshaft-800 bg-mineshaft-600 p-3">
<FontAwesomeIcon icon={faCheck} size="2x" className="text-primary-500" />
</div>
<p className="text-md mt-2 font-semibold">Secret Shared</p>
<p className="mt-2 text-sm text-mineshaft-300">
<strong>{requesterUsername}</strong> has now been notified of your shared secret, and will
be able to access it shortly.
</p>
</div>
</div>
);
};
@@ -0,0 +1,18 @@
import { faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const SecretValueAlreadySharedContainer = () => {
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
<div className="text-center">
<div className="mx-auto w-min rounded-md border border-mineshaft-800 bg-mineshaft-600 p-3">
<FontAwesomeIcon icon={faKey} size="2x" className="text-primary-500" />
</div>
<p className="text-md mt-2 font-semibold">Secret Already Shared</p>
<p className="mt-2 text-sm text-mineshaft-300">
A secret value has already been shared for this secret request.
</p>
</div>
</div>
);
};
@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { ViewSecretRequestByIDPage } from "./ViewSecretRequestByIDPage";
export const Route = createFileRoute("/secret-request/secret/$secretRequestId")({
component: ViewSecretRequestByIDPage,
beforeLoad: async ({ context }) => {
await context.queryClient
.ensureQueryData({
queryKey: authKeys.getAuthToken,
queryFn: fetchAuthToken
})
.catch(() => undefined);
}
});