mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 03:28:43 +00:00
Merge branch 'heads/main' into ENG-2785
This commit is contained in:
@@ -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't see the third-party service you'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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+150
@@ -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}
|
||||
|
||||
+2
-2
@@ -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) => {
|
||||
|
||||
+50
-21
@@ -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>
|
||||
|
||||
+51
-21
@@ -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>
|
||||
|
||||
@@ -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}`}>
|
||||
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||
${
|
||||
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||
}
|
||||
</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(() => {
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export { useGetUserWsKey, useUploadWsKey } from "./queries";
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user