Add actor to secret version history

This commit is contained in:
carlosmonastyrski
2025-03-05 17:20:57 -03:00
parent 3badcea95b
commit 30ccfbfc8e
15 changed files with 376 additions and 88 deletions

View File

@@ -0,0 +1,52 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
if (!hasSecretVersionV2UserActorId) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.uuid("userActorId");
t.foreign("userActorId").references("id").inTable(TableName.Users);
});
}
if (!hasSecretVersionV2IdentityActorId) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.uuid("identityActorId");
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
});
}
if (!hasSecretVersionV2ActorType) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.string("actorType");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
if (hasSecretVersionV2UserActorId) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.dropColumn("userActorId");
});
}
if (!hasSecretVersionV2IdentityActorId) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.dropColumn("identityActorId");
});
}
if (!hasSecretVersionV2ActorType) {
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
t.dropColumn("actorType");
});
}
}

View File

@@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
folderId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
userActorId: z.string().uuid().nullable().optional(),
identityActorId: z.string().uuid().nullable().optional(),
actorType: z.string().nullable().optional()
});
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;

View File

@@ -757,7 +757,11 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
actor: {
type: actor,
actorId
}
})
: [];
const updatedSecrets = secretUpdationCommits.length
@@ -803,7 +807,11 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
actor: {
type: actor,
actorId
}
})
: [];
const deletedSecret = secretDeletionCommits.length

View File

