Merge branch 'heads/main' into ENG-2785

This commit is contained in:
Daniel Hougaard
2025-09-24 04:24:02 +04:00
955 changed files with 30612 additions and 7796 deletions
@@ -0,0 +1,45 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faBuilding, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, Tooltip } from "@app/components/v2";
import { TAvailableAppConnection } from "@app/hooks/api/appConnections";
export const AppConnectionOption = ({
isSelected,
children,
...props
}: OptionProps<TAvailableAppConnection>) => {
const isCreateOption = props.data.id === "_create";
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
{isCreateOption ? (
<div className="flex items-center gap-x-1 text-mineshaft-400">
<FontAwesomeIcon icon={faPlus} size="sm" />
<span className="mr-auto">Create New Connection</span>
</div>
) : (
<>
<p className="truncate">{children}</p>
{!props.data.projectId && (
<Tooltip content="This connection belongs to your organization.">
<div className="ml-2 mr-auto">
<Badge className="flex h-5 w-min items-center gap-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300 hover:text-bunker-300">
<FontAwesomeIcon icon={faBuilding} size="sm" />
Organization
</Badge>
</div>
</Tooltip>
)}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</>
)}
</div>
</components.Option>
);
};
@@ -0,0 +1 @@
export * from "./AppConnectionOption";
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { useOrganization, useWorkspace } from "@app/context";
import { useOrganization, useProject } from "@app/context";
import { useToggle } from "@app/hooks";
import { createNotification } from "../notifications";
@@ -51,7 +51,7 @@ export default function NavHeader({
isProtectedBranch = false,
protectionPolicyName
}: Props): JSX.Element {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { currentOrg } = useOrganization();
const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false);
@@ -79,7 +79,7 @@ export default function NavHeader({
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
<div className="truncate text-sm font-semibold text-bunker-300">
{currentWorkspace?.name}
{currentProject?.name}
</div>
</>
)}
@@ -93,7 +93,7 @@ export default function NavHeader({
{pageName === "Secrets" ? (
<Link
to="/projects/secret-management/$projectId/overview"
params={{ projectId: currentWorkspace.id }}
params={{ projectId: currentProject.id }}
className="text-sm font-semibold text-primary/80 hover:text-primary"
>
{pageName}
@@ -129,7 +129,7 @@ export default function NavHeader({
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
<Link
to="/projects/secret-management/$projectId/secrets/$envSlug"
params={{ projectId: currentWorkspace.id, envSlug: routerEnvSlug }}
params={{ projectId: currentProject.id, envSlug: routerEnvSlug }}
className="text-sm font-semibold text-primary/80 hover:text-primary"
>
{userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name}
@@ -192,7 +192,7 @@ export default function NavHeader({
<Link
to="/projects/secret-management/$projectId/secrets/$envSlug"
params={{
projectId: currentWorkspace.id,
projectId: currentProject.id,
envSlug: routerEnvSlug || ""
}}
search={(query) => ({ ...query, secretPath: newSecretPath })}
@@ -3,7 +3,7 @@ import { useFormContext, useWatch } from "react-hook-form";
import { MongoAbility, MongoQuery } from "@casl/ability";
import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSub, useProject } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries";
import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types";
@@ -36,15 +36,15 @@ export const useAccessTree = (
searchPath: string,
subject: ProjectPermissionSub
) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
const { control } = useFormContext<AccessTreeForm>();
const metadata = useWatch({ control, name: "metadata" });
const [nodes, setNodes] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]);
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
const [environment, setEnvironment] = useState(currentProject.environments[0]?.slug ?? "");
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
currentWorkspace.id
currentProject.id
);
const [levelFolderMap, setLevelFolderMap] = useState<LevelFolderMap>({});
@@ -279,7 +279,7 @@ export const useAccessTree = (
environment,
setEnvironment,
isLoading: isPending,
environments: currentWorkspace.environments,
environments: currentProject.environments,
secretName,
setSecretName,
viewMode,
@@ -1,8 +1,10 @@
import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react";
import { AbilityTuple, MongoAbility } from "@casl/ability";
import { Can } from "@casl/react";
import { TooltipProps } from "@app/components/v2/Tooltip/Tooltip";
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
import { useOrgPermission } from "@app/context/OrgPermissionContext";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { AccessRestrictedBanner, Tooltip } from "../v2";
@@ -14,7 +16,7 @@ export const OrgPermissionGuardBanner = () => {
);
};
type Props = {
type Props<T extends AbilityTuple> = {
label?: ReactNode;
// this prop is used when there exist already a tooltip as helper text for users
// so when permission is allowed same tooltip will be reused to show helpertext
@@ -22,9 +24,18 @@ type Props = {
allowedLabel?: string;
renderGuardBanner?: boolean;
tooltipProps?: Omit<TooltipProps, "children">;
} & BoundCanProps<TOrgPermission>;
I: T[0];
ability?: MongoAbility<T>;
children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
passThrough?: boolean;
} & (
| { an: T[1] }
| {
a: T[1];
}
);
export const OrgPermissionCan: FunctionComponent<Props> = ({
export const OrgPermissionCan: FunctionComponent<Props<OrgPermissionSet>> = ({
label = "Access restricted",
children,
passThrough = true,
@@ -41,9 +52,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
{(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type.
const finalChild =
typeof children === "function"
? children(isAllowed, ability as TOrgPermission)
: children;
typeof children === "function" ? children(isAllowed, ability as any) : children;
if (!isAllowed && passThrough) {
return (
@@ -0,0 +1,17 @@
import { OrgPermissionCan } from "./OrgPermissionCan";
import { ProjectPermissionCan } from "./ProjectPermissionCan";
interface PermissionCanProps {
type: "project" | "org";
I: any;
a: any;
children: (isAllowed: boolean, ability?: any) => React.ReactNode;
}
export const VariablePermissionCan = ({ type, children, ...props }: PermissionCanProps) => {
if (type === "project") {
return <ProjectPermissionCan {...props}>{children}</ProjectPermissionCan>;
}
return <OrgPermissionCan {...props}>{children}</OrgPermissionCan>;
};
@@ -3,3 +3,4 @@ export { GlobPermissionInfo } from "./GlobPermissionInfo";
export { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan";
export * from "./VariablePermissionCan";
@@ -0,0 +1,76 @@
import { useEffect, useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
import { CreatePkiSyncForm } from "./forms";
import { PkiSyncModalHeader } from "./PkiSyncModalHeader";
import { PkiSyncSelect } from "./PkiSyncSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
selectSync?: PkiSync | null;
};
type ContentProps = {
onComplete: (pkiSync: TPkiSync) => void;
selectedSync: PkiSync | null;
setSelectedSync: (selectedSync: PkiSync | null) => void;
};
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
if (selectedSync) {
return (
<CreatePkiSyncForm
onComplete={onComplete}
onCancel={() => setSelectedSync(null)}
destination={selectedSync}
/>
);
}
return <PkiSyncSelect onSelect={setSelectedSync} />;
};
export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
const [selectedSync, setSelectedSync] = useState<PkiSync | null>(selectSync);
useEffect(() => {
setSelectedSync(selectSync);
}, [selectSync]);
return (
<Modal
{...props}
onOpenChange={(isOpen) => {
if (!isOpen) setSelectedSync(null);
onOpenChange(isOpen);
}}
>
<ModalContent
title={
selectedSync ? (
<PkiSyncModalHeader isConfigured={false} destination={selectedSync} />
) : (
"Add Sync"
)
}
className="max-w-2xl"
bodyClassName="overflow-visible"
subTitle={
selectedSync ? undefined : "Select a third-party service to sync certificates to."
}
>
<Content
onComplete={() => {
setSelectedSync(null);
onOpenChange(false);
}}
selectedSync={selectedSync}
setSelectedSync={setSelectedSync}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,56 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useDeletePkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync?: TPkiSync;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onComplete?: () => void;
};
export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete }: Props) => {
const deleteSync = useDeletePkiSync();
if (!pkiSync) return null;
const { id: syncId, name, destination, projectId } = pkiSync;
const handleDeletePkiSync = async () => {
const destinationName = PKI_SYNC_MAP[destination].name;
try {
await deleteSync.mutateAsync({
syncId,
projectId,
destination
});
createNotification({
text: `Successfully deleted ${destinationName} PKI Sync`,
type: "success"
});
if (onComplete) onComplete();
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete ${destinationName} PKI Sync`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeletePkiSync}
/>
);
};
@@ -0,0 +1,29 @@
import { PkiSyncEditFields } from "@app/components/pki-syncs/types";
import { Modal, ModalContent } from "@app/components/v2";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
import { EditPkiSyncForm } from "./forms";
import { PkiSyncModalHeader } from "./PkiSyncModalHeader";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
pkiSync?: TPkiSync;
fields: PkiSyncEditFields;
};
export const EditPkiSyncModal = ({ pkiSync, onOpenChange, fields, ...props }: Props) => {
if (!pkiSync) return null;
return (
<Modal {...props} onOpenChange={onOpenChange}>
<ModalContent
title={<PkiSyncModalHeader isConfigured destination={pkiSync.destination} />}
className="max-w-2xl"
bodyClassName="overflow-visible"
>
<EditPkiSyncForm onComplete={() => onOpenChange(false)} fields={fields} pkiSync={pkiSync} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,95 @@
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useTriggerPkiSyncImportCertificates } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync?: TPkiSync;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
pkiSync: TPkiSync;
onComplete: () => void;
};
const Content = ({ pkiSync, onComplete }: ContentProps) => {
const { id: syncId, destination, projectId } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name;
const triggerImportCertificates = useTriggerPkiSyncImportCertificates();
const handleTriggerImportCertificates = async () => {
try {
await triggerImportCertificates.mutateAsync({
syncId,
destination,
projectId
});
createNotification({
text: `Successfully triggered certificate import for ${destinationName} Sync`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to trigger certificate import for ${destinationName} Sync`,
type: "error"
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleTriggerImportCertificates();
}}
>
<p className="mb-8 text-sm text-mineshaft-200">
Are you sure you want to import certificates from this {destinationName} destination into
Infisical?
</p>
<p className="mb-6 text-xs text-bunker-300">
This operation will retrieve certificates from {destinationName} and make them available in
your PKI subscriber. Only certificates that are not already imported will be processed.
</p>
<div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
<Button
type="submit"
isLoading={triggerImportCertificates.isPending}
colorSchema="secondary"
>
Import Certificates
</Button>
</div>
</form>
);
};
export const PkiSyncImportCertificatesModal = ({ isOpen, onOpenChange, pkiSync }: Props) => {
if (!pkiSync) return null;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Import Certificates"
subTitle={`Import certificates into Infisical from this ${destinationName} Sync destination.`}
>
<Content pkiSync={pkiSync} onComplete={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,112 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
className?: string;
mini?: boolean;
};
export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync;
const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
useEffect(() => {
if (importStatus === PkiSyncStatus.Succeeded) {
setTimeout(() => setHide(true), 3000);
} else {
setHide(false);
}
}, [importStatus]);
const failureMessage = useMemo(() => {
if (importStatus === PkiSyncStatus.Failed) {
if (lastImportMessage)
try {
return JSON.stringify(JSON.parse(lastImportMessage), null, 2);
} catch {
return lastImportMessage;
}
return "An Unknown Error Occurred.";
}
return null;
}, [importStatus, lastImportMessage]);
if (!importStatus || hide) return null;
let variant: BadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let tooltipContent: ReactNode;
switch (importStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
label = "Importing Certificates...";
tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`;
icon = faDownload;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Import Certificates";
icon = faTriangleExclamation;
tooltipContent = (
<div className="flex flex-col gap-2 whitespace-normal py-1">
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">
{mini ? "Failed to Import Certificates" : "Failure Reason"}
</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
</div>
)}
</div>
);
break;
case PkiSyncStatus.Succeeded:
default:
// only show success for a bit...
if (lastImportedAt && differenceInSeconds(new Date(), lastImportedAt) > 15) return null;
tooltipContent = "Successfully imported certificates.";
variant = "success";
label = "Certificates Imported";
icon = faCheck;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip>
);
};
@@ -0,0 +1,49 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
destination: PkiSync;
isConfigured: boolean;
};
export const PkiSyncModalHeader = ({ destination, isConfigured }: Props) => {
const destinationDetails = PKI_SYNC_MAP[destination];
return (
<div className="flex w-full items-start gap-2">
<img
alt={`${destinationDetails.name} logo`}
src={`/images/integrations/${destinationDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
{destinationDetails.name} Certificate Sync
<a
target="_blank"
href={`https://infisical.com/docs/integrations/pki-syncs/${destination}`}
className="mb-1 ml-1"
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>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
? `Edit ${destinationDetails.name} Certificate Sync`
: `Sync certificates to ${destinationDetails.name}`}
</p>
</div>
</div>
);
};
@@ -0,0 +1,87 @@
import { createNotification } from "@app/components/notifications";
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useTriggerPkiSyncRemoveCertificates } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync?: TPkiSync;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
pkiSync: TPkiSync;
onComplete: () => void;
};
const Content = ({ pkiSync, onComplete }: ContentProps) => {
const { id: syncId, destination, projectId } = pkiSync;
const destinationName = PKI_SYNC_MAP[destination].name;
const triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates();
const handleTriggerRemoveCertificates = async () => {
try {
await triggerRemoveCertificates.mutateAsync({
syncId,
destination,
projectId
});
createNotification({
text: `Successfully triggered certificate removal for ${destinationName} Sync`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to trigger certificate removal for ${destinationName} Sync`,
type: "error"
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleTriggerRemoveCertificates();
}}
>
<p className="mb-8 text-sm text-mineshaft-200">
Are you sure you want to remove certificates synced by Infisical from this {destinationName}{" "}
destination?
</p>
<div className="mt-8 flex w-full items-center justify-between gap-2">
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
<Button type="submit" isLoading={triggerRemoveCertificates.isPending} colorSchema="danger">
Remove Certificates
</Button>
</div>
</form>
);
};
export const PkiSyncRemoveCertificatesModal = ({ isOpen, onOpenChange, pkiSync }: Props) => {
if (!pkiSync) return null;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Remove Certificates"
subTitle={`Remove certificates synced by Infisical from this ${destinationName} Sync destination.`}
>
<Content pkiSync={pkiSync} onComplete={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,112 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
className?: string;
mini?: boolean;
};
export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync;
const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
useEffect(() => {
if (removeStatus === PkiSyncStatus.Succeeded) {
setTimeout(() => setHide(true), 3000);
} else {
setHide(false);
}
}, [removeStatus]);
const failureMessage = useMemo(() => {
if (removeStatus === PkiSyncStatus.Failed) {
if (lastRemoveMessage)
try {
return JSON.stringify(JSON.parse(lastRemoveMessage), null, 2);
} catch {
return lastRemoveMessage;
}
return "An Unknown Error Occurred.";
}
return null;
}, [removeStatus, lastRemoveMessage]);
if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let tooltipContent: ReactNode;
switch (removeStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
label = "Removing Certificates...";
tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`;
icon = faEraser;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Remove Certificates";
icon = faTriangleExclamation;
tooltipContent = (
<div className="flex flex-col gap-2 whitespace-normal py-1">
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<div className="text-xs">
{mini ? "Failed to Remove Certificates" : "Failure Reason"}
</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
</div>
)}
</div>
);
break;
case PkiSyncStatus.Succeeded:
default:
// only show success for a bit...
if (lastRemovedAt && differenceInSeconds(new Date(), lastRemovedAt) > 15) return null;
tooltipContent = "Successfully removed certificates.";
variant = "success";
label = "Certificates Removed";
icon = faCheck;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
</Tooltip>
);
};
@@ -0,0 +1,155 @@
import { useMemo } from "react";
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { PkiSync, usePkiSyncOptions } from "@app/hooks/api/pkiSyncs";
import { UpgradePlanModal } from "../license/UpgradePlanModal";
type Props = {
onSelect: (destination: PkiSync) => void;
};
export const PkiSyncSelect = ({ onSelect }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: pkiSyncOptions } = usePkiSyncOptions();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
initPerPage: 16
});
const filteredOptions = useMemo(
() =>
pkiSyncOptions?.filter(({ destination }) => {
const { name } = PKI_SYNC_MAP[destination];
return (
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
destination.toLowerCase().includes(search.toLowerCase())
);
}) ?? [],
[pkiSyncOptions, search]
);
useResetPageHelper({
totalCount: filteredOptions.length,
offset,
setPage
});
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search options..."
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
/>
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
{filteredOptions.slice(offset, perPage * page)?.map(({ destination, enterprise }) => {
const { image, name } = PKI_SYNC_MAP[destination];
return (
<button
key={destination}
type="button"
onClick={() =>
enterprise && !subscription.enterpriseCertificateSyncs
? handlePopUpOpen("upgradePlan")
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
height={40}
width={40}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{!filteredOptions?.length && (
<EmptyState
className="col-span-full mt-40"
title="No Certificate Syncs match search"
icon={faSearch}
/>
)}
</div>
{Boolean(filteredOptions.length) && (
<Pagination
startAdornment={
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more services.</p>
<p>
{`If you don't see the third-party
service you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
<span className="text-xs">
Don&#39;t see the third-party service you&#39;re looking for?
</span>
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
</div>
</Tooltip>
}
count={filteredOptions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[16]}
/>
)}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
/>
</div>
);
};
@@ -0,0 +1,55 @@
import {
faCheck,
faExclamationTriangle,
faHourglass,
faRotate,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = {
status: PkiSyncStatus;
} & Omit<BadgeProps, "children" | "variant">;
export const PkiSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"];
let text: string;
let icon: IconDefinition;
switch (status) {
case PkiSyncStatus.Failed:
variant = "danger";
text = "Failed to Sync";
icon = faExclamationTriangle;
break;
case PkiSyncStatus.Succeeded:
variant = "success";
text = "Synced";
icon = faCheck;
break;
case PkiSyncStatus.Pending:
variant = "primary";
text = "Queued";
icon = faHourglass;
break;
case PkiSyncStatus.Running:
default:
variant = "primary";
text = "Syncing";
icon = faRotate;
break;
}
return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
<FontAwesomeIcon
icon={icon}
className={status === PkiSyncStatus.Running ? "animate-spin" : ""}
/>
<span>{text}</span>
</Badge>
);
};
@@ -0,0 +1,121 @@
import { faPlug, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Badge,
EmptyState,
IconButton,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSyncs: TPkiSync[];
onEdit: (pkiSync: TPkiSync) => void;
onDelete: (pkiSync: TPkiSync) => void;
onTrigger: (pkiSync: TPkiSync) => void;
};
const getSyncStatusBadge = (status?: string) => {
switch (status) {
case "SUCCESS":
return <Badge variant="success">Success</Badge>;
case "FAILED":
return <Badge variant="danger">Failed</Badge>;
case "RUNNING":
return <Badge variant="primary">Running</Badge>;
case "PENDING":
default:
return <Badge variant="primary">Pending</Badge>;
}
};
export const PkiSyncTable = ({ pkiSyncs, onEdit, onDelete, onTrigger }: Props) => {
if (!pkiSyncs.length) {
return (
<div className="flex h-96 items-center justify-center">
<EmptyState title="No PKI syncs created" icon={faPlug}>
Start by creating a PKI sync to synchronize certificates with external services
</EmptyState>
</div>
);
}
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Destination</Th>
<Th>Connection</Th>
<Th>Auto Sync</Th>
<Th>Status</Th>
<Th>Last Sync</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{pkiSyncs.map((pkiSync) => (
<Tr
key={pkiSync.id}
className="cursor-pointer hover:bg-mineshaft-700"
onClick={() => onEdit(pkiSync)}
>
<Td>{pkiSync.name}</Td>
<Td>
<div className="flex items-center space-x-2">
<Badge variant="primary">{pkiSync.destination}</Badge>
</div>
</Td>
<Td>{pkiSync.appConnectionName || "Unknown"}</Td>
<Td>
<Badge variant={pkiSync.isAutoSyncEnabled ? "success" : "danger"}>
{pkiSync.isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</Td>
<Td>{getSyncStatusBadge(pkiSync.syncStatus ?? undefined)}</Td>
<Td>
{pkiSync.lastSyncedAt
? new Date(pkiSync.lastSyncedAt).toLocaleDateString()
: "Never"}
</Td>
<Td>
<div className="flex items-center space-x-2">
<IconButton
size="sm"
variant="plain"
ariaLabel="Trigger sync"
onClick={(e) => {
e.stopPropagation();
onTrigger(pkiSync);
}}
>
<FontAwesomeIcon icon={faRefresh} />
</IconButton>
<IconButton
size="sm"
variant="plain"
ariaLabel="Delete"
onClick={(e) => {
e.stopPropagation();
onDelete(pkiSync);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1,37 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
import { TPkiSyncForm } from "./schemas";
export const AzureKeyVaultPkiSyncFields = () => {
const { control, setValue } = useFormContext<
TPkiSyncForm & { destination: PkiSync.AzureKeyVault }
>();
return (
<>
<PkiSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultBaseUrl", "");
}}
/>
<Controller
name="destinationConfig.vaultBaseUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure Key Vault URL. This is the base URL for your Azure Key Vault, e.g. https://example.vault.azure.net."
>
<Input {...field} placeholder="https://example.vault.azure.net" />
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,245 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2";
import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas";
type Props = {
onComplete: (pkiSync: TPkiSync) => void;
destination: PkiSync;
onCancel: () => void;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
{ name: "Source", key: "source", fields: ["subscriberId"] },
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
{ name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] }
];
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => {
const createPkiSync = useCreatePkiSync();
const { currentProject } = useProject();
const { name: destinationName } = PKI_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const { syncOption } = usePkiSyncOption(destination);
const formMethods = useForm<TPkiSyncForm>({
resolver: zodResolver(PkiSyncFormSchema),
defaultValues: {
destination,
isAutoSyncEnabled: false,
syncOptions: {
canImportCertificates: false,
canRemoveCertificates: false,
certificateNameSchema: syncOption?.defaultCertificateNameSchema
}
} as Partial<TPkiSyncForm>,
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, destinationConfig, ...formData }: TPkiSyncForm) => {
try {
const pkiSync = await createPkiSync.mutateAsync({
...formData,
connectionId: connection.id,
projectId: currentProject.id,
destinationConfig
});
createNotification({
text: `Successfully added ${destinationName} Certificate Sync`,
type: "success"
});
onComplete(pkiSync);
} catch (err: Error | unknown) {
console.error(err);
setShowConfirmation(false);
createNotification({
title: `Failed to add ${destinationName} Certificate Sync`,
text: err instanceof Error ? err.message : "An unknown error occurred",
type: "error"
});
}
};
const handlePrev = () => {
if (selectedTabIndex === 0) {
onCancel();
return;
}
setSelectedTabIndex((prev) => prev - 1);
};
const { handleSubmit, trigger, control } = formMethods;
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const handleNext = async () => {
if (isFinalStep) {
setShowConfirmation(true);
return;
}
const isValid = await isStepValid(selectedTabIndex);
if (!isValid) return;
setSelectedTabIndex((prev) => prev + 1);
};
const isTabEnabled = async (index: number) => {
let isEnabled = true;
for (let i = index - 1; i >= 0; i -= 1) {
// eslint-disable-next-line no-await-in-loop
isEnabled = isEnabled && (await isStepValid(i));
}
return isEnabled;
};
if (showConfirmation)
return (
<>
<div className="flex flex-col rounded-sm border border-l-[2px] border-mineshaft-600 border-l-primary bg-mineshaft-700/80 px-4 py-3">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
Certificate Sync Behavior
</div>
<p className="mt-1 text-sm text-bunker-200">
Only certificates managed by Infisical will be affected during sync operations.
Certificates not created or managed by Infisical will remain untouched, and changes made
to Infisical-managed certificates directly in the destination service may be overwritten
by future syncs.
</p>
</div>
<div className="mt-4 flex gap-4">
<Button
isDisabled={createPkiSync.isPending}
isLoading={createPkiSync.isPending}
onClick={handleSubmit(onSubmit)}
colorSchema="secondary"
>
I Understand
</Button>
<Button
isDisabled={createPkiSync.isPending}
variant="plain"
onClick={() => setShowConfirmation(false)}
colorSchema="secondary"
>
Cancel
</Button>
</div>
</>
);
return (
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
<FormProvider {...formMethods}>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
{FORM_TABS.map((tab, index) => (
<Tab
onClick={async (e) => {
e.preventDefault();
const isEnabled = await isTabEnabled(index);
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
}}
className={({ selected }) =>
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
key={tab.key}
>
{index + 1}. {tab.name}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<PkiSyncSourceFields />
</Tab.Panel>
<Tab.Panel>
<PkiSyncDestinationFields />
</Tab.Panel>
<Tab.Panel>
<PkiSyncOptionsFields destination={destination} />
<Controller
control={control}
name="isAutoSyncEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={
value
? "Certificates will automatically be synced when changes occur in the source subscriber."
: "Certificates will not automatically be synced when changes occur in the source subscriber. You can still trigger syncs manually."
}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="auto-sync-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
);
}}
/>
</Tab.Panel>
<Tab.Panel>
<PkiSyncDetailsFields />
</Tab.Panel>
<Tab.Panel>
<PkiSyncReviewFields />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button onClick={handleNext} colorSchema="secondary">
{isFinalStep ? "Create Sync" : "Next"}
</Button>
{selectedTabIndex > 0 && (
<Button onClick={handlePrev} colorSchema="secondary">
Back
</Button>
)}
</div>
</form>
);
};
@@ -0,0 +1,108 @@
import { ReactNode } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { PkiSyncEditFields } from "@app/components/pki-syncs/types";
import { Button, ModalClose } from "@app/components/v2";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs";
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
import { TPkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas";
type Props = {
onComplete: (pkiSync: TPkiSync) => void;
pkiSync: TPkiSync;
fields: PkiSyncEditFields;
};
export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
const updatePkiSync = useUpdatePkiSync();
const { name: destinationName } = PKI_SYNC_MAP[pkiSync.destination];
const formMethods = useForm<TPkiSyncForm>({
resolver: zodResolver(UpdatePkiSyncFormSchema),
defaultValues: {
...pkiSync,
description: pkiSync.description ?? "",
connection: {
id: pkiSync.connectionId,
name: pkiSync.appConnectionName
}
} as Partial<TPkiSyncForm>,
reValidateMode: "onChange"
});
const onSubmit = async ({ connection, ...formData }: TPkiSyncForm) => {
try {
const updatedPkiSync = await updatePkiSync.mutateAsync({
syncId: pkiSync.id,
...formData,
connectionId: connection.id,
projectId: pkiSync.projectId,
destination: pkiSync.destination
});
createNotification({
text: `Successfully updated ${destinationName} PKI Sync`,
type: "success"
});
onComplete(updatedPkiSync);
} catch (err: any) {
console.error(err);
createNotification({
title: `Failed to update ${destinationName} PKI Sync`,
text: err.message,
type: "error"
});
}
};
let Component: ReactNode;
switch (fields) {
case PkiSyncEditFields.Destination:
Component = <PkiSyncDestinationFields />;
break;
case PkiSyncEditFields.Options:
Component = <PkiSyncOptionsFields destination={pkiSync.destination} />;
break;
case PkiSyncEditFields.Source:
Component = <PkiSyncSourceFields />;
break;
case PkiSyncEditFields.Details:
default:
Component = <PkiSyncDetailsFields />;
break;
}
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = formMethods;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...formMethods}>{Component}</FormProvider>
<div className="flex w-full justify-between gap-4 pt-4">
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
<Button
isLoading={isSubmitting}
isDisabled={!isDirty || isSubmitting}
type="submit"
colorSchema="secondary"
>
Update PKI Sync
</Button>
</div>
</form>
);
};
@@ -0,0 +1,89 @@
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { TPkiSyncForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
};
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
const { permission } = useProjectPermission();
const { control, watch } = useFormContext<TPkiSyncForm>();
const { currentProject } = useProject();
const destination = watch("destination");
const app = PKI_SYNC_CONNECTION_MAP[destination];
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentProject.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
const appName = APP_CONNECTION_MAP[PKI_SYNC_CONNECTION_MAP[destination]].name;
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Specify the {appName} Connection to use to connect to {connectionName} and configure
destination parameters.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label={`${connectionName} Connection`}
>
<FilterableSelect
value={value}
onChange={(newValue) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
</p>
)}
</>
);
};
@@ -0,0 +1,23 @@
import { useFormContext } from "react-hook-form";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncDestinationFields = () => {
const { watch } = useFormContext<TPkiSyncForm>();
const destination = watch("destination");
switch (destination) {
case PkiSync.AzureKeyVault:
return <AzureKeyVaultPkiSyncFields />;
default:
return (
<div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700">
<p>Unsupported destination: {destination}</p>
</div>
);
}
};
@@ -0,0 +1,51 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncDetailsFields = () => {
const { control } = useFormContext<TPkiSyncForm>();
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Provide a name and description for this Certificate Sync.
</p>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
helperText="Must be slug-friendly"
isError={Boolean(error)}
errorText={error?.message}
label="Name"
>
<Input value={value} onChange={onChange} placeholder="my-certificate-sync" />
</FormControl>
)}
control={control}
name="name"
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Description"
>
<TextArea
value={value}
onChange={onChange}
placeholder="Describe the purpose of this sync..."
className="!resize-none"
rows={4}
/>
</FormControl>
)}
control={control}
name="description"
/>
</>
);
};
@@ -0,0 +1,150 @@
import { Controller, useFormContext } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FormControl, Input, Switch, Tooltip } from "@app/components/v2";
import { PkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
import { TPkiSyncForm } from "../schemas";
type Props = {
destination?: PkiSync;
};
export const PkiSyncOptionsFields = ({ destination }: Props) => {
const { control, watch } = useFormContext<TPkiSyncForm>();
const currentDestination = destination || watch("destination");
const { syncOption } = usePkiSyncOption(currentDestination);
return (
<>
<p className="mb-4 text-sm text-bunker-300">Configure how certificates should be synced.</p>
{/*
TODO: Re-enable this when we have a way to import certificates
<Controller
control={control}
name="syncOptions.canImportCertificates"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="can-import-certificates"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Auto Import Certificates{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, Infisical will automatically import certificates from the PKI
subscriber to the destination during sync operations.
</p>
<p className="mt-4">
This allows you to automatically populate your destination with certificates
issued by your Certificate Authority.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
*/}
<Controller
control={control}
name="syncOptions.canRemoveCertificates"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
id="can-remove-certificates"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>
Enable Certificate Removal{" "}
<Tooltip
className="max-w-md"
content={
<>
<p>
When enabled, Infisical will remove certificates from the destination during
a sync if they are no longer managed by Infisical.
</p>
<p className="mt-4">
Disable this option if you intend to manage some certificates manually
outside of Infisical.
</p>
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
<Controller
control={control}
name="syncOptions.certificateNameSchema"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText={
<div className="flex flex-col gap-3">
<span>
When a certificate is synced, the certificate name schema will be applied before
it reaches the destination.
</span>
<div className="flex flex-col">
<span>Available placeholders:</span>
<ul className="list-disc pl-4 text-sm">
<li>
<code>{"{{certificateId}}"}</code> - The unique ID of the certificate
</li>
</ul>
</div>
{syncOption?.forbiddenCharacters && syncOption.forbiddenCharacters.length > 0 && (
<div className="flex flex-col">
<span className="text-yellow">
Character restrictions for {syncOption.name}:
</span>
<div className="text-xs text-bunker-300">
The following characters are not allowed:{" "}
{syncOption.forbiddenCharacters.split("").join(" ")}
</div>
</div>
)}
</div>
}
isError={Boolean(error)}
isOptional
errorText={error?.message}
label="Certificate Name Schema"
helperText="Infisical strongly advises setting a Certificate Name Schema to ensure that Infisical only manages the specific certificates you intend to manage, keeping everything else untouched."
>
<Input
value={value || ""}
onChange={(e) => onChange(e.target.value || undefined)}
placeholder={
syncOption?.defaultCertificateNameSchema || "INFISICAL_{{certificateId}}"
}
/>
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1 @@
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
@@ -0,0 +1,99 @@
import { useFormContext } from "react-hook-form";
import { Badge, GenericFieldLabel } from "@app/components/v2";
import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncReviewFields = () => {
const { watch } = useFormContext<TPkiSyncForm>();
const { currentProject } = useProject();
const { data: pkiSubscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
const getSubscriberName = (subscriberId?: string) => {
const subscriber = pkiSubscribers.find((sub) => sub.id === subscriberId);
return subscriber?.name || "Unknown";
};
const {
name,
description,
connection,
subscriberId,
syncOptions,
destination,
destinationConfig,
isAutoSyncEnabled
} = watch();
const destinationName = PKI_SYNC_MAP[destination].name;
return (
<div className="mb-4 flex flex-col gap-6">
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Source</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="PKI Subscriber">
{getSubscriberName(subscriberId)}
</GenericFieldLabel>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Destination</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Connection">{connection?.name}</GenericFieldLabel>
<GenericFieldLabel label="Service">{destinationName}</GenericFieldLabel>
{destinationConfig && "vaultBaseUrl" in destinationConfig && (
<GenericFieldLabel label="Vault URL">
{destinationConfig.vaultBaseUrl}
</GenericFieldLabel>
)}
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Sync Options</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Auto-Sync">
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
{/* Hidden for now - Import certificates functionality disabled
{syncOptions?.canImportCertificates !== undefined && (
<GenericFieldLabel label="Import Certificates">
<Badge variant={syncOptions.canImportCertificates ? "success" : "danger"}>
{syncOptions.canImportCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
)}
*/}
{syncOptions?.canRemoveCertificates !== undefined && (
<GenericFieldLabel label="Remove Certificates">
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
)}
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Details</span>
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
</div>
</div>
</div>
);
};
@@ -0,0 +1,53 @@
import { Controller, useFormContext } from "react-hook-form";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { useProject } from "@app/context";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
import { TPkiSyncForm } from "./schemas";
export const PkiSyncSourceFields = () => {
const { control } = useFormContext<TPkiSyncForm>();
const { currentProject } = useProject();
const { data: pkiSubscribers = [], isLoading } = useListWorkspacePkiSubscribers(
currentProject?.id || ""
);
return (
<>
<p className="mb-4 text-sm text-bunker-300">
Specify the PKI subscriber where you would like to sync certificates from.
</p>
<Controller
control={control}
name="subscriberId"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl label="PKI Subscriber" isError={Boolean(error)} errorText={error?.message}>
<FilterableSelect
value={pkiSubscribers.find((sub) => sub.id === value)}
onChange={(selectedSubscriber) => {
if (Array.isArray(selectedSubscriber)) return;
if (
selectedSubscriber &&
!Array.isArray(selectedSubscriber) &&
"id" in selectedSubscriber
) {
onChange(selectedSubscriber.id);
} else {
onChange(undefined);
}
}}
options={pkiSubscribers}
isLoading={isLoading}
placeholder="Select PKI subscriber..."
getOptionLabel={(option) => option?.name || ""}
getOptionValue={(option) => option?.id || ""}
/>
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,8 @@
export { CreatePkiSyncForm } from "./CreatePkiSyncForm";
export { EditPkiSyncForm } from "./EditPkiSyncForm";
export { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
export { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
export { PkiSyncOptionsFields } from "./PkiSyncOptionsFields/PkiSyncOptionsFields";
export { PkiSyncReviewFields } from "./PkiSyncReviewFields";
export { PkiSyncSourceFields } from "./PkiSyncSourceFields";
export { PkiSyncFormSchema, type TPkiSyncForm } from "./schemas";
@@ -0,0 +1,74 @@
import { z } from "zod";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
export const PkiSyncFormSchema = z.object({
name: z
.string()
.trim()
.min(1, "Name is required")
.max(255, "Name must be less than 255 characters"),
description: z.string().optional(),
destination: z.nativeEnum(PkiSync),
isAutoSyncEnabled: z.boolean().default(true),
subscriberId: z.string().min(1, "PKI Subscriber is required"),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z.string().max(255, "Connection name must be less than 255 characters")
}),
destinationConfig: z.object({
vaultBaseUrl: z.string().url("Valid URL is required")
}),
syncOptions: z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(false),
certificateNameSchema: z
.string()
.optional()
.refine(
(val) => {
if (!val) return true;
const allowedOptionalPlaceholders = ["{{environment}}"];
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
.join("|");
const allowedContentRegex = new RegExp(
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
);
const contentIsValid = allowedContentRegex.test(val);
if (val.trim()) {
const certificateIdRegex = /\{\{certificateId\}\}/;
const certificateIdIsPresent = certificateIdRegex.test(val);
return contentIsValid && certificateIdIsPresent;
}
return contentIsValid;
},
{
message:
"Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
}
)
})
});
export type TPkiSyncForm = z.infer<typeof PkiSyncFormSchema>;
export const UpdatePkiSyncFormSchema = PkiSyncFormSchema.partial().merge(
z.object({
name: z
.string()
.trim()
.min(1, "Name is required")
.max(255, "Name must be less than 255 characters"),
destination: z.nativeEnum(PkiSync),
connection: z.object({
id: z.string().uuid("Invalid connection ID format"),
name: z.string().max(255, "Connection name must be less than 255 characters")
})
})
);
@@ -0,0 +1,9 @@
export { CreatePkiSyncModal } from "./CreatePkiSyncModal";
export { DeletePkiSyncModal } from "./DeletePkiSyncModal";
export { EditPkiSyncModal } from "./EditPkiSyncModal";
export { PkiSyncImportCertificatesModal } from "./PkiSyncImportCertificatesModal";
export { PkiSyncImportStatusBadge } from "./PkiSyncImportStatusBadge";
export { PkiSyncRemoveCertificatesModal } from "./PkiSyncRemoveCertificatesModal";
export { PkiSyncRemoveStatusBadge } from "./PkiSyncRemoveStatusBadge";
export { PkiSyncStatusBadge } from "./PkiSyncStatusBadge";
export { PkiSyncTable } from "./PkiSyncTable";
@@ -0,0 +1,6 @@
export enum PkiSyncEditFields {
Details = "details",
Options = "options",
Source = "source",
Destination = "destination"
}
@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useUpdateProject } from "@app/hooks/api";
const baseFormSchema = z.object({
@@ -37,35 +37,35 @@ type Props = {
};
export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { mutateAsync, isPending } = useUpdateProject();
const { handleSubmit, control, reset, watch } = useForm<BaseFormData | FormDataWithSlug>({
resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
});
const currentSlug = showSlugField ? watch("slug") : currentWorkspace?.slug;
const currentSlug = showSlugField ? watch("slug") : currentProject?.slug;
useEffect(() => {
if (currentWorkspace) {
if (currentProject) {
reset({
name: currentWorkspace.name,
description: currentWorkspace.description ?? "",
...(showSlugField && { slug: currentWorkspace.slug })
name: currentProject.name,
description: currentProject.description ?? "",
...(showSlugField && { slug: currentProject.slug })
});
}
}, [currentWorkspace, showSlugField]);
}, [currentProject, showSlugField]);
const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => {
try {
if (!currentWorkspace?.id) return;
if (!currentProject?.id) return;
await mutateAsync({
projectID: currentWorkspace.id,
projectId: currentProject.id,
newProjectName: data.name,
newProjectDescription: data.description,
...(showSlugField &&
"slug" in data && {
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
newSlug: data.slug !== currentProject.slug ? data.slug : undefined
})
});
@@ -105,7 +105,7 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
variant="outline_bg"
size="sm"
onClick={() => {
navigator.clipboard.writeText(currentWorkspace?.id || "");
navigator.clipboard.writeText(currentProject?.id || "");
createNotification({
text: "Copied project ID to clipboard",
type: "success"
@@ -34,10 +34,10 @@ import {
useUser
} from "@app/context";
import { getProjectHomePage, getProjectLottieIcon } from "@app/helpers/project";
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
import { useCreateWorkspace, useGetExternalKmsList, useGetUserProjects } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
const formSchema = z.object({
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
@@ -89,7 +89,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
const { permission } = useOrgPermission();
const { user } = useUser();
const createWs = useCreateWorkspace();
const { refetch: refetchWorkspaces } = useGetUserWorkspaces();
const { refetch: refetchWorkspaces } = useGetUserProjects();
const { subscription } = useSubscription();
const canReadProjectTemplates = permission.can(
@@ -0,0 +1,88 @@
import { useForm } from "react-hook-form";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { useRequestProjectAccess } from "@app/hooks/api";
import { Project } from "@app/hooks/api/projects/types";
type ContentProps = {
projectId: string;
onComplete: () => void;
};
const Content = ({ projectId, onComplete }: ContentProps) => {
const form = useForm<{ note: string }>();
const requestProjectAccess = useRequestProjectAccess();
const onFormSubmit = ({ note }: { note: string }) => {
if (requestProjectAccess.isPending) return;
requestProjectAccess.mutate(
{
comment: note,
projectId
},
{
onSuccess: () => {
createNotification({
type: "success",
title: "Project Access Request Sent",
text: "Project admins will receive an email of your request"
});
onComplete();
}
}
);
};
return (
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<FormControl label="Note">
<Input {...form.register("note")} />
</FormControl>
<div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={form.formState.isSubmitting}>
Submit Request
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
type RequestProjectAccessModalProps = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
project?: Project;
onComplete?: () => void;
};
export const RequestProjectAccessModal = ({
isOpen,
onOpenChange,
project,
onComplete
}: RequestProjectAccessModalProps) => {
if (!project) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Confirm Access Request"
subTitle={`Requesting access to project ${project?.name}. You may include an optional note for project admins to review your request.`}
>
<Content
onComplete={() => {
onOpenChange(false);
if (onComplete) onComplete();
}}
projectId={project?.id}
/>
</ModalContent>
</Modal>
);
};
@@ -1 +1,2 @@
export { NewProjectModal } from "./NewProjectModal";
export * from "./RequestProjectAccessModal";
@@ -1,18 +1,20 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
import { Modal, ModalContent } from "@app/components/v2";
import { ProjectEnv } from "@app/hooks/api/projects/types";
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
type SharedProps = {
secretPath: string;
environment?: string;
environments?: WorkspaceEnv[];
environments?: ProjectEnv[];
};
type Props = {
@@ -24,14 +26,23 @@ type ContentProps = {
onComplete: (secretRotation: TSecretRotationV2) => void;
selectedRotation: SecretRotation | null;
setSelectedRotation: (selectedRotation: SecretRotation | null) => void;
initialFormData?: Partial<TSecretRotationV2Form>;
onCancel: () => void;
} & SharedProps;
const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentProps) => {
const Content = ({
setSelectedRotation,
selectedRotation,
initialFormData,
onCancel,
...props
}: ContentProps) => {
if (selectedRotation) {
return (
<SecretRotationV2Form
onCancel={() => setSelectedRotation(null)}
onCancel={onCancel}
type={selectedRotation}
initialFormData={initialFormData}
{...props}
/>
);
@@ -42,12 +53,60 @@ const Content = ({ setSelectedRotation, selectedRotation, ...props }: ContentPro
export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }: Props) => {
const [selectedRotation, setSelectedRotation] = useState<SecretRotation | null>(null);
const [initialFormData, setInitialFormData] = useState<Partial<TSecretRotationV2Form>>();
const {
location: {
search: { connectionId, connectionName, ...search },
pathname
}
} = useRouterState();
const navigate = useNavigate();
useEffect(() => {
if (connectionId && connectionName) {
const storedFormData = localStorage.getItem("secretRotationFormData");
if (!storedFormData) return;
let form: Partial<TSecretRotationV2Form> = {};
try {
form = JSON.parse(storedFormData) as TSecretRotationV2Form;
} catch {
return;
} finally {
localStorage.removeItem("secretRotationFormData");
}
onOpenChange(true);
setSelectedRotation(form.type ?? null);
setInitialFormData({
...form,
connection: { id: connectionId, name: connectionName }
});
navigate({
to: pathname,
search
});
}
}, [connectionId, connectionName]);
const handleReset = () => {
setSelectedRotation(null);
setInitialFormData(undefined);
};
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) setSelectedRotation(null);
if (!open) {
handleReset();
}
onOpenChange(open);
}}
>
@@ -84,9 +143,11 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
>
<Content
onComplete={() => {
setSelectedRotation(null);
handleReset();
onOpenChange(false);
}}
onCancel={handleReset}
initialFormData={initialFormData}
selectedRotation={selectedRotation}
setSelectedRotation={setSelectedRotation}
{...props}
@@ -9,14 +9,14 @@ import {
IS_ROTATION_DUAL_CREDENTIALS,
SECRET_ROTATION_MAP
} from "@app/helpers/secretRotationsV2";
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
import { ProjectEnv } from "@app/hooks/api/projects/types";
import { TSecretRotationV2Form } from "./schemas";
import { SecretRotationV2ConnectionField } from "./SecretRotationV2ConnectionField";
type Props = {
isUpdate: boolean;
environments?: WorkspaceEnv[];
environments?: ProjectEnv[];
};
export const SecretRotationV2ConfigurationFields = ({ isUpdate, environments }: Props) => {
@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/helpers/secretRotationsV2";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretRotationV2Form } from "./schemas";
@@ -18,19 +21,26 @@ type Props = {
};
export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretRotationV2Form>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretRotationV2Form>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const rotationType = watch("type");
const app = SECRET_ROTATION_CONNECTION_MAP[rotationType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const { currentProject } = useProject();
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentProject.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
const appName = APP_CONNECTION_MAP[app].name;
@@ -66,37 +76,56 @@ export const SecretRotationV2ConnectionField = ({ onChange: callback, isUpdate }
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
// store for oauth callback connections
localStorage.setItem("secretRotationFormData", JSON.stringify(watch()));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
{!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
You do not have access to any {appName} Connections. Contact an admin to create one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// remove form storage, not oauth connection
localStorage.removeItem("secretRotationFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentProject.type}
projectId={currentProject.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};
@@ -11,8 +11,9 @@ import { SecretRotationV2ParametersFields } from "@app/components/secret-rotatio
import { SecretRotationV2ReviewFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields";
import { SecretRotationV2SecretsMappingFields } from "@app/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields";
import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { IS_ROTATION_DUAL_CREDENTIALS, SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { ProjectEnv } from "@app/hooks/api/projects/types";
import {
SecretRotation,
TSecretRotationV2,
@@ -22,7 +23,6 @@ import {
useCreateSecretRotationV2,
useUpdateSecretRotationV2
} from "@app/hooks/api/secretRotationsV2/mutations";
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
import { SecretRotationV2FormSchema, TSecretRotationV2Form } from "./schemas";
@@ -32,8 +32,9 @@ type Props = {
onCancel: () => void;
secretPath: string;
environment?: string;
environments?: WorkspaceEnv[];
environments?: ProjectEnv[];
secretRotation?: TSecretRotationV2;
initialFormData?: Partial<TSecretRotationV2Form>;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Form)[] }[] = [
@@ -64,11 +65,12 @@ export const SecretRotationV2Form = ({
environment: envSlug,
secretPath,
secretRotation,
environments
environments,
initialFormData
}: Props) => {
const createSecretRotation = useCreateSecretRotationV2();
const updateSecretRotation = useUpdateSecretRotationV2();
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { name: rotationType } = SECRET_ROTATION_MAP[type];
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
@@ -80,7 +82,7 @@ export const SecretRotationV2Form = ({
defaultValues: secretRotation
? {
...secretRotation,
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
environment: currentProject?.environments.find((env) => env.slug === envSlug),
secretPath
}
: {
@@ -91,9 +93,10 @@ export const SecretRotationV2Form = ({
hours: 0,
minutes: 0
},
environment: currentWorkspace?.environments.find((env) => env.slug === envSlug),
environment: currentProject?.environments.find((env) => env.slug === envSlug),
secretPath,
...(rotationOption!.template as object) // can't infer type since we don't know which specific type it is
...((rotationOption?.template as object) ?? {}), // can't infer type since we don't know which specific type it is
...(initialFormData as object)
},
reValidateMode: "onChange"
});
@@ -115,7 +118,7 @@ export const SecretRotationV2Form = ({
connectionId: connection.id,
environment: environment.slug,
projectId: currentWorkspace.id
projectId: currentProject.id
});
try {
const rotation = await mutation;
@@ -1,7 +1,9 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2";
import {
SecretScanningDataSource,
@@ -21,16 +23,19 @@ type ContentProps = {
onComplete: (dataSource: TSecretScanningDataSource) => void;
selectedDataSource: SecretScanningDataSource | null;
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
onCancel: () => void;
};
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
const Content = ({
setSelectedDataSource,
selectedDataSource,
onCancel,
...props
}: ContentProps) => {
if (selectedDataSource) {
return (
<SecretScanningDataSourceForm
onCancel={() => setSelectedDataSource(null)}
type={selectedDataSource}
{...props}
/>
<SecretScanningDataSourceForm onCancel={onCancel} type={selectedDataSource} {...props} />
);
}
@@ -41,12 +46,60 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
null
);
const [initialFormData, setInitialFormData] = useState<Partial<TSecretScanningDataSourceForm>>();
const {
location: {
search: { connectionId, connectionName, ...search },
pathname
}
} = useRouterState();
const navigate = useNavigate();
useEffect(() => {
if (connectionId && connectionName) {
const storedFormData = localStorage.getItem("secretScanningDataSourceFormData");
if (!storedFormData) return;
let form: Partial<TSecretScanningDataSourceForm> = {};
try {
form = JSON.parse(storedFormData) as TSecretScanningDataSourceForm;
} catch {
return;
} finally {
localStorage.removeItem("secretScanningDataSourceFormData");
}
onOpenChange(true);
setSelectedDataSource(form.type ?? null);
setInitialFormData({
...form,
connection: { id: connectionId, name: connectionName }
});
navigate({
to: pathname,
search
});
}
}, [connectionId, connectionName]);
const resetModal = () => {
setSelectedDataSource(null);
setInitialFormData(undefined);
};
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) setSelectedDataSource(null);
if (!open) {
resetModal();
}
onOpenChange(open);
}}
>
@@ -83,11 +136,13 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
>
<Content
onComplete={() => {
setSelectedDataSource(null);
resetModal();
onOpenChange(false);
}}
onCancel={resetModal}
selectedDataSource={selectedDataSource}
setSelectedDataSource={setSelectedDataSource}
initialFormData={initialFormData}
{...props}
/>
</ModalContent>
@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretScanningDataSourceForm } from "./schemas";
@@ -21,19 +24,26 @@ export const SecretScanningDataSourceConnectionField = ({
onChange: callback,
isUpdate
}: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretScanningDataSourceForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const dataSourceType = watch("type");
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const { currentProject } = useProject();
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentProject.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
return (
@@ -67,37 +77,57 @@ export const SecretScanningDataSourceConnectionField = ({
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
// store for oauth callback connections
localStorage.setItem("secretScanningDataSourceFormData", JSON.stringify(watch()));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
isDisabled={isUpdate}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{!isUpdate && availableConnections?.length === 0 && (
{!isUpdate && !isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {connectionName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${connectionName} Connections. Contact an admin to create one.`
)}
You do not have access to any {connectionName} Connections. Contact an admin to create
one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// remove form storage, not oauth connection
localStorage.removeItem("secretScanningDataSourceFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentProject.type}
projectId={currentProject.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};
@@ -6,7 +6,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import {
SecretScanningDataSource,
@@ -27,6 +27,7 @@ type Props = {
type: SecretScanningDataSource;
onCancel: () => void;
dataSource?: TSecretScanningDataSource;
initialFormData?: Partial<TSecretScanningDataSourceForm>;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
@@ -36,10 +37,16 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningData
{ name: "Review", key: "review", fields: [] }
];
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
export const SecretScanningDataSourceForm = ({
type,
onComplete,
onCancel,
dataSource,
initialFormData
}: Props) => {
const createDataSource = useCreateSecretScanningDataSource();
const updateDataSource = useUpdateSecretScanningDataSource();
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
@@ -48,7 +55,8 @@ export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataS
resolver: zodResolver(SecretScanningDataSourceSchema),
defaultValues: dataSource ?? {
type,
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
isAutoScanEnabled: true, // scott: this may need to be derived from type in the future
...(initialFormData as object)
},
reValidateMode: "onChange"
});
@@ -63,7 +71,7 @@ export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataS
: createDataSource.mutateAsync({
...formData,
connectionId: connection?.id,
projectId: currentWorkspace.id
projectId: currentProject.id
});
try {
const source = await mutation;
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
@@ -11,18 +12,21 @@ type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
selectSync?: SecretSync | null;
initialFormData?: Partial<TSecretSyncForm>;
};
type ContentProps = {
onComplete: (secretSync: TSecretSync) => void;
selectedSync: SecretSync | null;
setSelectedSync: (selectedSync: SecretSync | null) => void;
initialFormData?: Partial<TSecretSyncForm>;
};
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
const Content = ({ onComplete, setSelectedSync, selectedSync, initialFormData }: ContentProps) => {
if (selectedSync) {
return (
<CreateSecretSyncForm
initialFormData={initialFormData}
onComplete={onComplete}
onCancel={() => setSelectedSync(null)}
destination={selectedSync}
@@ -33,7 +37,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
return <SecretSyncSelect onSelect={setSelectedSync} />;
};
export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
export const CreateSecretSyncModal = ({
onOpenChange,
selectSync = null,
initialFormData,
...props
}: Props) => {
const [selectedSync, setSelectedSync] = useState<SecretSync | null>(selectSync);
useEffect(() => {
@@ -67,6 +76,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
}}
selectedSync={selectedSync}
setSelectedSync={setSelectedSync}
initialFormData={initialFormData}
/>
</ModalContent>
</Modal>
@@ -8,7 +8,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import {
SecretSync,
@@ -29,6 +29,7 @@ type Props = {
onComplete: (secretSync: TSecretSync) => void;
destination: SecretSync;
onCancel: () => void;
initialFormData?: Partial<TSecretSyncForm>;
};
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[] }[] = [
@@ -39,14 +40,20 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretSyncForm)[]
{ name: "Review", key: "review", fields: [] }
];
export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Props) => {
export const CreateSecretSyncForm = ({
destination,
onComplete,
onCancel,
initialFormData
}: Props) => {
const createSecretSync = useCreateSecretSync();
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
// scoot: right now we only do this when creating a connection so we know index 1
const [selectedTabIndex, setSelectedTabIndex] = useState(initialFormData ? 1 : 0);
const { syncOption } = useSecretSyncOption(destination);
@@ -59,7 +66,8 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
initialSyncBehavior: syncOption?.canImportSecrets
? undefined
: SecretSyncInitialSyncBehavior.OverwriteDestination
}
},
...initialFormData
} as Partial<TSecretSyncForm>,
reValidateMode: "onChange"
});
@@ -70,7 +78,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
...formData,
connectionId: connection.id,
environment: environment.slug,
projectId: currentWorkspace.id
projectId: currentProject.id
});
createNotification({
@@ -1,14 +1,17 @@
import { Controller, useFormContext } from "react-hook-form";
import { SingleValue } from "react-select";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AppConnectionOption } from "@app/components/app-connections";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs";
import { usePopUp } from "@app/hooks";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { TSecretSyncForm } from "./schemas";
@@ -17,19 +20,26 @@ type Props = {
};
export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretSyncForm>();
const { permission } = useProjectPermission();
const { control, watch, setValue } = useFormContext<TSecretSyncForm>();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[destination];
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const { currentProject } = useProject();
const { data: availableConnections, isPending } = useListAvailableAppConnections(
app,
currentProject.id
);
const connectionName = APP_CONNECTION_MAP[app].name;
const canCreateConnection = permission.can(
OrgPermissionAppConnectionActions.Create,
OrgPermissionSubjects.AppConnections
ProjectPermissionAppConnectionActions.Create,
ProjectPermissionSub.AppConnections
);
const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name;
@@ -51,36 +61,55 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
<FilterableSelect
value={value}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
handlePopUpOpen("addConnection");
onChange(null);
// store for oauth callback connections
localStorage.setItem("secretSyncFormData", JSON.stringify(watch()));
if (callback) callback();
return;
}
onChange(newValue);
if (callback) callback();
}}
isLoading={isPending}
options={availableConnections}
options={[
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
...(availableConnections ?? [])
]}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
control={control}
name="connection"
/>
{availableConnections?.length === 0 && (
{!isPending && !availableConnections?.length && !canCreateConnection && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
<>
You do not have access to any {appName} Connections. Create one from the{" "}
<Link to="/organization/app-connections" className="underline">
App Connections
</Link>{" "}
page.
</>
) : (
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
)}
You do not have access to any {appName} Connections. Contact an admin to create one.
</p>
)}
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => {
// remove form storage, not oauth connection
localStorage.removeItem("secretSyncFormData");
handlePopUpToggle("addConnection", isOpen);
}}
projectType={currentProject.type}
projectId={currentProject.id}
app={app}
onComplete={(connection) => {
if (connection) {
setValue("connection", connection);
}
}}
/>
</>
);
};
@@ -4,7 +4,7 @@ import { subject } from "@casl/ability";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useProjectPermission, useWorkspace } from "@app/context";
import { useProject, useProjectPermission } from "@app/context";
import {
ProjectPermissionSecretSyncActions,
ProjectPermissionSub
@@ -16,7 +16,7 @@ export const SecretSyncSourceFields = () => {
const { control, watch, setError, clearErrors } = useFormContext<TSecretSyncForm>();
const { permission } = useProjectPermission();
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const selectedEnvironment = watch("environment");
const selectedSecretPath = watch("secretPath");
@@ -48,7 +48,7 @@ export const SecretSyncSourceFields = () => {
</p>
<Controller
defaultValue={currentWorkspace.environments[0]}
defaultValue={currentProject.environments[0]}
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
@@ -56,7 +56,7 @@ export const SecretSyncSourceFields = () => {
<FilterableSelect
value={value}
onChange={onChange}
options={currentWorkspace.environments}
options={currentProject.environments}
placeholder="Select environment..."
getOptionLabel={(option) => option?.name}
getOptionValue={(option) => option?.id}
@@ -13,7 +13,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useGetSecretReferenceTree } from "@app/hooks/api";
import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types";
@@ -29,17 +29,54 @@ const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
export const hasSecretReference = (value: string | undefined) =>
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
const createNodeId = (node: TSecretReferenceTraceNode): string =>
`${node.environment}:${node.secretPath}:${node.key}`;
const isCircularReference = (
node: TSecretReferenceTraceNode,
visitedPath: Set<string>
): boolean => {
const nodeId = createNodeId(node);
return visitedPath.has(nodeId);
};
const hasCircularReferences = (
node: TSecretReferenceTraceNode,
visitedPath: Set<string> = new Set()
): boolean => {
const nodeId = createNodeId(node);
if (visitedPath.has(nodeId)) {
return true;
}
const newVisitedPath = new Set([...visitedPath, nodeId]);
return node.children.some((child) => hasCircularReferences(child, newVisitedPath));
};
export const SecretReferenceNode = ({
node,
isRoot,
secretKey
secretKey,
visitedPath = new Set()
}: {
node: TSecretReferenceTraceNode;
isRoot?: boolean;
secretKey?: string;
visitedPath?: Set<string>;
}) => {
const [isOpen, setIsOpen] = useState(false);
const hasChildren = node.children.length > 0;
const nodeId = createNodeId(node);
const isCircular = !isRoot && isCircularReference(node, visitedPath);
const newVisitedPath = isCircular ? visitedPath : new Set([...visitedPath, nodeId]);
const safeChildren = isCircular
? []
: node.children.filter((child) => !isCircularReference(child, newVisitedPath));
const hasChildren = safeChildren.length > 0;
return (
<li>
@@ -77,8 +114,12 @@ export const SecretReferenceNode = ({
<Collapsible.Content className={twMerge("mt-4", style.collapsibleContent)}>
{hasChildren && (
<ul>
{node.children.map((el, index) => (
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
{safeChildren.map((el, index) => (
<SecretReferenceNode
node={el}
key={`${el.key}-${index + 1}`}
visitedPath={newVisitedPath}
/>
))}
</ul>
)}
@@ -89,8 +130,8 @@ export const SecretReferenceNode = ({
};
export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data, isPending, isError, error } = useGetSecretReferenceTree({
secretPath,
@@ -102,6 +143,9 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const tree = data?.tree;
const secretValue = data?.value;
// Check if the tree contains circular references
const hasCirculars = tree ? hasCircularReferences(tree) : false;
useEffect(() => {
if (error instanceof AxiosError) {
const err = error?.response?.data as TApiErrors;
@@ -130,9 +174,25 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
);
}
if (tree?.children?.length === 0) {
return (
<div className="flex items-center justify-center py-4">
<span className="text-mineshaft-400">This secret does not contain references</span>
</div>
);
}
return (
<div>
<FormControl label="Expanded value">
<FormControl
label="Expanded value"
tooltipText={
hasCirculars
? "This secret contains circular references. Value shown is resolved once, with circular paths truncated in the reference tree below."
: undefined
}
tooltipClassName="max-w-md break-words"
>
<SecretInput
key="value-overriden"
isReadOnly
@@ -15,7 +15,7 @@ import {
ModalContent,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
@@ -115,11 +115,10 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
}
});
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { mutateAsync: createWsTag } = useCreateWsTag();
const [showHexInput, setShowHexInput] = useState<boolean>(false);
const selectedTagColor = watch("color", secretTagsColors[0].hex);
@@ -130,7 +129,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
const onFormSubmit = async ({ slug, color }: FormData) => {
try {
await createWsTag({
workspaceID: workspaceId,
projectId,
tagColor: color,
tagSlug: slug
});
@@ -1,9 +1,9 @@
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons";
import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useDebounce, useToggle } from "@app/hooks";
import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api";
@@ -55,6 +55,8 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
secretPath?: string;
environment?: string;
containerClassName?: string;
isLoadingValue?: boolean;
isErrorLoadingValue?: boolean;
};
type ReferenceItem = {
@@ -76,8 +78,8 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
},
ref
) => {
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const [debouncedValue] = useDebounce(value, 100);
@@ -101,7 +103,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
let predicate = suggestionSourceValue;
if (isDeep) {
const [envSlug, ...folderPaths] = suggestionSourceValue.split(".");
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
const isValidEnvSlug = currentProject?.environments.find((e) => e.slug === envSlug);
suggestionSourceEnv = isValidEnvSlug ? envSlug : undefined;
suggestionSourceSecretPath = `/${folderPaths.slice(0, -1)?.join("/")}`;
predicate = folderPaths[folderPaths.length - 1];
@@ -125,7 +127,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
viewSecretValue: false,
environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "",
workspaceId,
projectId,
options: {
enabled: isPopupOpen
}
@@ -133,7 +135,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const { data: folders } = useGetProjectFolders({
environment: suggestionSource.environment || "",
path: suggestionSource.secretPath || "",
projectId: workspaceId,
projectId,
options: {
enabled: isPopupOpen
}
@@ -148,7 +150,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
if (!suggestionSource.isDeep) {
// At first level only environments and secrets
(currentWorkspace?.environments || []).forEach(({ name, slug }) => {
(currentProject?.environments || []).forEach(({ name, slug }) => {
if (name.toLowerCase().startsWith(predicate))
suggestionsArr.push({
label: name,
@@ -175,13 +177,29 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
type: ReferenceType.SECRET
});
});
if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) {
suggestionsArr.push({
label: "No matches found",
slug: "__no_match__",
type: ReferenceType.SECRET
});
}
return suggestionsArr;
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
}, [
secrets,
folders,
currentProject?.environments,
isPopupOpen,
suggestionSource.value,
suggestionSource.predicate
]);
const handleSuggestionSelect = (selectIndex?: number) => {
const selectedSuggestion =
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
if (!selectedSuggestion) {
if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") {
return;
}
@@ -226,21 +244,40 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
if (isPopupOpen) {
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex + 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
let nextIndex = mod(prevIndex + 1, suggestions.length);
// Skip "no match" messages
while (
nextIndex < suggestions.length &&
suggestions[nextIndex].slug === "__no_match__"
) {
nextIndex = mod(nextIndex + 1, suggestions.length);
}
// If we only have no-match messages, don't highlight anything
if (suggestions[nextIndex]?.slug === "__no_match__") {
return -1;
}
popoverContentRef.current?.children?.[nextIndex]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
return nextIndex;
});
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex - 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
let prevIdx = mod(prevIndex - 1, suggestions.length);
// Skip "no match" messages
while (prevIdx >= 0 && suggestions[prevIdx].slug === "__no_match__") {
prevIdx = mod(prevIdx - 1, suggestions.length);
}
// If we only have no-match messages, don't highlight anything
if (suggestions[prevIdx]?.slug === "__no_match__") {
return -1;
}
popoverContentRef.current?.children?.[prevIdx]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
return prevIdx;
});
} else if (e.key === "Enter" && highlightedIndex >= 0) {
e.preventDefault();
@@ -279,11 +316,16 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
ref={handleRef}
onKeyDown={handleKeyDown}
value={value}
onFocus={() => setIsFocused.on()}
onFocus={(evt) => {
if (props.onFocus) props.onFocus(evt);
setIsFocused.on();
}}
onBlur={(evt) => {
// should not on blur when its mouse down selecting a item from suggestion
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
setIsFocused.off();
if (props.onBlur) props.onBlur(evt);
}}
onChange={(e) => onChange?.(e.target.value)}
containerClassName={containerClassName}
@@ -304,7 +346,12 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
{suggestions.map((item, i) => {
let entryIcon;
let subText;
if (item.type === ReferenceType.SECRET) {
const isNoMatchMessage = item.slug === "__no_match__";
if (isNoMatchMessage) {
entryIcon = <FontAwesomeIcon icon={faSearch} className="text-gray-400" />;
subText = "No results";
} else if (item.type === ReferenceType.SECRET) {
entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />;
subText = "Secret";
} else if (item.type === ReferenceType.ENVIRONMENT) {
@@ -315,10 +362,28 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
subText = "Folder";
}
return (
return isNoMatchMessage ? (
<div
tabIndex={0}
role="button"
role="status"
aria-label="no-match-message"
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div className="text-md relative flex w-full cursor-default select-none items-center justify-between px-2 py-2 opacity-75 outline-none transition-all">
<div className="flex w-full items-start gap-2">
<div className="mt-1 flex items-center">{entryIcon}</div>
<div className="text-md w-10/12 truncate text-left">
<span className="text-gray-400">{item.label}</span>
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
{subText}
</div>
</div>
</div>
</div>
</div>
) : (
<button
type="button"
onKeyDown={(e) => {
if (e.key === "Enter") handleSuggestionSelect(i);
}}
@@ -330,8 +395,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
handleSuggestionSelect(i);
}}
onMouseEnter={() => setHighlightedIndex(i)}
style={{ pointerEvents: "auto" }}
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
className="flex w-full items-center justify-between border-none border-mineshaft-600 bg-transparent p-0 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div
@@ -342,14 +406,14 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
<div className="flex w-full items-start gap-2">
<div className="mt-1 flex items-center">{entryIcon}</div>
<div className="text-md w-10/12 truncate text-left">
{item.label}
<span>{item.label}</span>
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
{subText}
</div>
</div>
</div>
</div>
</div>
</button>
);
})}
</div>
+4 -1
View File
@@ -12,6 +12,7 @@ type Props = {
isReadOnly?: boolean;
autoCapitalization?: boolean;
containerClassName?: string;
warning?: ReactNode;
};
const inputVariants = cva(
@@ -84,6 +85,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
size = "md",
isReadOnly,
autoCapitalization,
warning,
...props
},
ref
@@ -116,10 +118,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
onInput={handleInput}
className={twMerge(
leftIcon ? "pl-10" : "pl-2.5",
rightIcon ? "pr-10" : "pr-2.5",
rightIcon || warning ? "pr-10" : "pr-2.5",
inputVariants({ className, isError, size, isRounded, variant })
)}
/>
{Boolean(warning) && !rightIcon && warning}
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
</div>
);
@@ -7,7 +7,16 @@ import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPa
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
const syntaxHighlight = (
content?: string | null,
isVisible?: boolean,
isImport?: boolean,
isLoadingValue?: boolean,
isErrorLoadingValue?: boolean
) => {
if (isLoadingValue) return HIDDEN_SECRET_VALUE;
if (isErrorLoadingValue)
return <span className="ph-no-capture text-red/75">Error loading secret value.</span>;
if (isImport && !content) return "IMPORTED";
if (content === "") return "EMPTY";
if (!content) return "EMPTY";
@@ -20,7 +29,8 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
skipNext = true;
return (
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
&#36;&#123;<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#36;&#123;
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#125;
</span>
);
@@ -48,6 +58,8 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
isDisabled?: boolean;
containerClassName?: string;
canEditButNotView?: boolean;
isLoadingValue?: boolean;
isErrorLoadingValue?: boolean;
};
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
@@ -65,6 +77,8 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
isReadOnly,
onFocus,
canEditButNotView,
isLoadingValue,
isErrorLoadingValue,
...props
},
ref
@@ -83,7 +97,9 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
{syntaxHighlight(
value,
isVisible || (isSecretFocused && !valueAlwaysHidden),
isImport
isImport,
isLoadingValue,
isErrorLoadingValue
)}
</span>
</code>
@@ -114,7 +130,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
}}
value={value || ""}
{...props}
readOnly={isReadOnly}
readOnly={isReadOnly || isLoadingValue || isErrorLoadingValue}
/>
</div>
</div>
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetFoldersByEnv } from "@app/hooks/api";
@@ -35,12 +35,12 @@ export const SecretPathInput = ({
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const [debouncedInputValue] = useDebounce(inputValue, 200);
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { folderNames: folders } = useGetFoldersByEnv({
path: secretPath,
environments: [environment || currentWorkspace?.environments?.[0].slug || ""],
projectId: workspaceId
environments: [environment || currentProject?.environments?.[0].slug || ""],
projectId
});
useEffect(() => {
+8
View File
@@ -320,6 +320,14 @@ export const ROUTE_PATHS = Object.freeze({
PkiSubscriberDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
),
IntegrationsListPage: setRoute(
"/projects/cert-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
),
PkiSyncDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/integrations/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
)
},
Ssh: {
@@ -1,4 +1,4 @@
import { MongoAbility } from "@casl/ability";
import { ForcedSubject, MongoAbility } from "@casl/ability";
export enum OrgPermissionActions {
Read = "read",
@@ -21,6 +21,13 @@ export enum OrgGatewayPermissionActions {
AttachGateways = "attach-gateways"
}
export enum OrgRelayPermissionActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
CreateTemplates = "create-templates",
@@ -32,6 +39,7 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
export enum OrgPermissionSubjects {
Workspace = "workspace",
Project = "project",
Role = "role",
Member = "member",
Settings = "settings",
@@ -50,6 +58,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share",
GithubOrgSync = "github-org-sync",
GithubOrgSyncManual = "github-org-sync-manual",
@@ -109,6 +118,7 @@ export type AppConnectionSubjectFields = {
export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
| [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionActions, OrgPermissionSubjects.Member]
@@ -126,7 +136,6 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [
@@ -134,14 +143,14 @@ export type OrgPermissionSet =
OrgPermissionSubjects.MachineIdentityAuthTemplate
]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
// TODO(scott): add back once org UI refactored
// | [
// OrgPermissionAppConnectionActions,
// (
// | OrgPermissionSubjects.AppConnections
// | (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
// )
// ];
| [OrgRelayPermissionActions, OrgPermissionSubjects.Relay]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
| [
OrgPermissionAppConnectionActions,
(
| OrgPermissionSubjects.AppConnections
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -0,0 +1,22 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useParams } from "@tanstack/react-router";
import { projectKeys } from "@app/hooks/api";
import { fetchProjectById } from "@app/hooks/api/projects/queries";
export const useProject = () => {
const params = useParams({
strict: false
});
if (!params.projectId) {
throw new Error("Missing project id");
}
const { data: currentProject } = useSuspenseQuery({
queryKey: projectKeys.getProjectById(params.projectId),
queryFn: () => fetchProjectById(params.projectId as string),
staleTime: Infinity
});
return { currentProject, projectId: currentProject.id };
};
@@ -0,0 +1 @@
export { useProject } from "./ProjectContext";
@@ -22,8 +22,8 @@ export const useProjectPermission = () => {
const {
data: { permission, membership, assumedPrivilegeDetails }
} = useSuspenseQuery({
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId: projectId }),
queryFn: () => fetchUserProjectPermissions({ workspaceId: projectId }),
queryKey: roleQueryKeys.getUserProjectPermissions({ projectId }),
queryFn: () => fetchUserProjectPermissions({ projectId }),
staleTime: Infinity,
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
@@ -62,6 +62,16 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionPkiSyncActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SyncCertificates = "sync-certificates",
ImportCertificates = "import-certificates",
RemoveCertificates = "remove-certificates"
}
export enum ProjectPermissionIdentityActions {
Read = "read",
Create = "create",
@@ -154,6 +164,14 @@ export enum ProjectPermissionAuditLogsActions {
Read = "read"
}
export enum ProjectPermissionAppConnectionActions {
Read = "read-app-connections",
Create = "create-app-connections",
Edit = "edit-app-connections",
Delete = "delete-app-connections",
Connect = "connect-app-connections"
}
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
@@ -173,8 +191,13 @@ export type IdentityManagementSubjectFields = {
identityId: string;
};
export type AppConnectionSubjectFields = {
connectionId: string;
};
export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.PkiSyncs
| ProjectPermissionSub.Secrets
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity
@@ -184,7 +207,8 @@ export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.SecretEvents;
| ProjectPermissionSub.SecretEvents
| ProjectPermissionSub.AppConnections;
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to",
@@ -258,12 +282,14 @@ export enum ProjectPermissionSub {
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs",
PkiSyncs = "pki-syncs",
Kmip = "kmip",
Commits = "commits",
SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings",
SecretScanningConfigs = "secret-scanning-configs",
SecretEvents = "secret-events"
SecretEvents = "secret-events",
AppConnections = "app-connections"
}
export type SecretSubjectFields = {
@@ -302,6 +328,10 @@ export type SecretSyncSubjectFields = {
secretPath: string;
};
export type PkiSyncSubjectFields = {
subscriberId: string;
};
export type SecretRotationSubjectFields = {
environment: string;
secretPath: string;
@@ -349,6 +379,13 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
)
]
| [
ProjectPermissionPkiSyncActions,
(
| ProjectPermissionSub.PkiSyncs
| (ForcedSubject<ProjectPermissionSub.PkiSyncs> & PkiSyncSubjectFields)
)
]
| [
ProjectPermissionActions,
(
@@ -431,6 +468,13 @@ export type ProjectPermissionSet =
| ProjectPermissionSub.SecretEvents
| (ForcedSubject<ProjectPermissionSub.SecretEvents> & SecretEventSubjectFields)
)
]
| [
ProjectPermissionAppConnectionActions,
(
| ProjectPermissionSub.AppConnections
| (ForcedSubject<ProjectPermissionSub.AppConnections> & AppConnectionSubjectFields)
)
];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
@@ -1,22 +0,0 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useParams } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
export const useWorkspace = () => {
const params = useParams({
strict: false
});
if (!params.projectId) {
throw new Error("Missing project id");
}
const { data: currentWorkspace } = useSuspenseQuery({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId as string),
staleTime: Infinity
});
return { currentWorkspace };
};
@@ -1 +0,0 @@
export { useWorkspace } from "./WorkspaceContext";
+1 -1
View File
@@ -9,6 +9,7 @@ export {
OrgPermissionSubjects,
useOrgPermission
} from "./OrgPermissionContext";
export { useProject } from "./ProjectContext";
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
@@ -29,4 +30,3 @@ export {
export { useServerConfig } from "./ServerConfigContext";
export { useSubscription } from "./SubscriptionContext";
export { useUser } from "./UserContext";
export { useWorkspace } from "./WorkspaceContext";
+9
View File
@@ -8,6 +8,7 @@ import {
faServer,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { useRouterState } from "@tanstack/react-router";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
@@ -221,3 +222,11 @@ export const AWS_REGIONS = [
{ name: "AWS GovCloud (US-East)", slug: "us-gov-east-1" },
{ name: "AWS GovCloud (US-West)", slug: "us-gov-west-1" }
];
export const useGetAppConnectionOauthReturnUrl = () => {
const {
location: { pathname }
} = useRouterState();
return pathname;
};
+3
View File
@@ -8,6 +8,7 @@ export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record<
LogProvider,
{ name: string; image?: string; icon?: IconDefinition; size?: number }
> = {
[LogProvider.Azure]: { name: "Azure", image: "Microsoft Azure.png", size: 60 },
[LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 },
[LogProvider.Custom]: { name: "Custom", icon: faCode },
[LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" },
@@ -25,6 +26,8 @@ export function getProviderUrl(
return logStream.credentials.url;
case LogProvider.Splunk:
return `https://${logStream.credentials.hostname}:8088/services/collector/event`;
case LogProvider.Azure:
return `${logStream.credentials.dceUrl}/dataCollectionRules/${logStream.credentials.dcrId}/streams/Custom-${logStream.credentials.cltName}_CL`;
default:
throw new Error(
`Unhandled provider in getProviderUrl: ${(logStream as TAuditLogStream).provider}`
+19
View File
@@ -0,0 +1,19 @@
import { AppConnection } from "@app/hooks/api/appConnections";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
export const PKI_SYNC_MAP: Record<
PkiSync,
{
name: string;
image: string;
}
> = {
[PkiSync.AzureKeyVault]: {
name: "Azure Key Vault",
image: "Microsoft Azure.png"
}
};
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault
};
+5 -5
View File
@@ -1,6 +1,6 @@
import { apiRequest } from "@app/config/request";
import { createWorkspace } from "@app/hooks/api/workspace/queries";
import { ProjectType, WorkspaceEnv } from "@app/hooks/api/workspace/types";
import { createWorkspace } from "@app/hooks/api/projects/queries";
import { ProjectEnv, ProjectType } from "@app/hooks/api/projects/types";
const secretsToBeAdded = [
{
@@ -47,8 +47,8 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
});
try {
const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", {
workspaceId: project.id,
const { data } = await apiRequest.post("/api/v4/secrets/batch", {
projectId: project.id,
environment: "dev",
secretPath: "/",
secrets: secretsToBeAdded
@@ -74,7 +74,7 @@ export const getProjectBaseURL = (type: ProjectType) => {
// @ts-expect-error akhilmhdh: will remove this later
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const getProjectHomePage = (type: ProjectType, environments: WorkspaceEnv[]) => {
export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]) => {
switch (type) {
case ProjectType.SecretManager:
return "/projects/secret-management/$projectId/overview" as const;
@@ -16,8 +16,8 @@ import {
export const accessApprovalKeys = {
getAccessApprovalPolicies: (projectSlug: string) =>
[{ projectSlug }, "access-approval-policies"] as const,
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
[{ workspaceId, environment }, "access-approval-policy"] as const,
getAccessApprovalPolicyOfABoard: (projectId: string, environment: string) =>
[{ projectId, environment }, "access-approval-policy"] as const,
getAccessApprovalRequests: (
projectSlug: string,
@@ -1,7 +1,7 @@
import { EnforcementLevel, PolicyType } from "../policies/enums";
import { ProjectEnv } from "../projects/types";
import { TProjectPermission } from "../roles/types";
import { ApprovalStatus } from "../secretApprovalRequest/types";
import { WorkspaceEnv } from "../workspace/types";
export type TAccessApprovalPolicy = {
id: string;
@@ -9,7 +9,7 @@ export type TAccessApprovalPolicy = {
approvals: number;
secretPath: string;
workspace: string;
environments: WorkspaceEnv[];
environments: ProjectEnv[];
projectId: string;
policyType: PolicyType;
approversRequired: boolean;
@@ -1,3 +1,4 @@
export * from "./enums";
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -20,7 +20,10 @@ export const useCreateAppConnection = () => {
return data.appConnection;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() })
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
}
});
};
@@ -35,9 +38,10 @@ export const useUpdateAppConnection = () => {
return data.appConnection;
},
onSuccess: (_, { connectionId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
// queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
}
});
};
@@ -50,9 +54,10 @@ export const useDeleteAppConnection = () => {
return data;
},
onSuccess: (_, { connectionId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
onSuccess: ({ projectId, app }) => {
queryClient.invalidateQueries({ queryKey: appConnectionKeys.list(projectId) });
queryClient.invalidateQueries({ queryKey: appConnectionKeys.listAvailable(app, projectId) });
// queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) });
}
});
};
+105 -63
View File
@@ -5,29 +5,36 @@ import { apiRequest } from "@app/config/request";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
TAppConnection,
TAppConnectionMap,
TAppConnectionOptions,
TAvailableAppConnection,
TAvailableAppConnectionsResponse,
TGetAppConnection,
TListAppConnections
} from "@app/hooks/api/appConnections/types";
import {
TAppConnectionOption,
TAppConnectionOptionMap
} from "@app/hooks/api/appConnections/types/app-options";
import { ProjectType } from "@app/hooks/api/projects/types";
export const appConnectionKeys = {
all: ["app-connection"] as const,
options: () => [...appConnectionKeys.all, "options"] as const,
list: () => [...appConnectionKeys.all, "list"] as const,
listAvailable: (app: AppConnection) => [...appConnectionKeys.all, app, "list-available"] as const,
listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
byId: (app: AppConnection, connectionId: string) =>
[...appConnectionKeys.all, app, "by-id", connectionId] as const
options: (projectType?: ProjectType) =>
[...appConnectionKeys.all, "options", ...(projectType ? [projectType] : [])] as const,
list: (projectId?: string | null) =>
[...appConnectionKeys.all, "list", ...(projectId ? [projectId] : [])] as const,
listAvailable: (app: AppConnection, projectId?: string | null) =>
[...appConnectionKeys.all, app, "list-available", ...(projectId ? [projectId] : [])] as const
// scott: may need these in the future but not using now
// getUsage: (app: AppConnection, connectionId: string) =>
// [...appConnectionKeys.all, "usage", app, connectionId] as const
// listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
// scott: we will need this once we have individual app connection page
// byId: (app: AppConnection, connectionId: string) =>
// [...appConnectionKeys.all, app, "by-id", connectionId] as const
};
export const useAppConnectionOptions = (
projectType?: ProjectType,
options?: Omit<
UseQueryOptions<
TAppConnectionOption[],
@@ -39,10 +46,11 @@ export const useAppConnectionOptions = (
>
) => {
return useQuery({
queryKey: appConnectionKeys.options(),
queryKey: appConnectionKeys.options(projectType),
queryFn: async () => {
const { data } = await apiRequest.get<TAppConnectionOptions>(
"/api/v1/app-connections/options"
"/api/v1/app-connections/options",
{ params: { projectType } }
);
return data.appConnectionOptions;
@@ -64,6 +72,7 @@ export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
};
export const useListAppConnections = (
projectId?: string,
options?: Omit<
UseQueryOptions<
TAppConnection[],
@@ -75,10 +84,12 @@ export const useListAppConnections = (
>
) => {
return useQuery({
queryKey: appConnectionKeys.list(),
queryKey: appConnectionKeys.list(projectId),
queryFn: async () => {
const { data } =
await apiRequest.get<TListAppConnections<TAppConnection>>("/api/v1/app-connections");
const { data } = await apiRequest.get<TListAppConnections<TAppConnection>>(
"/api/v1/app-connections",
{ params: { projectId } }
);
return data.appConnections;
},
@@ -88,6 +99,7 @@ export const useListAppConnections = (
export const useListAvailableAppConnections = (
app: AppConnection,
projectId: string,
options?: Omit<
UseQueryOptions<
TAvailableAppConnection[],
@@ -99,10 +111,11 @@ export const useListAvailableAppConnections = (
>
) => {
return useQuery({
queryKey: appConnectionKeys.listAvailable(app),
queryKey: appConnectionKeys.listAvailable(app, projectId),
queryFn: async () => {
const { data } = await apiRequest.get<TAvailableAppConnectionsResponse>(
`/api/v1/app-connections/${app}/available`
`/api/v1/app-connections/${app}/available`,
{ params: { projectId } }
);
return data.appConnections;
@@ -111,53 +124,82 @@ export const useListAvailableAppConnections = (
});
};
export const useListAppConnectionsByApp = <T extends AppConnection>(
app: T,
options?: Omit<
UseQueryOptions<
TAppConnectionMap[T][],
unknown,
TAppConnectionMap[T][],
ReturnType<typeof appConnectionKeys.listByApp>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.listByApp(app),
queryFn: async () => {
const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
`/api/v1/app-connections/${app}`
);
// scott: may need these in the future but not using now
// export const useGetAppConnectionUsageById = (
// app: AppConnection,
// connectionId: string,
// options?: Omit<
// UseQueryOptions<
// AppConnectionUsage,
// unknown,
// AppConnectionUsage,
// ReturnType<typeof appConnectionKeys.getUsage>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.getUsage(app, connectionId),
// queryFn: async () => {
// const { data } = await apiRequest.get<AppConnectionUsage>(
// `/api/v1/app-connections/${app}/${connectionId}/usage`
// );
//
// return data;
// },
// ...options
// });
// };
return data.appConnections;
},
...options
});
};
// scott: may need these in the future but not using now
// export const useListAppConnectionsByApp = <T extends AppConnection>(
// app: T,
// options?: Omit<
// UseQueryOptions<
// TAppConnectionMap[T][],
// unknown,
// TAppConnectionMap[T][],
// ReturnType<typeof appConnectionKeys.listByApp>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.listByApp(app),
// queryFn: async () => {
// const { data } = await apiRequest.get<TListAppConnections<TAppConnectionMap[T]>>(
// `/api/v1/app-connections/${app}`
// );
//
// return data.appConnections;
// },
// ...options
// });
// };
export const useGetAppConnectionById = <T extends AppConnection>(
app: T,
connectionId: string,
options?: Omit<
UseQueryOptions<
TAppConnectionMap[T],
unknown,
TAppConnectionMap[T],
ReturnType<typeof appConnectionKeys.byId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.byId(app, connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
`/api/v1/app-connections/${app}/${connectionId}`
);
return data.appConnection;
},
...options
});
};
// scott: we will need this once we have individual app connection page
// export const useGetAppConnectionById = <T extends AppConnection>(
// app: T,
// connectionId: string,
// options?: Omit<
// UseQueryOptions<
// TAppConnectionMap[T],
// unknown,
// TAppConnectionMap[T],
// ReturnType<typeof appConnectionKeys.byId>
// >,
// "queryKey" | "queryFn"
// >
// ) => {
// return useQuery({
// queryKey: appConnectionKeys.byId(app, connectionId),
// queryFn: async () => {
// const { data } = await apiRequest.get<TGetAppConnection<TAppConnectionMap[T]>>(
// `/api/v1/app-connections/${app}/${connectionId}`
// );
//
// return data.appConnection;
// },
// ...options
// });
// };
@@ -116,10 +116,11 @@ export type TAppConnection =
| TNetlifyConnection
| TOktaConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
// scott: we will need this once we have individual app connection page
// export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] };
export type TAppConnectionResponse = { appConnection: TAppConnection };
export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppConnection[] };
@@ -133,6 +134,7 @@ export type TCreateAppConnectionDTO = Pick<
| "description"
| "isPlatformManagedCredentials"
| "gatewayId"
| "projectId"
>;
export type TUpdateAppConnectionDTO = Partial<
@@ -150,43 +152,60 @@ export type TDeleteAppConnectionDTO = {
connectionId: string;
};
export type TAppConnectionMap = {
[AppConnection.AWS]: TAwsConnection;
[AppConnection.GitHub]: TGitHubConnection;
[AppConnection.GitHubRadar]: TGitHubRadarConnection;
[AppConnection.GCP]: TGcpConnection;
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
[AppConnection.AzureDevOps]: TAzureDevOpsConnection;
[AppConnection.AzureADCS]: TAzureADCSConnection;
[AppConnection.Databricks]: TDatabricksConnection;
[AppConnection.Humanitec]: THumanitecConnection;
[AppConnection.TerraformCloud]: TTerraformCloudConnection;
[AppConnection.Vercel]: TVercelConnection;
[AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MsSql]: TMsSqlConnection;
[AppConnection.MySql]: TMySqlConnection;
[AppConnection.OracleDB]: TOracleDBConnection;
[AppConnection.Camunda]: TCamundaConnection;
[AppConnection.Windmill]: TWindmillConnection;
[AppConnection.Auth0]: TAuth0Connection;
[AppConnection.HCVault]: THCVaultConnection;
[AppConnection.LDAP]: TLdapConnection;
[AppConnection.TeamCity]: TTeamCityConnection;
[AppConnection.OCI]: TOCIConnection;
[AppConnection.OnePass]: TOnePassConnection;
[AppConnection.Heroku]: THerokuConnection;
[AppConnection.Render]: TRenderConnection;
[AppConnection.Flyio]: TFlyioConnection;
[AppConnection.GitLab]: TGitLabConnection;
[AppConnection.Cloudflare]: TCloudflareConnection;
[AppConnection.Bitbucket]: TBitbucketConnection;
[AppConnection.Zabbix]: TZabbixConnection;
[AppConnection.Railway]: TRailwayConnection;
[AppConnection.Checkly]: TChecklyConnection;
[AppConnection.Supabase]: TSupabaseConnection;
[AppConnection.DigitalOcean]: TDigitalOceanConnection;
[AppConnection.Netlify]: TNetlifyConnection;
[AppConnection.Okta]: TOktaConnection;
};
// scott: we will need this once we have individual app connection page
// export type TAppConnectionMap = {
// [AppConnection.AWS]: TAwsConnection;
// [AppConnection.GitHub]: TGitHubConnection;
// [AppConnection.GitHubRadar]: TGitHubRadarConnection;
// [AppConnection.GCP]: TGcpConnection;
// [AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
// [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
// [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
// [AppConnection.AzureDevOps]: TAzureDevOpsConnection;
// [AppConnection.AzureADCS]: TAzureADCSConnection;
// [AppConnection.Databricks]: TDatabricksConnection;
// [AppConnection.Humanitec]: THumanitecConnection;
// [AppConnection.TerraformCloud]: TTerraformCloudConnection;
// [AppConnection.Vercel]: TVercelConnection;
// [AppConnection.Postgres]: TPostgresConnection;
// [AppConnection.MsSql]: TMsSqlConnection;
// [AppConnection.MySql]: TMySqlConnection;
// [AppConnection.OracleDB]: TOracleDBConnection;
// [AppConnection.Camunda]: TCamundaConnection;
// [AppConnection.Windmill]: TWindmillConnection;
// [AppConnection.Auth0]: TAuth0Connection;
// [AppConnection.HCVault]: THCVaultConnection;
// [AppConnection.LDAP]: TLdapConnection;
// [AppConnection.TeamCity]: TTeamCityConnection;
// [AppConnection.OCI]: TOCIConnection;
// [AppConnection.OnePass]: TOnePassConnection;
// [AppConnection.Heroku]: THerokuConnection;
// [AppConnection.Render]: TRenderConnection;
// [AppConnection.Flyio]: TFlyioConnection;
// [AppConnection.GitLab]: TGitLabConnection;
// [AppConnection.Cloudflare]: TCloudflareConnection;
// [AppConnection.Bitbucket]: TBitbucketConnection;
// [AppConnection.Zabbix]: TZabbixConnection;
// [AppConnection.Railway]: TRailwayConnection;
// [AppConnection.Checkly]: TChecklyConnection;
// [AppConnection.Supabase]: TSupabaseConnection;
// [AppConnection.DigitalOcean]: TDigitalOceanConnection;
// [AppConnection.Netlify]: TNetlifyConnection;
// [AppConnection.Okta]: TOktaConnection;
// };
// scott: we will need this once we have individual app connection page
// export type AppConnectionUsage = {
// projects: Array<{
// id: string;
// name: string;
// slug: string;
// type: ProjectType;
// resources: {
// secretSyncs: Array<{ id: string; name: string }>;
// externalCas: Array<{ id: string; name: string }>;
// secretRotations: Array<{ id: string; name: string }>;
// dataSources: Array<{ id: string; name: string }>;
// };
// }>;
// };
@@ -1,3 +1,5 @@
import { ProjectType } from "@app/hooks/api/projects/types";
export type TRootAppConnection = {
id: string;
name: string;
@@ -8,4 +10,11 @@ export type TRootAppConnection = {
updatedAt: string;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
projectId?: string | null;
project?: {
name: string;
type: ProjectType;
slug: string;
id: string;
} | null;
};
@@ -8,7 +8,7 @@ export const useAssumeProjectPrivileges = () =>
useMutation({
mutationFn: async ({ projectId, actorId, actorType }: TProjectAssumePrivilegesDTO) => {
const { data } = await apiRequest.post<{ message: string }>(
`/api/v1/workspace/${projectId}/assume-privileges`,
`/api/v1/projects/${projectId}/assume-privileges`,
{ actorId, actorType }
);
@@ -20,7 +20,7 @@ export const useRemoveAssumeProjectPrivilege = () =>
useMutation({
mutationFn: async ({ projectId }: { projectId: string }) => {
const { data } = await apiRequest.delete<{ message: string }>(
`/api/v1/workspace/${projectId}/assume-privileges`
`/api/v1/projects/${projectId}/assume-privileges`
);
return data;
@@ -1,4 +1,5 @@
export enum LogProvider {
Azure = "azure",
Cribl = "cribl",
Custom = "custom",
Datadog = "datadog",
@@ -1,4 +1,5 @@
import { LogProvider } from "../enums";
import { TAzureProviderLogStream } from "./providers/azure-provider";
import { TCriblProviderLogStream } from "./providers/cribl-provider";
import { TCustomProviderLogStream } from "./providers/custom-provider";
import { TDatadogProviderLogStream } from "./providers/datadog-provider";
@@ -8,9 +9,11 @@ export type TAuditLogStream =
| TCustomProviderLogStream
| TDatadogProviderLogStream
| TSplunkProviderLogStream
| TAzureProviderLogStream
| TCriblProviderLogStream;
export type TAuditLogStreamProviderMap = {
[LogProvider.Azure]: TAzureProviderLogStream;
[LogProvider.Cribl]: TCriblProviderLogStream;
[LogProvider.Custom]: TCustomProviderLogStream;
[LogProvider.Datadog]: TDatadogProviderLogStream;
@@ -0,0 +1,14 @@
import { LogProvider } from "../../enums";
import { TRootProviderLogStream } from "./root-provider";
export type TAzureProviderLogStream = TRootProviderLogStream & {
provider: LogProvider.Azure;
credentials: {
tenantId: string;
clientId: string;
clientSecret: string;
dceUrl: string;
dcrId: string;
cltName: string;
};
};
+33 -6
View File
@@ -20,7 +20,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_SECRET]: "Create secret",
[EventType.UPDATE_SECRET]: "Update secret",
[EventType.DELETE_SECRET]: "Delete secret",
[EventType.GET_WORKSPACE_KEY]: "Read project key",
[EventType.GET_PROJECT_KEY]: "Read project key",
[EventType.AUTHORIZE_INTEGRATION]: "Authorize integration",
[EventType.UPDATE_INTEGRATION_AUTH]: "Update integration auth",
[EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration",
@@ -45,8 +45,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_ENVIRONMENT]: "Create environment",
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
[EventType.DELETE_ENVIRONMENT]: "Delete environment",
[EventType.ADD_WORKSPACE_MEMBER]: "Add member",
[EventType.REMOVE_WORKSPACE_MEMBER]: "Remove member",
[EventType.ADD_PROJECT_MEMBER]: "Add member",
[EventType.REMOVE_PROJECT_MEMBER]: "Remove member",
[EventType.CREATE_FOLDER]: "Create folder",
[EventType.UPDATE_FOLDER]: "Update folder",
[EventType.DELETE_FOLDER]: "Delete folder",
@@ -58,8 +58,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_SECRET_IMPORT]: "Create secret import",
[EventType.UPDATE_SECRET_IMPORT]: "Update secret import",
[EventType.DELETE_SECRET_IMPORT]: "Delete secret import",
[EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS]: "Update denied permissions",
[EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role",
[EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS]: "Update denied permissions",
[EventType.UPDATE_USER_PROJECT_ROLE]: "Update user role",
[EventType.CREATE_CA]: "Create CA",
[EventType.GET_CA]: "Get CA",
[EventType.UPDATE_CA]: "Update CA",
@@ -132,6 +132,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_APP_CONNECTION]: "Create App Connection",
[EventType.UPDATE_APP_CONNECTION]: "Update App Connection",
[EventType.DELETE_APP_CONNECTION]: "Delete App Connection",
[EventType.GET_APP_CONNECTION_USAGE]: "Get App Connection Usage",
[EventType.MIGRATE_APP_CONNECTION]: "Migrate App Connection",
[EventType.GET_SECRET_SYNCS]: "List secret syncs",
[EventType.GET_SECRET_SYNC]: "Get Secret Sync",
[EventType.CREATE_SECRET_SYNC]: "Create Secret Sync",
@@ -140,6 +142,14 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets",
[EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets",
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets",
[EventType.GET_PKI_SYNCS]: "List PKI syncs",
[EventType.GET_PKI_SYNC]: "Get PKI sync",
[EventType.CREATE_PKI_SYNC]: "Create PKI sync",
[EventType.UPDATE_PKI_SYNC]: "Update PKI sync",
[EventType.DELETE_PKI_SYNC]: "Delete PKI sync",
[EventType.PKI_SYNC_SYNC_CERTIFICATES]: "PKI Sync synced certificates",
[EventType.PKI_SYNC_IMPORT_CERTIFICATES]: "PKI Sync imported certificates",
[EventType.PKI_SYNC_REMOVE_CERTIFICATES]: "PKI Sync removed certificates",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
"OIDC group membership mapping assigned user to groups",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
@@ -192,6 +202,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity",
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity",
[EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS]: "Clear LDAP Auth lockouts",
[EventType.SECRET_SCANNING_DATA_SOURCE_LIST]: "List Secret Scanning Data Sources",
[EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source",
@@ -219,7 +230,23 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_ORG]: "Update Organization",
[EventType.CREATE_PROJECT]: "Create Project",
[EventType.UPDATE_PROJECT]: "Update Project",
[EventType.DELETE_PROJECT]: "Delete Project"
[EventType.DELETE_PROJECT]: "Delete Project",
[EventType.CREATE_SECRET_REMINDER]: "Create Secret Reminder",
[EventType.GET_SECRET_REMINDER]: "Get Secret Reminder",
[EventType.DELETE_SECRET_REMINDER]: "Delete Secret Reminder",
[EventType.DASHBOARD_LIST_SECRETS]: "Dashboard List Secrets",
[EventType.DASHBOARD_GET_SECRET_VALUE]: "Dashboard Get Secret Value",
[EventType.DASHBOARD_GET_SECRET_VERSION_VALUE]: "Dashboard Get Secret Version Value",
[EventType.CREATE_PROJECT_ROLE]: "Create Project Role",
[EventType.UPDATE_PROJECT_ROLE]: "Update Project Role",
[EventType.DELETE_PROJECT_ROLE]: "Delete Project Role",
[EventType.CREATE_ORG_ROLE]: "Create Org Role",
[EventType.UPDATE_ORG_ROLE]: "Update Org Role",
[EventType.DELETE_ORG_ROLE]: "Delete Org Role"
};
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
+33 -6
View File
@@ -26,7 +26,7 @@ export enum EventType {
CREATE_SECRET = "create-secret",
UPDATE_SECRET = "update-secret",
DELETE_SECRET = "delete-secret",
GET_WORKSPACE_KEY = "get-workspace-key",
GET_PROJECT_KEY = "get-project-key",
AUTHORIZE_INTEGRATION = "authorize-integration",
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
@@ -54,12 +54,13 @@ export enum EventType {
UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS = "clear-identity-ldap-auth-lockouts",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
ADD_WORKSPACE_MEMBER = "add-workspace-member",
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
ADD_PROJECT_MEMBER = "add-project-member",
REMOVE_PROJECT_MEMBER = "remove-project-member",
CREATE_FOLDER = "create-folder",
UPDATE_FOLDER = "update-folder",
DELETE_FOLDER = "delete-folder",
@@ -71,8 +72,8 @@ export enum EventType {
CREATE_SECRET_IMPORT = "create-secret-import",
UPDATE_SECRET_IMPORT = "update-secret-import",
DELETE_SECRET_IMPORT = "delete-secret-import",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
UPDATE_USER_PROJECT_ROLE = "update-user-project-role",
UPDATE_USER_PROJECT_DENIED_PERMISSIONS = "update-user-project-denied-permissions",
CREATE_CA = "create-certificate-authority",
GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority",
@@ -140,6 +141,8 @@ export enum EventType {
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
GET_APP_CONNECTION_USAGE = "get-app-connection-usage",
MIGRATE_APP_CONNECTION = "migrate-app-connection",
GET_SECRET_SYNCS = "get-secret-syncs",
GET_SECRET_SYNC = "get-secret-sync",
CREATE_SECRET_SYNC = "create-secret-sync",
@@ -148,6 +151,14 @@ export enum EventType {
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
GET_PKI_SYNCS = "get-pki-syncs",
GET_PKI_SYNC = "get-pki-sync",
CREATE_PKI_SYNC = "create-pki-sync",
UPDATE_PKI_SYNC = "update-pki-sync",
DELETE_PKI_SYNC = "delete-pki-sync",
PKI_SYNC_SYNC_CERTIFICATES = "pki-sync-sync-certificates",
PKI_SYNC_IMPORT_CERTIFICATES = "pki-sync-import-certificates",
PKI_SYNC_REMOVE_CERTIFICATES = "pki-sync-remove-certificates",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client",
@@ -213,5 +224,21 @@ export enum EventType {
CREATE_PROJECT = "create-project",
UPDATE_PROJECT = "update-project",
DELETE_PROJECT = "delete-project"
DELETE_PROJECT = "delete-project",
CREATE_SECRET_REMINDER = "create-secret-reminder",
GET_SECRET_REMINDER = "get-secret-reminder",
DELETE_SECRET_REMINDER = "delete-secret-reminder",
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value",
CREATE_PROJECT_ROLE = "create-project-role",
UPDATE_PROJECT_ROLE = "update-project-role",
DELETE_PROJECT_ROLE = "delete-project-role",
CREATE_ORG_ROLE = "create-org-role",
UPDATE_ORG_ROLE = "update-org-role",
DELETE_ORG_ROLE = "delete-org-role"
}
+7 -7
View File
@@ -7,10 +7,10 @@ import { TReactQueryOptions } from "@app/types/reactQuery";
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
export const auditLogKeys = {
getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
[{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const
getAuditLogs: (projectId: string | null, filters: TGetAuditLogsFilter) =>
[{ projectId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (projectId: string) =>
[{ projectId }, "audit-log-actor-filters"] as const
};
export const useGetAuditLogs = (
@@ -56,12 +56,12 @@ export const useGetAuditLogs = (
});
};
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
export const useGetAuditLogActorFilterOpts = (projectId: string) => {
return useQuery({
queryKey: auditLogKeys.getAuditLogActorFilterOpts(workspaceId),
queryKey: auditLogKeys.getAuditLogActorFilterOpts(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<{ actors: Actor[] }>(
`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
`/api/v1/projects/${projectId}/audit-logs/filters/actors`
);
return data.actors;
}
+14 -6
View File
@@ -129,7 +129,7 @@ interface DeleteSecretEvent {
}
interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY;
type: EventType.GET_PROJECT_KEY;
metadata: {
keyId: string;
};
@@ -361,7 +361,7 @@ interface DeleteEnvironmentEvent {
}
interface AddWorkspaceMemberEvent {
type: EventType.ADD_WORKSPACE_MEMBER;
type: EventType.ADD_PROJECT_MEMBER;
metadata: {
userId: string;
email: string;
@@ -369,7 +369,7 @@ interface AddWorkspaceMemberEvent {
}
interface RemoveWorkspaceMemberEvent {
type: EventType.REMOVE_WORKSPACE_MEMBER;
type: EventType.REMOVE_PROJECT_MEMBER;
metadata: {
userId: string;
email: string;
@@ -500,7 +500,7 @@ interface DeleteSecretImportEvent {
}
interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
type: EventType.UPDATE_USER_PROJECT_ROLE;
metadata: {
userId: string;
email: string;
@@ -510,7 +510,7 @@ interface UpdateUserRole {
}
interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
type: EventType.UPDATE_USER_PROJECT_DENIED_PERMISSIONS;
metadata: {
userId: string;
email: string;
@@ -874,6 +874,13 @@ interface IntegrationSyncedEvent {
};
}
interface ClearIdentityLdapAuthLockoutsEvent {
type: EventType.CLEAR_IDENTITY_LDAP_AUTH_LOCKOUTS;
metadata: {
identityId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -958,7 +965,8 @@ export type Event =
| GetCertificateTemplateEstConfig
| UpdateProjectWorkflowIntegrationConfig
| GetProjectWorkflowIntegrationConfig
| IntegrationSyncedEvent;
| IntegrationSyncedEvent
| ClearIdentityLdapAuthLockoutsEvent;
export type AuditLog = {
id: string;
+2 -2
View File
@@ -5,8 +5,8 @@ import { apiRequest } from "@app/config/request";
import { SessionStorageKeys } from "@app/const";
import { organizationKeys } from "../organization/queries";
import { projectKeys } from "../projects";
import { setAuthToken } from "../reactQuery";
import { workspaceKeys } from "../workspace";
import {
CompleteAccountDTO,
CompleteAccountSignupDTO,
@@ -101,7 +101,7 @@ export const useSelectOrganization = () => {
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [organizationKeys.getUserOrganizations, workspaceKeys.getAllUserWorkspace]
queryKey: [organizationKeys.getUserOrganizations, projectKeys.getAllUserProjects]
});
}
});
+6 -6
View File
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { projectKeys } from "../projects";
import { CaType } from "./enums";
import { caKeys } from "./queries";
import {
@@ -118,8 +118,8 @@ export const useImportCaCertificate = (projectId: string) => {
);
return data;
},
onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) });
onSuccess: (_, { caId }) => {
queryClient.invalidateQueries({ queryKey: projectKeys.getProjectCas({ projectId }) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaCerts(caId) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaCert(caId) });
queryClient.invalidateQueries({
@@ -142,7 +142,7 @@ export const useCreateCertificate = () => {
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
queryKey: projectKeys.forProjectCertificates(projectSlug)
});
}
});
@@ -158,8 +158,8 @@ export const useRenewCa = () => {
);
return data;
},
onSuccess: (_, { caId, projectSlug }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceCas({ projectSlug }) });
onSuccess: ({ projectId }, { caId }) => {
queryClient.invalidateQueries({ queryKey: projectKeys.getProjectCas({ projectId }) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaById(caId) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaCert(caId) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaCerts(caId) });
+1
View File
@@ -182,4 +182,5 @@ export type TRenewCaResponse = {
certificate: string;
certificateChain: string;
serialNumber: string;
projectId: string;
};
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { caKeys } from "../ca/queries";
import { workspaceKeys } from "../workspace";
import { projectKeys } from "../projects";
import { certTemplateKeys } from "./queries";
import {
TCertificateTemplate,
@@ -29,7 +29,7 @@ export const useCreateCertTemplate = () => {
},
onSuccess: ({ caId }, { projectId }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceCertificateTemplates(projectId)
queryKey: projectKeys.getProjectCertificateTemplates(projectId)
});
queryClient.invalidateQueries({ queryKey: caKeys.getCaCertTemplates(caId) });
}
@@ -49,7 +49,7 @@ export const useUpdateCertTemplate = () => {
},
onSuccess: ({ caId }, { projectId, id }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceCertificateTemplates(projectId)
queryKey: projectKeys.getProjectCertificateTemplates(projectId)
});
queryClient.invalidateQueries({ queryKey: certTemplateKeys.getCertTemplateById(id) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaCertTemplates(caId) });
@@ -68,7 +68,7 @@ export const useDeleteCertTemplate = () => {
},
onSuccess: ({ caId }, { projectId, id }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceCertificateTemplates(projectId)
queryKey: projectKeys.getProjectCertificateTemplates(projectId)
});
queryClient.invalidateQueries({ queryKey: certTemplateKeys.getCertTemplateById(id) });
queryClient.invalidateQueries({ queryKey: caKeys.getCaCertTemplates(caId) });
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
import { workspaceKeys } from "../workspace";
import { projectKeys } from "../projects";
import {
TCertificate,
TDeleteCertDTO,
@@ -25,7 +25,7 @@ export const useDeleteCert = () => {
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
queryKey: projectKeys.forProjectCertificates(projectSlug)
});
}
});
@@ -47,7 +47,7 @@ export const useRevokeCert = () => {
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
queryKey: projectKeys.forProjectCertificates(projectSlug)
});
queryClient.invalidateQueries({
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
@@ -68,7 +68,7 @@ export const useImportCertificate = () => {
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
queryKey: projectKeys.forProjectCertificates(projectSlug)
});
}
});
+65 -11
View File
@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { apiRequest } from "@app/config/request";
@@ -10,13 +10,15 @@ import {
DashboardProjectSecretsOverview,
DashboardProjectSecretsOverviewResponse,
DashboardSecretsOrderBy,
DashboardSecretValue,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO
TGetDashboardProjectSecretsQuickSearchDTO,
TGetSecretValueDTO
} from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
@@ -73,6 +75,15 @@ export const dashboardKeys = {
...dashboardKeys.all(),
"accessible-secrets",
{ projectId, secretPath, environment, filterByAction }
] as const,
getSecretValuesRoot: () => [...dashboardKeys.all(), "secrets-values"] as const,
getSecretValue: ({ environment, secretPath, secretKey, isOverride }: TGetSecretValueDTO) =>
[
...dashboardKeys.getSecretValuesRoot(),
environment,
secretPath,
secretKey,
isOverride
] as const
};
@@ -174,6 +185,8 @@ export const useGetProjectSecretsOverview = (
"queryKey" | "queryFn"
>
) => {
const queryClient = useQueryClient();
return useQuery({
...options,
// wait for all values to be available
@@ -193,8 +206,8 @@ export const useGetProjectSecretsOverview = (
includeSecretRotations,
environments
}),
queryFn: () =>
fetchProjectSecretsOverview({
queryFn: async () => {
const resp = fetchProjectSecretsOverview({
secretPath,
search,
limit,
@@ -208,7 +221,14 @@ export const useGetProjectSecretsOverview = (
includeDynamicSecrets,
includeSecretRotations,
environments
}),
});
queryClient.invalidateQueries({
queryKey: dashboardKeys.getSecretValuesRoot()
});
return resp;
},
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
const { secrets, secretRotations, ...select } = data;
const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
@@ -254,7 +274,6 @@ export const useGetProjectSecretsDetails = (
search = "",
includeSecrets,
includeFolders,
viewSecretValue,
includeImports,
includeDynamicSecrets,
includeSecretRotations,
@@ -270,6 +289,8 @@ export const useGetProjectSecretsDetails = (
"queryKey" | "queryFn"
>
) => {
const queryClient = useQueryClient();
return useQuery({
...options,
// wait for all values to be available
@@ -286,7 +307,6 @@ export const useGetProjectSecretsDetails = (
limit,
orderBy,
orderDirection,
viewSecretValue,
offset,
projectId,
environment,
@@ -297,14 +317,13 @@ export const useGetProjectSecretsDetails = (
includeSecretRotations,
tags
}),
queryFn: () =>
fetchProjectSecretsDetails({
queryFn: async () => {
const resp = await fetchProjectSecretsDetails({
secretPath,
search,
limit,
orderBy,
orderDirection,
viewSecretValue,
offset,
projectId,
environment,
@@ -314,7 +333,14 @@ export const useGetProjectSecretsDetails = (
includeDynamicSecrets,
includeSecretRotations,
tags
}),
});
queryClient.invalidateQueries({
queryKey: dashboardKeys.getSecretValuesRoot()
});
return resp;
},
select: useCallback(
(data: Awaited<ReturnType<typeof fetchProjectSecretsDetails>>) => ({
...data,
@@ -471,3 +497,31 @@ export const useGetAccessibleSecrets = ({
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
});
};
export const fetchSecretValue = async (params: TGetSecretValueDTO) => {
const { data } = await apiRequest.get<DashboardSecretValue>("/api/v1/dashboard/secret-value", {
params
});
return data;
};
export const useGetSecretValue = (
params: TGetSecretValueDTO,
options?: Omit<
UseQueryOptions<
DashboardSecretValue,
unknown,
DashboardSecretValue,
ReturnType<typeof dashboardKeys.getSecretValue>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: dashboardKeys.getSecretValue(params),
queryFn: async () => fetchSecretValue(params),
staleTime: 1000 * 60,
...options
});
};
+18 -1
View File
@@ -112,7 +112,6 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
TGetDashboardProjectSecretsOverviewDTO,
"environments"
> & {
viewSecretValue: boolean;
environment: string;
includeImports?: boolean;
tags: Record<string, boolean>;
@@ -156,3 +155,21 @@ export type TGetAccessibleSecretsDTO = {
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};
export type TGetSecretValueDTO = {
projectId: string;
secretKey: string;
environment: string;
secretPath: string;
isOverride?: boolean;
};
export type DashboardSecretValue =
| {
value: string;
valueOverride: undefined;
}
| {
value: undefined;
valueOverride: string;
};
@@ -59,6 +59,11 @@ export enum DynamicSecretAwsIamAuth {
IRSA = "irsa"
}
export enum DynamicSecretAwsIamCredentialType {
IamUser = "iam-user",
TemporaryCredentials = "temporary-credentials"
}
export type TDynamicSecretProvider =
| {
type: DynamicSecretProviders.SqlDatabase;
@@ -97,6 +102,7 @@ export type TDynamicSecretProvider =
inputs:
| {
method: DynamicSecretAwsIamAuth.AccessKey;
credentialType: DynamicSecretAwsIamCredentialType;
accessKey: string;
secretAccessKey: string;
region: string;
@@ -107,6 +113,7 @@ export type TDynamicSecretProvider =
}
| {
method: DynamicSecretAwsIamAuth.AssumeRole;
credentialType: DynamicSecretAwsIamCredentialType;
roleArn: string;
region: string;
awsPath?: string;
@@ -116,6 +123,7 @@ export type TDynamicSecretProvider =
}
| {
method: DynamicSecretAwsIamAuth.IRSA;
credentialType: DynamicSecretAwsIamCredentialType;
region: string;
awsPath?: string;
policyDocument?: string;
@@ -7,27 +7,27 @@ import { Commit, CommitHistoryItem, CommitWithChanges, RollbackPreview } from ".
export const commitKeys = {
count: ({
workspaceId,
projectId,
environment,
directory
}: {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
}) => [{ workspaceId, environment, directory }, "folder-commits-count"] as const,
}) => [{ projectId, environment, directory }, "folder-commits-count"] as const,
history: ({
workspaceId,
projectId,
environment,
directory
}: {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
}) => [{ workspaceId, environment, directory }, "folder-commits"] as const,
}) => [{ projectId, environment, directory }, "folder-commits"] as const,
details: ({ workspaceId, commitId }: { workspaceId: string; commitId: string }) =>
[{ workspaceId, commitId }, "commit-details"] as const,
details: ({ projectId, commitId }: { projectId: string; commitId: string }) =>
[{ projectId, commitId }, "commit-details"] as const,
rollbackPreview: ({
folderId,
@@ -45,11 +45,11 @@ export const commitKeys = {
};
const fetchFolderCommitsCount = async ({
workspaceId,
projectId,
environment,
directory
}: {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
}) => {
@@ -59,7 +59,7 @@ const fetchFolderCommitsCount = async ({
params: {
environment,
path: directory,
projectId: workspaceId
projectId
}
}
);
@@ -67,7 +67,7 @@ const fetchFolderCommitsCount = async ({
};
const fetchFolderCommitHistory = async (
workspaceId: string,
projectId: string,
environment: string,
directory: string,
offset: number = 0,
@@ -87,7 +87,7 @@ const fetchFolderCommitHistory = async (
params: {
environment,
path: directory,
projectId: workspaceId,
projectId,
offset,
limit,
search,
@@ -97,12 +97,12 @@ const fetchFolderCommitHistory = async (
return res.data;
};
export const fetchCommitDetails = async (workspaceId: string, commitId: string) => {
export const fetchCommitDetails = async (projectId: string, commitId: string) => {
const { data } = await apiRequest.get<CommitWithChanges>(
`/api/v1/pit/commits/${commitId}/changes`,
{
params: {
projectId: workspaceId
projectId
}
}
);
@@ -113,7 +113,7 @@ export const fetchRollbackPreview = async (
folderId: string,
commitId: string,
envSlug: string,
workspaceId: string,
projectId: string,
deepRollback: boolean,
secretPath: string
): Promise<RollbackPreview[]> => {
@@ -125,7 +125,7 @@ export const fetchRollbackPreview = async (
environment: envSlug,
deepRollback,
secretPath,
projectId: workspaceId
projectId
}
}
);
@@ -135,7 +135,7 @@ export const fetchRollbackPreview = async (
const fetchRollback = async (
folderId: string,
commitId: string,
workspaceId: string,
projectId: string,
deepRollback: boolean,
message?: string,
envSlug?: string
@@ -147,17 +147,17 @@ const fetchRollback = async (
deepRollback,
message,
environment: envSlug,
projectId: workspaceId
projectId
}
);
return data;
};
const fetchRevert = async (commitId: string, workspaceId: string) => {
const fetchRevert = async (commitId: string, projectId: string) => {
const { data } = await apiRequest.post<{ success: boolean; message: string }>(
`/api/v1/pit/commits/${commitId}/revert`,
{
projectId: workspaceId
projectId
}
);
return data;
@@ -180,9 +180,9 @@ export const useCommitRevert = ({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
commitKeys.details({ workspaceId: projectId, commitId }),
commitKeys.history({ workspaceId: projectId, environment, directory }),
commitKeys.count({ workspaceId: projectId, environment, directory })
commitKeys.details({ projectId, commitId }),
commitKeys.history({ projectId, environment, directory }),
commitKeys.count({ projectId, environment, directory })
]
});
}
@@ -190,7 +190,7 @@ export const useCommitRevert = ({
};
export const useCommitRollback = ({
workspaceId,
projectId,
commitId,
folderId,
deepRollback,
@@ -198,7 +198,7 @@ export const useCommitRollback = ({
directory,
envSlug
}: {
workspaceId: string;
projectId: string;
commitId: string;
folderId: string;
deepRollback: boolean;
@@ -209,13 +209,13 @@ export const useCommitRollback = ({
const queryClient = useQueryClient();
return useMutation({
mutationFn: (message: string) =>
fetchRollback(folderId, commitId, workspaceId, deepRollback, message, envSlug),
fetchRollback(folderId, commitId, projectId, deepRollback, message, envSlug),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
commitKeys.details({ workspaceId, commitId }),
commitKeys.history({ workspaceId, environment, directory }),
commitKeys.count({ workspaceId, environment, directory })
commitKeys.details({ projectId, commitId }),
commitKeys.history({ projectId, environment, directory }),
commitKeys.count({ projectId, environment, directory })
]
});
}
@@ -223,31 +223,31 @@ export const useCommitRollback = ({
};
export const useGetFolderCommitsCount = ({
workspaceId,
projectId,
environment,
directory,
isPaused
}: {
workspaceId: string;
projectId: string;
environment: string;
directory: string;
isPaused?: boolean;
}) =>
useQuery({
enabled: Boolean(workspaceId && environment) && !isPaused,
queryKey: commitKeys.count({ workspaceId, environment, directory }),
queryFn: () => fetchFolderCommitsCount({ workspaceId, environment, directory })
enabled: Boolean(projectId && environment) && !isPaused,
queryKey: commitKeys.count({ projectId, environment, directory }),
queryFn: () => fetchFolderCommitsCount({ projectId, environment, directory })
});
export const useGetFolderCommitHistory = ({
workspaceId,
projectId,
environment,
directory,
limit = 20,
search,
sort = "desc"
}: {
workspaceId: string;
projectId: string;
environment: string;
directory: string;
limit?: number;
@@ -256,10 +256,10 @@ export const useGetFolderCommitHistory = ({
}) => {
return useInfiniteQuery({
initialPageParam: 0,
queryKey: [commitKeys.history({ workspaceId, environment, directory }), limit, search, sort],
queryKey: [commitKeys.history({ projectId, environment, directory }), limit, search, sort],
queryFn: ({ pageParam }) =>
fetchFolderCommitHistory(workspaceId, environment, directory, pageParam, limit, search, sort),
enabled: Boolean(workspaceId && environment),
fetchFolderCommitHistory(projectId, environment, directory, pageParam, limit, search, sort),
enabled: Boolean(projectId && environment),
select: (data) => {
return (data?.pages ?? [])
?.map((page) => page.commits)
@@ -280,11 +280,11 @@ export const useGetFolderCommitHistory = ({
});
};
export const useGetCommitDetails = (workspaceId: string, commitId: string) => {
export const useGetCommitDetails = (projectId: string, commitId: string) => {
return useQuery({
queryKey: commitKeys.details({ workspaceId, commitId }),
queryFn: () => fetchCommitDetails(workspaceId, commitId),
enabled: Boolean(workspaceId) && Boolean(commitId)
queryKey: commitKeys.details({ projectId, commitId }),
queryFn: () => fetchCommitDetails(projectId, commitId),
enabled: Boolean(projectId) && Boolean(commitId)
});
};
+1 -1
View File
@@ -138,7 +138,7 @@ export const useListProjectGroupUsers = ({
});
const { data } = await apiRequest.get<{ users: TGroupUser[]; totalCount: number }>(
`/api/v2/workspace/${projectId}/groups/${id}/users`,
`/api/v1/projects/${projectId}/groups/${id}/users`,
{
params
}
@@ -18,6 +18,7 @@ import {
AddIdentityTlsCertAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
ClearIdentityLdapAuthLockoutsDTO,
ClearIdentityUniversalAuthLockoutsDTO,
ClientSecretData,
CreateIdentityDTO,
@@ -1432,7 +1433,11 @@ export const useAddIdentityLdapAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
lockoutEnabled,
lockoutThreshold,
lockoutDurationSeconds,
lockoutCounterResetSeconds
}) => {
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
`/api/v1/auth/ldap-auth/identities/${identityId}`,
@@ -1448,7 +1453,11 @@ export const useAddIdentityLdapAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
lockoutEnabled,
lockoutThreshold,
lockoutDurationSeconds,
lockoutCounterResetSeconds
}
);
return data.identityLdapAuth;
@@ -1481,7 +1490,11 @@ export const useUpdateIdentityLdapAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
lockoutEnabled,
lockoutThreshold,
lockoutDurationSeconds,
lockoutCounterResetSeconds
}) => {
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
`/api/v1/auth/ldap-auth/identities/${identityId}`,
@@ -1497,7 +1510,11 @@ export const useUpdateIdentityLdapAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
lockoutEnabled,
lockoutThreshold,
lockoutDurationSeconds,
lockoutCounterResetSeconds
}
);
return data.identityLdapAuth;
@@ -1532,3 +1549,22 @@ export const useDeleteIdentityLdapAuth = () => {
}
});
};
export const useClearIdentityLdapAuthLockouts = () => {
const queryClient = useQueryClient();
return useMutation<number, object, ClearIdentityLdapAuthLockoutsDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { deleted }
} = await apiRequest.post<{ deleted: number }>(
`/api/v1/auth/ldap-auth/identities/${identityId}/clear-lockouts`
);
return deleted;
},
onSuccess: (_, { identityId }) => {
queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityLdapAuth(identityId)
});
}
});
};
+21 -2
View File
@@ -1,7 +1,7 @@
import { OrderByDirection } from "../generic/types";
import { OrgIdentityOrderBy } from "../organization/types";
import { Project, ProjectUserMembershipTemporaryMode } from "../projects/types";
import { TOrgRole } from "../roles/types";
import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types";
import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums";
export type IdentityTrustedIp = {
@@ -54,7 +54,7 @@ export type IdentityMembershipOrg = {
export type IdentityMembership = {
id: string;
identity: Identity;
project: Pick<Workspace, "id" | "name" | "type">;
project: Pick<Project, "id" | "name" | "type">;
roles: Array<
{
id: string;
@@ -603,6 +603,11 @@ export type AddIdentityLdapAuthDTO = {
accessTokenTrustedIps: {
ipAddress: string;
}[];
lockoutEnabled: boolean;
lockoutThreshold: number;
lockoutDurationSeconds: number;
lockoutCounterResetSeconds: number;
};
export type UpdateIdentityLdapAuthDTO = {
@@ -625,6 +630,11 @@ export type UpdateIdentityLdapAuthDTO = {
accessTokenTrustedIps?: {
ipAddress: string;
}[];
lockoutEnabled?: boolean;
lockoutThreshold?: number;
lockoutDurationSeconds?: number;
lockoutCounterResetSeconds?: number;
};
export type DeleteIdentityLdapAuthDTO = {
@@ -650,6 +660,15 @@ export type IdentityLdapAuth = {
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
lockoutEnabled: boolean;
lockoutThreshold: number;
lockoutDurationSeconds: number;
lockoutCounterResetSeconds: number;
};
export type ClearIdentityLdapAuthLockoutsDTO = {
identityId: string;
};
export type AddIdentityTokenAuthDTO = {
+1 -2
View File
@@ -20,7 +20,6 @@ export * from "./identityProjectAdditionalPrivilege";
export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys";
export * from "./kms";
export * from "./ldapConfig";
export * from "./oidcConfig";
@@ -29,6 +28,7 @@ export * from "./organization";
export * from "./pkiAlerts";
export * from "./pkiCollections";
export * from "./pkiSubscriber";
export * from "./projects";
export * from "./projectUserAdditionalPrivilege";
export * from "./rateLimit";
export * from "./roles";
@@ -54,4 +54,3 @@ export * from "./trustedIps";
export * from "./users";
export * from "./webhooks";
export * from "./workflowIntegrations";
export * from "./workspace";
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TReactQueryOptions } from "@app/types/reactQuery";
import { workspaceKeys } from "../workspace";
import { projectKeys } from "../projects";
import {
App,
BitbucketEnvironment,
@@ -970,7 +970,7 @@ export const useAuthorizeIntegration = () => {
},
onSuccess: (res) => {
queryClient.invalidateQueries({
queryKey: { queryKey: workspaceKeys.getWorkspaceAuthorization(res.workspace) }
queryKey: { queryKey: projectKeys.getProjectAuthorization(res.workspace) }
});
}
});
@@ -1016,7 +1016,7 @@ export const useSaveIntegrationAccessToken = () => {
},
onSuccess: (res) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceAuthorization(res.workspace)
queryKey: projectKeys.getProjectAuthorization(res.workspace)
});
}
});
@@ -1035,10 +1035,10 @@ export const useDeleteIntegrationAuths = () => {
),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId)
queryKey: projectKeys.getProjectAuthorization(workspaceId)
});
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId)
queryKey: projectKeys.getProjectIntegrations(workspaceId)
});
}
});
@@ -1052,10 +1052,10 @@ export const useDeleteIntegrationAuth = () => {
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration-auth/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId)
queryKey: projectKeys.getProjectAuthorization(workspaceId)
});
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId)
queryKey: projectKeys.getProjectIntegrations(workspaceId)
});
}
});
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstac
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { projectKeys } from "../projects";
import {
IntegrationMetadataSyncMode,
TCloudIntegration,
@@ -121,7 +121,7 @@ export const useCreateIntegration = () => {
},
onSuccess: (res) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceIntegrations(res.workspace)
queryKey: projectKeys.getProjectIntegrations(res.workspace)
});
}
});
@@ -141,10 +141,10 @@ export const useDeleteIntegration = () => {
),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId)
queryKey: projectKeys.getProjectIntegrations(workspaceId)
});
queryClient.invalidateQueries({
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId)
queryKey: projectKeys.getProjectAuthorization(workspaceId)
});
}
});
-1
View File
@@ -1 +0,0 @@
export { useGetUserWsKey, useUploadWsKey } from "./queries";
-43
View File
@@ -1,43 +0,0 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { UploadWsKeyDTO, UserWsKeyPair } from "./types";
const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
};
export const fetchUserWsKey = async (projectId: string) => {
const { data } = await apiRequest.get<UserWsKeyPair>(
`/api/v2/workspace/${projectId}/encrypted-key`
);
return data;
};
export const useGetUserWsKey = (workspaceID: string) =>
useQuery({
queryKey: encKeyKeys.getUserWorkspaceKey(workspaceID),
queryFn: () => fetchUserWsKey(workspaceID),
enabled: Boolean(workspaceID)
});
// mutations
export const uploadWsKey = async ({ workspaceId, userId, encryptedKey, nonce }: UploadWsKeyDTO) => {
return apiRequest.post(`/api/v1/workspace/${workspaceId}/key`, {
key: { userId, encryptedKey, nonce }
});
};
export const useUploadWsKey = () =>
useMutation<object, object, UploadWsKeyDTO>({
mutationFn: async ({ encryptedKey, nonce, userId, workspaceId }) => {
return uploadWsKey({
workspaceId,
userId,
encryptedKey,
nonce
});
}
});
-29
View File
@@ -1,29 +0,0 @@
export type UserWsKeyPair = {
id: string;
encryptedKey: string;
nonce: string;
sender: Sender;
receiver: string;
workspace: string;
createdAt: string;
updatedAt: string;
__v: number;
};
export type Sender = {
id: string;
email: string;
createdAt: string;
updatedAt: string;
__v: number;
firstName: string;
lastName: string;
publicKey: string;
};
export type UploadWsKeyDTO = {
userId: string;
encryptedKey: string;
nonce: string;
workspaceId: string;
};
+2 -2
View File
@@ -75,7 +75,7 @@ export const useUpdateProjectKms = (projectId: string) => {
mutationFn: async (
updatedData: { type: KmsType.Internal } | { type: KmsType.External; kmsId: string }
) => {
const { data } = await apiRequest.patch(`/api/v1/workspace/${projectId}/kms`, {
const { data } = await apiRequest.patch(`/api/v1/projects/${projectId}/kms`, {
kms: updatedData
});
@@ -91,7 +91,7 @@ export const useLoadProjectKmsBackup = (projectId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (backup: string) => {
const { data } = await apiRequest.post(`/api/v1/workspace/${projectId}/kms/backup`, {
const { data } = await apiRequest.post(`/api/v1/projects/${projectId}/kms/backup`, {
backup
});

Some files were not shown because too many files have changed in this diff Show More