Merge branch 'main' into feat/remindersImprovement

This commit is contained in:
Carlos Monastyrski
2025-07-17 17:35:41 -03:00
460 changed files with 8384 additions and 2476 deletions
@@ -3,35 +3,8 @@ import crypto from "crypto";
import { NavigateFn } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { localStorageService } from "@app/helpers/localStorage";
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: botPublicKey,
privateKey: PRIVATE_KEY
});
return { encryptedKey: ciphertext, nonce };
};
import { TCloudIntegration } from "@app/hooks/api/types";
export const createIntegrationMissingEnvVarsNotification = (
slug: string,
@@ -0,0 +1,14 @@
import { TChecklySync } from "@app/hooks/api/secretSyncs/types/checkly-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TChecklySync;
};
export const ChecklySyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -0,0 +1,14 @@
import { TCloudflareWorkersSync } from "@app/hooks/api/secretSyncs/types/cloudflare-workers-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TCloudflareWorkersSync;
};
export const CloudflareWorkersSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -7,7 +7,9 @@ import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfiguration
import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol";
import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol";
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
import { ChecklySyncDestinationCol } from "./ChecklySyncDestinationCol";
import { CloudflarePagesSyncDestinationCol } from "./CloudflarePagesSyncDestinationCol";
import { CloudflareWorkersSyncDestinationCol } from "./CloudflareWorkersSyncDestinationCol";
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
@@ -19,6 +21,7 @@ import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol";
import { SupabaseSyncDestinationCol } from "./SupabaseSyncDestinationCol";
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
@@ -75,10 +78,16 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <GitLabSyncDestinationCol secretSync={secretSync} />;
case SecretSync.CloudflarePages:
return <CloudflarePagesSyncDestinationCol secretSync={secretSync} />;
case SecretSync.CloudflareWorkers:
return <CloudflareWorkersSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Zabbix:
return <ZabbixSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Railway:
return <RailwaySyncDestinationCol secretSync={secretSync} />;
case SecretSync.Checkly:
return <ChecklySyncDestinationCol secretSync={secretSync} />;
case SecretSync.Supabase:
return <SupabaseSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -0,0 +1,14 @@
import { TSupabaseSync } from "@app/hooks/api/secretSyncs/types/supabase";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TSupabaseSync;
};
export const SupabaseSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -115,8 +115,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
secondaryText = "Vault ID";
break;
case SecretSync.AzureDevOps:
primaryText = destinationConfig.devopsProjectName;
secondaryText = destinationConfig.devopsProjectId;
primaryText = destinationConfig.devopsProjectName || destinationConfig.devopsProjectId;
secondaryText = destinationConfig.devopsProjectName
? destinationConfig.devopsProjectId
: "Project ID";
break;
case SecretSync.Heroku:
primaryText = destinationConfig.appName;
@@ -145,6 +147,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.projectName;
secondaryText = destinationConfig.environment;
break;
case SecretSync.CloudflareWorkers:
primaryText = destinationConfig.scriptId;
secondaryText = "Script ID";
break;
case SecretSync.Zabbix:
if (destinationConfig.scope === ZabbixSyncScope.Host) {
primaryText = destinationConfig.hostName;
@@ -157,8 +163,16 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
}
break;
case SecretSync.Railway:
primaryText = "Railway Project";
secondaryText = destinationConfig.projectName;
primaryText = destinationConfig.projectName;
secondaryText = "Railway Project";
break;
case SecretSync.Checkly:
primaryText = destinationConfig.accountName;
secondaryText = "Checkly Account";
break;
case SecretSync.Supabase:
primaryText = destinationConfig.projectName;
secondaryText = "Supabase Project";
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
@@ -174,7 +174,7 @@ export const SecretOverviewTableRow = ({
)}
{isSecretEmpty && (
<Tooltip content="Empty value">
<FontAwesomeIcon size="sm" icon={faCircle} />
<FontAwesomeIcon size="sm" icon={faCircle} className="text-yellow" />
</Tooltip>
)}
</div>
@@ -1,15 +1,19 @@
import { Modal, ModalContent } from "@app/components/v2";
import { ProjectPermissionActions } from "@app/context";
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
import { SpecificPrivilegeSecretForm } from "@app/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection";
export const RequestAccessModal = ({
isOpen,
onOpenChange,
policies
policies,
...props
}: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
policies: TAccessApprovalPolicy[];
selectedActions?: ProjectPermissionActions[];
secretPath?: string;
}) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -18,7 +22,11 @@ export const RequestAccessModal = ({
title="Request Access"
subTitle="Request access to any secrets and resources based on the predefined policies."
>
<SpecificPrivilegeSecretForm onClose={() => onOpenChange(false)} policies={policies} />
<SpecificPrivilegeSecretForm
onClose={() => onOpenChange(false)}
policies={policies}
{...props}
/>
</ModalContent>
</Modal>
);
@@ -255,6 +255,11 @@ export const ReviewAccessRequestModal = ({
return "You are not the reviewer in this step.";
};
// users can always reject (cancel) their own request
const isRejectionDisabled = request.isRequestedByCurrentUser
? false
: !(request.isApprover && request.isSelfApproveAllowed) && !bypassApproval;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -445,7 +450,7 @@ export const ReviewAccessRequestModal = ({
onCheckedChange={(checked) => setBypassApproval(checked === true)}
isChecked={bypassApproval}
id="byPassApproval"
className={twMerge("mr-2", bypassApproval ? "!border-red/30 !bg-red/10" : "")}
className={twMerge("mr-2", bypassApproval ? "!border-red/50 !bg-red/30" : "")}
>
<span className="text-xs text-red">
Approve without waiting for requirements to be met (bypass policy protection)
@@ -489,14 +494,7 @@ export const ReviewAccessRequestModal = ({
</Button>
<Button
isLoading={isLoading === "rejected"}
isDisabled={
!!isLoading ||
(!(
request.isApprover &&
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
) &&
!bypassApproval)
}
isDisabled={!!isLoading || isRejectionDisabled}
onClick={() => handleReview("rejected")}
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
size="sm"
@@ -21,6 +21,7 @@ import {
Tag,
Tooltip
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { getMemberLabel } from "@app/helpers/members";
import { policyDetails } from "@app/helpers/policies";
@@ -203,6 +204,7 @@ const Form = ({
const formUserBypassers = watch("userBypassers");
const formGroupBypassers = watch("groupBypassers");
const formEnvironment = watch("environment")?.slug;
const bypasserCount = (formUserBypassers || []).length + (formGroupBypassers || []).length;
const handleCreatePolicy = async ({
@@ -474,7 +476,11 @@ const Form = ({
errorText={error?.message}
className="flex-1"
>
<Input {...field} value={field.value || ""} />
<SecretPathInput
{...field}
value={field.value || ""}
environment={formEnvironment}
/>
</FormControl>
)}
/>
@@ -6,16 +6,19 @@ import {
faCheckCircle,
faChevronDown,
faCodeBranch,
faCodeMerge,
faMagnifyingGlass,
faSearch
faSearch,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSearch } from "@tanstack/react-router";
import { formatDistance } from "date-fns";
import { format, formatDistance } from "date-fns";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
@@ -25,7 +28,8 @@ import {
EmptyState,
Input,
Pagination,
Skeleton
Skeleton,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
@@ -308,7 +312,9 @@ export const SecretApprovalRequest = () => {
createdAt,
reviewers,
status,
committerUser
committerUser,
hasMerged,
updatedAt
} = secretApproval;
const isReviewed = reviewers.some(
({ status: reviewStatus, userId }) =>
@@ -317,7 +323,7 @@ export const SecretApprovalRequest = () => {
return (
<div
key={reqId}
className="flex flex-col border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
className="flex border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setSelectedApprovalId(secretApproval.id)}
@@ -325,23 +331,46 @@ export const SecretApprovalRequest = () => {
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
}}
>
<div className="mb-1 text-sm">
<FontAwesomeIcon
icon={faCodeBranch}
size="sm"
className="mr-1.5 text-mineshaft-300"
/>
{secretApproval.isReplicated
? `${commits.length} secret pending import`
: generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
<div className="flex flex-col">
<div className="mb-1 text-sm">
<FontAwesomeIcon
icon={faCodeBranch}
size="sm"
className="mr-1.5 text-mineshaft-300"
/>
{secretApproval.isReplicated
? `${commits.length} secret pending import`
: generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
</div>
<span className="text-xs leading-3 text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser ? (
<>
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email})
</>
) : (
<span className="text-gray-600">Deleted User</span>
)}
{!isReviewed && status === "open" && " - Review required"}
</span>
</div>
<span className="text-xs leading-3 text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email})
{!isReviewed && status === "open" && " - Review required"}
</span>
{status === "close" && (
<Tooltip
content={updatedAt ? format(new Date(updatedAt), "M/dd/yyyy h:mm a") : ""}
>
<div className="my-auto ml-auto">
<Badge
variant={hasMerged ? "success" : "danger"}
className="flex h-min items-center gap-1"
>
<FontAwesomeIcon icon={hasMerged ? faCodeMerge : faXmark} />
{hasMerged ? "Merged" : "Rejected"}
</Badge>
</div>
</Tooltip>
)}
</div>
);
})}
@@ -168,7 +168,7 @@ export const SecretApprovalRequestAction = ({
isChecked={byPassApproval}
id="byPassApproval"
checkIndicatorBg="text-white"
className={twMerge("mr-2", byPassApproval ? "!border-red/30 !bg-red/10" : "")}
className={twMerge("mr-2", byPassApproval ? "!border-red/50 !bg-red/30" : "")}
>
<span className="text-sm">
Merge without waiting for approval (bypass secret change policy)
@@ -250,10 +250,17 @@ export const SecretApprovalRequestChanges = ({
secretApprovalRequestDetails.isReplicated
)}
</div>
<span className="-mt-1 flex items-center space-x-2 text-xs text-gray-400">
By {secretApprovalRequestDetails?.committerUser?.firstName} (
{secretApprovalRequestDetails?.committerUser?.email})
</span>
<p className="-mt-1 text-xs text-gray-400">
By{" "}
{secretApprovalRequestDetails?.committerUser ? (
<>
{secretApprovalRequestDetails?.committerUser?.firstName} (
{secretApprovalRequestDetails?.committerUser?.email})
</>
) : (
<span className="text-gray-500">Deleted User</span>
)}
</p>
</div>
{!hasMerged &&
secretApprovalRequestDetails.status === "open" &&
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons";
import { faArrowDown, faArrowUp, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@@ -10,10 +10,12 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { PermissionDeniedBanner } from "@app/components/permissions";
import {
Button,
Checkbox,
ContentLoader,
Modal,
ModalContent,
PageHeader,
Pagination,
Tooltip
} from "@app/components/v2";
@@ -46,7 +48,9 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { RequestAccessModal } from "@app/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/components/RequestAccessModal";
import { SecretRotationListView } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView";
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
@@ -114,7 +118,10 @@ const Page = () => {
const [snapshotId, setSnapshotId] = useState<string | null>(null);
const isRollbackMode = Boolean(snapshotId);
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["snapshots"] as const);
const { popUp, handlePopUpClose, handlePopUpToggle, handlePopUpOpen } = usePopUp([
"snapshots",
"requestAccess"
] as const);
// env slug
const workspaceId = currentWorkspace?.id || "";
@@ -132,6 +139,26 @@ const Page = () => {
}
);
const canEditSecrets = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
);
const canDeleteSecrets = permission.can(
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
);
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
@@ -257,6 +284,8 @@ const Page = () => {
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : ""
);
const { pathPolicies, hasPathPolicies } = usePathAccessPolicies({ secretPath, environment });
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
workspaceId,
environment,
@@ -476,8 +505,55 @@ const Page = () => {
setFilter(defaultFilterState);
setDebouncedSearchFilter("");
};
return (
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
<PageHeader
title={
currentWorkspace.environments.find((env) => env.slug === environment)?.name ?? environment
}
description={
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/api"
target="_blank"
rel="noopener noreferrer"
>
Infisical API
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
, and
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
>
more
</a>
.
</p>
}
/>
<SecretV2MigrationSection />
{!isRollbackMode ? (
<>
@@ -500,8 +576,15 @@ const Page = () => {
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
isPITEnabled={isPITEnabled}
hasPathPolicies={hasPathPolicies}
onRequestAccess={(params) => handlePopUpOpen("requestAccess", params)}
/>
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div
className={twMerge(
"thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300",
isNotEmpty && "rounded-b-none"
)}
>
<div className="flex flex-col" id="dashboard">
{isNotEmpty && (
<div
@@ -548,6 +631,62 @@ const Page = () => {
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{hasPathPolicies &&
// eslint-disable-next-line no-nested-ternary
(!canReadSecret ? (
<div
className={twMerge(
"flex border-l-2 border-l-primary bg-mineshaft-700 px-4 py-2",
isNotEmpty ? "border-b border-b-mineshaft-600" : ""
)}
>
<div className="flex items-center text-sm">
<FontAwesomeIcon
icon={faInfoCircle}
className="ml-[0.15rem] mr-[1.65rem] text-primary"
/>
<span>You do not have permission to read secrets in this folder</span>
</div>
<Button
variant="outline_bg"
size="xs"
className="ml-auto"
onClick={() =>
handlePopUpOpen("requestAccess", [ProjectPermissionActions.Read])
}
>
Request Access
</Button>
</div>
) : !canEditSecrets || !canDeleteSecrets ? (
<div className="flex border-b border-l-2 border-b-mineshaft-600 border-l-primary bg-mineshaft-700 px-4 py-2">
<div className="flex items-center text-sm">
<FontAwesomeIcon
icon={faInfoCircle}
className="ml-[0.15rem] mr-[1.65rem] text-primary"
/>
<span>
You do not have permission to {!canEditSecrets ? "edit" : ""}
{!canEditSecrets && !canDeleteSecrets ? " or " : ""}
{!canDeleteSecrets ? "delete" : ""} secrets in this folder
</span>
</div>
<Button
variant="outline_bg"
size="xs"
className="ml-auto"
onClick={() =>
handlePopUpOpen("requestAccess", [
...(!canEditSecrets ? [ProjectPermissionActions.Edit] : []),
...(!canDeleteSecrets ? [ProjectPermissionActions.Delete] : [])
])
}
>
Request Access
</Button>
</div>
) : null)}
{canReadSecretImports && Boolean(imports?.length) && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
@@ -636,6 +775,17 @@ const Page = () => {
/>
</ModalContent>
</Modal>
{!!pathPolicies && (
<RequestAccessModal
policies={pathPolicies}
isOpen={popUp.requestAccess.isOpen}
onOpenChange={() => {
handlePopUpClose("requestAccess");
}}
selectedActions={popUp.requestAccess.data}
secretPath={pathPolicies?.[0]?.secretPath}
/>
)}
<SecretDropzone
environment={environment}
workspaceId={workspaceId}
@@ -46,6 +46,7 @@ import {
DropdownSubMenuTrigger,
IconButton,
Modal,
ModalClose,
ModalContent,
Tooltip
} from "@app/components/v2";
@@ -53,11 +54,13 @@ import {
ProjectPermissionActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import {
ProjectPermissionCommitsActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
@@ -127,6 +130,8 @@ type Props = {
}[];
}[];
isPITEnabled: boolean;
onRequestAccess: (actions: ProjectPermissionActions[]) => void;
hasPathPolicies: boolean;
};
export const ActionBar = ({
@@ -147,7 +152,9 @@ export const ActionBar = ({
protectedBranchPolicyName,
importedBy,
isPITEnabled = false,
usedBySecretSyncs
usedBySecretSyncs,
onRequestAccess,
hasPathPolicies
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addFolder",
@@ -159,7 +166,8 @@ export const ActionBar = ({
"misc",
"upgradePlan",
"replicateFolder",
"confirmUpload"
"confirmUpload",
"requestAccess"
] as const);
const isProtectedBranch = Boolean(protectedBranchPolicyName);
const { subscription } = useSubscription();
@@ -180,6 +188,7 @@ export const ActionBar = ({
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const handleFolderCreate = async (folderName: string, description: string | null) => {
try {
@@ -807,27 +816,50 @@ export const ActionBar = ({
</ProjectPermissionCan>
</div>
<div className="flex items-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
</Button>
)}
</ProjectPermissionCan>
{hasPathPolicies ? (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() =>
permission.can(
ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
)
? openPopUp(PopUpNames.CreateSecretForm)
: handlePopUpOpen("requestAccess", [ProjectPermissionActions.Create])
}
className="h-10 rounded-r-none"
>
Add Secret
</Button>
) : (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
</Button>
)}
</ProjectPermissionCan>
)}
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
@@ -1166,6 +1198,27 @@ export const ActionBar = ({
)}
</ModalContent>
</Modal>
<Modal
isOpen={popUp?.requestAccess?.isOpen}
onOpenChange={(open) => handlePopUpToggle("requestAccess", open)}
>
<ModalContent title="Access Restricted">
<p className="mb-2 text-bunker-300">You do not have permission to perform this action.</p>
<p className="text-bunker-300">Request access to perform this action in this folder.</p>
<div className="mt-8 flex items-center gap-4">
<ModalClose asChild>
<Button onClick={() => onRequestAccess(popUp?.requestAccess.data)}>
Request Access
</Button>
</ModalClose>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</ModalContent>
</Modal>
</>
);
};
@@ -1,10 +1,5 @@
import { Controller, useForm } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
faCheckCircle,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { faCheckCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
@@ -206,20 +201,6 @@ export const AzureEntraIdInputForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/azure-entra-id"
>
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<div className="flex-grow">
@@ -12,7 +12,12 @@ import {
} from "react-icons/si";
import { VscAzure } from "react-icons/vsc";
import { faAws, faGithub, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
import {
faArrowUpRightFromSquare,
faBookOpen,
faClock,
faDatabase
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
@@ -152,6 +157,15 @@ const DYNAMIC_SECRET_LIST = [
}
];
const DynamicSecretDetails = Object.fromEntries(
DYNAMIC_SECRET_LIST.map((ds) => [ds.provider, ds.title])
);
const UniqueLinks: Record<string, string> = {
[DynamicSecretProviders.SqlDatabase]: "postgresql", // gotta pick one...
[DynamicSecretProviders.MongoAtlas]: "mongo-atlas"
};
export const CreateDynamicSecretForm = ({
isOpen,
onToggle,
@@ -169,10 +183,31 @@ export const CreateDynamicSecretForm = ({
setSelectedProvider(null);
};
const modalTitle = selectedProvider ? DynamicSecretDetails[selectedProvider] : null;
return (
<Modal isOpen={isOpen} onOpenChange={(state) => handleFormReset(state)}>
<ModalContent
title="Dynamic secret setup"
title={
<div className="flex items-center">
<span>{modalTitle ? `${modalTitle} Dynamic Secret` : "Dynamic Secrets"} </span>
<a
href={`https://infisical.com/docs/documentation/platform/dynamic-secrets/${selectedProvider ? (UniqueLinks[selectedProvider] ?? selectedProvider) : "overview"}`}
target="_blank"
className="mb-0.5 ml-1.5"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
</div>
}
subTitle="Configure dynamic secret parameters"
className="my-4 max-w-3xl"
>
@@ -1,10 +1,5 @@
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
faQuestionCircle,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
@@ -293,20 +288,6 @@ export const KubernetesInputForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
@@ -1,6 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -220,20 +218,6 @@ export const LdapInputForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/ldap"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
@@ -1,6 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -185,20 +183,6 @@ export const SnowflakeInputForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/snowflake"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
@@ -1,6 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -146,20 +144,6 @@ export const TotpInputForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<Controller
@@ -1,10 +1,5 @@
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
faQuestionCircle,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
@@ -285,20 +280,6 @@ export const EditDynamicSecretKubernetesForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
@@ -1,6 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -186,20 +184,6 @@ export const EditDynamicSecretSnowflakeForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/snowflake"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
@@ -1,6 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -138,20 +136,6 @@ export const EditDynamicSecretTotpForm = ({
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<a
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
target="_blank"
rel="noopener noreferrer"
>
<div className="mb-1 ml-2 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-xxs"
/>
</div>
</a>
</div>
<div className="flex flex-col">
<Controller
@@ -262,7 +262,7 @@ export const SecretDropzone = ({
className={twMerge(
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 px-2 py-4 text-sm text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
isDragActive && "opacity-100",
!isSmaller && "mx-auto w-full max-w-3xl flex-col space-y-4 py-20",
!isSmaller && "mx-auto mt-40 w-full max-w-3xl flex-col space-y-4 py-20",
isLoading && "bg-bunker-800"
)}
>
@@ -7,8 +7,12 @@ type Props = {
export const AzureDevOpsSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { devopsProjectName }
destinationConfig: { devopsProjectName, devopsProjectId }
} = secretSync;
return <GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>;
return (
<GenericFieldLabel label={devopsProjectName ? "Project" : "Project ID"}>
{devopsProjectName || devopsProjectId}
</GenericFieldLabel>
);
};
@@ -0,0 +1,12 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TChecklySync } from "@app/hooks/api/secretSyncs/types/checkly-sync";
type Props = {
secretSync: TChecklySync;
};
export const ChecklySyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return <GenericFieldLabel label="Account">{destinationConfig.accountName}</GenericFieldLabel>;
};
@@ -0,0 +1,14 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TCloudflareWorkersSync } from "@app/hooks/api/secretSyncs/types/cloudflare-workers-sync";
type Props = {
secretSync: TCloudflareWorkersSync;
};
export const CloudflareWorkersSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { scriptId }
} = secretSync;
return <GenericFieldLabel label="Script ID">{scriptId}</GenericFieldLabel>;
};
@@ -18,7 +18,9 @@ import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigura
import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationSection";
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
import { ChecklySyncDestinationSection } from "./ChecklySyncDestinationSection";
import { CloudflarePagesSyncDestinationSection } from "./CloudflarePagesSyncDestinationSection";
import { CloudflareWorkersSyncDestinationSection } from "./CloudflareWorkersSyncDestinationSection";
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
@@ -30,6 +32,7 @@ import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSecti
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection";
import { SupabaseSyncDestinationSection } from "./SupabaseSyncDestinationSection";
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection";
@@ -116,12 +119,21 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.CloudflarePages:
DestinationComponents = <CloudflarePagesSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.CloudflareWorkers:
DestinationComponents = <CloudflareWorkersSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Zabbix:
DestinationComponents = <ZabbixSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Railway:
DestinationComponents = <RailwaySyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Checkly:
DestinationComponents = <ChecklySyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Supabase:
DestinationComponents = <SupabaseSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -0,0 +1,12 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSupabaseSync } from "@app/hooks/api/secretSyncs/types/supabase";
type Props = {
secretSync: TSupabaseSync;
};
export const SupabaseSyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return <GenericFieldLabel label="Project">{destinationConfig.projectName}</GenericFieldLabel>;
};
@@ -60,8 +60,11 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
case SecretSync.CloudflareWorkers:
case SecretSync.Zabbix:
case SecretSync.Railway:
case SecretSync.Supabase:
case SecretSync.Checkly:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -1,18 +1,12 @@
import { useWorkspace } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
import { SecretSharingSection } from "../SecretSharingSection";
import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection";
import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => {
const { currentWorkspace } = useWorkspace();
return (
<div>
<EnvironmentSection />
@@ -22,7 +16,6 @@ export const ProjectGeneralTab = () => {
<SecretSnapshotsLegacySection />
<PointInTimeVersionLimitSection />
<BackfillSecretReferenceSecretion />
{currentWorkspace?.version !== ProjectVersion.V3 && <RebuildSecretIndicesSection />}
</div>
);
};
@@ -1,93 +0,0 @@
import { createNotification } from "@app/components/notifications";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUserWsKey, useNameWorkspaceSecrets } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { fetchWorkspaceSecrets } from "@app/hooks/api/workspace/queries";
export const RebuildSecretIndicesSection = () => {
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const [isIndexing, setIsIndexing] = useToggle();
const { data: decryptFileKey } = useGetUserWsKey(currentWorkspace.id);
if (!currentWorkspace) return null;
const onRebuildIndices = async () => {
if (!currentWorkspace?.id) return;
setIsIndexing.on();
try {
const encryptedSecrets = await fetchWorkspaceSecrets(currentWorkspace.id);
if (!currentWorkspace || !decryptFileKey) {
return;
}
const key = decryptAssymmetric({
ciphertext: decryptFileKey.encryptedKey,
nonce: decryptFileKey.nonce,
publicKey: decryptFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
secretId: encryptedSecret.id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace.id,
secretsToUpdate
});
createNotification({
text: "Successfully rebuilt secret indices",
type: "success"
});
} catch (err) {
console.log(err);
} finally {
setIsIndexing.off();
}
};
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
if (!isAdmin) {
return null;
}
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Rebuild Secret Indices</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
This will rebuild indices of all secrets in the project.
</p>
<Button
variant="outline_bg"
isLoading={isIndexing}
onClick={onRebuildIndices}
isDisabled={!isAdmin}
>
Rebuild Secret Indices
</Button>
</div>
);
};