diff --git a/backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts b/backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts new file mode 100644 index 000000000..dc2756b74 --- /dev/null +++ b/backend/src/db/migrations/20240605074539_make-secret-sharing-public.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId"); + const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId"); + + if (await knex.schema.hasTable(TableName.SecretSharing)) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + if (hasOrgIdColumn) t.uuid("orgId").nullable().alter(); + if (hasUserIdColumn) t.uuid("userId").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasOrgIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "orgId"); + const hasUserIdColumn = await knex.schema.hasColumn(TableName.SecretSharing, "userId"); + + if (await knex.schema.hasTable(TableName.SecretSharing)) { + await knex.schema.alterTable(TableName.SecretSharing, (t) => { + if (hasOrgIdColumn) t.uuid("orgId").notNullable().alter(); + if (hasUserIdColumn) t.uuid("userId").notNullable().alter(); + }); + } +} diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index 6fa104ebe..c8d938861 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({ tag: z.string(), hashedHex: z.string(), expiresAt: z.date(), - userId: z.string().uuid(), - orgId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + orgId: z.string().uuid().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), expiresAfterViews: z.number().nullable().optional() diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index ef0ad891f..ad54a151a 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -70,8 +70,15 @@ export const creationLimit: RateLimitOptions = { // Public endpoints to avoid brute force attacks export const publicEndpointLimit: RateLimitOptions = { - // Shared Secrets + // Read Shared Secrets timeWindow: 60 * 1000, max: () => getRateLimiterConfig().publicEndpointLimit, keyGenerator: (req) => req.realIp }; + +export const publicSecretShareCreationLimit: RateLimitOptions = { + // Create Shared Secrets + timeWindow: 60 * 1000, + max: 5, + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts index 6cb551698..4ec2737fb 100644 --- a/backend/src/server/routes/v1/secret-sharing-router.ts +++ b/backend/src/server/routes/v1/secret-sharing-router.ts @@ -1,7 +1,12 @@ import { z } from "zod"; import { SecretSharingSchema } from "@app/db/schemas"; -import { publicEndpointLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { + publicEndpointLimit, + publicSecretShareCreationLimit, + readLimit, + writeLimit +} from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -72,7 +77,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => server.route({ method: "POST", - url: "/", + url: "/public", config: { rateLimit: writeLimit }, @@ -82,9 +87,42 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) => 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"), + expiresAt: z.string(), + 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({ + method: "POST", + url: "/", + config: { + rateLimit: publicSecretShareCreationLimit + }, + schema: { + body: z.object({ + encryptedValue: z.string(), + iv: z.string(), + tag: z.string(), + hashedHex: z.string(), + expiresAt: z.string(), expiresAfterViews: z.number() }), response: { diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index ccbce0a52..012b0f130 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,8 +1,13 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; 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 = { permissionService: Pick; @@ -31,6 +36,24 @@ export const secretSharingServiceFactory = ({ } = createSharedSecretInput; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); if (!permission) throw new UnauthorizedError({ name: "User not in org" }); + + if (new Date(expiresAt) < new Date()) { + throw new BadRequestError({ message: "Expiration date cannot be in the past" }); + } + + // Limit Expiry Time to 1 month + const expiryTime = new Date(expiresAt).getTime(); + const currentTime = new Date().getTime(); + const thirtyDays = 30 * 24 * 60 * 60 * 1000; + if (expiryTime - currentTime > thirtyDays) { + throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" }); + } + + // Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext) + if (encryptedValue.length > 13000) { + throw new BadRequestError({ message: "Shared secret value too long" }); + } + const newSharedSecret = await secretSharingDAL.create({ encryptedValue, iv, @@ -44,6 +67,36 @@ export const secretSharingServiceFactory = ({ return { id: newSharedSecret.id }; }; + const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => { + const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput; + if (new Date(expiresAt) < new Date()) { + throw new BadRequestError({ message: "Expiration date cannot be in the past" }); + } + + // Limit Expiry Time to 1 month + const expiryTime = new Date(expiresAt).getTime(); + const currentTime = new Date().getTime(); + const thirtyDays = 30 * 24 * 60 * 60 * 1000; + if (expiryTime - currentTime > thirtyDays) { + throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" }); + } + + // Limit Input ciphertext length to 13000 (equivalent to 10,000 characters of Plaintext) + if (encryptedValue.length > 13000) { + throw new BadRequestError({ message: "Shared secret value too long" }); + } + + const newSharedSecret = await secretSharingDAL.create({ + encryptedValue, + iv, + tag, + hashedHex, + expiresAt, + expiresAfterViews + }); + return { id: newSharedSecret.id }; + }; + const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => { const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput; const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); @@ -54,6 +107,7 @@ export const secretSharingServiceFactory = ({ const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => { const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex }); + if (!sharedSecret) return; if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) { return; } @@ -77,6 +131,7 @@ export const secretSharingServiceFactory = ({ return { createSharedSecret, + createPublicSharedSecret, getSharedSecrets, deleteSharedSecretById, getActiveSharedSecretByIdAndHashedHex diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts index 5f35b2848..769bb4479 100644 --- a/backend/src/services/secret-sharing/secret-sharing-types.ts +++ b/backend/src/services/secret-sharing/secret-sharing-types.ts @@ -8,14 +8,16 @@ export type TSharedSecretPermission = { orgId: string; }; -export type TCreateSharedSecretDTO = { +export type TCreatePublicSharedSecretDTO = { encryptedValue: string; iv: string; tag: string; hashedHex: string; expiresAt: Date; expiresAfterViews: number; -} & TSharedSecretPermission; +}; + +export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO; export type TDeleteSharedSecretDTO = { sharedSecretId: string; diff --git a/docs/images/platform/secret-sharing/public-view.png b/docs/images/platform/secret-sharing/public-view.png index 8b4077c65..9673fcd37 100644 Binary files a/docs/images/platform/secret-sharing/public-view.png and b/docs/images/platform/secret-sharing/public-view.png differ diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 4d13b4602..880d2f021 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -24,7 +24,8 @@ export const publicPaths = [ "/login/provider/error", // TODO: change "/login/sso", "/admin/signup", - "/shared/secret/[id]" + "/shared/secret/[id]", + "/share-secret" ]; export const languageMap = { diff --git a/frontend/src/hooks/api/secretSharing/mutations.ts b/frontend/src/hooks/api/secretSharing/mutations.ts index e21cc08f6..e0c1dcc3c 100644 --- a/frontend/src/hooks/api/secretSharing/mutations.ts +++ b/frontend/src/hooks/api/secretSharing/mutations.ts @@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => { }); }; +export const useCreatePublicSharedSecret = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (inputData: TCreateSharedSecretRequest) => { + const { data } = await apiRequest.post( + "/api/v1/secret-sharing/public", + inputData + ); + return data; + }, + onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"]) + }); +}; + export const useDeleteSharedSecret = () => { const queryClient = useQueryClient(); - return useMutation< - TSharedSecret, - { message: string }, - { sharedSecretId: string } - >({ + return useMutation({ mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => { const { data } = await apiRequest.delete( `/api/v1/secret-sharing/${sharedSecretId}` diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts index c7970fabc..886b0a82e 100644 --- a/frontend/src/hooks/api/secretSharing/queries.ts +++ b/frontend/src/hooks/api/secretSharing/queries.ts @@ -17,6 +17,7 @@ export const useGetSharedSecrets = () => { export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => { return useQuery({ queryFn: async () => { + if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" }); const { data } = await apiRequest.get( `/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}` ); diff --git a/frontend/src/pages/share-secret/index.tsx b/frontend/src/pages/share-secret/index.tsx new file mode 100644 index 000000000..53b034650 --- /dev/null +++ b/frontend/src/pages/share-secret/index.tsx @@ -0,0 +1,24 @@ +import Head from "next/head"; + +import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage"; + +const ShareNewPublicSecretPage = () => { + return ( + <> + + Securely Share Secrets | Infisical + + + + + +
+ +
+ + ); +}; + +export default ShareNewPublicSecretPage; + +ShareNewPublicSecretPage.requireAuth = false; diff --git a/frontend/src/pages/shared/secret/[id]/index.tsx b/frontend/src/pages/shared/secret/[id]/index.tsx index 7f53d962d..bda56347b 100644 --- a/frontend/src/pages/shared/secret/[id]/index.tsx +++ b/frontend/src/pages/shared/secret/[id]/index.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage"; -const SecretApproval = () => { +const SecretSharedPublicPage = () => { return ( <> @@ -12,13 +12,13 @@ const SecretApproval = () => { -
- +
+
); }; -export default SecretApproval; +export default SecretSharedPublicPage; -SecretApproval.requireAuth = false; +SecretSharedPublicPage.requireAuth = false; diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx new file mode 100644 index 000000000..f8201fb9e --- /dev/null +++ b/frontend/src/views/ShareSecretPage/components/AddShareSecretForm.tsx @@ -0,0 +1,229 @@ +import crypto from "crypto"; + +import { Controller } from "react-hook-form"; +import { AxiosError } from "axios"; +import * as yup from "yup"; + +import { createNotification } from "@app/components/notifications"; +import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto"; +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing"; + +const schema = yup.object({ + value: yup.string().max(10000).required().label("Shared Secret Value"), + expiresAfterViews: yup.number().min(1).required().label("Expires After Views"), + expiresInValue: yup.number().min(1).required().label("Expiration Value"), + expiresInUnit: yup.string().required().label("Expiration Unit") +}); + +export type FormData = yup.InferType; + +export const AddShareSecretForm = ({ + isPublic, + inModal, + handleSubmit, + control, + isSubmitting, + setNewSharedSecret +}: { + isPublic: boolean; + inModal: boolean; + handleSubmit: any; + control: any; + isSubmitting: boolean; + setNewSharedSecret: (value: string) => void; +}) => { + const publicSharedSecretCreator = useCreatePublicSharedSecret(); + const privateSharedSecretCreator = useCreateSharedSecret(); + const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator; + + const expirationUnitsAndActions = [ + { + unit: "Minutes", + action: (expiresAt: Date, expiresInValue: number) => + expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue) + }, + { + unit: "Hours", + action: (expiresAt: Date, expiresInValue: number) => + expiresAt.setHours(expiresAt.getHours() + expiresInValue) + }, + { + unit: "Days", + action: (expiresAt: Date, expiresInValue: number) => + expiresAt.setDate(expiresAt.getDate() + expiresInValue) + }, + { + unit: "Weeks", + action: (expiresAt: Date, expiresInValue: number) => + expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7) + } + ]; + const onFormSubmit = async ({ + value, + expiresInValue, + expiresInUnit, + expiresAfterViews + }: FormData) => { + try { + const key = crypto.randomBytes(16).toString("hex"); + const hashedHex = crypto.createHash("sha256").update(key).digest("hex"); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext: value, + key + }); + + const expiresAt = new Date(); + const updateExpiresAt = expirationUnitsAndActions.find( + (item) => item.unit === expiresInUnit + )?.action; + if (updateExpiresAt && expiresInValue) { + updateExpiresAt(expiresAt, expiresInValue); + } + + const { id } = await createSharedSecret.mutateAsync({ + encryptedValue: ciphertext, + iv, + tag, + hashedHex, + expiresAt, + expiresAfterViews + }); + setNewSharedSecret( + `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( + hashedHex + )}-${encodeURIComponent(key)}` + ); + + createNotification({ + text: "Successfully created a shared secret", + type: "success" + }); + } catch (err) { + console.error(err); + const axiosError = err as AxiosError; + if (axiosError?.response?.status === 401) { + createNotification({ + text: "You do not have access to create shared secrets", + type: "error" + }); + } else { + createNotification({ + text: "Failed to create a shared secret", + type: "error" + }); + } + } + }; + return ( +
+
+
+ ( + + + + )} + /> +
+
+
+ ( + + + + )} + /> +
+
+

