mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: secret sharing supports expiry on view count + multi-line secret value
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews");
|
||||
const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
if (!hasExpiresAfterViewsColumn) {
|
||||
t.integer("expiresAfterViews").nullable();
|
||||
t.timestamp("expiresAt").nullable().alter();
|
||||
}
|
||||
|
||||
if (hasSecretNameColumn) {
|
||||
t.dropColumn("name");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasExpiresAfterViewsColumn = await knex.schema.hasColumn(TableName.SecretSharing, "expiresAfterViews");
|
||||
const hasSecretNameColumn = await knex.schema.hasColumn(TableName.SecretSharing, "name");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
if (hasExpiresAfterViewsColumn) {
|
||||
t.dropColumn("expiresAfterViews");
|
||||
t.timestamp("expiresAt").notNullable().alter();
|
||||
}
|
||||
|
||||
if (!hasSecretNameColumn) {
|
||||
t.string("name").notNullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -9,16 +9,16 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.date(),
|
||||
expiresAt: z.date().nullable().optional(),
|
||||
userId: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
expiresAfterViews: z.number().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
||||
@@ -23,8 +23,8 @@ export const UsersSchema = z.object({
|
||||
isGhost: z.boolean().default(false),
|
||||
username: z.string(),
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||
consecutiveFailedMfaAttempts: z.number().optional(),
|
||||
isLocked: z.boolean().optional(),
|
||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||
isLocked: z.boolean().default(false).nullable().optional(),
|
||||
temporaryLockDateEnd: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
hashedHex: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: SecretSharingSchema.pick({ name: true, encryptedValue: true, iv: true, tag: true, expiresAt: true })
|
||||
200: SecretSharingSchema.pick({
|
||||
encryptedValue: true,
|
||||
iv: true,
|
||||
tag: true,
|
||||
expiresAt: true,
|
||||
expiresAfterViews: true
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
@@ -55,11 +61,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
);
|
||||
if (!sharedSecret) return undefined;
|
||||
return {
|
||||
name: sharedSecret.name,
|
||||
encryptedValue: sharedSecret.encryptedValue,
|
||||
iv: sharedSecret.iv,
|
||||
tag: sharedSecret.tag,
|
||||
expiresAt: sharedSecret.expiresAt
|
||||
expiresAt: sharedSecret.expiresAt,
|
||||
expiresAfterViews: sharedSecret.expiresAfterViews
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -72,14 +78,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string(),
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
|
||||
message: "Expires at should be a future date"
|
||||
})
|
||||
expiresAt: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
||||
expiresAfterViews: z.number().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -89,19 +96,19 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { name, encryptedValue, iv, tag, hashedHex, expiresAt } = req.body;
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name,
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt: new Date(expiresAt)
|
||||
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
|
||||
expiresAfterViews
|
||||
});
|
||||
return { id: sharedSecret.id };
|
||||
}
|
||||
|
||||
@@ -16,17 +16,28 @@ export const secretSharingServiceFactory = ({
|
||||
secretSharingDAL
|
||||
}: TSecretSharingServiceFactoryDep) => {
|
||||
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } =
|
||||
createSharedSecretInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
name,
|
||||
const {
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
} = createSharedSecretInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews,
|
||||
userId: actorId,
|
||||
orgId
|
||||
});
|
||||
@@ -43,9 +54,18 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||
if (sharedSecret && sharedSecret.expiresAt < new Date()) {
|
||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||
return;
|
||||
}
|
||||
if (sharedSecret.expiresAfterViews != null && sharedSecret.expiresAfterViews >= 0) {
|
||||
if (sharedSecret.expiresAfterViews === 0) {
|
||||
await secretSharingDAL.deleteById(sharedSecretId);
|
||||
return;
|
||||
}
|
||||
await secretSharingDAL.updateById(sharedSecretId, {
|
||||
expiresAfterViews: sharedSecret.expiresAfterViews - 1
|
||||
});
|
||||
}
|
||||
return sharedSecret;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ export type TSharedSecretPermission = {
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = {
|
||||
name: string;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
hashedHex: string;
|
||||
expiresAt: Date;
|
||||
expiresAt?: Date;
|
||||
expiresAfterViews?: number;
|
||||
} & TSharedSecretPermission;
|
||||
|
||||
export type TDeleteSharedSecretDTO = {
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
export type TSharedSecret = {
|
||||
id: string;
|
||||
name: string;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
hashedHex: string;
|
||||
userId: string;
|
||||
expiresAt: Date;
|
||||
orgId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
} & TCreateSharedSecretRequest;
|
||||
|
||||
export type TCreateSharedSecretRequest = {
|
||||
name: string;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
hashedHex: string;
|
||||
expiresAt: Date;
|
||||
expiresAt?: Date;
|
||||
expiresAfterViews?: number;
|
||||
};
|
||||
|
||||
export type TViewSharedSecretResponse = {
|
||||
name: string;
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
expiresAt: Date;
|
||||
expiresAt?: Date;
|
||||
expiresAfterViews?: number;
|
||||
};
|
||||
|
||||
export type TDeleteSharedSecretRequest = {
|
||||
|
||||
@@ -9,9 +9,7 @@ import { AxiosError } from "axios";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
encryptSymmetric,
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
@@ -22,7 +20,8 @@ import {
|
||||
ModalContent,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
@@ -63,10 +62,10 @@ const expirationUnitsAndActions = [
|
||||
];
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().max(100).required().label("Shared Secret Name"),
|
||||
value: yup.string().max(1000).required().label("Shared Secret Value"),
|
||||
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
||||
expiresInUnit: yup.string().required().label("Expiration Unit")
|
||||
expiresAfterViews: yup.number().min(1).optional().label("Expires After Views"),
|
||||
expiresInValue: yup.number().min(1).optional().label("Expiration Value"),
|
||||
expiresInUnit: yup.string().optional().label("Expiration Unit")
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
@@ -91,9 +90,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const createSharedSecret = useCreateSharedSecret();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||
const [expiryOption, setExpiryOption] = useState<"time" | "views">("time");
|
||||
const hasSharedSecret = Boolean(newSharedSecret);
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
initialState: false,
|
||||
initialState: false
|
||||
});
|
||||
|
||||
const copyUrlToClipboard = () => {
|
||||
@@ -106,10 +106,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}, [isUrlCopied]);
|
||||
|
||||
const onFormSubmit = async ({ name, value, expiresInValue, expiresInUnit }: FormData) => {
|
||||
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({
|
||||
@@ -117,25 +121,26 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
key
|
||||
});
|
||||
|
||||
|
||||
const expiresAt = new Date();
|
||||
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||
(item) => item.unit === expiresInUnit
|
||||
)?.action;
|
||||
if (updateExpiresAt) {
|
||||
if (updateExpiresAt && expiresInValue) {
|
||||
updateExpiresAt(expiresAt, expiresInValue);
|
||||
}
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
name,
|
||||
encryptedValue: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAt: expiryOption === "time" ? expiresAt : undefined,
|
||||
expiresAfterViews: expiryOption === "views" ? expiresAfterViews : undefined
|
||||
});
|
||||
setnewSharedSecret(
|
||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}`
|
||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||
hashedHex
|
||||
)}-${encodeURIComponent(key)}`
|
||||
);
|
||||
|
||||
createNotification({
|
||||
@@ -168,90 +173,114 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
setnewSharedSecret("");
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Share a Secret"
|
||||
subTitle="This link is only accessible once. Please share this link with intended recipients. "
|
||||
>
|
||||
<ModalContent
|
||||
title="Share a Secret"
|
||||
subTitle="This link is only accessible once. Please share this link with intended recipients. "
|
||||
>
|
||||
{!hasSharedSecret ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Shared Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Type your secret identifier" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Shared Secret Value"
|
||||
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"
|
||||
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 justify-end">
|
||||
<div className="w-3/5">
|
||||
<div>
|
||||
<p className="mb-2 flex items-center text-sm font-normal text-mineshaft-400">
|
||||
Set Expiry Based On
|
||||
</p>
|
||||
<div className="mb-4 flex w-full flex-row justify-start">
|
||||
<p className="mb-0.5 mr-1 flex items-center text-sm font-normal text-mineshaft-400">
|
||||
Time
|
||||
</p>
|
||||
<Switch
|
||||
id="expiryOption"
|
||||
onCheckedChange={(value) => setExpiryOption(value ? "views" : "time")}
|
||||
isChecked={expiryOption === "views"}
|
||||
/>
|
||||
<p className="mb-0.5 ml-3 flex items-center text-sm font-normal text-mineshaft-400">
|
||||
Views
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{expiryOption === "views" ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInValue"
|
||||
name="expiresAfterViews"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expiration Value"
|
||||
className="mb-4 w-full"
|
||||
label="Expires After Views"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" min={0} />
|
||||
<Input {...field} type="number" min={1} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2/5 pl-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInUnit"
|
||||
defaultValue={expirationUnitsAndActions[0].unit}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expiration 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 className="flex w-full flex-row justify-end">
|
||||
<div className="w-3/5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInValue"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expiration Value"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" min={0} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2/5 pl-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInUnit"
|
||||
defaultValue={expirationUnitsAndActions[0].unit}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expiration 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 className="mt-8 flex items-center">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
|
||||
@@ -8,7 +8,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const formatDate = (date: Date): string => (date ? new Date(date).toUTCString() : "");
|
||||
|
||||
const isExpired = (expiresAt: Date): boolean => new Date(expiresAt) < new Date();
|
||||
const isExpired = (expiresAt: Date | number | undefined): boolean => {
|
||||
if (typeof expiresAt === "number") {
|
||||
return expiresAt <= 0;
|
||||
}
|
||||
if (expiresAt instanceof Date) {
|
||||
return new Date(expiresAt) < new Date();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getValidityStatusText = (expiresAt: Date): string =>
|
||||
isExpired(expiresAt) ? "Expired " : "Valid for ";
|
||||
@@ -26,31 +34,38 @@ const timeAgo = (inputDate: Date, currentDate: Date): string => {
|
||||
const elapsedYears = Math.abs(Math.floor(elapsedDays / 365));
|
||||
|
||||
if (elapsedYears > 0) {
|
||||
return `${elapsedYears} year${elapsedYears === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
return `${elapsedYears} year${elapsedYears === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedMonths > 0) {
|
||||
return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedWeeks > 0) {
|
||||
return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedDays > 0) {
|
||||
return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedHours > 0) {
|
||||
return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
if (elapsedMinutes > 0) {
|
||||
return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
}
|
||||
return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${
|
||||
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||
}`;
|
||||
};
|
||||
|
||||
export const ShareSecretsRow = ({
|
||||
@@ -82,29 +97,41 @@ export const ShareSecretsRow = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpired(row.expiresAt)) {
|
||||
if (isExpired(row.expiresAt || row.expiresAfterViews)) {
|
||||
onSecretExpiration(row.id);
|
||||
}
|
||||
}, [isExpired(row.expiresAt)]);
|
||||
}, [isExpired(row.expiresAt || row.expiresAfterViews)]);
|
||||
|
||||
return (
|
||||
<Tr key={row.id}>
|
||||
<Td>{row.name}</Td>
|
||||
<Td>{`${row.encryptedValue.substring(0, 5)}...`}</Td>
|
||||
<Td>
|
||||
<p className="text-sm text-yellow-400">{timeAgo(row.createdAt, currentTime)}</p>
|
||||
<p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p>
|
||||
</Td>
|
||||
<Td>
|
||||
<p className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}>
|
||||
{getValidityStatusText(row.expiresAt) + timeAgo(row.expiresAt, currentTime)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{formatDate(row.expiresAt)}</p>
|
||||
{row.expiresAfterViews ? (
|
||||
<p
|
||||
className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}
|
||||
>
|
||||
Valid for {row.expiresAfterViews} more views
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p
|
||||
className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}
|
||||
>
|
||||
{getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{formatDate(row.expiresAt!)}</p>
|
||||
</>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteSharedSecretConfirmation", {
|
||||
name: row.name,
|
||||
name: "delete",
|
||||
id: row.id
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,9 +32,17 @@ type Props = {
|
||||
export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { isLoading, data = [] } = useGetSharedSecrets();
|
||||
|
||||
let tableData = data.filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) > new Date())
|
||||
let tableData = data.filter(
|
||||
(secret) =>
|
||||
(secret.expiresAt && new Date(secret.expiresAt) > new Date()) ||
|
||||
(secret.expiresAfterViews && secret.expiresAfterViews > 0)
|
||||
);
|
||||
const handleSecretExpiration = () => {
|
||||
tableData = data.filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) > new Date());
|
||||
tableData = data.filter(
|
||||
(secret) =>
|
||||
(secret.expiresAt && new Date(secret.expiresAt) > new Date()) ||
|
||||
(secret.expiresAfterViews && secret.expiresAfterViews > 0)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -42,7 +50,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Secret Name</Th> <Th>Created</Th> <Th>Valid Until</Th>
|
||||
<Th>Encrypted Secret</Th> <Th>Created</Th> <Th>Valid Until</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
</THead>
|
||||
|
||||
Reference in New Issue
Block a user