mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add actor to secret version history
This commit is contained in:
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
|
|||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
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>;
|
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||||
|
|||||||
@@ -757,7 +757,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const updatedSecrets = secretUpdationCommits.length
|
const updatedSecrets = secretUpdationCommits.length
|
||||||
@@ -803,7 +807,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const deletedSecret = secretDeletionCommits.length
|
const deletedSecret = secretDeletionCommits.length
|
||||||
|
|||||||
@@ -710,6 +710,10 @@ export const secretReplicationServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
keyEncoding: doc.keyEncoding,
|
keyEncoding: doc.keyEncoding,
|
||||||
@@ -741,6 +745,10 @@ export const secretReplicationServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
filter: {
|
filter: {
|
||||||
|
|||||||
@@ -370,7 +370,20 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
const secrets = await secretV2BridgeDAL.insertMany(
|
const secrets = await secretV2BridgeDAL.insertMany(
|
||||||
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
|
||||||
secretVersions.map(
|
secretVersions.map(
|
||||||
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({
|
({
|
||||||
|
latestSecretVersion,
|
||||||
|
version,
|
||||||
|
updatedAt,
|
||||||
|
createdAt,
|
||||||
|
secretId,
|
||||||
|
envId,
|
||||||
|
id,
|
||||||
|
tags,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType,
|
||||||
|
...el
|
||||||
|
}) => ({
|
||||||
...el,
|
...el,
|
||||||
id: secretId,
|
id: secretId,
|
||||||
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
|
||||||
|
|||||||
@@ -1039,7 +1039,9 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
userDAL,
|
||||||
|
identityDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
||||||
|
|||||||
@@ -111,7 +111,15 @@ export const secretRawSchema = z.object({
|
|||||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
createdAt: z.date(),
|
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({
|
export const ProjectPermissionSchema = z.object({
|
||||||
|
|||||||
@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-sche
|
|||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||||
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
||||||
// akhilmhdh: JS regex with global save state in .test
|
// akhilmhdh: JS regex with global save state in .test
|
||||||
@@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor,
|
||||||
tx
|
tx
|
||||||
}: TFnSecretBulkInsert) => {
|
}: TFnSecretBulkInsert) => {
|
||||||
const sanitizedInputSecrets = inputSecrets.map(
|
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(
|
const newSecrets = await secretDAL.insertMany(
|
||||||
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
|
||||||
tx
|
tx
|
||||||
@@ -106,6 +111,9 @@ export const fnSecretBulkInsert = async ({
|
|||||||
sanitizedInputSecrets.map((el) => ({
|
sanitizedInputSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
folderId,
|
folderId,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType: actor?.type,
|
||||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@@ -157,8 +165,12 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
actor
|
||||||
}: TFnSecretBulkUpdate) => {
|
}: 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(
|
const sanitizedInputSecrets = inputSecrets.map(
|
||||||
({
|
({
|
||||||
filter,
|
filter,
|
||||||
@@ -216,7 +228,10 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
folderId,
|
folderId,
|
||||||
secretId
|
secretId,
|
||||||
|
userActorId,
|
||||||
|
identityActorId,
|
||||||
|
actorType: actor?.type
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
@@ -616,6 +631,11 @@ export const reshapeBridgeSecret = (
|
|||||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||||
value: string;
|
value: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
actor?: {
|
||||||
|
actorType?: string;
|
||||||
|
actorId?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
tags?: {
|
tags?: {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -636,6 +656,7 @@ export const reshapeBridgeSecret = (
|
|||||||
_id: secret.id,
|
_id: secret.id,
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
user: secret.userId,
|
user: secret.userId,
|
||||||
|
actor: secret.actor,
|
||||||
tags: secret.tags,
|
tags: secret.tags,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
secretReminderRepeatDays: secret.reminderRepeatDays,
|
secretReminderRepeatDays: secret.reminderRepeatDays,
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ import {
|
|||||||
} from "./secret-v2-bridge-types";
|
} from "./secret-v2-bridge-types";
|
||||||
import { TSecretVersionV2DALFactory } from "./secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "./secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
|
|
||||||
type TSecretV2BridgeServiceFactoryDep = {
|
type TSecretV2BridgeServiceFactoryDep = {
|
||||||
secretDAL: TSecretV2BridgeDALFactory;
|
secretDAL: TSecretV2BridgeDALFactory;
|
||||||
@@ -85,6 +87,8 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "find">;
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "find">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
||||||
@@ -107,7 +111,9 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
userDAL,
|
||||||
|
identityDAL
|
||||||
}: TSecretV2BridgeServiceFactoryDep) => {
|
}: TSecretV2BridgeServiceFactoryDep) => {
|
||||||
const $validateSecretReferences = async (
|
const $validateSecretReferences = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -301,6 +307,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -483,6 +493,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1230,6 +1244,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1490,6 +1508,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||||
@@ -1522,6 +1544,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
|
||||||
@@ -1690,13 +1716,39 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] });
|
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,
|
...el,
|
||||||
|
actor: actorEntity,
|
||||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is a backfilling API for secret references
|
// this is a backfilling API for secret references
|
||||||
@@ -1956,6 +2008,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
type: doc.type,
|
type: doc.type,
|
||||||
@@ -1982,6 +2038,10 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
filter: {
|
filter: {
|
||||||
|
|||||||
@@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type TRequireReferenceIfValue =
|
type TRequireReferenceIfValue =
|
||||||
@@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,10 @@ export const secretServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -429,6 +433,10 @@ export const secretServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -822,6 +830,10 @@ export const secretServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -931,7 +943,11 @@ export const secretServiceFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2404,6 +2420,10 @@ export const secretServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -2514,6 +2534,10 @@ export const secretServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -2848,6 +2872,10 @@ export const secretServiceFactory = ({
|
|||||||
secretDAL,
|
secretDAL,
|
||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
@@ -2879,6 +2907,10 @@ export const secretServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
actorId
|
||||||
|
},
|
||||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
filter: {
|
filter: {
|
||||||
|
|||||||
@@ -322,6 +322,10 @@ export type TFnSecretBulkInsert = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||||
|
actor?: {
|
||||||
|
type?: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TFnSecretBulkUpdate = {
|
export type TFnSecretBulkUpdate = {
|
||||||
@@ -336,6 +340,10 @@ export type TFnSecretBulkUpdate = {
|
|||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "deleteTagsManySecret">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
|
actor?: {
|
||||||
|
type?: string;
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAttachSecretTagsDTO = {
|
export type TAttachSecretTagsDTO = {
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ export type SecretVersions = {
|
|||||||
skipMultilineEncoding?: boolean;
|
skipMultilineEncoding?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
actor?: {
|
||||||
|
actorId?: string,
|
||||||
|
actorType: string,
|
||||||
|
name?: string
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// dto
|
// dto
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import {
|
|||||||
faPlus,
|
faPlus,
|
||||||
faShare,
|
faShare,
|
||||||
faTag,
|
faTag,
|
||||||
faTrash
|
faTrash,
|
||||||
|
faUser,
|
||||||
|
faDesktop,
|
||||||
|
faServer
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 { format } from "date-fns";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
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 { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/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 { CreateReminderForm } from "./CreateReminderForm";
|
||||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||||
@@ -120,7 +125,9 @@ export const SecretDetailSidebar = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace.id);
|
||||||
|
|
||||||
const cannotEditSecret = permission.cannot(
|
const cannotEditSecret = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
@@ -201,6 +208,41 @@ export const SecretDetailSidebar = ({
|
|||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
const secretReminderNote = watch("reminderNote");
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
@@ -618,7 +660,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
||||||
<div className="mb-2 pl-1">Version History</div>
|
<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]">
|
<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 className="flex flex-row">
|
||||||
<div key={id} className="flex w-full flex-col space-y-1">
|
<div key={id} className="flex w-full flex-col space-y-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -633,36 +675,29 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="relative w-10">
|
<div className="relative w-10">
|
||||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-col w-full cursor-default">
|
||||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
{actor && (
|
||||||
Value:
|
<div className="flex flex-row">
|
||||||
</div>
|
<div className="flex flex-row w-fit text-sm">
|
||||||
<div className="group break-all pl-1 font-mono">
|
Modified by:
|
||||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
<Tooltip content={actor.name}>
|
||||||
<button
|
<div onClick={() => onModifyHistoryClick(actor.actorId, actor.actorType)} className="cursor-pointer">
|
||||||
type="button"
|
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2"/>
|
||||||
className="select-none"
|
</div>
|
||||||
onClick={(e) => {
|
</Tooltip>
|
||||||
navigator.clipboard.writeText(secretValue || "");
|
</div>
|
||||||
const target = e.currentTarget;
|
</div>
|
||||||
target.style.borderBottom = "1px dashed";
|
)}
|
||||||
target.style.paddingBottom = "-1px";
|
<div className="flex flex-row">
|
||||||
|
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||||
// Create and insert popup
|
Value:
|
||||||
const popup = document.createElement("div");
|
</div>
|
||||||
popup.className =
|
<div className="group break-all pl-1 font-mono">
|
||||||
"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";
|
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||||
popup.textContent = "Copied!";
|
<button
|
||||||
target.parentElement?.appendChild(popup);
|
type="button"
|
||||||
|
className="select-none"
|
||||||
// Remove popup and border after delay
|
onClick={(e) => {
|
||||||
setTimeout(() => {
|
|
||||||
popup.remove();
|
|
||||||
target.style.borderBottom = "none";
|
|
||||||
}, 3000);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
navigator.clipboard.writeText(secretValue || "");
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
target.style.borderBottom = "1px dashed";
|
target.style.borderBottom = "1px dashed";
|
||||||
@@ -680,51 +715,72 @@ export const SecretDetailSidebar = ({
|
|||||||
popup.remove();
|
popup.remove();
|
||||||
target.style.borderBottom = "none";
|
target.style.borderBottom = "none";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
{secretValue}
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
</button>
|
const target = e.currentTarget;
|
||||||
<button
|
target.style.borderBottom = "1px dashed";
|
||||||
type="button"
|
target.style.paddingBottom = "-1px";
|
||||||
className="ml-1 cursor-pointer"
|
|
||||||
onClick={(e) => {
|
// Create and insert popup
|
||||||
e.stopPropagation();
|
const popup = document.createElement("div");
|
||||||
e.currentTarget
|
popup.className =
|
||||||
.closest(".group")
|
"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";
|
||||||
?.classList.remove("show-value");
|
popup.textContent = "Copied!";
|
||||||
}}
|
target.parentElement?.appendChild(popup);
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
// 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.stopPropagation();
|
||||||
e.currentTarget
|
e.currentTarget
|
||||||
.closest(".group")
|
.closest(".group")
|
||||||
?.classList.remove("show-value");
|
?.classList.remove("show-value");
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<FontAwesomeIcon icon={faEyeSlash} />
|
e.stopPropagation();
|
||||||
</button>
|
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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user