mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 17:26:36 +00:00
requested changes
This commit is contained in:
@@ -23,12 +23,6 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
|
|||||||
subjectFields?: SecretSubjectFields
|
subjectFields?: SecretSubjectFields
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (subjectFields) {
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
|
||||||
} else {
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (subjectFields) {
|
if (subjectFields) {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
@@ -40,6 +34,12 @@ export function throwIfMissingSecretReadValueOrDescribePermission(
|
|||||||
ProjectPermissionSub.Secrets
|
ProjectPermissionSub.Secrets
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
if (subjectFields) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -834,6 +834,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/accessible-secrets",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
filterByAction: z
|
||||||
|
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
|
||||||
|
.default(ProjectPermissionSecretActions.ReadValue)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretPath: z.string().optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { projectId, environment, secretPath, filterByAction } = req.query;
|
||||||
|
|
||||||
|
const { secrets } = await server.services.secret.getAccessibleSecrets({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
projectId,
|
||||||
|
filterByAction
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/secrets-by-keys",
|
url: "/secrets-by-keys",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
TCreateSecretDTO,
|
TCreateSecretDTO,
|
||||||
TDeleteManySecretDTO,
|
TDeleteManySecretDTO,
|
||||||
TDeleteSecretDTO,
|
TDeleteSecretDTO,
|
||||||
|
TGetAccessibleSecretsDTO,
|
||||||
TGetASecretDTO,
|
TGetASecretDTO,
|
||||||
TGetSecretReferencesTreeDTO,
|
TGetSecretReferencesTreeDTO,
|
||||||
TGetSecretsDTO,
|
TGetSecretsDTO,
|
||||||
@@ -200,7 +201,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
|
|
||||||
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
|
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
|
||||||
references.forEach((el) => {
|
references.forEach((el) => {
|
||||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
environment: el.environment,
|
environment: el.environment,
|
||||||
secretPath: el.secretPath,
|
secretPath: el.secretPath,
|
||||||
secretName: el.secretKey,
|
secretName: el.secretKey,
|
||||||
@@ -1291,7 +1292,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||||
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined,
|
expandSecretReferences: shouldExpandSecretReferences && viewSecretValue ? expandSecretReferences : undefined,
|
||||||
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
|
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => {
|
||||||
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
environment: expandEnvironment,
|
environment: expandEnvironment,
|
||||||
@@ -1347,7 +1348,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
let secretValue = secret.encryptedValue
|
let secretValue = secret.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||||
: "";
|
: "";
|
||||||
if (shouldExpandSecretReferences && secretValue) {
|
if (shouldExpandSecretReferences && secretValue && viewSecretValue) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const expandedSecretValue = await expandSecretReferences({
|
const expandedSecretValue = await expandSecretReferences({
|
||||||
environment,
|
environment,
|
||||||
@@ -2613,6 +2614,86 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
return { tree: stackTrace, value: expandedValue };
|
return { tree: stackTrace, value: expandedValue };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAccessibleSecrets = async ({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetAccessibleSecretsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
|
if (!folder) return { secrets: [] };
|
||||||
|
|
||||||
|
const secrets = await secretDAL.findByFolderIds([folder.id]);
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecrets = secrets
|
||||||
|
.filter((el) => {
|
||||||
|
if (
|
||||||
|
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: el.key,
|
||||||
|
secretTags: el.tags.map((i) => i.slug)
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterByAction === ProjectPermissionSecretActions.ReadValue) {
|
||||||
|
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: el.key,
|
||||||
|
secretTags: el.tags.map((i) => i.slug)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((secret) => {
|
||||||
|
return reshapeBridgeSecret(
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
{
|
||||||
|
...secret,
|
||||||
|
value: secret.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
|
comment: secret.encryptedComment
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||||
|
: ""
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
secrets: decryptedSecrets
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createSecret,
|
createSecret,
|
||||||
deleteSecret,
|
deleteSecret,
|
||||||
@@ -2630,6 +2711,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecretsMultiEnv,
|
getSecretsMultiEnv,
|
||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsByFolderMappings,
|
getSecretsByFolderMappings,
|
||||||
getSecretById
|
getSecretById,
|
||||||
|
getAccessibleSecrets
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -351,3 +351,10 @@ export type TGetSecretsRawByFolderMappingsDTO = {
|
|||||||
filters: TFindSecretsByFolderIdsFilter;
|
filters: TFindSecretsByFolderIdsFilter;
|
||||||
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetAccessibleSecretsDTO = {
|
||||||
|
environment: string;
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import {
|
|||||||
TDeleteManySecretRawDTO,
|
TDeleteManySecretRawDTO,
|
||||||
TDeleteSecretDTO,
|
TDeleteSecretDTO,
|
||||||
TDeleteSecretRawDTO,
|
TDeleteSecretRawDTO,
|
||||||
|
TGetAccessibleSecretsDTO,
|
||||||
TGetASecretByIdRawDTO,
|
TGetASecretByIdRawDTO,
|
||||||
TGetASecretDTO,
|
TGetASecretDTO,
|
||||||
TGetASecretRawDTO,
|
TGetASecretRawDTO,
|
||||||
@@ -1312,6 +1313,39 @@ export const secretServiceFactory = ({
|
|||||||
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
|
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAccessibleSecrets = async ({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
environment,
|
||||||
|
filterByAction
|
||||||
|
}: TGetAccessibleSecretsDTO) => {
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
|
if (!shouldUseSecretV2Bridge) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Project version does not support this endpoint.",
|
||||||
|
name: "ProjectVersionNotSupported"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const secrets = await secretV2BridgeService.getAccessibleSecrets({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
const getSecretsRaw = async ({
|
const getSecretsRaw = async ({
|
||||||
projectId,
|
projectId,
|
||||||
path,
|
path,
|
||||||
@@ -3261,6 +3295,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsRawByFolderMappings,
|
getSecretsRawByFolderMappings,
|
||||||
getSecretAccessList,
|
getSecretAccessList,
|
||||||
getSecretByIdRaw
|
getSecretByIdRaw,
|
||||||
|
getAccessibleSecrets
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-
|
|||||||
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
|
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||||
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
|
|
||||||
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
|
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
|
||||||
|
|
||||||
@@ -180,6 +181,12 @@ export enum SecretsOrderBy {
|
|||||||
Name = "name" // "key" for secrets but using name for use across resources
|
Name = "name" // "key" for secrets but using name for use across resources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TGetAccessibleSecretsDTO = {
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetSecretsRawDTO = {
|
export type TGetSecretsRawDTO = {
|
||||||
expandSecretReferences?: boolean;
|
expandSecretReferences?: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
|
useGetAccessibleSecrets,
|
||||||
useGetProjectSecretsDetails,
|
useGetProjectSecretsDetails,
|
||||||
useGetProjectSecretsOverview,
|
useGetProjectSecretsOverview,
|
||||||
useGetProjectSecretsQuickSearch
|
useGetProjectSecretsQuickSearch
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DashboardSecretsOrderBy,
|
DashboardSecretsOrderBy,
|
||||||
TDashboardProjectSecretsQuickSearch,
|
TDashboardProjectSecretsQuickSearch,
|
||||||
TDashboardProjectSecretsQuickSearchResponse,
|
TDashboardProjectSecretsQuickSearchResponse,
|
||||||
|
TGetAccessibleSecretsDTO,
|
||||||
TGetDashboardProjectSecretsByKeys,
|
TGetDashboardProjectSecretsByKeys,
|
||||||
TGetDashboardProjectSecretsDetailsDTO,
|
TGetDashboardProjectSecretsDetailsDTO,
|
||||||
TGetDashboardProjectSecretsOverviewDTO,
|
TGetDashboardProjectSecretsOverviewDTO,
|
||||||
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
|
|||||||
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
||||||
import { groupBy, unique } from "@app/lib/fn/array";
|
import { groupBy, unique } from "@app/lib/fn/array";
|
||||||
|
|
||||||
|
import { SecretV3Raw } from "../types";
|
||||||
|
|
||||||
export const dashboardKeys = {
|
export const dashboardKeys = {
|
||||||
all: () => ["dashboard"] as const,
|
all: () => ["dashboard"] as const,
|
||||||
getDashboardSecrets: ({
|
getDashboardSecrets: ({
|
||||||
@@ -58,7 +61,14 @@ export const dashboardKeys = {
|
|||||||
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
|
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
|
||||||
"quick-search",
|
"quick-search",
|
||||||
params
|
params
|
||||||
] as const
|
] as const,
|
||||||
|
getAccessibleSecrets: ({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction
|
||||||
|
}: TGetAccessibleSecretsDTO) =>
|
||||||
|
[...dashboardKeys.all(), { projectId, secretPath, environment, filterByAction }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchProjectSecretsOverview = async ({
|
export const fetchProjectSecretsOverview = async ({
|
||||||
@@ -295,6 +305,22 @@ export const fetchProjectSecretsQuickSearch = async ({
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchAccessibleSecrets = async ({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction
|
||||||
|
}: TGetAccessibleSecretsDTO) => {
|
||||||
|
const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
|
||||||
|
"/api/v1/dashboard/accessible-secrets",
|
||||||
|
{
|
||||||
|
params: { projectId, secretPath, environment, filterByAction }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.secrets;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetProjectSecretsQuickSearch = (
|
export const useGetProjectSecretsQuickSearch = (
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
@@ -357,3 +383,32 @@ export const useGetProjectSecretsQuickSearch = (
|
|||||||
placeholderData: (previousData) => previousData
|
placeholderData: (previousData) => previousData
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetAccessibleSecrets = ({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction,
|
||||||
|
options
|
||||||
|
}: TGetAccessibleSecretsDTO & {
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
SecretV3Raw[],
|
||||||
|
unknown,
|
||||||
|
SecretV3Raw[],
|
||||||
|
ReturnType<typeof dashboardKeys.getAccessibleSecrets>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>;
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
...options,
|
||||||
|
queryKey: dashboardKeys.getAccessibleSecrets({
|
||||||
|
projectId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
filterByAction
|
||||||
|
}),
|
||||||
|
queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||||
@@ -101,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
keys: string[];
|
keys: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetAccessibleSecretsDTO = {
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
filterByAction:
|
||||||
|
| ProjectPermissionSecretActions.DescribeSecret
|
||||||
|
| ProjectPermissionSecretActions.ReadValue;
|
||||||
|
};
|
||||||
|
|||||||
+47
-42
@@ -27,9 +27,11 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Blur } from "@app/components/v2/Blur";
|
import { Blur } from "@app/components/v2/Blur";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
|
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { SecretType } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultValue?: string | null;
|
defaultValue?: string | null;
|
||||||
@@ -79,6 +81,9 @@ export const SecretEditRow = ({
|
|||||||
value: defaultValue || null
|
value: defaultValue || null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
const [isDeleting, setIsDeleting] = useToggle();
|
const [isDeleting, setIsDeleting] = useToggle();
|
||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -120,6 +125,11 @@ export const SecretEditRow = ({
|
|||||||
reset({ value });
|
reset({ value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeleteSecret = useCallback(async () => {
|
const handleDeleteSecret = useCallback(async () => {
|
||||||
setIsDeleting.on();
|
setIsDeleting.on();
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
@@ -229,48 +239,43 @@ export const SecretEditRow = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Read}
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
a={ProjectPermissionSub.Secrets}
|
<Modal>
|
||||||
>
|
<ModalTrigger asChild>
|
||||||
{(isAllowed) => (
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<Tooltip
|
||||||
<Modal>
|
content={
|
||||||
<ModalTrigger asChild>
|
hasSecretReference(defaultValue || "")
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
? "Secret Reference Tree"
|
||||||
<Tooltip
|
: "Secret does not contain references"
|
||||||
content={
|
}
|
||||||
hasSecretReference(defaultValue || "")
|
|
||||||
? "Secret Reference Tree"
|
|
||||||
: "Secret does not contain references"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="reference-tree"
|
|
||||||
className="h-full"
|
|
||||||
isDisabled={!hasSecretReference(defaultValue || "") || !isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faProjectDiagram} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent
|
|
||||||
title="Secret Reference Details"
|
|
||||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
|
|
||||||
>
|
>
|
||||||
<SecretReferenceTree
|
<IconButton
|
||||||
secretPath={secretPath}
|
variant="plain"
|
||||||
environment={environment}
|
ariaLabel="reference-tree"
|
||||||
secretKey={secretName}
|
className="h-full"
|
||||||
/>
|
isDisabled={!hasSecretReference(defaultValue || "") || !canReadSecretValue}
|
||||||
</ModalContent>
|
>
|
||||||
</Modal>
|
<FontAwesomeIcon icon={faProjectDiagram} />
|
||||||
</div>
|
</IconButton>
|
||||||
)}
|
</Tooltip>
|
||||||
</ProjectPermissionCan>
|
</div>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Secret Reference Details"
|
||||||
|
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
|
||||||
|
>
|
||||||
|
<SecretReferenceTree
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
secretKey={secretName}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
|||||||
+34
-26
@@ -20,8 +20,9 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useDebounce } from "@app/hooks";
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
@@ -32,7 +33,7 @@ const formSchema = z.object({
|
|||||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||||
),
|
),
|
||||||
secrets: z
|
secrets: z
|
||||||
.object({ key: z.string(), value: z.string().optional() })
|
.object({ secretKey: z.string(), secretValue: z.string().optional() })
|
||||||
.array()
|
.array()
|
||||||
.min(1, "Select one or more secrets to copy")
|
.min(1, "Select one or more secrets to copy")
|
||||||
});
|
});
|
||||||
@@ -78,34 +79,38 @@ export const CopySecretsFromBoard = ({
|
|||||||
const selectedEnvSlug = watch("environment");
|
const selectedEnvSlug = watch("environment");
|
||||||
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
|
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
|
||||||
|
|
||||||
const { data: secrets, isPending: isSecretsLoading } = useGetProjectSecrets({
|
const { data: accessibleSecrets, isPending: isAccessibleSecretsLoading } =
|
||||||
workspaceId,
|
useGetAccessibleSecrets({
|
||||||
environment: selectedEnvSlug.slug,
|
projectId: workspaceId,
|
||||||
secretPath: debouncedEnvCopySecretPath,
|
secretPath: debouncedEnvCopySecretPath,
|
||||||
options: {
|
environment: selectedEnvSlug.slug,
|
||||||
enabled:
|
filterByAction: shouldIncludeValues
|
||||||
Boolean(workspaceId) &&
|
? ProjectPermissionSecretActions.ReadValue
|
||||||
Boolean(selectedEnvSlug) &&
|
: ProjectPermissionSecretActions.DescribeSecret,
|
||||||
Boolean(debouncedEnvCopySecretPath) &&
|
options: {
|
||||||
isOpen
|
enabled:
|
||||||
}
|
Boolean(workspaceId) &&
|
||||||
});
|
Boolean(selectedEnvSlug) &&
|
||||||
|
Boolean(debouncedEnvCopySecretPath) &&
|
||||||
|
isOpen
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("secrets", []);
|
setValue("secrets", []);
|
||||||
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
|
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
|
||||||
|
|
||||||
const handleSecSelectAll = () => {
|
const handleSecSelectAll = () => {
|
||||||
if (secrets) {
|
if (accessibleSecrets) {
|
||||||
setValue("secrets", secrets, { shouldDirty: true });
|
setValue("secrets", accessibleSecrets, { shouldDirty: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (data: TFormSchema) => {
|
const handleFormSubmit = async (data: TFormSchema) => {
|
||||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||||
data.secrets.forEach(({ key, value }) => {
|
data.secrets.forEach(({ secretKey, secretValue }) => {
|
||||||
secretsToBePulled[key] = {
|
secretsToBePulled[secretKey] = {
|
||||||
value: (shouldIncludeValues && value) || "",
|
value: (shouldIncludeValues && secretValue) || "",
|
||||||
comments: [""]
|
comments: [""]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -202,19 +207,19 @@ export const CopySecretsFromBoard = ({
|
|||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
placeholder={
|
placeholder={
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
isSecretsLoading
|
isAccessibleSecretsLoading
|
||||||
? "Loading secrets..."
|
? "Loading secrets..."
|
||||||
: secrets?.length
|
: accessibleSecrets?.length
|
||||||
? "Select secrets..."
|
? "Select secrets..."
|
||||||
: "No secrets found..."
|
: "No secrets found..."
|
||||||
}
|
}
|
||||||
isLoading={isSecretsLoading}
|
isLoading={isAccessibleSecretsLoading}
|
||||||
options={secrets}
|
options={accessibleSecrets}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
isMulti
|
isMulti
|
||||||
getOptionValue={(option) => option.key}
|
getOptionValue={(option) => option.secretKey}
|
||||||
getOptionLabel={(option) => option.key}
|
getOptionLabel={(option) => option.secretKey}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -235,7 +240,10 @@ export const CopySecretsFromBoard = ({
|
|||||||
<Switch
|
<Switch
|
||||||
id="populate-include-value"
|
id="populate-include-value"
|
||||||
isChecked={shouldIncludeValues}
|
isChecked={shouldIncludeValues}
|
||||||
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)}
|
onCheckedChange={(isChecked) => {
|
||||||
|
setValue("secrets", []);
|
||||||
|
setShouldIncludeValues(isChecked as boolean);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Include secret values
|
Include secret values
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
+106
-111
@@ -13,8 +13,8 @@ import {
|
|||||||
faShare,
|
faShare,
|
||||||
faTag,
|
faTag,
|
||||||
faTrash,
|
faTrash,
|
||||||
faUser,
|
faTriangleExclamation,
|
||||||
faTriangleExclamation
|
faUser
|
||||||
} 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";
|
||||||
@@ -728,8 +728,8 @@ export const SecretDetailSidebar = ({
|
|||||||
<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(
|
{secretVersion?.map(
|
||||||
({ createdAt, secretValue, version, id, secretValueHidden, actor }, index) => (
|
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
|
||||||
<div key={`secret-version-${index + 1}`} 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">
|
||||||
<div className="w-10">
|
<div className="w-10">
|
||||||
@@ -739,72 +739,50 @@ export const SecretDetailSidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
<div className="flex w-full cursor-default">
|
||||||
</div>
|
<div className="relative w-10">
|
||||||
<div className="flex w-full cursor-default">
|
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||||
<div className="relative w-10">
|
</div>
|
||||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
<div className="flex w-full cursor-default flex-col">
|
||||||
</div>
|
{actor && (
|
||||||
<div className="flex w-full cursor-default flex-col">
|
<div className="flex flex-row">
|
||||||
{actor && (
|
<div className="flex w-fit flex-row text-sm">
|
||||||
<div className="flex flex-row">
|
Modified by:
|
||||||
<div className="flex w-fit flex-row text-sm">
|
<Tooltip
|
||||||
Modified by:
|
content={getModifiedByName(actor.actorType, actor.name)}
|
||||||
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
|
||||||
<div
|
|
||||||
onClick={() =>
|
|
||||||
onModifyHistoryClick(
|
|
||||||
actor.actorId,
|
|
||||||
actor.actorType,
|
|
||||||
actor.membershipId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||||
icon={getModifiedByIcon(actor.actorType)}
|
<div
|
||||||
className="ml-2"
|
onClick={() =>
|
||||||
/>
|
onModifyHistoryClick(
|
||||||
</div>
|
actor.actorId,
|
||||||
</Tooltip>
|
actor.actorType,
|
||||||
|
actor.membershipId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={getModifiedByIcon(actor.actorType)}
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
<div className="flex flex-row">
|
||||||
<div className="flex flex-row">
|
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
Value:
|
||||||
Value:
|
</div>
|
||||||
</div>
|
<div className="group break-all pl-1 font-mono">
|
||||||
<div className="group break-all pl-1 font-mono">
|
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="select-none text-left"
|
||||||
className="select-none text-left"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
if (secretValueHidden) return;
|
||||||
if (secretValueHidden) return;
|
|
||||||
|
|
||||||
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 (secretValueHidden) return;
|
|
||||||
|
|
||||||
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";
|
||||||
@@ -822,13 +800,30 @@ export const SecretDetailSidebar = ({
|
|||||||
popup.remove();
|
popup.remove();
|
||||||
target.style.borderBottom = "none";
|
target.style.borderBottom = "none";
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (secretValueHidden) return;
|
||||||
<Tooltip
|
|
||||||
className="break-normal text-xs"
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
content="You do not have permission to view this secret value"
|
navigator.clipboard.writeText(secretValue || "");
|
||||||
isDisabled={!secretValueHidden}
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -837,50 +832,50 @@ export const SecretDetailSidebar = ({
|
|||||||
>
|
>
|
||||||
{secretValueHidden ? "Hidden" : secretValue}
|
{secretValueHidden ? "Hidden" : secretValue}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="ml-1 cursor-pointer"
|
||||||
className="ml-1 cursor-pointer"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.currentTarget
|
|
||||||
.closest(".group")
|
|
||||||
?.classList.remove("show-value");
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
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
|
||||||
</div>
|
.closest(".group")
|
||||||
<span className="group-[.show-value]:hidden">
|
?.classList.remove("show-value");
|
||||||
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
|
}
|
||||||
<button
|
}}
|
||||||
type="button"
|
>
|
||||||
className="ml-1 cursor-pointer"
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
onClick={(e) => {
|
</button>
|
||||||
e.currentTarget
|
</div>
|
||||||
.closest(".group")
|
<span className="group-[.show-value]:hidden">
|
||||||
?.classList.add("show-value");
|
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
|
||||||
}}
|
<button
|
||||||
onKeyDown={(e) => {
|
type="button"
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
className="ml-1 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
e.currentTarget
|
e.currentTarget
|
||||||
.closest(".group")
|
.closest(".group")
|
||||||
?.classList.add("show-value");
|
?.classList.add("show-value");
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
>
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
<FontAwesomeIcon icon={faEye} />
|
e.currentTarget
|
||||||
</button>
|
.closest(".group")
|
||||||
</span>
|
?.classList.add("show-value");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEye} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user