mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
Merge pull request #3616 from Infisical/ENG-2783
feat(secret-sharing): Specify Emails
This commit is contained in:
@@ -11,10 +11,13 @@ export const secretSharingKeys = {
|
||||
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
|
||||
],
|
||||
getSecretById: (arg: {
|
||||
id: string;
|
||||
hashedHex: string | null;
|
||||
password?: string;
|
||||
email?: string;
|
||||
hash?: string;
|
||||
}) => ["shared-secret", arg],
|
||||
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
|
||||
};
|
||||
|
||||
@@ -70,20 +73,34 @@ export const useGetSecretRequests = ({
|
||||
export const useGetActiveSharedSecretById = ({
|
||||
sharedSecretId,
|
||||
hashedHex,
|
||||
password
|
||||
password,
|
||||
email,
|
||||
hash
|
||||
}: {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string | null;
|
||||
password?: string;
|
||||
|
||||
// For secrets shared to specific emails (optional)
|
||||
email?: string;
|
||||
hash?: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
|
||||
queryKey: secretSharingKeys.getSecretById({
|
||||
id: sharedSecretId,
|
||||
hashedHex,
|
||||
password,
|
||||
email,
|
||||
hash
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/shared/public/${sharedSecretId}`,
|
||||
{
|
||||
...(hashedHex && { hashedHex }),
|
||||
password
|
||||
password,
|
||||
email,
|
||||
hash
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export type TCreateSharedSecretRequest = {
|
||||
expiresAt: Date;
|
||||
expiresAfterViews?: number;
|
||||
accessType?: SecretSharingAccessType;
|
||||
emails?: string[];
|
||||
};
|
||||
|
||||
export type TCreateSecretRequestRequestDTO = {
|
||||
|
||||
@@ -91,7 +91,7 @@ export const ShareSecretPage = () => {
|
||||
Infisical
|
||||
</a>
|
||||
<br />
|
||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||
235 2nd st, San Francisco, California, 94105, United States. 🇺🇸
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,19 @@ 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 {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
|
||||
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing";
|
||||
@@ -33,7 +45,24 @@ const schema = z.object({
|
||||
secret: z.string().min(1),
|
||||
expiresIn: z.string(),
|
||||
viewLimit: z.string(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).optional()
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).optional(),
|
||||
emails: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
const emails = val
|
||||
.split(",")
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => email !== "");
|
||||
if (emails.length > 100) return false;
|
||||
return emails.every((email) => z.string().email().safeParse(email).success);
|
||||
},
|
||||
{
|
||||
message: "Must be a comma-separated list of valid emails (max 100) or empty."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
@@ -49,7 +78,7 @@ export const ShareSecretForm = ({
|
||||
value,
|
||||
allowSecretSharingOutsideOrganization = true
|
||||
}: Props) => {
|
||||
const [secretLink, setSecretLink] = useState("");
|
||||
const [secretLink, setSecretLink] = useState<string | null>(null);
|
||||
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
@@ -66,7 +95,9 @@ export const ShareSecretForm = ({
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
secret: value || ""
|
||||
secret: value || "",
|
||||
viewLimit: "-1",
|
||||
expiresIn: "3600000"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,32 +107,45 @@ export const ShareSecretForm = ({
|
||||
secret,
|
||||
expiresIn,
|
||||
viewLimit,
|
||||
accessType
|
||||
accessType,
|
||||
emails
|
||||
}: FormData) => {
|
||||
try {
|
||||
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||
|
||||
const processedEmails = emails ? emails.split(",").map((e) => e.trim()) : undefined;
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
name,
|
||||
password,
|
||||
secretValue: secret,
|
||||
expiresAt,
|
||||
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
|
||||
accessType
|
||||
accessType,
|
||||
emails: processedEmails
|
||||
});
|
||||
|
||||
const link = `${window.location.origin}/shared/secret/${id}`;
|
||||
if (processedEmails && processedEmails.length > 0) {
|
||||
setSecretLink("");
|
||||
createNotification({
|
||||
text: `Shared secret link emailed to ${processedEmails.length} user(s).`,
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
const link = `${window.location.origin}/shared/secret/${id}`;
|
||||
|
||||
setSecretLink(link);
|
||||
|
||||
navigator.clipboard.writeText(link);
|
||||
setCopyTextSecret("secret");
|
||||
|
||||
createNotification({
|
||||
text: "Shared secret link copied to clipboard.",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -111,152 +155,230 @@ export const ShareSecretForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const hasSecretLink = Boolean(secretLink);
|
||||
|
||||
return !hasSecretLink ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{!isPublic && (
|
||||
if (secretLink === null)
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{!isPublic && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Name"
|
||||
isOptional
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="API Key"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
name="secret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Name (Optional)"
|
||||
label="Your Secret"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2"
|
||||
isRequired
|
||||
>
|
||||
<Input
|
||||
<textarea
|
||||
placeholder="Enter sensitive data to share via an encrypted link..."
|
||||
{...field}
|
||||
placeholder="API Key"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
|
||||
disabled={value !== undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="secret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Your Secret"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-2"
|
||||
isRequired
|
||||
>
|
||||
<textarea
|
||||
placeholder="Enter sensitive data to share via an encrypted link..."
|
||||
{...field}
|
||||
className="h-40 min-h-[70px] w-full rounded-md border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5 text-bunker-300 outline-none transition-all placeholder:text-mineshaft-400 hover:border-primary-400/30 focus:border-primary-400/50 group-hover:mr-2"
|
||||
disabled={value !== undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
aria-autocomplete="none"
|
||||
data-form-type="other"
|
||||
/>
|
||||
</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="viewLimit"
|
||||
defaultValue="-1"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Max Views" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{viewLimitOptions.map(({ label, value: viewLimitValue }) => (
|
||||
<SelectItem value={String(viewLimitValue || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isPublic && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessType"
|
||||
defaultValue={SecretSharingAccessType.Organization}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="General Access" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
name="password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{allowSecretSharingOutsideOrganization && (
|
||||
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
|
||||
)}
|
||||
<SelectItem value={SecretSharingAccessType.Organization}>
|
||||
People within your organization
|
||||
</SelectItem>
|
||||
</Select>
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
aria-autocomplete="none"
|
||||
data-form-type="other"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create Secret Link
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
|
||||
{!isPublic && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="accessType"
|
||||
defaultValue={SecretSharingAccessType.Organization}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText={
|
||||
allowSecretSharingOutsideOrganization ? undefined : (
|
||||
<span className="text-red-500">Feature enforced by organization</span>
|
||||
)
|
||||
}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Switch
|
||||
className={`ml-0 mr-2 bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-primary ${!allowSecretSharingOutsideOrganization ? "opacity-50" : ""}`}
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
containerClassName="flex-row-reverse w-fit"
|
||||
isChecked={
|
||||
field.value === SecretSharingAccessType.Organization ||
|
||||
!allowSecretSharingOutsideOrganization
|
||||
}
|
||||
isDisabled={!allowSecretSharingOutsideOrganization}
|
||||
onCheckedChange={(v) =>
|
||||
onChange(
|
||||
v ? SecretSharingAccessType.Organization : SecretSharingAccessType.Anyone
|
||||
)
|
||||
}
|
||||
id="org-access-only"
|
||||
>
|
||||
Limit access to people within organization
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Advanced Settings</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="p-0">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
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="viewLimit"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Max Views"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{viewLimitOptions.map(({ label, value: viewLimitValue }) => (
|
||||
<SelectItem value={String(viewLimitValue || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isPublic && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="emails"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Authorized Emails"
|
||||
isOptional
|
||||
tooltipText="Unique secret links will be emailed to each individual. The secret will only be accessible to those links."
|
||||
tooltipClassName="max-w-sm"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="[email protected], [email protected]"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create Secret Link
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
if (secretLink === "")
|
||||
return (
|
||||
<>
|
||||
<div className="mt-1 flex w-full items-center justify-center gap-2">
|
||||
<FontAwesomeIcon icon={faCheck} className="text-green-500" />
|
||||
<span>Shared secret link has been emailed to select users.</span>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-6 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => setSecretLink(null)}
|
||||
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
||||
>
|
||||
Share Another Secret
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
@@ -265,7 +387,7 @@ export const ShareSecretForm = ({
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(secretLink);
|
||||
navigator.clipboard.writeText(secretLink || "");
|
||||
setCopyTextSecret("Copied");
|
||||
}}
|
||||
>
|
||||
@@ -277,7 +399,7 @@ export const ShareSecretForm = ({
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => setSecretLink("")}
|
||||
onClick={() => setSecretLink(null)}
|
||||
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
||||
>
|
||||
Share Another Secret
|
||||
|
||||
@@ -175,7 +175,7 @@ export const ViewSecretRequestByIDPage = () => {
|
||||
Infisical
|
||||
</a>
|
||||
<br />
|
||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||
235 2nd st, San Francisco, California, 94105, United States. 🇺🇸
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,14 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
|
||||
select: (el) => el.key
|
||||
});
|
||||
const email = useSearch({
|
||||
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
|
||||
select: (el) => el.email
|
||||
});
|
||||
const hash = useSearch({
|
||||
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
|
||||
select: (el) => el.hash
|
||||
});
|
||||
const [password, setPassword] = useState<string>();
|
||||
const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey);
|
||||
|
||||
@@ -49,7 +57,9 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
} = useGetActiveSharedSecretById({
|
||||
sharedSecretId: id,
|
||||
hashedHex,
|
||||
password
|
||||
password,
|
||||
email,
|
||||
hash
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -57,15 +67,16 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
const isUnauthorized =
|
||||
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 401;
|
||||
|
||||
const isForbidden =
|
||||
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 403;
|
||||
|
||||
const isInvalidCredential =
|
||||
((error as AxiosError)?.response?.data as { message: string })?.message ===
|
||||
"Invalid credentials";
|
||||
|
||||
const isEmailUnauthorized =
|
||||
((error as AxiosError)?.response?.data as { message: string })?.message ===
|
||||
"Email not authorized to view secret";
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized && !isInvalidCredential) {
|
||||
if (isUnauthorized && !isInvalidCredential && !isEmailUnauthorized) {
|
||||
// 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,
|
||||
@@ -85,10 +96,10 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isForbidden) {
|
||||
if (error) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "You do not have access to this shared secret."
|
||||
text: ((error as AxiosError)?.response?.data as { message: string })?.message
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
@@ -195,7 +206,7 @@ export const ViewSharedSecretByIDPage = () => {
|
||||
Infisical
|
||||
</a>
|
||||
<br />
|
||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||
235 2nd st, San Francisco, California, 94105, United States. 🇺🇸
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||
import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage";
|
||||
|
||||
const SharedSecretByIDPageQuerySchema = z.object({
|
||||
key: z.string().catch("")
|
||||
key: z.string().catch(""),
|
||||
email: z.string().optional(),
|
||||
hash: z.string().optional()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/shared/secret/$secretId")({
|
||||
|
||||
Reference in New Issue
Block a user