mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 02:26:26 +00:00
Merge pull request #1923 from Infisical/shubham/eng-984-make-secret-sharing-public-even-for-non-infisical-users
feat: allow sharing of secrets publicly + public page for secret sharing
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,8 +70,15 @@ export const creationLimit: RateLimitOptions = {
|
|||||||
|
|
||||||
// Public endpoints to avoid brute force attacks
|
// Public endpoints to avoid brute force attacks
|
||||||
export const publicEndpointLimit: RateLimitOptions = {
|
export const publicEndpointLimit: RateLimitOptions = {
|
||||||
// Shared Secrets
|
// Read Shared Secrets
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: () => getRateLimiterConfig().publicEndpointLimit,
|
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const publicSecretShareCreationLimit: RateLimitOptions = {
|
||||||
|
// Create Shared Secrets
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
max: 5,
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSharingSchema } from "@app/db/schemas";
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/",
|
url: "/public",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
@@ -82,9 +87,42 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z
|
expiresAt: z.string(),
|
||||||
.string()
|
expiresAfterViews: z.number()
|
||||||
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
}),
|
||||||
|
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()
|
expiresAfterViews: z.number()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { 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">;
|
||||||
@@ -31,6 +36,24 @@ export const secretSharingServiceFactory = ({
|
|||||||
} = createSharedSecretInput;
|
} = createSharedSecretInput;
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
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({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
iv,
|
iv,
|
||||||
@@ -44,6 +67,36 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: newSharedSecret.id };
|
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 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);
|
||||||
@@ -54,6 +107,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
|
|
||||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||||
|
if (!sharedSecret) return;
|
||||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,6 +131,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;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 467 KiB After Width: | Height: | Size: 1.2 MiB |
@@ -24,7 +24,8 @@ export const publicPaths = [
|
|||||||
"/login/provider/error", // TODO: change
|
"/login/provider/error", // TODO: change
|
||||||
"/login/sso",
|
"/login/sso",
|
||||||
"/admin/signup",
|
"/admin/signup",
|
||||||
"/shared/secret/[id]"
|
"/shared/secret/[id]",
|
||||||
|
"/share-secret"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const languageMap = {
|
export const languageMap = {
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const useGetSharedSecrets = () => {
|
|||||||
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" });
|
||||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||||
|
|
||||||
|
const ShareNewPublicSecretPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<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="" />
|
||||||
|
</Head>
|
||||||
|
<div className="dark h-full">
|
||||||
|
<ShareSecretPublicPage isNewSession />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareNewPublicSecretPage;
|
||||||
|
|
||||||
|
ShareNewPublicSecretPage.requireAuth = false;
|
||||||
@@ -2,7 +2,7 @@ import Head from "next/head";
|
|||||||
|
|
||||||
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||||
|
|
||||||
const SecretApproval = () => {
|
const SecretSharedPublicPage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -12,13 +12,13 @@ const SecretApproval = () => {
|
|||||||
<meta property="og:title" content="" />
|
<meta property="og:title" content="" />
|
||||||
<meta name="og:description" content="" />
|
<meta name="og:description" content="" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="h-full">
|
<div className="dark h-full">
|
||||||
<ShareSecretPublicPage />
|
<ShareSecretPublicPage isNewSession={false} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SecretApproval;
|
export default SecretSharedPublicPage;
|
||||||
|
|
||||||
SecretApproval.requireAuth = false;
|
SecretSharedPublicPage.requireAuth = false;
|
||||||
|
|||||||
@@ -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<typeof schema>;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<form className="flex w-full flex-col items-center" onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<div className={`${!inModal && "border border-mineshaft-600 bg-mineshaft-800 p-4"}`}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="value"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Shared Secret"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
isVisible
|
||||||
|
{...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-[70px]"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-row justify-center">
|
||||||
|
<div className="w-2/7 flex">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresAfterViews"
|
||||||
|
defaultValue={6}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-4 w-full"
|
||||||
|
label="Expires After Views"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText="Please enter a valid number of views"
|
||||||
|
>
|
||||||
|
<Input {...field} type="number" min={1} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/7 flex items-center justify-center px-2">
|
||||||
|
<p className="px-4 text-sm text-gray-400">OR</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-4/7 flex">
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div className="flex w-2/5 w-full justify-center">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresInValue"
|
||||||
|
defaultValue={6}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Expires after Time"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText="Please enter a valid time duration"
|
||||||
|
>
|
||||||
|
<Input {...field} type="number" min={0} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/5 w-full justify-center">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresInUnit"
|
||||||
|
defaultValue={expirationUnitsAndActions[0].unit}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Unit" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{expirationUnitsAndActions.map(({ unit }) => (
|
||||||
|
<SelectItem value={unit} key={unit}>
|
||||||
|
{unit}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center ${!inModal && "justify-left pt-1"}`}>
|
||||||
|
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
|
{inModal ? "Create" : "Share Secret"}
|
||||||
|
</Button>
|
||||||
|
{inModal && (
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,54 +1,14 @@
|
|||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
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 { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const expirationUnitsAndActions = [
|
import { AddShareSecretForm } from "./AddShareSecretForm";
|
||||||
{
|
import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret";
|
||||||
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 schema = yup.object({
|
const schema = yup.object({
|
||||||
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
||||||
@@ -65,9 +25,11 @@ type Props = {
|
|||||||
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
isPublic: boolean;
|
||||||
|
inModal: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
@@ -76,9 +38,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
const createSharedSecret = useCreateSharedSecret();
|
|
||||||
const { currentOrg } = useOrganization();
|
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>({
|
||||||
initialState: false
|
initialState: false
|
||||||
@@ -94,199 +55,54 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [isUrlCopied]);
|
}, [isUrlCopied]);
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
// eslint-disable-next-line no-nested-ternary
|
||||||
value,
|
return inModal ? (
|
||||||
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 (
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.createSharedSecret?.isOpen}
|
isOpen={popUp?.createSharedSecret?.isOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
handlePopUpToggle("createSharedSecret", open);
|
handlePopUpToggle("createSharedSecret", open);
|
||||||
reset();
|
reset();
|
||||||
setnewSharedSecret("");
|
setNewSharedSecret("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Share a Secret"
|
title="Share a Secret"
|
||||||
subTitle="This link is only accessible once. Please share this link with intended recipients. "
|
subTitle="Once you share a secret, the share link is only accessible once."
|
||||||
>
|
>
|
||||||
{!hasSharedSecret ? (
|
{!hasSharedSecret ? (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<AddShareSecretForm
|
||||||
<Controller
|
isPublic={isPublic}
|
||||||
control={control}
|
inModal={inModal}
|
||||||
name="value"
|
control={control}
|
||||||
defaultValue=""
|
handleSubmit={handleSubmit}
|
||||||
render={({ field, fieldState: { error } }) => (
|
isSubmitting={isSubmitting}
|
||||||
<FormControl
|
setNewSharedSecret={setNewSharedSecret}
|
||||||
label="Shared Secret"
|
/>
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<SecretInput
|
|
||||||
isVisible={false}
|
|
||||||
{...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]"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex w-full flex-row">
|
|
||||||
<div className="w-2/7 flex">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresAfterViews"
|
|
||||||
defaultValue={6}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
className="mb-4 w-full"
|
|
||||||
label="Expires After Views"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText="Please enter a valid number of views"
|
|
||||||
>
|
|
||||||
<Input {...field} type="number" min={1} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/7 flex items-center justify-center px-2">
|
|
||||||
<p className="px-4 text-sm text-gray-400">OR</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-4/7 flex">
|
|
||||||
<div className="flex w-full">
|
|
||||||
<div className="flex w-2/5 w-full justify-center">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresInValue"
|
|
||||||
defaultValue={6}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Expires after Time"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText="Please enter a valid time duration"
|
|
||||||
>
|
|
||||||
<Input {...field} type="number" min={0} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-3/5 w-full justify-center">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="expiresInUnit"
|
|
||||||
defaultValue={expirationUnitsAndActions[0].unit}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Unit"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{expirationUnitsAndActions.map(({ unit }) => (
|
|
||||||
<SelectItem value={unit} key={unit}>
|
|
||||||
{unit}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
<ViewAndCopySharedSecret
|
||||||
<p className="mr-4 break-all">{newSharedSecret}</p>
|
inModal={inModal}
|
||||||
<IconButton
|
newSharedSecret={newSharedSecret}
|
||||||
ariaLabel="copy icon"
|
isUrlCopied={isUrlCopied}
|
||||||
colorSchema="secondary"
|
copyUrlToClipboard={copyUrlToClipboard}
|
||||||
className="group relative"
|
/>
|
||||||
onClick={copyUrlToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
Click to Copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
) : !hasSharedSecret ? (
|
||||||
|
<AddShareSecretForm
|
||||||
|
isPublic={isPublic}
|
||||||
|
inModal={inModal}
|
||||||
|
control={control}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
setNewSharedSecret={setNewSharedSecret}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ViewAndCopySharedSecret
|
||||||
|
inModal={inModal}
|
||||||
|
newSharedSecret={newSharedSecret}
|
||||||
|
isUrlCopied={isUrlCopied}
|
||||||
|
copyUrlToClipboard={copyUrlToClipboard}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,18 @@ export const ShareSecretSection = () => {
|
|||||||
Share Secret
|
Share Secret
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ShareSecretsTable
|
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
<AddShareSecretModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
isPublic={false}
|
||||||
|
inModal
|
||||||
/>
|
/>
|
||||||
<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 +78,4 @@ export const ShareSecretSection = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className={`flex w-full justify-center ${!inModal ? "mx-auto max-w-[40rem]" : ""}`}>
|
||||||
|
<div className={`${!inModal ? "border border-mineshaft-600 bg-mineshaft-800 p-4" : ""}`}>
|
||||||
|
<div className="my-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{newSharedSecret}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyUrlToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Click to Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
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 { faArrowRight, 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 { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal";
|
||||||
import { SecretTable } from "./components";
|
import { SecretTable } from "./components";
|
||||||
|
|
||||||
export const ShareSecretPublicPage = () => {
|
export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id, key: urlEncodedPublicKey } = router.query;
|
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);
|
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||||
useEffect(() => {
|
|
||||||
if (!id || !publicKey) {
|
|
||||||
router.push("/404");
|
|
||||||
}
|
|
||||||
}, [id, publicKey]);
|
|
||||||
|
|
||||||
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
||||||
id as string,
|
id as string,
|
||||||
hashedHex as string
|
hashedHex as string
|
||||||
@@ -53,35 +54,107 @@ export const ShareSecretPublicPage = () => {
|
|||||||
navigator.clipboard.writeText(decryptedSecret);
|
navigator.clipboard.writeText(decryptedSecret);
|
||||||
setIsUrlCopied(true);
|
setIsUrlCopied(true);
|
||||||
};
|
};
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
|
<div className="h-screen bg-gradient-to-tr from-mineshaft-700 to-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="h-screen w-full flex-col items-center justify-center dark:[color-scheme:dark]">
|
||||||
<div className="my-4 flex justify-center md:my-8">
|
<div className="mb-4 flex justify-center pt-8 md:pt-16">
|
||||||
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
|
<Link href="https://infisical.com">
|
||||||
</div>
|
<Image
|
||||||
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
|
src="/images/gradientLogo.svg"
|
||||||
A secret has been shared with you securely via Infisical
|
height={90}
|
||||||
</p>
|
width={120}
|
||||||
<div className="flex min-h-screen w-full flex-col md:flex-row">
|
alt="Infisical logo"
|
||||||
{/* <DragonMainImage /> */}
|
className="cursor-pointer"
|
||||||
<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">
|
</Link>
|
||||||
Shared Secret
|
</div>
|
||||||
</p>
|
<h1 className="mt-6 mb-4 bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-2xl font-medium text-transparent">
|
||||||
<div className="mb-4 rounded-lg md:p-2">
|
{id ? "Someone shared a secret on Infisical with you." : "Share Secrets with Infisical"}
|
||||||
|
</h1>
|
||||||
|
<div className="m-auto mt-8 flex w-full max-w-xl justify-center px-4">
|
||||||
|
{id && (
|
||||||
<SecretTable
|
<SecretTable
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
decryptedSecret={decryptedSecret}
|
decryptedSecret={decryptedSecret}
|
||||||
isUrlCopied={isUrlCopied}
|
isUrlCopied={isUrlCopied}
|
||||||
copyUrlToClipboard={copyUrlToClipboard}
|
copyUrlToClipboard={copyUrlToClipboard}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isNewSession && (
|
||||||
|
<AddShareSecretModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
inModal={false}
|
||||||
|
isPublic
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-8 px-4 sm:my-8">
|
||||||
|
<div className="w-full border-t border-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="m-auto max-w-xl px-4">
|
||||||
|
{!isNewSession && (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center px-4 pb-4">
|
||||||
|
<Button
|
||||||
|
className="bg-mineshaft-700 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("createSharedSecret");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Share your own Secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="m-auto mb-8 flex flex max-w-xl flex-col justify-center gap-2 rounded-md border border-primary-500/30 bg-primary/5 p-6">
|
||||||
|
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||||
|
Safe, Secure, & Open Source
|
||||||
|
</p>
|
||||||
|
<p className="md:text-md text-sm">
|
||||||
|
Infisical is the #1 {" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline"
|
||||||
|
>
|
||||||
|
open source
|
||||||
|
</a>{" "}
|
||||||
|
secrets management platform for developers. <br className="hidden md:inline" />
|
||||||
|
<div className="pb-2" />
|
||||||
|
Infisical Secret Sharing uses end-to-end encrypted architecture to ensure that your secrets are truly private, even from our servers.
|
||||||
|
</p>
|
||||||
|
<Link href="https://infisical.com">
|
||||||
|
<span className="mt-4 cursor-pointer duration-200 hover:text-primary">
|
||||||
|
Learn More <FontAwesomeIcon icon={faArrowRight} />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bottom-0 flex w-full items-center justify-center bg-mineshaft-600 p-2 sm:absolute">
|
||||||
|
<p className="text-center text-sm text-mineshaft-300">
|
||||||
|
© 2024{" "}
|
||||||
|
<a className="text-primary" href="https://infisical.com">
|
||||||
|
Infisical
|
||||||
|
</a>
|
||||||
|
. All rights reserved.
|
||||||
|
<br />
|
||||||
|
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic inModal />
|
||||||
</div>
|
</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,7 +1,7 @@
|
|||||||
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 = {
|
type Props = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -16,7 +16,7 @@ export const SecretTable = ({
|
|||||||
isUrlCopied,
|
isUrlCopied,
|
||||||
copyUrlToClipboard
|
copyUrlToClipboard
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div className="flex items-center rounded border border-solid border-mineshaft-700 bg-mineshaft-800 p-4">
|
<div className="flex w-full items-center justify-center rounded border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||||
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
||||||
{!isLoading && !decryptedSecret && (
|
{!isLoading && !decryptedSecret && (
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -26,19 +26,23 @@ export const SecretTable = ({
|
|||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
{!isLoading && decryptedSecret && (
|
{!isLoading && decryptedSecret && (
|
||||||
<>
|
<div className="dark relative flex h-full w-full items-center overflow-y-auto border border-mineshaft-700 bg-mineshaft-900 p-2">
|
||||||
<div className="min-w-[12rem] max-w-[20rem] flex-1 break-words pr-4">
|
<div className="thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll break-words pr-4 dark:[color-scheme:dark]">
|
||||||
<SecretInput isVisible value={decryptedSecret} readOnly />
|
<div className="align-center flex w-full min-w-full whitespace-pre-line">
|
||||||
|
{decryptedSecret}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="primary"
|
||||||
ariaLabel="copy to clipboard"
|
ariaLabel="copy to clipboard"
|
||||||
onClick={copyUrlToClipboard}
|
onClick={copyUrlToClipboard}
|
||||||
className="rounded p-2 hover:bg-gray-700"
|
className="mx-1 flex max-h-8 items-center rounded"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { DragonMainImage } from "./MainImage";
|
|
||||||
export { SecretTable } from "./SecretTable";
|
export { SecretTable } from "./SecretTable";
|
||||||
|
|||||||
Reference in New Issue
Block a user