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
@@ -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);
}
});