@@ -710,6 +710,10 @@ export const secretReplicationServiceFactory = ({
tx,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
keyEncoding: doc.keyEncoding,
@@ -741,6 +745,10 @@ export const secretReplicationServiceFactory = ({
tx,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyUpdatedSecrets.map((doc) => {
return {
filter: {

View File

@@ -370,7 +370,20 @@ export const secretSnapshotServiceFactory = ({
const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map(
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
userActorId,
identityActorId,
actorType,
...el
}) => ({
...el,
id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,

View File

@@ -1039,7 +1039,9 @@ export const registerRoutes = async (
secretApprovalRequestSecretDAL,
kmsService,
snapshotService,
resourceMetadataDAL
resourceMetadataDAL,
userDAL,
identityDAL
});
const secretApprovalRequestService = secretApprovalRequestServiceFactory({

View File

@@ -111,7 +111,15 @@ export const secretRawSchema = z.object({
secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
actor: z
.object({
actorId: z.string().nullable(),
actorType: z.string().nullable(),
name: z.string().nullable().optional()
})
.optional()
.nullable()
});
export const ProjectPermissionSchema = z.object({

View File

@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
});
}

View File

@@ -10,6 +10,7 @@ import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-sche
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type";
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
// akhilmhdh: JS regex with global save state in .test
@@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL,
actor,
tx
}: TFnSecretBulkInsert) => {
const sanitizedInputSecrets = inputSecrets.map(
@@ -90,6 +92,9 @@ export const fnSecretBulkInsert = async ({
})
);
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
const newSecrets = await secretDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx
@@ -106,6 +111,9 @@ export const fnSecretBulkInsert = async ({
sanitizedInputSecrets.map((el) => ({
...el,
folderId,
userActorId,
identityActorId,
actorType: actor?.type,
secretId: newSecretGroupedByKeyName[el.key][0].id
})),
tx
@@ -157,8 +165,12 @@ export const fnSecretBulkUpdate = async ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
resourceMetadataDAL
resourceMetadataDAL,
actor
}: TFnSecretBulkUpdate) => {
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
const sanitizedInputSecrets = inputSecrets.map(
({
filter,
@@ -216,7 +228,10 @@ export const fnSecretBulkUpdate = async ({
encryptedValue,
reminderRepeatDays,
folderId,
secretId
secretId,
userActorId,
identityActorId,
actorType: actor?.type
})
),
tx
@@ -616,6 +631,11 @@ export const reshapeBridgeSecret = (
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value: string;
comment: string;
actor?: {
actorType?: string;
actorId?: string;
name?: string;
};
tags?: {
id: string;
slug: string;
@@ -636,6 +656,7 @@ export const reshapeBridgeSecret = (
_id: secret.id,
id: secret.id,
user: secret.userId,
actor: secret.actor,
tags: secret.tags,
skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.reminderRepeatDays,

View File

@@ -62,6 +62,8 @@ import {
} from "./secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "./secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TIdentityDALFactory } from "../identity/identity-dal";
type TSecretV2BridgeServiceFactoryDep = {
secretDAL: TSecretV2BridgeDALFactory;
@@ -85,6 +87,8 @@ type TSecretV2BridgeServiceFactoryDep = {
>;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
userDAL: Pick<TUserDALFactory, "find">;
identityDAL: Pick<TIdentityDALFactory, "find">;
};
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
@@ -107,7 +111,9 @@ export const secretV2BridgeServiceFactory = ({
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
kmsService,
resourceMetadataDAL
resourceMetadataDAL,
userDAL,
identityDAL
}: TSecretV2BridgeServiceFactoryDep) => {
const $validateSecretReferences = async (
projectId: string,
@@ -301,6 +307,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -483,6 +493,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -1230,6 +1244,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -1490,6 +1508,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
resourceMetadataDAL
});
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
@@ -1522,6 +1544,10 @@ export const secretV2BridgeServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
});
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
@@ -1690,13 +1716,39 @@ export const secretV2BridgeServiceFactory = ({
projectId: folder.projectId
});
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
return secretVersions.map((el) =>
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
const userIds = Array.from(
new Set(secretVersions.map((version) => version.userActorId).filter(Boolean))
) as string[];
const users = userIds.length > 0 ? await userDAL.find({ $in: { id: userIds } }) : [];
const usersById = groupBy(users, (user) => user.id);
const identitiesIds = Array.from(
new Set(secretVersions.map((version) => version.identityActorId).filter(Boolean))
) as string[];
const identities = identitiesIds.length > 0 ? await identityDAL.find({ $in: { id: identitiesIds } }) : [];
const identitiesById = groupBy(identities, (identity) => identity.id);
return secretVersions.map((el) => {
let entityId;
let actorName;
if (el.userActorId) {
actorName = usersById[el.userActorId]?.[0]?.username;
entityId = el.userActorId;
} else if (el.identityActorId) {
actorName = identitiesById[el.identityActorId]?.[0]?.name;
entityId = el.identityActorId;
}
const actorEntity = el.actorType ? { actorType: el.actorType, actorId: entityId, name: actorName } : undefined;
return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
...el,
actor: actorEntity,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
})
);
});
});
};
// this is a backfilling API for secret references
@@ -1956,6 +2008,10 @@ export const secretV2BridgeServiceFactory = ({
secretTagDAL,
resourceMetadataDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
type: doc.type,
@@ -1982,6 +2038,10 @@ export const secretV2BridgeServiceFactory = ({
tx,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyUpdatedSecrets.map((doc) => {
return {
filter: {

View File

@@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = {
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
actorId: string;
};
};
type TRequireReferenceIfValue =
@@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = {
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
actorId: string;
};
tx?: Knex;
};

View File

@@ -284,6 +284,10 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -429,6 +433,10 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -822,6 +830,10 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -931,7 +943,11 @@ export const secretServiceFactory = ({
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
actor: {
type: actor,
actorId
}
})
);
@@ -2404,6 +2420,10 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -2514,6 +2534,10 @@ export const secretServiceFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx
})
);
@@ -2848,6 +2872,10 @@ export const secretServiceFactory = ({
secretDAL,
tx,
secretTagDAL,
actor: {
type: actor,
actorId
},
secretVersionTagDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@@ -2879,6 +2907,10 @@ export const secretServiceFactory = ({
tx,
secretTagDAL,
secretVersionTagDAL,
actor: {
type: actor,
actorId
},
inputSecrets: locallyUpdatedSecrets.map((doc) => {
return {
filter: {

View File

@@ -322,6 +322,10 @@ export type TFnSecretBulkInsert = {
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
actor?: {
type?: string;
actorId: string;
};
};
export type TFnSecretBulkUpdate = {
@@ -336,6 +340,10 @@ export type TFnSecretBulkUpdate = {
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
tx?: Knex;
actor?: {
type?: string;
actorId: string;
};
};
export type TAttachSecretTagsDTO = {

View File

@@ -101,6 +101,11 @@ export type SecretVersions = {
skipMultilineEncoding?: boolean;
createdAt: string;
updatedAt: string;
actor?: {
actorId?: string,
actorType: string,
name?: string
};
};
// dto

View File

@@ -9,11 +9,14 @@ import {
faPlus,
faShare,
faTag,
faTrash
faTrash,
faUser,
faDesktop,
faServer
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { Link, useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@@ -49,6 +52,8 @@ import { useGetSecretVersion } from "@app/hooks/api";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetWorkspaceUsers } from "@app/hooks/api";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -120,7 +125,9 @@ export const SecretDetailSidebar = ({
{}
);
const selectTagSlugs = selectedTags.map((i) => i.slug);
const navigate = useNavigate();
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace.id);
const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
@@ -201,6 +208,41 @@ export const SecretDetailSidebar = ({
const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote");
const getModifiedByIcon = (userType: string) => {
switch (userType) {
case ActorType.USER:
return faUser;
case ActorType.IDENTITY:
return faDesktop;
default:
return faServer;
}
}
const getUserMembershipId = (actorId: string) => {
return members.filter((member) => member.user?.id === actorId)?.[0].id || null;
}
const getLinkToModifyHistoryEntity = (actorId: string, actorType: string) => {
switch(actorType) {
case ActorType.USER:
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${getUserMembershipId(actorId)}`;
case ActorType.IDENTITY:
return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`;
default:
return null;
}
}
const onModifyHistoryClick = (actorId: string | undefined, actorType: string) => {
if (actorId && actorType !== ActorType.PLATFORM) {
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType);
if (redirectLink) {
navigate({ to: redirectLink });
}
}
}
return (
<>
<CreateReminderForm
@@ -618,7 +660,7 @@ export const SecretDetailSidebar = ({
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
{secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => (
<div className="flex flex-row">
<div key={id} className="flex w-full flex-col space-y-1">
<div className="flex items-center">
@@ -633,36 +675,29 @@ export const SecretDetailSidebar = ({
<div className="relative w-10">
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
</div>
<div className="flex flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
</div>
<div className="group break-all pl-1 font-mono">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<button
type="button"
className="select-none"
onClick={(e) => {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
<div className="flex flex-col w-full cursor-default">
{actor && (
<div className="flex flex-row">
<div className="flex flex-row w-fit text-sm">
Modified by:
<Tooltip content={actor.name}>
<div onClick={() => onModifyHistoryClick(actor.actorId, actor.actorType)} className="cursor-pointer">
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2"/>
</div>
</Tooltip>
</div>
</div>
)}
<div className="flex flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
</div>
<div className="group break-all pl-1 font-mono">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<button
type="button"
className="select-none"
onClick={(e) => {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
@@ -680,51 +715,72 @@ export const SecretDetailSidebar = ({
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
{secretValue}
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
{secretValue}
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}
}}
>
<FontAwesomeIcon icon={faEyeSlash} />
</button>
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}
}}
>
<FontAwesomeIcon icon={faEyeSlash} />
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget.closest(".group")?.classList.add("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.currentTarget
.closest(".group")
?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
<span className="group-[.show-value]:hidden">
{secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget.closest(".group")?.classList.add("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.currentTarget
.closest(".group")
?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div>
</div>