mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 17:27:40 +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({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string(),
|
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z.date(),
|
expiresAt: z.date().nullable().optional(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
expiresAfterViews: z.number().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export const UsersSchema = z.object({
|
|||||||
isGhost: z.boolean().default(false),
|
isGhost: z.boolean().default(false),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||||
consecutiveFailedMfaAttempts: z.number().optional(),
|
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||||
isLocked: z.boolean().optional(),
|
isLocked: z.boolean().default(false).nullable().optional(),
|
||||||
temporaryLockDateEnd: z.date().nullable().optional()
|
temporaryLockDateEnd: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,13 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
hashedHex: z.string()
|
hashedHex: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
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) => {
|
handler: async (req) => {
|
||||||
@@ -55,11 +61,11 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
);
|
);
|
||||||
if (!sharedSecret) return undefined;
|
if (!sharedSecret) return undefined;
|
||||||
return {
|
return {
|
||||||
name: sharedSecret.name,
|
|
||||||
encryptedValue: sharedSecret.encryptedValue,
|
encryptedValue: sharedSecret.encryptedValue,
|
||||||
iv: sharedSecret.iv,
|
iv: sharedSecret.iv,
|
||||||
tag: sharedSecret.tag,
|
tag: sharedSecret.tag,
|
||||||
expiresAt: sharedSecret.expiresAt
|
expiresAt: sharedSecret.expiresAt,
|
||||||
|
expiresAfterViews: sharedSecret.expiresAfterViews
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -72,14 +78,15 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string(),
|
|
||||||
encryptedValue: z.string(),
|
encryptedValue: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
|
expiresAt: z
|
||||||
message: "Expires at should be a future date"
|
.string()
|
||||||
})
|
.optional()
|
||||||
|
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
||||||
|
expiresAfterViews: z.number().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -89,19 +96,19 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
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({
|
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
name,
|
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt: new Date(expiresAt)
|
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
|
||||||
|
expiresAfterViews
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,28 @@ export const secretSharingServiceFactory = ({
|
|||||||
secretSharingDAL
|
secretSharingDAL
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
||||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } =
|
const {
|
||||||
createSharedSecretInput;
|
actor,
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
actorId,
|
||||||
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
orgId,
|
||||||
const newSharedSecret = await secretSharingDAL.create({
|
actorAuthMethod,
|
||||||
name,
|
actorOrgId,
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt,
|
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,
|
userId: actorId,
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
@@ -43,9 +54,18 @@ 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 && sharedSecret.expiresAt < new Date()) {
|
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||||
return;
|
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;
|
return sharedSecret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export type TSharedSecretPermission = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = {
|
export type TCreateSharedSecretDTO = {
|
||||||
name: string;
|
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt?: Date;
|
||||||
|
expiresAfterViews?: number;
|
||||||
} & TSharedSecretPermission;
|
} & TSharedSecretPermission;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
export type TSharedSecret = {
|
export type TSharedSecret = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
|
||||||
encryptedValue: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
hashedHex: string;
|
|
||||||
userId: string;
|
userId: string;
|
||||||
expiresAt: Date;
|
orgId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
} & TCreateSharedSecretRequest;
|
||||||
|
|
||||||
export type TCreateSharedSecretRequest = {
|
export type TCreateSharedSecretRequest = {
|
||||||
name: string;
|
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt?: Date;
|
||||||
|
expiresAfterViews?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TViewSharedSecretResponse = {
|
export type TViewSharedSecretResponse = {
|
||||||
name: string;
|
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
expiresAt: Date;
|
expiresAt?: Date;
|
||||||
|
expiresAfterViews?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteSharedSecretRequest = {
|
export type TDeleteSharedSecretRequest = {
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import { AxiosError } from "axios";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
encryptSymmetric,
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -22,7 +20,8 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
SecretInput,
|
SecretInput,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
|
Switch
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
@@ -63,10 +62,10 @@ const expirationUnitsAndActions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().max(100).required().label("Shared Secret Name"),
|
|
||||||
value: yup.string().max(1000).required().label("Shared Secret Value"),
|
value: yup.string().max(1000).required().label("Shared Secret Value"),
|
||||||
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
expiresAfterViews: yup.number().min(1).optional().label("Expires After Views"),
|
||||||
expiresInUnit: yup.string().required().label("Expiration Unit")
|
expiresInValue: yup.number().min(1).optional().label("Expiration Value"),
|
||||||
|
expiresInUnit: yup.string().optional().label("Expiration Unit")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -91,9 +90,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const createSharedSecret = useCreateSharedSecret();
|
const createSharedSecret = useCreateSharedSecret();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const [newSharedSecret, setnewSharedSecret] = useState("");
|
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||||
|
const [expiryOption, setExpiryOption] = useState<"time" | "views">("time");
|
||||||
const hasSharedSecret = Boolean(newSharedSecret);
|
const hasSharedSecret = Boolean(newSharedSecret);
|
||||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||||
initialState: false,
|
initialState: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const copyUrlToClipboard = () => {
|
const copyUrlToClipboard = () => {
|
||||||
@@ -106,10 +106,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [isUrlCopied]);
|
}, [isUrlCopied]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, value, expiresInValue, expiresInUnit }: FormData) => {
|
const onFormSubmit = async ({
|
||||||
|
value,
|
||||||
|
expiresInValue,
|
||||||
|
expiresInUnit,
|
||||||
|
expiresAfterViews
|
||||||
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?.id) return;
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
const key = crypto.randomBytes(16).toString("hex");
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
@@ -117,25 +121,26 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
key
|
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
|
||||||
)?.action;
|
)?.action;
|
||||||
if (updateExpiresAt) {
|
if (updateExpiresAt && expiresInValue) {
|
||||||
updateExpiresAt(expiresAt, expiresInValue);
|
updateExpiresAt(expiresAt, expiresInValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await createSharedSecret.mutateAsync({
|
const { id } = await createSharedSecret.mutateAsync({
|
||||||
name,
|
|
||||||
encryptedValue: ciphertext,
|
encryptedValue: ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt,
|
expiresAt: expiryOption === "time" ? expiresAt : undefined,
|
||||||
|
expiresAfterViews: expiryOption === "views" ? expiresAfterViews : undefined
|
||||||
});
|
});
|
||||||
setnewSharedSecret(
|
setnewSharedSecret(
|
||||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}`
|
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||||
|
hashedHex
|
||||||
|
)}-${encodeURIComponent(key)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -168,90 +173,114 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
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="This link is only accessible once. Please share this link with intended recipients. "
|
||||||
>
|
>
|
||||||
{!hasSharedSecret ? (
|
{!hasSharedSecret ? (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="value"
|
name="value"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Shared Secret Value"
|
label="Shared Secret"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
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 min-h-[100px]"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex w-full flex-row justify-end">
|
<div>
|
||||||
<div className="w-3/5">
|
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="expiresInValue"
|
name="expiresAfterViews"
|
||||||
defaultValue={1}
|
defaultValue={1}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Expiration Value"
|
className="mb-4 w-full"
|
||||||
|
label="Expires After Views"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} type="number" min={0} />
|
<Input {...field} type="number" min={1} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<div className="w-2/5 pl-4">
|
<div className="flex w-full flex-row justify-end">
|
||||||
<Controller
|
<div className="w-3/5">
|
||||||
control={control}
|
<Controller
|
||||||
name="expiresInUnit"
|
control={control}
|
||||||
defaultValue={expirationUnitsAndActions[0].unit}
|
name="expiresInValue"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
defaultValue={1}
|
||||||
<FormControl
|
render={({ field, fieldState: { error } }) => (
|
||||||
label="Expiration Unit"
|
<FormControl
|
||||||
errorText={error?.message}
|
label="Expiration Value"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Select
|
>
|
||||||
defaultValue={field.value}
|
<Input {...field} type="number" min={0} />
|
||||||
{...field}
|
</FormControl>
|
||||||
onValueChange={(e) => onChange(e)}
|
)}
|
||||||
className="w-full"
|
/>
|
||||||
>
|
</div>
|
||||||
{expirationUnitsAndActions.map(({ unit }) => (
|
<div className="w-2/5 pl-4">
|
||||||
<SelectItem value={unit} key={unit}>
|
<Controller
|
||||||
{unit}
|
control={control}
|
||||||
</SelectItem>
|
name="expiresInUnit"
|
||||||
))}
|
defaultValue={expirationUnitsAndActions[0].unit}
|
||||||
</Select>
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
</FormControl>
|
<FormControl
|
||||||
)}
|
label="Expiration Unit"
|
||||||
/>
|
errorText={error?.message}
|
||||||
</div>
|
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="mt-8 flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
const formatDate = (date: Date): string => (date ? new Date(date).toUTCString() : "");
|
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 =>
|
const getValidityStatusText = (expiresAt: Date): string =>
|
||||||
isExpired(expiresAt) ? "Expired " : "Valid for ";
|
isExpired(expiresAt) ? "Expired " : "Valid for ";
|
||||||
@@ -26,31 +34,38 @@ const timeAgo = (inputDate: Date, currentDate: Date): string => {
|
|||||||
const elapsedYears = Math.abs(Math.floor(elapsedDays / 365));
|
const elapsedYears = Math.abs(Math.floor(elapsedDays / 365));
|
||||||
|
|
||||||
if (elapsedYears > 0) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
if (elapsedMinutes > 0) {
|
||||||
return `${elapsedMinutes} minute${elapsedMinutes === 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"
|
|
||||||
}`;
|
}`;
|
||||||
|
}
|
||||||
|
return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${
|
||||||
|
elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ShareSecretsRow = ({
|
export const ShareSecretsRow = ({
|
||||||
@@ -82,29 +97,41 @@ export const ShareSecretsRow = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isExpired(row.expiresAt)) {
|
if (isExpired(row.expiresAt || row.expiresAfterViews)) {
|
||||||
onSecretExpiration(row.id);
|
onSecretExpiration(row.id);
|
||||||
}
|
}
|
||||||
}, [isExpired(row.expiresAt)]);
|
}, [isExpired(row.expiresAt || row.expiresAfterViews)]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr key={row.id}>
|
<Tr key={row.id}>
|
||||||
<Td>{row.name}</Td>
|
<Td>{`${row.encryptedValue.substring(0, 5)}...`}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<p className="text-sm text-yellow-400">{timeAgo(row.createdAt, currentTime)}</p>
|
<p className="text-sm text-yellow-400">{timeAgo(row.createdAt, currentTime)}</p>
|
||||||
<p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p>
|
<p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<p className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}>
|
{row.expiresAfterViews ? (
|
||||||
{getValidityStatusText(row.expiresAt) + timeAgo(row.expiresAt, currentTime)}
|
<p
|
||||||
</p>
|
className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}
|
||||||
<p className="text-xs text-gray-500">{formatDate(row.expiresAt)}</p>
|
>
|
||||||
|
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>
|
||||||
<Td>
|
<Td>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handlePopUpOpen("deleteSharedSecretConfirmation", {
|
handlePopUpOpen("deleteSharedSecretConfirmation", {
|
||||||
name: row.name,
|
name: "delete",
|
||||||
id: row.id
|
id: row.id
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,17 @@ type Props = {
|
|||||||
export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const { isLoading, data = [] } = useGetSharedSecrets();
|
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 = () => {
|
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 (
|
return (
|
||||||
@@ -42,7 +50,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<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" />
|
<Th aria-label="button" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
|
|||||||
Reference in New Issue
Block a user