mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
Merge pull request #3153 from Infisical/daniel/secret-requests
feat(secret-sharing): secret requests
This commit is contained in:
@@ -25,6 +25,7 @@ export const publicPaths = [
|
||||
"/login/sso",
|
||||
"/admin/signup",
|
||||
"/shared/secret/[id]",
|
||||
"/secret-request/secret/[id]",
|
||||
"/share-secret"
|
||||
];
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/organization/secret-scanning",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||
),
|
||||
SecretSharing: setRoute(
|
||||
"/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
|
||||
),
|
||||
SettingsPage: setRoute(
|
||||
"/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
||||
@@ -285,6 +289,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId")
|
||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
|
||||
ViewSecretRequestByIDPage: setRoute(
|
||||
"/secret-request/secret/$secretRequestId",
|
||||
"/secret-request/secret/$secretRequestId"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
|
||||
import { secretSharingKeys } from "./queries";
|
||||
import {
|
||||
TCreatedSharedSecret,
|
||||
TCreateSecretRequestRequestDTO,
|
||||
TCreateSharedSecretRequest,
|
||||
TDeleteSharedSecretRequest,
|
||||
TDeleteSecretRequestDTO,
|
||||
TDeleteSharedSecretRequestDTO,
|
||||
TRevealedSecretRequest,
|
||||
TRevealSecretRequestValueRequest,
|
||||
TSetSecretRequestValueRequest,
|
||||
TSharedSecret
|
||||
} from "./types";
|
||||
|
||||
@@ -15,7 +20,7 @@ export const useCreateSharedSecret = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||
"/api/v1/secret-sharing",
|
||||
"/api/v1/secret-sharing/shared",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
@@ -30,7 +35,7 @@ export const useCreatePublicSharedSecret = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||
"/api/v1/secret-sharing/public",
|
||||
"/api/v1/secret-sharing/shared/public",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
@@ -40,12 +45,50 @@ export const useCreatePublicSharedSecret = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateSecretRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSecretRequestRequestDTO) => {
|
||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||
"/api/v1/secret-sharing/requests",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() })
|
||||
});
|
||||
};
|
||||
|
||||
export const useSetSecretRequestValue = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TSetSecretRequestValueRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>(
|
||||
`/api/v1/secret-sharing/requests/${inputData.id}/set-value`,
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRevealSecretRequestValue = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TRevealSecretRequestValueRequest) => {
|
||||
const { data } = await apiRequest.post<TRevealedSecretRequest>(
|
||||
`/api/v1/secret-sharing/requests/${inputData.id}/reveal-value`,
|
||||
inputData
|
||||
);
|
||||
return data.secretRequest;
|
||||
}
|
||||
});
|
||||
};
|
||||
export const useDeleteSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequestDTO) => {
|
||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||
`/api/v1/secret-sharing/${sharedSecretId}`
|
||||
`/api/v1/secret-sharing/shared/${sharedSecretId}`
|
||||
);
|
||||
return data;
|
||||
},
|
||||
@@ -53,3 +96,19 @@ export const useDeleteSharedSecret = () => {
|
||||
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSharedSecrets() })
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSharedSecret, unknown, TDeleteSecretRequestDTO>({
|
||||
mutationFn: async ({ secretRequestId }: TDeleteSecretRequestDTO) => {
|
||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||
`/api/v1/secret-sharing/requests/${secretRequestId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,16 +2,20 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||
import { TGetSecretRequestByIdResponse, TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||
|
||||
export const secretSharingKeys = {
|
||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
||||
allSecretRequests: () => ["secretRequests"] as const,
|
||||
specificSecretRequests: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||
[...secretSharingKeys.allSecretRequests(), { offset, limit }] as const,
|
||||
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
|
||||
"shared-secret",
|
||||
arg
|
||||
]
|
||||
],
|
||||
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
|
||||
};
|
||||
|
||||
export const useGetSharedSecrets = ({
|
||||
@@ -30,7 +34,7 @@ export const useGetSharedSecrets = ({
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
|
||||
"/api/v1/secret-sharing/",
|
||||
"/api/v1/secret-sharing/shared",
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,6 +44,29 @@ export const useGetSharedSecrets = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSecretRequests = ({
|
||||
offset = 0,
|
||||
limit = 25
|
||||
}: {
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: secretSharingKeys.specificSecretRequests({ offset, limit }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
|
||||
"/api/v1/secret-sharing/requests",
|
||||
{
|
||||
params: {
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
export const useGetActiveSharedSecretById = ({
|
||||
sharedSecretId,
|
||||
hashedHex,
|
||||
@@ -53,7 +80,7 @@ export const useGetActiveSharedSecretById = ({
|
||||
queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||
`/api/v1/secret-sharing/shared/public/${sharedSecretId}`,
|
||||
{
|
||||
...(hashedHex && { hashedHex }),
|
||||
password
|
||||
@@ -65,3 +92,16 @@ export const useGetActiveSharedSecretById = ({
|
||||
enabled: Boolean(sharedSecretId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSecretRequestById = ({ secretRequestId }: { secretRequestId: string }) => {
|
||||
return useQuery({
|
||||
queryKey: secretSharingKeys.getSecretRequestById({ id: secretRequestId }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetSecretRequestByIdResponse>(
|
||||
`/api/v1/secret-sharing/requests/${secretRequestId}`
|
||||
);
|
||||
|
||||
return data.secretRequest;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,13 +6,21 @@ export type TSharedSecret = {
|
||||
updatedAt: Date;
|
||||
name: string | null;
|
||||
lastViewedAt?: Date;
|
||||
accessType: SecretSharingAccessType;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number | null;
|
||||
encryptedValue: string;
|
||||
encryptedSecret: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type TRevealedSecretRequest = {
|
||||
secretRequest: {
|
||||
secretValue: string;
|
||||
} & TSharedSecret;
|
||||
};
|
||||
|
||||
export type TCreatedSharedSecret = {
|
||||
id: string;
|
||||
};
|
||||
@@ -26,6 +34,21 @@ export type TCreateSharedSecretRequest = {
|
||||
accessType?: SecretSharingAccessType;
|
||||
};
|
||||
|
||||
export type TCreateSecretRequestRequestDTO = {
|
||||
name?: string;
|
||||
accessType?: SecretSharingAccessType;
|
||||
expiresAt: Date;
|
||||
};
|
||||
|
||||
export type TSetSecretRequestValueRequest = {
|
||||
secretValue: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TRevealSecretRequestValueRequest = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TViewSharedSecretResponse = {
|
||||
isPasswordProtected: boolean;
|
||||
secret: {
|
||||
@@ -38,10 +61,27 @@ export type TViewSharedSecretResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TDeleteSharedSecretRequest = {
|
||||
export type TGetSecretRequestByIdResponse = {
|
||||
secretRequest: {
|
||||
isSecretValueSet: boolean;
|
||||
accessType: SecretSharingAccessType;
|
||||
requester: {
|
||||
organizationName: string;
|
||||
username: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TDeleteSharedSecretRequestDTO = {
|
||||
sharedSecretId: string;
|
||||
};
|
||||
|
||||
export type TDeleteSecretRequestDTO = {
|
||||
secretRequestId: string;
|
||||
};
|
||||
|
||||
export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
|
||||
@@ -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,56 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
|
||||
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
|
||||
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
|
||||
|
||||
enum SecretSharingPageTabs {
|
||||
ShareSecret = "share-secret",
|
||||
RequestSecret = "request-secret"
|
||||
}
|
||||
|
||||
export const ShareSecretSection = () => {
|
||||
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
|
||||
<Badge variant="primary" className="ml-1 cursor-pointer text-xs">
|
||||
New
|
||||
</Badge>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
|
||||
<ShareSecretTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
|
||||
<RequestSecretTab />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
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}
|
||||
tooltipText="Select for how long someone is able to input the secret. If a secret is shared with you in time, it will remain available to you, even after the expiration."
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
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 { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddSecretRequestModal } from "./AddSecretRequestModal";
|
||||
import { RequestedSecretsTable } from "./RequestedSecretsTable";
|
||||
import { RevealSecretValueModal } from "./RevealSecretValueModal";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const RequestSecretTab = () => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createSecretRequest",
|
||||
"deleteSecretRequestConfirmation",
|
||||
"revealSecretRequestValue"
|
||||
] as const);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
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 {
|
||||
SecretSharingAccessType,
|
||||
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>
|
||||
<Badge variant="primary">
|
||||
<Tooltip
|
||||
content={
|
||||
row.accessType === SecretSharingAccessType.Anyone
|
||||
? "Anyone can input the secret."
|
||||
: "Only members of the organization can input the secret."
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{row.accessType === SecretSharingAccessType.Anyone
|
||||
? "Anyone"
|
||||
: "Organization Members"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</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 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
|
||||
});
|
||||
|
||||
handlePopUpOpen("revealSecretRequestValue", {
|
||||
secretValue: secretRequest.secretValue,
|
||||
secretRequestName: secretRequest.name
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
ariaLabel="reveal"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={isPending ? "animate-spin" : ""}
|
||||
icon={!isPending ? faEye : faSpinner}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Copy link">
|
||||
<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>
|
||||
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteSecretRequestConfirmation", {
|
||||
name: "delete",
|
||||
id: row.id
|
||||
});
|
||||
}}
|
||||
variant="plain"
|
||||
ariaLabel="delete"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
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>Access Type</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 requested 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-mineshaft-700 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="secondary">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>
|
||||
);
|
||||
};
|
||||
+6
-11
@@ -1,27 +1,27 @@
|
||||
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 { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddShareSecretModal } from "./AddShareSecretModal";
|
||||
import { ShareSecretsTable } from "./ShareSecretsTable";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const ShareSecretSection = () => {
|
||||
const deleteSharedSecret = useDeleteSharedSecret();
|
||||
export const ShareSecretTab = () => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createSharedSecret",
|
||||
"deleteSharedSecretConfirmation"
|
||||
] as const);
|
||||
|
||||
const deleteSecretShare = useDeleteSharedSecret();
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteSharedSecret.mutateAsync({
|
||||
deleteSecretShare.mutateAsync({
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
createNotification({
|
||||
@@ -41,11 +41,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 +0,0 @@
|
||||
export { ShareSecretSection } from "./ShareSecretSection";
|
||||
@@ -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("").default("share-secret")
|
||||
});
|
||||
|
||||
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 →
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+13
@@ -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 for is missing or has expired.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+80
@@ -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>
|
||||
);
|
||||
};
|
||||
+23
@@ -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>
|
||||
);
|
||||
};
|
||||
+18
@@ -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);
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/AddShareSecretModal";
|
||||
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal";
|
||||
|
||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||
import { SecretDetailSidebar } from "./SecretDetailSidebar";
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Route as authPasswordSetupPageRouteImport } from './pages/auth/Password
|
||||
import { Route as userLayoutImport } from './pages/user/layout'
|
||||
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
||||
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
||||
import { Route as publicViewSecretRequestByIDPageRouteImport } from './pages/public/ViewSecretRequestByIDPage/route'
|
||||
import { Route as authSignUpSsoPageRouteImport } from './pages/auth/SignUpSsoPage/route'
|
||||
import { Route as authLoginSsoPageRouteImport } from './pages/auth/LoginSsoPage/route'
|
||||
import { Route as authSelectOrgPageRouteImport } from './pages/auth/SelectOrgPage/route'
|
||||
@@ -354,6 +355,13 @@ const publicViewSharedSecretByIDPageRouteRoute =
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const publicViewSecretRequestByIDPageRouteRoute =
|
||||
publicViewSecretRequestByIDPageRouteImport.update({
|
||||
id: '/secret-request/secret/$secretRequestId',
|
||||
path: '/secret-request/secret/$secretRequestId',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const authSignUpSsoPageRouteRoute = authSignUpSsoPageRouteImport.update({
|
||||
id: '/sso',
|
||||
path: '/sso',
|
||||
@@ -1760,6 +1768,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof authSignUpSsoPageRouteImport
|
||||
parentRoute: typeof RestrictLoginSignupSignupImport
|
||||
}
|
||||
'/secret-request/secret/$secretRequestId': {
|
||||
id: '/secret-request/secret/$secretRequestId'
|
||||
path: '/secret-request/secret/$secretRequestId'
|
||||
fullPath: '/secret-request/secret/$secretRequestId'
|
||||
preLoaderRoute: typeof publicViewSecretRequestByIDPageRouteImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/shared/secret/$secretId': {
|
||||
id: '/shared/secret/$secretId'
|
||||
path: '/shared/secret/$secretId'
|
||||
@@ -3643,6 +3658,7 @@ export interface FileRoutesByFullPath {
|
||||
'/login/select-organization': typeof authSelectOrgPageRouteRoute
|
||||
'/login/sso': typeof authLoginSsoPageRouteRoute
|
||||
'/signup/sso': typeof authSignUpSsoPageRouteRoute
|
||||
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
|
||||
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
||||
'/admin': typeof adminLayoutRouteWithChildren
|
||||
'/personal-settings/': typeof userPersonalSettingsPageRouteRoute
|
||||
@@ -3817,6 +3833,7 @@ export interface FileRoutesByTo {
|
||||
'/login/select-organization': typeof authSelectOrgPageRouteRoute
|
||||
'/login/sso': typeof authLoginSsoPageRouteRoute
|
||||
'/signup/sso': typeof authSignUpSsoPageRouteRoute
|
||||
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
|
||||
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
||||
'/admin': typeof adminOverviewPageRouteRoute
|
||||
'/login/provider/error': typeof authProviderErrorPageRouteRoute
|
||||
@@ -3991,6 +4008,7 @@ export interface FileRoutesById {
|
||||
'/_restrict-login-signup/login/select-organization': typeof authSelectOrgPageRouteRoute
|
||||
'/_restrict-login-signup/login/sso': typeof authLoginSsoPageRouteRoute
|
||||
'/_restrict-login-signup/signup/sso': typeof authSignUpSsoPageRouteRoute
|
||||
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
|
||||
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout': typeof organizationLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/admin': typeof AuthenticateInjectOrgDetailsAdminRouteWithChildren
|
||||
@@ -4176,6 +4194,7 @@ export interface FileRouteTypes {
|
||||
| '/login/select-organization'
|
||||
| '/login/sso'
|
||||
| '/signup/sso'
|
||||
| '/secret-request/secret/$secretRequestId'
|
||||
| '/shared/secret/$secretId'
|
||||
| '/admin'
|
||||
| '/personal-settings/'
|
||||
@@ -4349,6 +4368,7 @@ export interface FileRouteTypes {
|
||||
| '/login/select-organization'
|
||||
| '/login/sso'
|
||||
| '/signup/sso'
|
||||
| '/secret-request/secret/$secretRequestId'
|
||||
| '/shared/secret/$secretId'
|
||||
| '/admin'
|
||||
| '/login/provider/error'
|
||||
@@ -4521,6 +4541,7 @@ export interface FileRouteTypes {
|
||||
| '/_restrict-login-signup/login/select-organization'
|
||||
| '/_restrict-login-signup/login/sso'
|
||||
| '/_restrict-login-signup/signup/sso'
|
||||
| '/secret-request/secret/$secretRequestId'
|
||||
| '/shared/secret/$secretId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout'
|
||||
| '/_authenticate/_inject-org-details/admin'
|
||||
@@ -4689,6 +4710,7 @@ export interface RootRouteChildren {
|
||||
publicShareSecretPageRouteRoute: typeof publicShareSecretPageRouteRoute
|
||||
middlewaresAuthenticateRoute: typeof middlewaresAuthenticateRouteWithChildren
|
||||
middlewaresRestrictLoginSignupRoute: typeof middlewaresRestrictLoginSignupRouteWithChildren
|
||||
publicViewSecretRequestByIDPageRouteRoute: typeof publicViewSecretRequestByIDPageRouteRoute
|
||||
publicViewSharedSecretByIDPageRouteRoute: typeof publicViewSharedSecretByIDPageRouteRoute
|
||||
}
|
||||
|
||||
@@ -4699,6 +4721,8 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
middlewaresAuthenticateRoute: middlewaresAuthenticateRouteWithChildren,
|
||||
middlewaresRestrictLoginSignupRoute:
|
||||
middlewaresRestrictLoginSignupRouteWithChildren,
|
||||
publicViewSecretRequestByIDPageRouteRoute:
|
||||
publicViewSecretRequestByIDPageRouteRoute,
|
||||
publicViewSharedSecretByIDPageRouteRoute:
|
||||
publicViewSharedSecretByIDPageRouteRoute,
|
||||
}
|
||||
@@ -4718,6 +4742,7 @@ export const routeTree = rootRoute
|
||||
"/share-secret",
|
||||
"/_authenticate",
|
||||
"/_restrict-login-signup",
|
||||
"/secret-request/secret/$secretRequestId",
|
||||
"/shared/secret/$secretId"
|
||||
]
|
||||
},
|
||||
@@ -4843,6 +4868,9 @@ export const routeTree = rootRoute
|
||||
"filePath": "auth/SignUpSsoPage/route.tsx",
|
||||
"parent": "/_restrict-login-signup/signup"
|
||||
},
|
||||
"/secret-request/secret/$secretRequestId": {
|
||||
"filePath": "public/ViewSecretRequestByIDPage/route.tsx"
|
||||
},
|
||||
"/shared/secret/$secretId": {
|
||||
"filePath": "public/ViewSharedSecretByIDPage/route.tsx"
|
||||
},
|
||||
|
||||
@@ -316,6 +316,7 @@ const sshRoutes = route("/ssh/$projectId", [
|
||||
export const routes = rootRoute("root.tsx", [
|
||||
index("index.tsx"),
|
||||
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
|
||||
route("/secret-request/secret/$secretRequestId", "public/ViewSecretRequestByIDPage/route.tsx"),
|
||||
route("/share-secret", "public/ShareSecretPage/route.tsx"),
|
||||
route("/cli-redirect", "auth/CliRedirectPage/route.tsx"),
|
||||
middleware("restrict-login-signup.tsx", [
|
||||
|
||||
Reference in New Issue
Block a user