mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
feat: allow sharing of secrets publicly
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.uuid("orgId").nullable().alter();
|
||||||
|
t.uuid("userId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.uuid("orgId").notNullable().alter();
|
||||||
|
t.uuid("userId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({
|
|||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
expiresAfterViews: z.number().nullable().optional()
|
expiresAfterViews: z.number().nullable().optional()
|
||||||
|
|||||||
@@ -70,6 +70,43 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/public",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
|
expiresAt: z
|
||||||
|
.string()
|
||||||
|
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
||||||
|
expiresAfterViews: z.number()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||||
|
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt: new Date(expiresAt),
|
||||||
|
expiresAfterViews
|
||||||
|
});
|
||||||
|
return { id: sharedSecret.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/",
|
url: "/",
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
|
import {
|
||||||
|
TCreatePublicSharedSecretDTO,
|
||||||
|
TCreateSharedSecretDTO,
|
||||||
|
TDeleteSharedSecretDTO,
|
||||||
|
TSharedSecretPermission
|
||||||
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
@@ -44,6 +49,19 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
||||||
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
|
||||||
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt,
|
||||||
|
expiresAfterViews
|
||||||
|
});
|
||||||
|
return { id: newSharedSecret.id };
|
||||||
|
};
|
||||||
|
|
||||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
@@ -77,6 +95,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createSharedSecret,
|
createSharedSecret,
|
||||||
|
createPublicSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getActiveSharedSecretByIdAndHashedHex
|
getActiveSharedSecretByIdAndHashedHex
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ export type TSharedSecretPermission = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews: number;
|
expiresAfterViews: number;
|
||||||
} & TSharedSecretPermission;
|
};
|
||||||
|
|
||||||
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
|
|||||||
@@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreatePublicSharedSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||||
|
const { data } = await apiRequest.post<TSharedSecret>(
|
||||||
|
"/api/v1/secret-sharing/public",
|
||||||
|
inputData
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useDeleteSharedSecret = () => {
|
export const useDeleteSharedSecret = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<
|
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
||||||
TSharedSecret,
|
|
||||||
{ message: string },
|
|
||||||
{ sharedSecretId: string }
|
|
||||||
>({
|
|
||||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
||||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||||
`/api/v1/secret-sharing/${sharedSecretId}`
|
`/api/v1/secret-sharing/${sharedSecretId}`
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const expirationUnitsAndActions = [
|
const expirationUnitsAndActions = [
|
||||||
@@ -65,9 +64,10 @@ type Props = {
|
|||||||
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
isPublic: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Props) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
@@ -76,8 +76,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
const createSharedSecret = useCreateSharedSecret();
|
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||||
const { currentOrg } = useOrganization();
|
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||||
|
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
||||||
|
|
||||||
const [newSharedSecret, setnewSharedSecret] = useState("");
|
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||||
const hasSharedSecret = Boolean(newSharedSecret);
|
const hasSharedSecret = Boolean(newSharedSecret);
|
||||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||||
@@ -101,7 +103,6 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
expiresAfterViews
|
expiresAfterViews
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?.id) return;
|
|
||||||
const key = crypto.randomBytes(16).toString("hex");
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
@@ -180,7 +181,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<SecretInput
|
<SecretInput
|
||||||
isVisible={false}
|
isVisible={false}
|
||||||
{...field}
|
{...field}
|
||||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
|
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const ShareSecretSection = () => {
|
|||||||
const onDeleteApproved = async () => {
|
const onDeleteApproved = async () => {
|
||||||
try {
|
try {
|
||||||
deleteSharedSecret.mutateAsync({
|
deleteSharedSecret.mutateAsync({
|
||||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id,
|
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted shared secret",
|
text: "Successfully deleted shared secret",
|
||||||
@@ -40,7 +40,6 @@ export const ShareSecretSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Secret Sharing</title>
|
<title>Secret Sharing</title>
|
||||||
@@ -60,14 +59,13 @@ export const ShareSecretSection = () => {
|
|||||||
Share Secret
|
Share Secret
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ShareSecretsTable
|
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic={false} />
|
||||||
/>
|
|
||||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||||
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
title={`Delete ${
|
||||||
} shared secret?`}
|
(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||||
|
} shared secret?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
|
||||||
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
|
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
|
||||||
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
|
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
|
||||||
@@ -75,4 +73,4 @@ export const ShareSecretSection = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { Button } from "@app/components/v2";
|
||||||
|
import { usePopUp, useTimedReset } from "@app/hooks";
|
||||||
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
import { DragonMainImage, SecretTable } from "./components";
|
import { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal";
|
||||||
|
import { SecretTable } from "./components";
|
||||||
|
|
||||||
export const ShareSecretPublicPage = () => {
|
export const ShareSecretPublicPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -53,36 +58,92 @@ export const ShareSecretPublicPage = () => {
|
|||||||
navigator.clipboard.writeText(decryptedSecret);
|
navigator.clipboard.writeText(decryptedSecret);
|
||||||
setIsUrlCopied(true);
|
setIsUrlCopied(true);
|
||||||
};
|
};
|
||||||
|
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["createSharedSecret"] as const);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
|
<div className="flex h-screen flex-col bg-bunker-800 text-gray-200">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Secret Shared | Infisical</title>
|
<title>Secret Shared | Infisical</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div className="my-4 flex justify-center md:my-8">
|
<div className="flex items-center justify-center p-4">
|
||||||
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
|
<Link href="https://infisical.com">
|
||||||
|
<Image
|
||||||
|
src="/images/biglogo.png"
|
||||||
|
height={60}
|
||||||
|
width={80}
|
||||||
|
alt="Infisical logo"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
|
|
||||||
A secret has been shared with you securely via Infisical
|
<div className="flex flex-1 flex-col items-center justify-center px-4">
|
||||||
</p>
|
<div className="flex w-full max-w-4xl flex-col items-center gap-4 md:gap-20">
|
||||||
<div className="flex min-h-screen w-full flex-col md:flex-row">
|
<p className="text-center text-xl font-semibold text-gray-200 md:text-3xl">
|
||||||
<DragonMainImage />
|
Secret Shared via Infisical
|
||||||
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
|
|
||||||
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
|
|
||||||
Shared Secret
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4 rounded-lg md:p-2">
|
<div className="flex w-full flex-grow flex-col gap-6 md:flex-row md:gap-12">
|
||||||
<SecretTable
|
<div className="flex-1 self-center pt-4 text-center md:pt-0 md:text-left">
|
||||||
isLoading={isLoading}
|
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||||
decryptedSecret={decryptedSecret}
|
Safe & Secure
|
||||||
isUrlCopied={isUrlCopied}
|
</p>
|
||||||
copyUrlToClipboard={copyUrlToClipboard}
|
<p className="md:text-md text-sm">
|
||||||
/>
|
Infisical uses <span className="text-primary">Zero Knowledge</span> to ensure that
|
||||||
|
your secrets are truly private (even from us).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 rounded-lg p-2">
|
||||||
|
<SecretTable
|
||||||
|
isLoading={isLoading}
|
||||||
|
decryptedSecret={decryptedSecret}
|
||||||
|
isUrlCopied={isUrlCopied}
|
||||||
|
copyUrlToClipboard={copyUrlToClipboard}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 self-center text-center md:text-right">
|
||||||
|
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||||
|
Open Source
|
||||||
|
</p>
|
||||||
|
<p className="md:text-md text-sm">
|
||||||
|
Infisical is open source. <br className="hidden md:inline" />
|
||||||
|
Check us out on{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mt-4 max-w-[600px] md:mt-0"
|
||||||
|
colorSchema="primary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("createSharedSecret");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share your own Secret
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic />
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<p className="text-center text-sm text-gray-200">
|
||||||
|
Developed by{" "}
|
||||||
|
<a className="text-primary" href="https://infisical.com">
|
||||||
|
Infisical
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
Open Source Secret Management{" "}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export const DragonMainImage = () => {
|
|
||||||
return (
|
|
||||||
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
|
|
||||||
<Image
|
|
||||||
src="/images/dragon-book.svg"
|
|
||||||
height={1000}
|
|
||||||
width={1413}
|
|
||||||
alt="Infisical Dragon - Came to send you a secret!"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { DragonMainImage } from "./MainImage";
|
|
||||||
export { SecretTable } from "./SecretTable";
|
export { SecretTable } from "./SecretTable";
|
||||||
|
|||||||
Reference in New Issue
Block a user