mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
fix: minor ui changes + delete expired secrets + address other feedback
This commit is contained in:
+2
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user