mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 21:28:18 +00:00
feat: use hash as pw & move to symmetric encrpytion
This commit is contained in:
@@ -8,7 +8,10 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await knex.schema.createTable(TableName.SecretSharing, (t) => {
|
await knex.schema.createTable(TableName.SecretSharing, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.string("name").notNullable();
|
t.string("name").notNullable();
|
||||||
t.text("signedValue").notNullable();
|
t.text("encryptedValue").notNullable();
|
||||||
|
t.text("iv").notNullable();
|
||||||
|
t.text("tag").notNullable();
|
||||||
|
t.text("hashedHex").notNullable();
|
||||||
t.timestamp("expiresAt").notNullable();
|
t.timestamp("expiresAt").notNullable();
|
||||||
t.uuid("userId").notNullable();
|
t.uuid("userId").notNullable();
|
||||||
t.uuid("orgId").notNullable();
|
t.uuid("orgId").notNullable();
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
signedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
|
|||||||
@@ -41,16 +41,24 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
hashedHex: z.string()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SecretSharingSchema.pick({ name: true, signedValue: true, expiresAt: true })
|
200: SecretSharingSchema.pick({ name: true, encryptedValue: true, iv: true, tag: true, expiresAt: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById(req.params.id);
|
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
|
||||||
|
req.params.id,
|
||||||
|
req.query.hashedHex
|
||||||
|
);
|
||||||
if (!sharedSecret) return undefined;
|
if (!sharedSecret) return undefined;
|
||||||
return {
|
return {
|
||||||
name: sharedSecret.name,
|
name: sharedSecret.name,
|
||||||
signedValue: sharedSecret.signedValue,
|
encryptedValue: sharedSecret.encryptedValue,
|
||||||
|
iv: sharedSecret.iv,
|
||||||
|
tag: sharedSecret.tag,
|
||||||
expiresAt: sharedSecret.expiresAt
|
expiresAt: sharedSecret.expiresAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -65,7 +73,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
signedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
|
expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
|
||||||
message: "Expires at should be a future date"
|
message: "Expires at should be a future date"
|
||||||
})
|
})
|
||||||
@@ -78,7 +89,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { name, signedValue, expiresAt } = req.body;
|
const { name, encryptedValue, iv, tag, hashedHex, expiresAt } = req.body;
|
||||||
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -86,7 +97,10 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
name,
|
name,
|
||||||
signedValue,
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
expiresAt: new Date(expiresAt)
|
expiresAt: new Date(expiresAt)
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ export const secretSharingServiceFactory = ({
|
|||||||
secretSharingDAL
|
secretSharingDAL
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, signedValue, expiresAt } =
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } =
|
||||||
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" });
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
name,
|
name,
|
||||||
signedValue,
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
orgId
|
orgId
|
||||||
@@ -38,8 +41,8 @@ export const secretSharingServiceFactory = ({
|
|||||||
return userSharedSecrets;
|
return userSharedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveSharedSecretById = async (sharedSecretId: string) => {
|
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||||
const sharedSecret = await secretSharingDAL.findById(sharedSecretId);
|
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||||
if (sharedSecret && sharedSecret.expiresAt < new Date()) {
|
if (sharedSecret && sharedSecret.expiresAt < new Date()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,6 +61,6 @@ export const secretSharingServiceFactory = ({
|
|||||||
createSharedSecret,
|
createSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getActiveSharedSecretById
|
getActiveSharedSecretByIdAndHashedHex
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ export type TSharedSecretPermission = {
|
|||||||
|
|
||||||
export type TCreateSharedSecretDTO = {
|
export type TCreateSharedSecretDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
signedValue: string;
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
} & TSharedSecretPermission;
|
} & TSharedSecretPermission;
|
||||||
|
|
||||||
|
|||||||
@@ -224,76 +224,6 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
|
|||||||
return plaintext;
|
return plaintext;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Return new base64, NaCl, public-secret key pair for signing.
|
|
||||||
* @returns {Object} obj
|
|
||||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
|
||||||
* @returns {String} obj.secretKey - base64, NaCl, secret key
|
|
||||||
*/
|
|
||||||
const generateSignKeyPair = (): {
|
|
||||||
publicKey: string;
|
|
||||||
secretKey: string;
|
|
||||||
} => {
|
|
||||||
const pair = nacl.sign.keyPair();
|
|
||||||
|
|
||||||
return {
|
|
||||||
publicKey: nacl.util.encodeBase64(pair.publicKey),
|
|
||||||
secretKey: nacl.util.encodeBase64(pair.secretKey)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type SignAsymmetricProps = {
|
|
||||||
message: string;
|
|
||||||
privateKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns asymmetrically signed [message] using [privateKey]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.message - message to sign
|
|
||||||
* @param {String} obj.privateKey - base64-encoded private key
|
|
||||||
* @returns {String} signedMessage - base64-encoded signed message
|
|
||||||
*/
|
|
||||||
const signAssymmetric = ({ message, privateKey }: SignAsymmetricProps): string => {
|
|
||||||
let signedMessage;
|
|
||||||
try {
|
|
||||||
signedMessage = nacl.sign(nacl.util.decodeUTF8(message), nacl.util.decodeBase64(privateKey));
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Failed to sign message", err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return nacl.util.encodeBase64(signedMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
type OpenSignedAsymmetricProps = {
|
|
||||||
signedMessage: string;
|
|
||||||
publicKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns asymmetrically decrypted [message] using [publicKey]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.signedMessage - signed message to decrypt
|
|
||||||
* @param {String} obj.publicKey - base64-encoded public key
|
|
||||||
* @returns {String} signedMessage - base64-encoded decrypted message
|
|
||||||
*/
|
|
||||||
const openSignedAssymmetric = ({ signedMessage, publicKey }: OpenSignedAsymmetricProps): string => {
|
|
||||||
let originalMessage;
|
|
||||||
try {
|
|
||||||
originalMessage = nacl.sign.open(
|
|
||||||
nacl.util.decodeBase64(signedMessage),
|
|
||||||
nacl.util.decodeBase64(publicKey)
|
|
||||||
);
|
|
||||||
if (!originalMessage) {
|
|
||||||
throw new Error("Signature verification failed");
|
|
||||||
}
|
|
||||||
originalMessage = nacl.util.encodeUTF8(originalMessage);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Failed to verify signature", err);
|
|
||||||
}
|
|
||||||
return originalMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
@@ -301,8 +231,5 @@ export {
|
|||||||
encryptAssymmetric,
|
encryptAssymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
generateKeyPair,
|
generateKeyPair,
|
||||||
generateSignKeyPair,
|
|
||||||
openSignedAssymmetric,
|
|
||||||
signAssymmetric,
|
|
||||||
verifyPrivateKey
|
verifyPrivateKey
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ export const useGetSharedSecrets = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetActiveSharedSecretById = (id: string) => {
|
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${id}`
|
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
signedValue: data.signedValue,
|
encryptedValue: data.encryptedValue,
|
||||||
|
iv: data.iv,
|
||||||
|
tag: data.tag,
|
||||||
expiresAt: data.expiresAt
|
expiresAt: data.expiresAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
export type TSharedSecret = {
|
export type TSharedSecret = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
signedValue: string;
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
hashedHex: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@@ -10,13 +13,18 @@ export type TSharedSecret = {
|
|||||||
|
|
||||||
export type TCreateSharedSecretRequest = {
|
export type TCreateSharedSecretRequest = {
|
||||||
name: string;
|
name: string;
|
||||||
signedValue: string;
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TViewSharedSecretResponse = {
|
export type TViewSharedSecretResponse = {
|
||||||
name: string;
|
name: string;
|
||||||
signedValue: string;
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -8,8 +10,7 @@ import * as yup from "yup";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
generateSignKeyPair,
|
encryptSymmetric,
|
||||||
signAssymmetric
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -91,7 +92,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { currentOrg } = useOrganization();
|
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,12 +110,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
try {
|
try {
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
const signingKeyPair = generateSignKeyPair();
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
const signedMessage = signAssymmetric({
|
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
message: value,
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
privateKey: signingKeyPair.secretKey
|
plaintext: value,
|
||||||
|
key
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
const expiresAt = new Date();
|
||||||
const updateExpiresAt = expirationUnitsAndActions.find(
|
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||||
(item) => item.unit === expiresInUnit
|
(item) => item.unit === expiresInUnit
|
||||||
@@ -125,13 +128,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
|
|
||||||
const { id } = await createSharedSecret.mutateAsync({
|
const { id } = await createSharedSecret.mutateAsync({
|
||||||
name,
|
name,
|
||||||
signedValue: signedMessage,
|
encryptedValue: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
});
|
});
|
||||||
setnewSharedSecret(
|
setnewSharedSecret(
|
||||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}`
|
||||||
signingKeyPair.publicKey
|
|
||||||
)}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -195,10 +199,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<SecretInput
|
<SecretInput
|
||||||
isVisible
|
isVisible
|
||||||
{...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"
|
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"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { openSignedAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
|
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
import { DragonMainImage, SecretTable } from "./components";
|
import { DragonMainImage, SecretTable } from "./components";
|
||||||
|
|
||||||
export const ShareSecretPublicPage = () => {
|
export const ShareSecretPublicPage = () => {
|
||||||
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 publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -21,12 +22,14 @@ export const ShareSecretPublicPage = () => {
|
|||||||
}
|
}
|
||||||
}, [id, publicKey]);
|
}, [id, publicKey]);
|
||||||
|
|
||||||
const { isLoading, data } = useGetActiveSharedSecretById(id as string);
|
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(id as string, hashedHex as string );
|
||||||
const decryptedSecret = useMemo(() => {
|
const decryptedSecret = useMemo(() => {
|
||||||
if (data && data.signedValue && publicKey) {
|
if (data && data.encryptedValue && publicKey) {
|
||||||
const res = openSignedAssymmetric({
|
const res = decryptSymmetric({
|
||||||
signedMessage: data.signedValue,
|
ciphertext: data.encryptedValue,
|
||||||
publicKey: publicKey as string
|
iv: data.iv,
|
||||||
|
tag: data.tag,
|
||||||
|
key,
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -34,7 +37,7 @@ export const ShareSecretPublicPage = () => {
|
|||||||
}, [data, publicKey]);
|
}, [data, publicKey]);
|
||||||
|
|
||||||
const [timeLeft, setTimeLeft] = useState("");
|
const [timeLeft, setTimeLeft] = useState("");
|
||||||
const [isUrlCopied,, setIsUrlCopied] = useTimedReset<boolean>({
|
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||||
initialState: false,
|
initialState: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,11 +47,11 @@ export const ShareSecretPublicPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateTimer = () => {
|
const updateTimer = () => {
|
||||||
if (data && data.expiresAt) {
|
if (data && data.expiresAt) {
|
||||||
const expirationTime = new Date(data.expiresAt).getTime();
|
const expirationTime = new Date(data.expiresAt).getTime();
|
||||||
const currentTime = new Date().getTime();
|
const currentTime = new Date().getTime();
|
||||||
const timeDifference = expirationTime - currentTime;
|
const timeDifference = expirationTime - currentTime;
|
||||||
|
|
||||||
if (timeDifference < 0) {
|
if (timeDifference < 0) {
|
||||||
setTimeLeft("Expired");
|
setTimeLeft("Expired");
|
||||||
} else {
|
} else {
|
||||||
@@ -59,7 +62,7 @@ export const ShareSecretPublicPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timer = setInterval(updateTimer, 1000);
|
const timer = setInterval(updateTimer, 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [data?.expiresAt]);
|
}, [data?.expiresAt]);
|
||||||
|
|||||||
Reference in New Issue
Block a user