OR

+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+
+
+ + {inModal && ( + + + + )} +
+
+
+ ); +}; diff --git a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx index 7ec9f95ad..d30432982 100644 --- a/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx +++ b/frontend/src/views/ShareSecretPage/components/AddShareSecretModal.tsx @@ -1,54 +1,14 @@ -import crypto from "crypto"; - import { useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; -import { AxiosError } from "axios"; import * as yup from "yup"; -import { createNotification } from "@app/components/notifications"; -import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto"; -import { - Button, - FormControl, - IconButton, - Input, - Modal, - ModalClose, - ModalContent, - SecretInput, - Select, - SelectItem -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; +import { Modal, ModalContent } from "@app/components/v2"; import { useTimedReset } from "@app/hooks"; -import { useCreateSharedSecret } from "@app/hooks/api/secretSharing"; import { UsePopUpState } from "@app/hooks/usePopUp"; -const expirationUnitsAndActions = [ - { - unit: "Minutes", - action: (expiresAt: Date, expiresInValue: number) => - expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue) - }, - { - unit: "Hours", - action: (expiresAt: Date, expiresInValue: number) => - expiresAt.setHours(expiresAt.getHours() + expiresInValue) - }, - { - unit: "Days", - action: (expiresAt: Date, expiresInValue: number) => - expiresAt.setDate(expiresAt.getDate() + expiresInValue) - }, - { - unit: "Weeks", - action: (expiresAt: Date, expiresInValue: number) => - expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7) - } -]; +import { AddShareSecretForm } from "./AddShareSecretForm"; +import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret"; const schema = yup.object({ value: yup.string().max(10000).required().label("Shared Secret Value"), @@ -65,9 +25,11 @@ type Props = { popUpName: keyof UsePopUpState<["createSharedSecret"]>, state?: boolean ) => void; + isPublic: boolean; + inModal: boolean; }; -export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { +export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => { const { control, reset, @@ -76,9 +38,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { } = useForm({ resolver: yupResolver(schema) }); - const createSharedSecret = useCreateSharedSecret(); - const { currentOrg } = useOrganization(); - const [newSharedSecret, setnewSharedSecret] = useState(""); + + const [newSharedSecret, setNewSharedSecret] = useState(""); const hasSharedSecret = Boolean(newSharedSecret); const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ initialState: false @@ -94,199 +55,54 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => { } }, [isUrlCopied]); - const onFormSubmit = async ({ - value, - expiresInValue, - expiresInUnit, - expiresAfterViews - }: FormData) => { - try { - if (!currentOrg?.id) return; - const key = crypto.randomBytes(16).toString("hex"); - const hashedHex = crypto.createHash("sha256").update(key).digest("hex"); - const { ciphertext, iv, tag } = encryptSymmetric({ - plaintext: value, - key - }); - - const expiresAt = new Date(); - const updateExpiresAt = expirationUnitsAndActions.find( - (item) => item.unit === expiresInUnit - )?.action; - if (updateExpiresAt && expiresInValue) { - updateExpiresAt(expiresAt, expiresInValue); - } - - const { id } = await createSharedSecret.mutateAsync({ - encryptedValue: ciphertext, - iv, - tag, - hashedHex, - expiresAt, - expiresAfterViews - }); - setnewSharedSecret( - `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( - hashedHex - )}-${encodeURIComponent(key)}` - ); - - createNotification({ - text: "Successfully created a shared secret", - type: "success" - }); - } catch (err) { - console.error(err); - const axiosError = err as AxiosError; - if (axiosError?.response?.status === 401) { - createNotification({ - text: "You do not have access to create shared secrets", - type: "error" - }); - } else { - createNotification({ - text: "Failed to create a shared secret", - type: "error" - }); - } - } - }; - - return ( + // eslint-disable-next-line no-nested-ternary + return inModal ? ( { handlePopUpToggle("createSharedSecret", open); reset(); - setnewSharedSecret(""); + setNewSharedSecret(""); }} > {!hasSharedSecret ? ( -
- ( - - - - )} - /> -
-
- ( - - - - )} - /> -
-
-

OR

-
-
-
-
- ( - - - - )} - /> -
-
- ( - - - - )} - /> -
-
-
-
-
- - - - -
- + ) : ( -
-

{newSharedSecret}

- - - - Click to Copy - - -
+ )}
+ ) : !hasSharedSecret ? ( + + ) : ( + ); }; diff --git a/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx b/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx index c71b9830f..a450d61f5 100644 --- a/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx +++ b/frontend/src/views/ShareSecretPage/components/ShareSecretSection.tsx @@ -22,7 +22,7 @@ export const ShareSecretSection = () => { const onDeleteApproved = async () => { try { deleteSharedSecret.mutateAsync({ - sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id, + sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id }); createNotification({ text: "Successfully deleted shared secret", @@ -40,7 +40,6 @@ export const ShareSecretSection = () => { }; return ( -
Secret Sharing @@ -60,14 +59,18 @@ export const ShareSecretSection = () => { Share Secret
- + - handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)} deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name} onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")} @@ -75,4 +78,4 @@ export const ShareSecretSection = () => { />
); -}; \ No newline at end of file +}; diff --git a/frontend/src/views/ShareSecretPage/components/ViewAndCopySharedSecret.tsx b/frontend/src/views/ShareSecretPage/components/ViewAndCopySharedSecret.tsx new file mode 100644 index 000000000..1efec0c36 --- /dev/null +++ b/frontend/src/views/ShareSecretPage/components/ViewAndCopySharedSecret.tsx @@ -0,0 +1,37 @@ +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { IconButton } from "@app/components/v2"; + +export const ViewAndCopySharedSecret = ({ + inModal, + newSharedSecret, + isUrlCopied, + copyUrlToClipboard +}: { + inModal: boolean; + newSharedSecret: string; + isUrlCopied: boolean; + copyUrlToClipboard: () => void; +}) => { + return ( +
+
+
+

{newSharedSecret}

+ + + + Click to Copy + + +
+
+
+ ); +}; diff --git a/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx b/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx index cd4d8b5c3..e21400817 100644 --- a/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx +++ b/frontend/src/views/ShareSecretPublicPage/ShareSecretPublicPage.tsx @@ -1,26 +1,27 @@ import { useEffect, useMemo } from "react"; import Head from "next/head"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/router"; +import { faArrowRight, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 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 { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal"; import { SecretTable } from "./components"; -export const ShareSecretPublicPage = () => { +export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean }) => { const router = useRouter(); const { id, key: urlEncodedPublicKey } = router.query; - const [hashedHex, key] = urlEncodedPublicKey!.toString().split("-"); + const [hashedHex, key] = urlEncodedPublicKey + ? urlEncodedPublicKey.toString().split("-") + : ["", ""]; const publicKey = decodeURIComponent(urlEncodedPublicKey as string); - useEffect(() => { - if (!id || !publicKey) { - router.push("/404"); - } - }, [id, publicKey]); - const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex( id as string, hashedHex as string @@ -53,35 +54,107 @@ export const ShareSecretPublicPage = () => { navigator.clipboard.writeText(decryptedSecret); setIsUrlCopied(true); }; + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const); return ( -
+
Secret Shared | Infisical - -
- Infisical logo -
-

- A secret has been shared with you securely via Infisical -

-
- {/* */} -
-

- Shared Secret -

-
+
+
+ + Infisical logo + +
+

+ {id ? "Someone shared a secret on Infisical with you." : "Share Secrets with Infisical"} +

+
+ {id && ( + )} +
+ + {isNewSession && ( + + )} + +
+
+
+ +
+ {!isNewSession && ( +
+ +
+ )} +
+

+ Safe, Secure, & Open Source +

+

+ Infisical is the #1 {" "} + + open source + {" "} + secrets management platform for developers.
+

+ Infisical Secret Sharing uses end-to-end encrypted architecture to ensure that your secrets are truly private, even from our servers. +

+ + + Learn More + +
+
+

+ © 2024{" "} + + Infisical + + . All rights reserved. +
+ 156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸 +

+
+
); diff --git a/frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx b/frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx deleted file mode 100644 index 49a7e17ed..000000000 --- a/frontend/src/views/ShareSecretPublicPage/components/MainImage.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import Image from "next/image"; - -export const DragonMainImage = () => { - return ( -
- Infisical Dragon - Came to send you a secret! -
- ); -}; diff --git a/frontend/src/views/ShareSecretPublicPage/components/SecretTable.tsx b/frontend/src/views/ShareSecretPublicPage/components/SecretTable.tsx index 14c5092e7..d2a17e566 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/SecretTable.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/SecretTable.tsx @@ -1,7 +1,7 @@ import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { EmptyState, IconButton, SecretInput, Td, Tr } from "@app/components/v2"; +import { EmptyState, IconButton, Td, Tr } from "@app/components/v2"; type Props = { isLoading: boolean; @@ -16,7 +16,7 @@ export const SecretTable = ({ isUrlCopied, copyUrlToClipboard }: Props) => ( -
+
{isLoading &&
Loading...
} {!isLoading && !decryptedSecret && ( @@ -26,19 +26,23 @@ export const SecretTable = ({ )} {!isLoading && decryptedSecret && ( - <> -
- +
+
+
+ {decryptedSecret} +
- + Copy - +
)}
); diff --git a/frontend/src/views/ShareSecretPublicPage/components/index.tsx b/frontend/src/views/ShareSecretPublicPage/components/index.tsx index 5a7b53a0d..530af7c2f 100644 --- a/frontend/src/views/ShareSecretPublicPage/components/index.tsx +++ b/frontend/src/views/ShareSecretPublicPage/components/index.tsx @@ -1,2 +1 @@ -export { DragonMainImage } from "./MainImage"; export { SecretTable } from "./SecretTable";