mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 15:28:12 +00:00
Merge branch 'main' into ENG-3723
This commit is contained in:
@@ -47,10 +47,12 @@ import {
|
||||
useGetWsTags
|
||||
} from "@app/hooks/api";
|
||||
import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/projects/types";
|
||||
import { queryClient } from "@app/hooks/api/reactQuery";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
@@ -152,6 +154,10 @@ const Page = () => {
|
||||
}
|
||||
}, [isBatchMode, projectId, environment, secretPath, loadPendingChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) setIsVisible(false);
|
||||
}, [environment]);
|
||||
|
||||
const canReadSecret = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
@@ -183,17 +189,6 @@ const Page = () => {
|
||||
})
|
||||
);
|
||||
|
||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: "*",
|
||||
secretTags: ["*"]
|
||||
}
|
||||
);
|
||||
|
||||
const canReadSecretImports = permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
@@ -271,7 +266,6 @@ const Page = () => {
|
||||
orderDirection,
|
||||
includeImports: canReadSecretImports && (isResourceTypeFiltered ? filter.include.import : true),
|
||||
includeFolders: isResourceTypeFiltered ? filter.include.folder : true,
|
||||
viewSecretValue: canReadSecretValue,
|
||||
includeDynamicSecrets:
|
||||
canReadDynamicSecret && (isResourceTypeFiltered ? filter.include.dynamic : true),
|
||||
includeSecrets: canReadSecret && (isResourceTypeFiltered ? filter.include.secret : true),
|
||||
@@ -347,6 +341,24 @@ const Page = () => {
|
||||
pendingChanges: changes,
|
||||
message
|
||||
});
|
||||
|
||||
if (!isProtectedBranch) {
|
||||
pendingChanges.secrets.forEach((secret) => {
|
||||
if (secret.type === "update" && secret.secretValue !== undefined) {
|
||||
queryClient.setQueryData(
|
||||
dashboardKeys.getSecretValue({
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secret.newSecretName ?? secret.secretKey,
|
||||
isOverride: false
|
||||
}),
|
||||
{ value: secret.secretValue }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: isProtectedBranch
|
||||
? "Requested changes have been sent for review"
|
||||
@@ -604,7 +616,9 @@ const Page = () => {
|
||||
return secrets;
|
||||
}
|
||||
|
||||
const mergedSecrets = [...(secrets || [])];
|
||||
const mergedSecrets = [...(secrets || [])] as (SecretV3RawSanitized & {
|
||||
originalKey?: string;
|
||||
})[];
|
||||
|
||||
pendingChanges.secrets.forEach((change) => {
|
||||
switch (change.type) {
|
||||
@@ -655,7 +669,8 @@ const Page = () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
__v: 0
|
||||
})) || []
|
||||
: mergedSecrets[updateIndex].tags
|
||||
: mergedSecrets[updateIndex].tags,
|
||||
originalKey: mergedSecrets[updateIndex].key
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface PendingSecretDelete extends BasePendingChange {
|
||||
type: PendingAction.Delete;
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretValueHidden: boolean;
|
||||
}
|
||||
|
||||
// Folder-related change types
|
||||
@@ -408,7 +409,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
|
||||
const mergedUpdate: PendingSecretUpdate = {
|
||||
...existingUpdate,
|
||||
secretKey: existingUpdate.secretKey,
|
||||
originalValue: existingUpdate.originalValue,
|
||||
originalValue: change.originalValue,
|
||||
originalComment: existingUpdate.originalComment,
|
||||
originalSkipMultilineEncoding: existingUpdate.originalSkipMultilineEncoding,
|
||||
originalTags: existingUpdate.originalTags,
|
||||
|
||||
+14
-3
@@ -13,6 +13,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Badge, Button, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { SecretVersionDiffView } from "@app/pages/secret-manager/CommitDetailsPage/components/SecretVersionDiffView";
|
||||
import { HIDDEN_SECRET_VALUE_API_MASK } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
import {
|
||||
PendingChange,
|
||||
@@ -105,7 +106,12 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
version: 1, // placeholder, not used
|
||||
secretKey: change.newSecretName ? existingSecret.key : undefined,
|
||||
secretValue:
|
||||
change.secretValue !== undefined ? (existingSecret.value ?? "") : undefined,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
change.secretValue !== undefined
|
||||
? change.existingSecret.secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE_API_MASK
|
||||
: (change.originalValue ?? "")
|
||||
: undefined,
|
||||
tags: change.tags ? (existingSecret.tags?.map((tag) => tag.slug) ?? []) : undefined,
|
||||
secretMetadata: change.secretMetadata ? existingSecret.secretMetadata : undefined,
|
||||
skipMultilineEncoding:
|
||||
@@ -130,7 +136,7 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
}
|
||||
|
||||
if (change.type === PendingAction.Delete) {
|
||||
const { secretKey, secretValue } = change;
|
||||
const { secretKey, secretValue, secretValueHidden } = change;
|
||||
return (
|
||||
<SecretVersionDiffView
|
||||
onDiscard={onDiscard}
|
||||
@@ -143,7 +149,12 @@ const RenderSecretChanges = ({ onDiscard, change }: RenderResourceProps) => {
|
||||
{
|
||||
version: 1, // placeholder, not used
|
||||
secretKey,
|
||||
secretValue
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
secretValue: secretValue
|
||||
? secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE_API_MASK
|
||||
: secretValue
|
||||
: undefined
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { faArrowRightArrowLeft, faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEllipsisH, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
@@ -199,14 +199,15 @@ export const EnvironmentTabs = ({ secretPath }: Props) => {
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
)}
|
||||
{currentProject.environments.length > 1 && (
|
||||
{/* scott: removing until we have time to update for fetching secret value */}
|
||||
{/* {currentProject.environments.length > 1 && (
|
||||
<Tab className="ml-auto" value={COMPARE_ENVIRONMENT_TAB}>
|
||||
<div className="flex items-center gap-x-2 whitespace-nowrap">
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} />
|
||||
Compare Environments
|
||||
</div>
|
||||
</Tab>
|
||||
)}
|
||||
)} */}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<Modal
|
||||
|
||||
+12
-15
@@ -20,12 +20,14 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useResyncSecretReplication } from "@app/hooks/api";
|
||||
import { TSecretImport } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretImportSecretRow } from "./SecretImportSecretRow";
|
||||
|
||||
type Props = {
|
||||
onDelete: () => void;
|
||||
environment: string;
|
||||
@@ -35,7 +37,10 @@ type Props = {
|
||||
importedSecrets: {
|
||||
key: string;
|
||||
value?: string;
|
||||
overriden: { env: string; secretPath: string };
|
||||
overridden: { env: string; secretPath: string };
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
isEmpty?: boolean;
|
||||
}[];
|
||||
searchTerm: string;
|
||||
onExpandReplicateSecrets: (id: string) => void;
|
||||
@@ -299,7 +304,6 @@ export const SecretImportItem = ({
|
||||
<tr>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
|
||||
{/* <td style={{ padding: "0.25rem 1rem" }}>Override</td> */}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -317,18 +321,11 @@ export const SecretImportItem = ({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{filteredImportedSecrets.map(({ key, value }, index) => (
|
||||
<tr key={`${id}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isReadOnly />
|
||||
</td>
|
||||
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td> */}
|
||||
</tr>
|
||||
{filteredImportedSecrets.map((secret, index) => (
|
||||
<SecretImportSecretRow
|
||||
secret={secret}
|
||||
key={`${id}-${secret.key}-${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+9
-3
@@ -64,18 +64,24 @@ export const computeImportedSecretRows = (
|
||||
const importedSecretEntries: {
|
||||
key: string;
|
||||
value?: string;
|
||||
overriden: {
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
overridden: {
|
||||
env: string;
|
||||
secretPath: string;
|
||||
};
|
||||
isEmpty?: boolean;
|
||||
}[] = [];
|
||||
|
||||
importedSec.secrets.forEach(({ key, value }) => {
|
||||
importedSec.secrets.forEach(({ key, value, env, path, isEmpty }) => {
|
||||
if (!importedEntry[key]) {
|
||||
importedSecretEntries.push({
|
||||
key,
|
||||
value,
|
||||
overriden: overridenSec?.[key]
|
||||
environment: env,
|
||||
secretPath: path,
|
||||
overridden: overridenSec?.[key],
|
||||
isEmpty
|
||||
});
|
||||
importedEntry[key] = true;
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { SecretInput } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetSecretValue } from "@app/hooks/api/dashboard/queries";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
type SecretImportSecretRowProps = {
|
||||
secret: {
|
||||
key: string;
|
||||
value?: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
isEmpty?: boolean;
|
||||
overridden: { env: string; secretPath: string };
|
||||
};
|
||||
};
|
||||
|
||||
export const SecretImportSecretRow = ({
|
||||
secret: { key, environment, secretPath = "/", isEmpty }
|
||||
}: SecretImportSecretRowProps) => {
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const canFetchSecretValue = !isEmpty;
|
||||
|
||||
const {
|
||||
data: secretValue,
|
||||
isPending: isPendingSecretValue,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
projectId: currentProject.id
|
||||
},
|
||||
{
|
||||
enabled: isFieldFocused && canFetchSecretValue
|
||||
}
|
||||
);
|
||||
|
||||
const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValue;
|
||||
|
||||
const getValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (isErrorFetchingSecretValue) return "Error loading secret value";
|
||||
|
||||
return secretValue?.value || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="h-10 w-1/2" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10 w-1/2" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput
|
||||
value={getValue()}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
isReadOnly
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
+158
-269
@@ -1,36 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateRight,
|
||||
faCheckCircle,
|
||||
faCopy,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faPlus,
|
||||
faProjectDiagram,
|
||||
faSearch,
|
||||
faServer,
|
||||
faShare,
|
||||
faTag,
|
||||
faTrash,
|
||||
faTriangleExclamation,
|
||||
faUser
|
||||
faTriangleExclamation
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
hasSecretReference,
|
||||
SecretReferenceTree
|
||||
} from "@app/components/secrets/SecretReferenceDetails";
|
||||
import { SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@@ -60,9 +51,13 @@ import {
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchSecretValue,
|
||||
useGetSecretValue
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
@@ -70,6 +65,7 @@ import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||
|
||||
import { HIDDEN_SECRET_VALUE } from "./SecretItem";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
import { SecretVersionItem } from "./SecretVersionItem";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -77,7 +73,7 @@ type Props = {
|
||||
secretPath: string;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
onClose: () => void;
|
||||
secret: SecretV3RawSanitized;
|
||||
secret: SecretV3RawSanitized & { originalKey?: string };
|
||||
onDeleteSecret: () => void;
|
||||
onSaveSecret: (
|
||||
orgSec: SecretV3RawSanitized,
|
||||
@@ -92,7 +88,7 @@ type Props = {
|
||||
export const SecretDetailSidebar = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
secret,
|
||||
secret: originalSecret,
|
||||
onDeleteSecret,
|
||||
onSaveSecret,
|
||||
tags,
|
||||
@@ -101,16 +97,90 @@ export const SecretDetailSidebar = ({
|
||||
secretPath,
|
||||
handleSecretShare
|
||||
}: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const canFetchSecretValue =
|
||||
Boolean(originalSecret) && !originalSecret.secretValueHidden && !originalSecret.isEmpty;
|
||||
|
||||
const fetchSecretValueParams = {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: originalSecret?.originalKey || originalSecret?.key,
|
||||
projectId: currentProject.id,
|
||||
isOverride: Boolean(originalSecret?.idOverride)
|
||||
};
|
||||
|
||||
const {
|
||||
data: secretValueData,
|
||||
isPending: isPendingSecretValue,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(fetchSecretValueParams, {
|
||||
enabled: canFetchSecretValue && isFieldFocused
|
||||
});
|
||||
|
||||
const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValue;
|
||||
const hasFetchedSecretValue = !canFetchSecretValue || Boolean(secretValueData);
|
||||
|
||||
const secret = {
|
||||
...originalSecret,
|
||||
value: originalSecret?.value ?? secretValueData?.value,
|
||||
valueOverride: originalSecret?.valueOverride ?? secretValueData?.valueOverride
|
||||
};
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const canEditSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secret.key,
|
||||
secretTags: ["*"]
|
||||
})
|
||||
);
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (secret.secretValueHidden) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
|
||||
if (isErrorFetchingSecretValue) return "Error loading secret value...";
|
||||
|
||||
return secret.value || "";
|
||||
};
|
||||
|
||||
const getOverrideDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (secret.secretValueHidden) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
|
||||
if (isErrorFetchingSecretValue) return "Error loading secret value...";
|
||||
|
||||
return secret.valueOverride || "";
|
||||
};
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
getValues,
|
||||
getFieldState,
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: secret,
|
||||
values: {
|
||||
...secret,
|
||||
valueOverride: getOverrideDefaultValue(),
|
||||
value: getDefaultValue()
|
||||
},
|
||||
disabled: !secret
|
||||
});
|
||||
|
||||
@@ -119,9 +189,6 @@ export const SecretDetailSidebar = ({
|
||||
"secretReferenceTree"
|
||||
] as const);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const tagFields = useFieldArray({
|
||||
control,
|
||||
name: "tags"
|
||||
@@ -139,7 +206,6 @@ export const SecretDetailSidebar = ({
|
||||
{}
|
||||
);
|
||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const cannotEditSecret = permission.cannot(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
@@ -205,7 +271,16 @@ export const SecretDetailSidebar = ({
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
await onSaveSecret(
|
||||
secret,
|
||||
{
|
||||
...secret,
|
||||
...data,
|
||||
value: getFieldState("value").isDirty ? data.value : undefined,
|
||||
valueOverride: getFieldState("valueOverride").isDirty ? data.valueOverride : undefined
|
||||
},
|
||||
() => reset()
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -218,58 +293,22 @@ export const SecretDetailSidebar = ({
|
||||
);
|
||||
}, [secret?.secretReminderRecipients]);
|
||||
|
||||
const getModifiedByIcon = (userType: string | undefined | null) => {
|
||||
switch (userType) {
|
||||
case ActorType.USER:
|
||||
return faUser;
|
||||
case ActorType.IDENTITY:
|
||||
return faDesktop;
|
||||
default:
|
||||
return faServer;
|
||||
}
|
||||
};
|
||||
const fetchValue = async () => {
|
||||
if (secretValueData) return secretValueData.valueOverride ?? secretValueData.value;
|
||||
|
||||
const getModifiedByName = (
|
||||
userType: string | undefined | null,
|
||||
userName: string | null | undefined
|
||||
) => {
|
||||
switch (userType) {
|
||||
case ActorType.PLATFORM:
|
||||
return "System-generated";
|
||||
case ActorType.IDENTITY:
|
||||
return userName || "Deleted Identity";
|
||||
case ActorType.USER:
|
||||
return userName || "Deleted User";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
try {
|
||||
const data = await fetchSecretValue(fetchSecretValueParams);
|
||||
|
||||
const getLinkToModifyHistoryEntity = (
|
||||
actorId: string,
|
||||
actorType: string,
|
||||
membershipId: string | null = ""
|
||||
) => {
|
||||
switch (actorType) {
|
||||
case ActorType.USER:
|
||||
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
|
||||
case ActorType.IDENTITY:
|
||||
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data);
|
||||
|
||||
const onModifyHistoryClick = (
|
||||
actorId: string | undefined | null,
|
||||
actorType: string | undefined | null,
|
||||
membershipId: string | undefined | null
|
||||
) => {
|
||||
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||
if (redirectLink) {
|
||||
navigate({ to: redirectLink });
|
||||
}
|
||||
return data?.valueOverride ?? data.value;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Error fetching secret value"
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -313,6 +352,7 @@ export const SecretDetailSidebar = ({
|
||||
title={`Secret – ${secret?.key}`}
|
||||
className="thin-scrollbar h-full"
|
||||
cardBodyClassName="pb-0"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(handleFormSubmit)}
|
||||
@@ -356,7 +396,14 @@ export const SecretDetailSidebar = ({
|
||||
>
|
||||
<div className="flex items-start gap-x-2">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret}
|
||||
isReadOnly={
|
||||
isReadOnly ||
|
||||
!isAllowed ||
|
||||
secret?.isRotatedSecret ||
|
||||
isLoadingSecretValue ||
|
||||
isErrorFetchingSecretValue
|
||||
}
|
||||
canEditButNotView={secret.secretValueHidden && canEditSecretValue}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
key="secret-value"
|
||||
@@ -364,6 +411,11 @@ export const SecretDetailSidebar = ({
|
||||
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
@@ -378,11 +430,17 @@ export const SecretDetailSidebar = ({
|
||||
className="px-2 py-[0.43rem] font-normal"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||
onClick={() => {
|
||||
const value = secret?.valueOverride ?? secret?.value;
|
||||
if (value) {
|
||||
handleSecretShare(value);
|
||||
onClick={async () => {
|
||||
let value: string | undefined;
|
||||
|
||||
if (hasFetchedSecretValue) {
|
||||
const values = getValues(["value", "valueOverride"]);
|
||||
value = secret.idOverride ? values[1] : values[0];
|
||||
} else {
|
||||
value = await fetchValue();
|
||||
}
|
||||
|
||||
handleSecretShare(value ?? "");
|
||||
}}
|
||||
>
|
||||
Share
|
||||
@@ -644,179 +702,21 @@ export const SecretDetailSidebar = ({
|
||||
<div className="dark flex max-h-[24rem] flex-1 cursor-default flex-col text-sm text-bunker-300">
|
||||
<div className="mb-0.5 text-mineshaft-400">Version History</div>
|
||||
<div className="thin-scrollbar flex flex-1 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, secretValueHidden, version, id, actor }) => (
|
||||
<div className="flex flex-row" key={id}>
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<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 w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<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
|
||||
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 text-left"
|
||||
onClick={(e) => {
|
||||
if (secretValueHidden) return;
|
||||
{secretVersion?.map((version) => (
|
||||
<SecretVersionItem
|
||||
canReadValue={!cannotReadSecretValue}
|
||||
secretVersion={version}
|
||||
secret={secret}
|
||||
currentVersion={secretVersion.length}
|
||||
onRevert={async (versionValue) => {
|
||||
await fetchValue();
|
||||
|
||||
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 || "");
|
||||
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
|
||||
className={twMerge(
|
||||
secretValueHidden && "text-xs text-bunker-300 opacity-40"
|
||||
)}
|
||||
>
|
||||
{secretValueHidden ? "Hidden" : secretValue}
|
||||
</span>
|
||||
</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 === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: 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>
|
||||
{!secret?.isRotatedSecret && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={() => setValue("value", secretValue, { shouldDirty: true })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
setTimeout(() => {
|
||||
setValue("value", versionValue, { shouldDirty: true });
|
||||
}, 5);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark flex flex-col text-sm text-bunker-300">
|
||||
@@ -942,26 +842,15 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-auto flex items-center space-x-2 pb-4">
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(secret?.value)
|
||||
? undefined
|
||||
: "Secret does not contain any references."
|
||||
}
|
||||
className="z-[100] text-center"
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="outline_bg"
|
||||
isDisabled={cannotReadSecretValue || secret.isEmpty}
|
||||
leftIcon={<FontAwesomeIcon icon={faProjectDiagram} />}
|
||||
onClick={() => handlePopUpOpen("secretReferenceTree", secretKey)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline_bg"
|
||||
isDisabled={cannotReadSecretValue || !hasSecretReference(secret?.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faProjectDiagram} />}
|
||||
onClick={() => handlePopUpOpen("secretReferenceTree", secretKey)}
|
||||
>
|
||||
Secret Reference Tree
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
Secret Reference Tree
|
||||
</Button>
|
||||
<Tooltip content="Copy Secret ID" className="z-[100]">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
|
||||
+135
-18
@@ -27,8 +27,8 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useProject
|
||||
useProject,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
@@ -47,6 +47,13 @@ import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { format } from "date-fns";
|
||||
import { CreateReminderForm } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchSecretValue,
|
||||
useGetSecretValue
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
@@ -56,11 +63,11 @@ import {
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { useBatchModeActions } from "../../SecretMainPage.store";
|
||||
|
||||
export const HIDDEN_SECRET_VALUE = "*****************************";
|
||||
export const HIDDEN_SECRET_VALUE = "*************************";
|
||||
export const HIDDEN_SECRET_VALUE_API_MASK = "<hidden-by-infisical>";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
secret: SecretV3RawSanitized & { originalKey?: string };
|
||||
onSaveSecret: (
|
||||
orgSec: SecretV3RawSanitized,
|
||||
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
|
||||
@@ -91,7 +98,7 @@ type Props = {
|
||||
|
||||
export const SecretItem = memo(
|
||||
({
|
||||
secret,
|
||||
secret: originalSecret,
|
||||
onSaveSecret,
|
||||
onDeleteSecret,
|
||||
onDetailViewSecret,
|
||||
@@ -114,9 +121,43 @@ export const SecretItem = memo(
|
||||
] as const);
|
||||
const { currentProject } = useProject();
|
||||
const { permission } = useProjectPermission();
|
||||
const { isRotatedSecret } = secret;
|
||||
const { removePendingChange } = useBatchModeActions();
|
||||
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const canFetchSecretValue =
|
||||
!originalSecret.secretValueHidden &&
|
||||
!originalSecret.isEmpty &&
|
||||
pendingAction !== PendingAction.Create;
|
||||
|
||||
const fetchSecretValueParams = {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: originalSecret.originalKey || originalSecret.key,
|
||||
projectId: currentProject.id,
|
||||
isOverride: Boolean(originalSecret.idOverride)
|
||||
};
|
||||
|
||||
const {
|
||||
data: secretValueData,
|
||||
isPending: isPendingSecretValueData,
|
||||
isError: isErrorFetchingSecretValue
|
||||
} = useGetSecretValue(fetchSecretValueParams, {
|
||||
enabled: canFetchSecretValue && (isVisible || isFieldFocused)
|
||||
});
|
||||
|
||||
const isLoadingSecretValue = canFetchSecretValue && isPendingSecretValueData;
|
||||
const hasFetchedSecretValue = !canFetchSecretValue || Boolean(secretValueData);
|
||||
|
||||
const secret = {
|
||||
...originalSecret,
|
||||
value: originalSecret.value ?? secretValueData?.value,
|
||||
valueOverride: originalSecret.valueOverride ?? secretValueData?.valueOverride
|
||||
};
|
||||
|
||||
const { isRotatedSecret } = secret;
|
||||
|
||||
const autoSaveTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const isAutoSavingRef = useRef(false);
|
||||
|
||||
@@ -139,10 +180,27 @@ export const SecretItem = memo(
|
||||
);
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return undefined;
|
||||
|
||||
if (secret.secretValueHidden && !isPending) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
return secret.valueOverride || secret.value || "";
|
||||
|
||||
if (isErrorFetchingSecretValue) return undefined;
|
||||
|
||||
return secret.value || "";
|
||||
};
|
||||
|
||||
const getOverrideDefaultValue = () => {
|
||||
if (isLoadingSecretValue) return undefined;
|
||||
|
||||
if (secret.secretValueHidden && !isPending) {
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
|
||||
if (isErrorFetchingSecretValue) return undefined;
|
||||
|
||||
return secret.valueOverride || "";
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -154,14 +212,17 @@ export const SecretItem = memo(
|
||||
reset,
|
||||
getValues,
|
||||
trigger,
|
||||
formState: { isDirty, isSubmitting, errors }
|
||||
formState: { isDirty, isSubmitting, errors },
|
||||
getFieldState
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: {
|
||||
...secret,
|
||||
valueOverride: getOverrideDefaultValue(),
|
||||
value: getDefaultValue()
|
||||
},
|
||||
values: {
|
||||
...secret,
|
||||
valueOverride: getOverrideDefaultValue(),
|
||||
value: getDefaultValue()
|
||||
},
|
||||
resolver: zodResolver(formSchema)
|
||||
@@ -255,7 +316,10 @@ export const SecretItem = memo(
|
||||
);
|
||||
|
||||
const isReadOnlySecret =
|
||||
isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete);
|
||||
isReadOnly ||
|
||||
isRotatedSecret ||
|
||||
(isPending && pendingAction === PendingAction.Delete) ||
|
||||
isLoadingSecretValue;
|
||||
|
||||
const { secretValueHidden } = secret;
|
||||
|
||||
@@ -322,12 +386,36 @@ export const SecretItem = memo(
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
const [overrideValue, value] = getValues(["value", "valueOverride"]);
|
||||
if (isOverridden) {
|
||||
navigator.clipboard.writeText(value as string);
|
||||
const fetchValue = async () => {
|
||||
if (secretValueData) return secretValueData;
|
||||
|
||||
try {
|
||||
const data = await fetchSecretValue(fetchSecretValueParams);
|
||||
|
||||
queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to fetch secret value"
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenToClipboard = async () => {
|
||||
if (hasFetchedSecretValue) {
|
||||
const [overrideValue, value] = getValues(["value", "valueOverride"]);
|
||||
if (isOverridden) {
|
||||
navigator.clipboard.writeText(value as string);
|
||||
} else {
|
||||
navigator.clipboard.writeText(overrideValue as string);
|
||||
}
|
||||
} else {
|
||||
navigator.clipboard.writeText(overrideValue as string);
|
||||
const data = await fetchValue();
|
||||
navigator.clipboard.writeText((data.valueOverride ?? data.value) as string);
|
||||
}
|
||||
setIsSecValueCopied.on();
|
||||
};
|
||||
@@ -410,7 +498,7 @@ export const SecretItem = memo(
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{secretValueHidden && !isOverridden && !isPending && (
|
||||
{secretValueHidden && !getFieldState("value").isDirty && (
|
||||
<Tooltip
|
||||
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
||||
>
|
||||
@@ -424,10 +512,18 @@ export const SecretItem = memo(
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
isLoadingValue={isLoadingSecretValue && Boolean(secret.idOverride)}
|
||||
isErrorLoadingValue={isErrorFetchingSecretValue}
|
||||
key="value-overriden"
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
{...field}
|
||||
onFocus={() => {
|
||||
if (secret.idOverride) setIsFieldFocused.on();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
containerClassName="py-1.5 rounded-md transition-all"
|
||||
/>
|
||||
)}
|
||||
@@ -439,6 +535,8 @@ export const SecretItem = memo(
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
isLoadingValue={isLoadingSecretValue}
|
||||
isErrorLoadingValue={isErrorFetchingSecretValue}
|
||||
isReadOnly={isReadOnlySecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible && (!secretValueHidden || isPending)}
|
||||
@@ -446,6 +544,13 @@ export const SecretItem = memo(
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
onFocus={() => {
|
||||
setIsFieldFocused.on();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFieldFocused.off();
|
||||
field.onBlur();
|
||||
}}
|
||||
defaultValue={
|
||||
secretValueHidden && !isPending ? HIDDEN_SECRET_VALUE : undefined
|
||||
}
|
||||
@@ -613,7 +718,7 @@ export const SecretItem = memo(
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -682,7 +787,19 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="share-secret"
|
||||
onClick={() => onShareSecret(secret)}
|
||||
onClick={async () => {
|
||||
if (hasFetchedSecretValue) {
|
||||
onShareSecret(secret);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await fetchValue();
|
||||
|
||||
onShareSecret({
|
||||
...secret,
|
||||
...data
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip content="Share Secret">
|
||||
<FontAwesomeSymbol
|
||||
|
||||
+23
-15
@@ -466,7 +466,12 @@ export const SecretListView = ({
|
||||
);
|
||||
|
||||
const handleSecretDelete = useCallback(async () => {
|
||||
const { key, id: secretId, value } = popUp.deleteSecret?.data as SecretV3RawSanitized;
|
||||
const {
|
||||
key,
|
||||
id: secretId,
|
||||
value,
|
||||
secretValueHidden
|
||||
} = popUp.deleteSecret?.data as SecretV3RawSanitized;
|
||||
try {
|
||||
if (isBatchMode) {
|
||||
const deleteChange: PendingSecretDelete = {
|
||||
@@ -475,7 +480,8 @@ export const SecretListView = ({
|
||||
secretKey: key,
|
||||
secretValue: value || "",
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret"
|
||||
resourceType: "secret",
|
||||
secretValueHidden
|
||||
};
|
||||
|
||||
addPendingChange(deleteChange, {
|
||||
@@ -616,19 +622,21 @@ export const SecretListView = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
|
||||
secret={popUp.secretDetail.data as SecretV3RawSanitized}
|
||||
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
|
||||
onClose={() => handlePopUpClose("secretDetail")}
|
||||
onSaveSecret={handleSaveSecret}
|
||||
tags={wsTags}
|
||||
onCreateTag={() => handlePopUpOpen("createTag")}
|
||||
handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })}
|
||||
/>
|
||||
{popUp.secretDetail.data && (
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
|
||||
secret={popUp.secretDetail.data as SecretV3RawSanitized}
|
||||
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
|
||||
onClose={() => handlePopUpClose("secretDetail")}
|
||||
onSaveSecret={handleSaveSecret}
|
||||
tags={wsTags}
|
||||
onCreateTag={() => handlePopUpOpen("createTag")}
|
||||
handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })}
|
||||
/>
|
||||
)}
|
||||
<CreateTagModal
|
||||
isOpen={popUp.createTag.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
||||
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
import { useState } from "react";
|
||||
import { faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateRight,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faServer,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { fetchSecretVersionValue } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, SecretVersions } from "@app/hooks/api/secrets/types";
|
||||
|
||||
interface SecretVersionItemProps {
|
||||
secretVersion: SecretVersions;
|
||||
secret: SecretV3RawSanitized;
|
||||
currentVersion: number;
|
||||
onRevert: (secretValue: string) => void;
|
||||
canReadValue: boolean;
|
||||
}
|
||||
|
||||
export const SecretVersionItem = ({
|
||||
secretVersion: { createdAt, version, actor, secretValueHidden },
|
||||
secret,
|
||||
currentVersion,
|
||||
onRevert,
|
||||
canReadValue
|
||||
}: SecretVersionItemProps) => {
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getModifiedByIcon = (userType: string | undefined | null) => {
|
||||
switch (userType) {
|
||||
case ActorType.USER:
|
||||
return faUser;
|
||||
case ActorType.IDENTITY:
|
||||
return faDesktop;
|
||||
default:
|
||||
return faServer;
|
||||
}
|
||||
};
|
||||
|
||||
const getModifiedByName = (
|
||||
userType: string | undefined | null,
|
||||
userName: string | null | undefined
|
||||
) => {
|
||||
switch (userType) {
|
||||
case ActorType.PLATFORM:
|
||||
return "System-generated";
|
||||
case ActorType.IDENTITY:
|
||||
return userName || "Deleted Identity";
|
||||
case ActorType.USER:
|
||||
return userName || "Deleted User";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const getLinkToModifyHistoryEntity = (
|
||||
actorId: string,
|
||||
actorType: string,
|
||||
membershipId: string | null = ""
|
||||
) => {
|
||||
switch (actorType) {
|
||||
case ActorType.USER:
|
||||
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
|
||||
case ActorType.IDENTITY:
|
||||
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onModifyHistoryClick = (
|
||||
actorId: string | undefined | null,
|
||||
actorType: string | undefined | null,
|
||||
membershipId: string | undefined | null
|
||||
) => {
|
||||
if (actorType && actorId && actorType !== ActorType.PLATFORM) {
|
||||
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId);
|
||||
if (redirectLink) {
|
||||
navigate({ to: redirectLink });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [secretValue, setSecretValue] = useState<string | null>(null);
|
||||
const [isFetchingValue, setIsFetchingValue] = useState(false);
|
||||
const handleGetSecretValue = async () => {
|
||||
if (secretValue) return secretValue;
|
||||
try {
|
||||
setIsFetchingValue(true);
|
||||
const value = await fetchSecretVersionValue(secret.id, version);
|
||||
setSecretValue(value);
|
||||
return value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to fetch secret version value"
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
setIsFetchingValue(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyValue = async (
|
||||
e: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
) => {
|
||||
const value = await handleGetSecretValue();
|
||||
navigator.clipboard.writeText(value || "");
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<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 w-full cursor-default flex-col">
|
||||
{actor && (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex w-fit flex-row text-sm">
|
||||
Modified by:
|
||||
<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 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 text-left"
|
||||
onClick={async (e) => {
|
||||
if (secretValueHidden) return;
|
||||
|
||||
await handleCopyValue(e);
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (secretValueHidden) return;
|
||||
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
await handleCopyValue(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={twMerge(secretValueHidden && "text-xs text-bunker-300 opacity-40")}
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{secretValueHidden
|
||||
? "Hidden"
|
||||
: isFetchingValue
|
||||
? "****"
|
||||
: ((secretValue || <span className="text-mineshaft-400">EMPTY</span>) ??
|
||||
"Error fetching secret value...")}
|
||||
</span>
|
||||
</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 === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget.closest(".group")?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
****
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={async (e) => {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
await handleGetSecretValue();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
await handleGetSecretValue();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!secret?.isRotatedSecret && canReadValue && (
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === currentVersion ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={async () => {
|
||||
if (secretValue) {
|
||||
onRevert(secretValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = await handleGetSecretValue();
|
||||
|
||||
onRevert(value);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+9
-39
@@ -15,13 +15,13 @@ import { twMerge } from "tailwind-merge";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { SecretRotationV2StatusBadge } from "@app/components/secret-rotations-v2/SecretRotationV2StatusBadge";
|
||||
import { IconButton, Modal, ModalContent, TableContainer, Tag, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
import { SecretRotationSecretRow } from "./SecretRotationSecretRow";
|
||||
|
||||
type Props = {
|
||||
secretRotation: TSecretRotationV2;
|
||||
onEdit: () => void;
|
||||
@@ -209,43 +209,13 @@ export const SecretRotationItem = ({
|
||||
<tbody>
|
||||
{secrets.map((secret, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
secret ? undefined : "You do not have permission to view this secret."
|
||||
}
|
||||
>
|
||||
<tr
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
className="h-full last:!border-b-0 hover:bg-mineshaft-700"
|
||||
>
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>
|
||||
{secret?.key ?? "********"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<InfisicalSecretInput
|
||||
isReadOnly
|
||||
value={secret.value}
|
||||
secretPath={secretRotation.folder.path}
|
||||
environment={secretRotation.environment.slug}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
<SecretRotationSecretRow
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||
secret={secret}
|
||||
environment={secretRotation.environment.slug}
|
||||
secretPath={secretRotation.folder.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { useProject } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetSecretValue } from "@app/hooks/api/dashboard/queries";
|
||||
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
interface SecretRotationSecretRowProps {
|
||||
secret: TSecretRotationV2["secrets"][number];
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
export const SecretRotationSecretRow = ({
|
||||
secret,
|
||||
environment,
|
||||
secretPath
|
||||
}: SecretRotationSecretRowProps) => {
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data: secretValue, isPending: isLoadingSecretValue } = useGetSecretValue(
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secret?.key ?? "",
|
||||
projectId: currentProject.id
|
||||
},
|
||||
{
|
||||
enabled: isFieldFocused && Boolean(secret)
|
||||
}
|
||||
);
|
||||
|
||||
const getValue = () => {
|
||||
if (isLoadingSecretValue) return HIDDEN_SECRET_VALUE;
|
||||
|
||||
if (!secretValue) return "Error loading secret value";
|
||||
|
||||
return secretValue.value || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={secret ? undefined : "You do not have permission to view this secret."}
|
||||
>
|
||||
<tr className="h-full last:!border-b-0 hover:bg-mineshaft-700">
|
||||
<td className="flex h-full items-center" style={{ padding: "0.5rem 1rem" }}>
|
||||
<span className={twMerge(!secret && "blur")}>{secret?.key ?? "********"}</span>
|
||||
</td>
|
||||
<td className="col-span-2 h-full w-full" style={{ padding: "0.5rem 1rem" }}>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!secret ? (
|
||||
<div className="h-full pl-4 blur">********</div>
|
||||
) : secret.secretValueHidden ? (
|
||||
<Blur
|
||||
className="py-0"
|
||||
tooltipText="You do not have permission to read the value of this secret."
|
||||
/>
|
||||
) : (
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
value={getValue()}
|
||||
onFocus={() => setIsFieldFocused.on()}
|
||||
onBlur={() => setIsFieldFocused.off()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user