fix: minor ui changes + delete expired secrets + address other feedback

This commit is contained in:
ShubhamPalriwala
2024-05-29 14:10:47 +05:30
parent f85a35fde8
commit b5b778e241
14 changed files with 174 additions and 203 deletions
@@ -11,7 +11,9 @@ export async function up(knex: Knex): Promise<void> {
t.text("signedValue").notNullable(); t.text("signedValue").notNullable();
t.timestamp("expiresAt").notNullable(); t.timestamp("expiresAt").notNullable();
t.uuid("userId").notNullable(); t.uuid("userId").notNullable();
t.uuid("orgId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true); t.timestamps(true, true, true);
}); });
+1
View File
@@ -13,6 +13,7 @@ export const SecretSharingSchema = z.object({
signedValue: z.string(), signedValue: z.string(),
expiresAt: z.date(), expiresAt: z.date(),
userId: z.string().uuid(), userId: z.string().uuid(),
orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()
}); });
@@ -21,7 +21,6 @@ export enum OrgPermissionSubjects {
Groups = "groups", Groups = "groups",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
SecretSharing = "secret-sharing",
Identity = "identity" Identity = "identity"
} }
@@ -37,7 +36,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.SecretSharing]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity];
@@ -62,10 +60,6 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretSharing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretSharing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretSharing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
@@ -130,10 +124,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretSharing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.SecretSharing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretSharing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
+2 -1
View File
@@ -794,7 +794,8 @@ export const registerRoutes = async (
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
queueService, queueService,
identityAccessTokenDAL identityAccessTokenDAL,
secretSharingDAL
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
@@ -3,10 +3,12 @@ import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
type TDailyResourceCleanUpQueueServiceFactoryDep = { type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">; auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
}; };
@@ -15,12 +17,14 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
export const dailyResourceCleanUpQueueServiceFactory = ({ export const dailyResourceCleanUpQueueServiceFactory = ({
auditLogDAL, auditLogDAL,
queueService, queueService,
identityAccessTokenDAL identityAccessTokenDAL,
secretSharingDAL
}: TDailyResourceCleanUpQueueServiceFactoryDep) => { }: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await auditLogDAL.pruneAuditLog(); await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens(); await identityAccessTokenDAL.removeExpiredTokens();
await secretSharingDAL.pruneExpiredSharedSecrets();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });
@@ -1,5 +1,8 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>; export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
@@ -7,10 +10,18 @@ export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory
export const secretSharingDALFactory = (db: TDbClient) => { export const secretSharingDALFactory = (db: TDbClient) => {
const sharedSecretOrm = ormify(db, TableName.SecretSharing); const sharedSecretOrm = ormify(db, TableName.SecretSharing);
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing).where("expiresAt", "<", today).del();
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" });
}
};
return { return {
create: sharedSecretOrm.create, ...sharedSecretOrm,
find: sharedSecretOrm.find, pruneExpiredSharedSecrets
findById: sharedSecretOrm.findById,
deleteById: sharedSecretOrm.deleteById
}; };
}; };
@@ -1,7 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
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 { TSecretSharingDALFactory } from "./secret-sharing-dal"; import { TSecretSharingDALFactory } from "./secret-sharing-dal";
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types"; import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
@@ -21,12 +19,13 @@ export const secretSharingServiceFactory = ({
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, signedValue, expiresAt } = const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, signedValue, expiresAt } =
createSharedSecretInput; createSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretSharing); if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const newSharedSecret = await secretSharingDAL.create({ const newSharedSecret = await secretSharingDAL.create({
name, name,
signedValue, signedValue,
expiresAt, expiresAt,
userId: actorId userId: actorId,
orgId
}); });
return { id: newSharedSecret.id }; return { id: newSharedSecret.id };
}; };
@@ -34,8 +33,8 @@ export const secretSharingServiceFactory = ({
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);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretSharing); if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const userSharedSecrets = await secretSharingDAL.find({ userId: actorId }, { sort: [["expiresAt", "asc"]] }); const userSharedSecrets = await secretSharingDAL.find({ userId: actorId, orgId }, { sort: [["expiresAt", "asc"]] });
return userSharedSecrets; return userSharedSecrets;
}; };
@@ -50,7 +49,7 @@ export const secretSharingServiceFactory = ({
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretSharing); if (!permission) throw new UnauthorizedError({ name: "User not in org" });
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId); const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
return deletedSharedSecret; return deletedSharedSecret;
}; };
@@ -19,7 +19,6 @@ export enum OrgPermissionSubjects {
Groups = "groups", Groups = "groups",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
SecretSharing = "secret-sharing",
Identity = "identity" Identity = "identity"
} }
@@ -35,7 +34,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.SecretSharing]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity];
@@ -19,11 +19,12 @@ import {
Modal, Modal,
ModalClose, ModalClose,
ModalContent, ModalContent,
SecretInput,
Select, Select,
SelectItem SelectItem
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing"; import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -89,22 +90,20 @@ 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 [isUrlCopied, setIsUrlCopied] = useToggle(false);
const hasSharedSecret = Boolean(newSharedSecret); const hasSharedSecret = Boolean(newSharedSecret);
const [isUrlCopied,, setIsUrlCopied] = useTimedReset<boolean>({
useEffect(() => { initialState: false,
let timer: NodeJS.Timeout; });
if (isUrlCopied) {
timer = setTimeout(() => setIsUrlCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isUrlCopied]);
const copyUrlToClipboard = () => { const copyUrlToClipboard = () => {
navigator.clipboard.writeText(newSharedSecret); navigator.clipboard.writeText(newSharedSecret);
setIsUrlCopied.on(); setIsUrlCopied(true);
}; };
useEffect(() => {
if (isUrlCopied) {
setTimeout(() => setIsUrlCopied(false), 2000);
}
}, [isUrlCopied]);
const onFormSubmit = async ({ name, value, expiresInValue, expiresInUnit }: FormData) => { const onFormSubmit = async ({ name, value, expiresInValue, expiresInUnit }: FormData) => {
try { try {
@@ -195,7 +194,11 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="Type your secret value" /> <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"
/>
</FormControl> </FormControl>
)} )}
/> />
@@ -1,12 +1,8 @@
import { useState } from "react";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2";
import { Button, Checkbox, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing"; import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
@@ -15,90 +11,67 @@ import { ShareSecretsTable } from "./ShareSecretsTable";
type DeleteModalData = { name: string; id: string }; type DeleteModalData = { name: string; id: string };
export const ShareSecretSection = withPermission( export const ShareSecretSection = () => {
() => { const deleteSharedSecret = useDeleteSharedSecret();
const deleteSharedSecret = useDeleteSharedSecret(); const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
const [showExpiredSharedSecrets, setShowExpiredSharedSecrets] = useState(false); "createSharedSecret",
"deleteSharedSecretConfirmation"
] as const);
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ const onDeleteApproved = async () => {
"createSharedSecret", try {
"deleteSharedSecretConfirmation" deleteSharedSecret.mutateAsync({
] as const); sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id,
});
createNotification({
text: "Successfully deleted shared secret",
type: "success"
});
const onDeleteApproved = async () => { handlePopUpClose("deleteSharedSecretConfirmation");
try { } catch (err) {
deleteSharedSecret.mutateAsync({ console.error(err);
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id, createNotification({
}); text: "Failed to delete shared secret",
createNotification({ type: "error"
text: "Successfully deleted shared secret", });
type: "success" }
}); };
handlePopUpClose("deleteSharedSecretConfirmation"); return (
} catch (err) { <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
console.error(err); <div className="mb-2 flex justify-between">
createNotification({ <p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p>
text: "Failed to delete shared secret",
type: "error"
});
}
};
return ( <Button
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> colorSchema="primary"
<div className="mb-2 flex justify-between"> leftIcon={<FontAwesomeIcon icon={faPlus} />}
<p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p> onClick={() => {
<OrgPermissionCan handlePopUpOpen("createSharedSecret");
I={OrgPermissionActions.Create} }}
a={OrgPermissionSubjects.SecretSharing} >
> Share Secret
{(isAllowed) => ( </Button>
<Button
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("createSharedSecret");
}}
isDisabled={!isAllowed}
>
Share Secret
</Button>
)}
</OrgPermissionCan>
</div>
<div className="mb-8 flex items-center justify-between">
<p className="flex-grow text-gray-400">
Every secret shared can be accessed with the URL (shown during creation) before its
expiry.
</p>
<Checkbox
className="shrink-0 data-[state=checked]:bg-primary"
id="showInactive"
isChecked={showExpiredSharedSecrets}
onCheckedChange={(state) => {
setShowExpiredSharedSecrets(state as boolean);
}}
>
Show expired shared secrets too
</Checkbox>
</div>
<ShareSecretsTable
handlePopUpOpen={handlePopUpOpen}
showExpiredSharedSecrets={showExpiredSharedSecrets}
/>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
} shared secret?`}
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
onDeleteApproved={onDeleteApproved}
/>
</div> </div>
); <div className="mb-8 flex items-center justify-between">
}, <p className="flex-grow text-gray-400">
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.SecretSharing } Every secret shared can be accessed with the URL (shown during creation) before its
); expiry.
</p>
</div>
<ShareSecretsTable
handlePopUpOpen={handlePopUpOpen}
/>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
} shared secret?`}
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
onDeleteApproved={onDeleteApproved}
/>
</div>
);
};
@@ -2,9 +2,7 @@ import { useEffect, useState } from "react";
import { faTrashCan } from "@fortawesome/free-solid-svg-icons"; import { faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Td, Tr } from "@app/components/v2"; import { IconButton, Td, Tr } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { TSharedSecret } from "@app/hooks/api/secretSharing"; import { TSharedSecret } from "@app/hooks/api/secretSharing";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -28,38 +26,31 @@ 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"} ${ return `${elapsedYears} year${elapsedYears === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
} }
if (elapsedMonths > 0) { if (elapsedMonths > 0) {
return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${ return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
} }
if (elapsedWeeks > 0) { if (elapsedWeeks > 0) {
return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${ return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
} }
if (elapsedDays > 0) { if (elapsedDays > 0) {
return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${ return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
} }
if (elapsedHours > 0) { if (elapsedHours > 0) {
return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${ return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
} }
if (elapsedMinutes > 0) { if (elapsedMinutes > 0) {
return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${ return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
} }
return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${ return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
elapsedMilliseconds >= 0 ? "ago" : "from now" }`;
}`;
}; };
export const ShareSecretsRow = ({ export const ShareSecretsRow = ({
@@ -110,26 +101,20 @@ export const ShareSecretsRow = ({
<p className="text-xs text-gray-500">{formatDate(row.expiresAt)}</p> <p className="text-xs text-gray-500">{formatDate(row.expiresAt)}</p>
</Td> </Td>
<Td> <Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.SecretSharing} <IconButton
onClick={() =>
handlePopUpOpen("deleteSharedSecretConfirmation", {
name: row.name,
id: row.id
})
}
colorSchema="danger"
ariaLabel="delete"
> >
{(isAllowed) => ( <FontAwesomeIcon icon={faTrashCan} />
<IconButton </IconButton>
onClick={() =>
handlePopUpOpen("deleteSharedSecretConfirmation", {
name: row.name,
id: row.id
})
}
colorSchema="danger"
ariaLabel="delete"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
); );
@@ -28,29 +28,22 @@ type Props = {
id: string; id: string;
} }
) => void; ) => void;
showExpiredSharedSecrets: boolean;
}; };
export const ShareSecretsTable = ({ handlePopUpOpen, showExpiredSharedSecrets }: Props) => { export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
const [tableData, setTableData] = useState<TSharedSecret[]>([]); const [tableData, setTableData] = useState<TSharedSecret[]>([]);
const { isLoading, data = [] } = useGetSharedSecrets(); const { isLoading, data = [] } = useGetSharedSecrets();
useEffect(() => { useEffect(() => {
if (!isLoading) { if (!isLoading) {
if (!showExpiredSharedSecrets) { setTableData(data);
setTableData(data.filter((secret) => new Date(secret.expiresAt) > new Date()));
} else {
setTableData(data);
}
} }
}, [isLoading, data, showExpiredSharedSecrets]); }, [isLoading, data]);
const handleSecretExpiration = () => { const handleSecretExpiration = () => {
if (!showExpiredSharedSecrets) { setTableData(
setTableData( data.filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) > new Date())
data.filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) > new Date()) );
);
}
}; };
return ( return (
@@ -77,7 +70,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen, showExpiredSharedSecrets }:
{!isLoading && tableData && tableData?.length === 0 && ( {!isLoading && tableData && tableData?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400"> <Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
<EmptyState title="No secrets shared yet!" icon={faKey} /> <EmptyState title="No secrets shared currently" icon={faKey} />
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -5,7 +5,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { openSignedAssymmetric } from "@app/components/utilities/cryptography/crypto"; import { openSignedAssymmetric } from "@app/components/utilities/cryptography/crypto";
import { useToggle } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
import { DragonMainImage, SecretTable } from "./components"; import { DragonMainImage, SecretTable } from "./components";
@@ -34,42 +34,46 @@ export const ShareSecretPublicPage = () => {
}, [data, publicKey]); }, [data, publicKey]);
const [timeLeft, setTimeLeft] = useState(""); const [timeLeft, setTimeLeft] = useState("");
const [isUrlCopied, setIsUrlCopied] = useToggle(false); const [isUrlCopied,, setIsUrlCopied] = useTimedReset<boolean>({
initialState: false,
});
const millisecondsPerDay = 1000 * 60 * 60 * 24;
const millisecondsPerHour = 1000 * 60 * 60;
const millisecondsPerMinute = 1000 * 60;
useEffect(() => { useEffect(() => {
const updateTimer = () => { const updateTimer = () => {
if (data && data.expiresAt) { if (data && data.expiresAt) {
const expiryDate = new Date(data.expiresAt).getTime(); const expirationTime = new Date(data.expiresAt).getTime();
const now = new Date().getTime(); const currentTime = new Date().getTime();
const distance = expiryDate - now; const timeDifference = expirationTime - currentTime;
if (distance < 0) { if (timeDifference < 0) {
setTimeLeft("Expired"); setTimeLeft("Expired");
} else { } else {
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const hoursRemaining = Math.floor((timeDifference % millisecondsPerDay) / millisecondsPerHour);
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); const minutesRemaining = Math.floor((timeDifference % millisecondsPerHour) / millisecondsPerMinute);
const seconds = Math.floor((distance % (1000 * 60)) / 1000); const secondsRemaining = Math.floor((timeDifference % millisecondsPerMinute) / 1000);
setTimeLeft(`${hours}h ${minutes}m ${seconds}s`); setTimeLeft(`${hoursRemaining}h ${minutesRemaining}m ${secondsRemaining}s`);
} }
} }
}; };
const timer = setInterval(updateTimer, 1000); const timer = setInterval(updateTimer, 1000);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [data?.expiresAt]); }, [data?.expiresAt]);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout;
if (isUrlCopied) { if (isUrlCopied) {
timer = setTimeout(() => setIsUrlCopied.off(), 2000); setTimeout(() => setIsUrlCopied(false), 2000);
} }
return () => clearTimeout(timer);
}, [isUrlCopied]); }, [isUrlCopied]);
const copyUrlToClipboard = () => { const copyUrlToClipboard = () => {
navigator.clipboard.writeText(decryptedSecret as string); navigator.clipboard.writeText(decryptedSecret);
setIsUrlCopied.on(); setIsUrlCopied(true);
}; };
return ( return (
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
EmptyState, EmptyState,
IconButton, IconButton,
SecretInput,
Table, Table,
TableContainer, TableContainer,
TBody, TBody,
@@ -47,7 +48,13 @@ export const SecretTable = ({
<Td>{sharedSecret.name}</Td> <Td>{sharedSecret.name}</Td>
<Td> <Td>
<div className="flex items-center md:space-x-2"> <div className="flex items-center md:space-x-2">
<div className="max-w-[20rem] flex-1 break-words">{decryptedSecret}</div> <div className="max-w-[20rem] flex-1 break-words">
<SecretInput
isVisible
value={decryptedSecret}
readOnly
/>
</div>
<IconButton <IconButton
ariaLabel="copy to clipboard" ariaLabel="copy to clipboard"
onClick={copyUrlToClipboard} onClick={copyUrlToClipboard}