mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 06:29:37 +00:00
Merge branch 'main' into feat/point-in-time-revamp
This commit is contained in:
Generated
+1
@@ -25,6 +25,7 @@
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@lexical/react": "^0.29.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.12.0",
|
||||
"@lottiefiles/dotlottie-web": "^0.38.2",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@lexical/react": "^0.29.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.12.0",
|
||||
"@lottiefiles/dotlottie-web": "^0.38.2",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -78,11 +78,14 @@ export default function CodeInputStep({
|
||||
const resendVerificationEmail = async () => {
|
||||
setIsResendingVerificationEmail(true);
|
||||
setIsLoading(true);
|
||||
await mutateAsync({ email });
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsResendingVerificationEmail(false);
|
||||
}, 2000);
|
||||
try {
|
||||
await mutateAsync({ email });
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsResendingVerificationEmail(false);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const DeleteProjectTemplateModal = ({ isOpen, onOpenChange, template }: P
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteProjectTemplate}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeTemplate.isOpen}
|
||||
title={`Are you sure want to delete ${projectTemplate.name}?`}
|
||||
title={`Are you sure you want to delete ${projectTemplate.name}?`}
|
||||
deleteKey="confirm"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeTemplate", isOpen)}
|
||||
onDeleteApproved={handleRemoveTemplate}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DeleteSecretRotationV2Modal = ({
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteSecretRotation}
|
||||
>
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ const Content = ({ secretRotation }: ContentProps) => {
|
||||
let Component: ReactNode;
|
||||
switch (generatedCredentialsResponse.type) {
|
||||
case SecretRotation.PostgresCredentials:
|
||||
case SecretRotation.MySqlCredentials:
|
||||
case SecretRotation.MsSqlCredentials:
|
||||
Component = (
|
||||
<ViewSqlCredentialsRotationGeneratedCredentials
|
||||
|
||||
+2
@@ -1,11 +1,13 @@
|
||||
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
|
||||
import { ViewRotationGeneratedCredentialsDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/ViewRotationGeneratedCredentialsDisplay";
|
||||
import { TMsSqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mssql-credentials-rotation";
|
||||
import { TMySqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mysql-credentials-rotation";
|
||||
import { TPostgresCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/postgres-credentials-rotation";
|
||||
|
||||
type Props = {
|
||||
generatedCredentialsResponse:
|
||||
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMySqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { SqlCredentialsRotationParametersFields } from "./shared";
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ import { SqlCredentialsRotationReviewFields } from "./shared";
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations
|
||||
import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema";
|
||||
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
|
||||
import { MySqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mysql-credentials-rotation-schema";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
|
||||
@@ -17,6 +18,7 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
|
||||
AzureClientSecretRotationSchema,
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
MySqlCredentialsRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
]),
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const MySqlCredentialsRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.MySqlCredentials)
|
||||
})
|
||||
.merge(SqlCredentialsRotationSchema)
|
||||
.merge(BaseSecretRotationSchema);
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
TSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningDataSourceForm } from "./forms";
|
||||
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
|
||||
import { SecretScanningDataSourceSelect } from "./SecretScanningDataSourceSelect";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onComplete: (dataSource: TSecretScanningDataSource) => void;
|
||||
selectedDataSource: SecretScanningDataSource | null;
|
||||
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
|
||||
};
|
||||
|
||||
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
|
||||
if (selectedDataSource) {
|
||||
return (
|
||||
<SecretScanningDataSourceForm
|
||||
onCancel={() => setSelectedDataSource(null)}
|
||||
type={selectedDataSource}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretScanningDataSourceSelect onSelect={setSelectedDataSource} />;
|
||||
};
|
||||
|
||||
export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...props }: Props) => {
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSelectedDataSource(null);
|
||||
onOpenChange(open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={
|
||||
selectedDataSource ? (
|
||||
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
|
||||
) : (
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
Add Data Source
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
|
||||
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>
|
||||
)
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
|
||||
subTitle={
|
||||
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
|
||||
}
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<Content
|
||||
onComplete={() => {
|
||||
setSelectedDataSource(null);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
selectedDataSource={selectedDataSource}
|
||||
setSelectedDataSource={setSelectedDataSource}
|
||||
{...props}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
TSecretScanningDataSource,
|
||||
useDeleteSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
dataSource?: TSecretScanningDataSource;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onComplete?: () => void;
|
||||
};
|
||||
|
||||
export const DeleteSecretScanningDataSourceModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
dataSource,
|
||||
onComplete
|
||||
}: Props) => {
|
||||
const deleteDataSource = useDeleteSecretScanningDataSource();
|
||||
|
||||
if (!dataSource) return null;
|
||||
|
||||
const { id: dataSourceId, name, type, projectId } = dataSource;
|
||||
|
||||
const handleDeleteDataSource = async () => {
|
||||
const dataSourceType = SECRET_SCANNING_DATA_SOURCE_MAP[type].name;
|
||||
|
||||
try {
|
||||
await deleteDataSource.mutateAsync({
|
||||
dataSourceId,
|
||||
type,
|
||||
projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted ${dataSourceType} Data Source`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (onComplete) onComplete();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to delete ${dataSourceType} Data Source`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteDataSource}
|
||||
>
|
||||
<p className="mt-1 font-inter text-sm text-mineshaft-400">
|
||||
Findings associated with this data source will be preserved.
|
||||
</p>
|
||||
</DeleteActionModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningDataSourceForm } from "./forms";
|
||||
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
dataSource?: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const EditSecretScanningDataSourceModal = ({
|
||||
dataSource,
|
||||
onOpenChange,
|
||||
...props
|
||||
}: Props) => {
|
||||
if (!dataSource) return null;
|
||||
|
||||
return (
|
||||
<Modal {...props} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title={<SecretScanningDataSourceModalHeader isConfigured type={dataSource.type} />}
|
||||
className="max-w-2xl"
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<SecretScanningDataSourceForm
|
||||
onComplete={() => onOpenChange(false)}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
dataSource={dataSource}
|
||||
type={dataSource.type}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
type: SecretScanningDataSource;
|
||||
isConfigured: boolean;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Props) => {
|
||||
const dataSourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<img
|
||||
alt={`${dataSourceDetails.name} logo`}
|
||||
src={`/images/integrations/${dataSourceDetails.image}`}
|
||||
className="h-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
{dataSourceDetails.name} Data Source
|
||||
<a
|
||||
target="_blank"
|
||||
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
|
||||
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" : "Connect a"} {dataSourceDetails.name} Data Source
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
useSecretScanningDataSourceOptions
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: SecretScanningDataSource) => void;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceSelect = ({ onSelect }: Props) => {
|
||||
const { isPending, data: dataSourceOptions } = useSecretScanningDataSourceOptions();
|
||||
|
||||
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="grid grid-cols-3 gap-2">
|
||||
{dataSourceOptions?.map(({ type }) => {
|
||||
const { image, name, size } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={type}
|
||||
onClick={() => onSelect(type)}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
width={size}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<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="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
|
||||
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
status: SecretScanningScanStatus;
|
||||
statusMessage?: string | null;
|
||||
className?: string;
|
||||
scannedAt?: string | null;
|
||||
};
|
||||
|
||||
export const SecretScanningScanStatusBadge = ({
|
||||
status,
|
||||
statusMessage,
|
||||
className,
|
||||
scannedAt
|
||||
}: Props) => {
|
||||
if (status === SecretScanningScanStatus.Failed) {
|
||||
let errorMessage = statusMessage;
|
||||
if (statusMessage) {
|
||||
try {
|
||||
errorMessage = JSON.stringify(JSON.parse(statusMessage), null, 2);
|
||||
} catch {
|
||||
errorMessage = statusMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="max-w-sm select-text"
|
||||
content={
|
||||
<div className="flex flex-col gap-2 whitespace-normal py-1">
|
||||
<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">Failure Reason</div>
|
||||
</div>
|
||||
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">{errorMessage}</div>
|
||||
{scannedAt && (
|
||||
<div className="mt-1 text-xs text-mineshaft-400">
|
||||
Attempted {formatDistance(new Date(scannedAt), new Date(), { addSuffix: true })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
Scan Error
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
|
||||
return (
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
|
||||
<span>Scanning</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="success"
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
<span>Complete</span>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { MultiValue } from "react-select";
|
||||
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
TGitHubRadarConnectionRepository,
|
||||
useGitHubRadarConnectionListRepositories
|
||||
} from "@app/hooks/api/appConnections/github-radar";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
|
||||
|
||||
enum ScanMethod {
|
||||
AllRepositories = "all-repositories",
|
||||
SelectRepositories = "select-repositories"
|
||||
}
|
||||
|
||||
export const GitHubDataSourceConfigFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.GitHub;
|
||||
}
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ control, name: "connection.id" });
|
||||
const isUpdate = Boolean(watch("id"));
|
||||
|
||||
const { data: repositories, isPending: areRepositoriesLoading } =
|
||||
useGitHubRadarConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) });
|
||||
|
||||
const includeRepos = watch("config.includeRepos");
|
||||
|
||||
const scanMethod =
|
||||
!includeRepos || includeRepos[0] === "*"
|
||||
? ScanMethod.AllRepositories
|
||||
: ScanMethod.SelectRepositories;
|
||||
|
||||
useEffect(() => {
|
||||
if (!includeRepos) {
|
||||
setValue("config.includeRepos", ["*"]);
|
||||
}
|
||||
}, [includeRepos, setValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretScanningDataSourceConnectionField
|
||||
isUpdate={isUpdate}
|
||||
onChange={() => {
|
||||
if (scanMethod === ScanMethod.SelectRepositories) {
|
||||
setValue("config.includeRepos", []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormControl label="Scan Repositories">
|
||||
<Select
|
||||
value={scanMethod}
|
||||
onValueChange={(val) => {
|
||||
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!connectionId}
|
||||
>
|
||||
{Object.values(ScanMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={method} key={method}>
|
||||
{method.replace("-", " ")}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{scanMethod === ScanMethod.SelectRepositories && (
|
||||
<Controller
|
||||
name="config.includeRepos"
|
||||
defaultValue={["*"]}
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Include Repositories"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the repository you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={areRepositoriesLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
isMulti
|
||||
value={repositories?.filter((repository) => value.includes(repository.name))}
|
||||
onChange={(newValue) => {
|
||||
onChange(
|
||||
newValue
|
||||
? (newValue as MultiValue<TGitHubRadarConnectionRepository>).map(
|
||||
(p) => p.name
|
||||
)
|
||||
: null
|
||||
);
|
||||
}}
|
||||
options={repositories}
|
||||
placeholder="Select repositories..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Switch } from "@app/components/v2";
|
||||
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConfigFields = () => {
|
||||
const { watch, control } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
const type = watch("type");
|
||||
|
||||
const Component = COMPONENT_MAP[type];
|
||||
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">Connect and configure your Data Source.</p>
|
||||
<Component />
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAutoScanEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? `Scans will automatically be triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source.`
|
||||
: "Manually trigger scans to detect secret leaks."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-scan-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-[9.6rem]">Auto-Scan {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigFields";
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
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 { OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onChange?: VoidFunction;
|
||||
isUpdate?: boolean;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConnectionField = ({
|
||||
onChange: callback,
|
||||
isUpdate
|
||||
}: Props) => {
|
||||
const { permission } = useOrgPermission();
|
||||
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
const dataSourceType = watch("type");
|
||||
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
|
||||
|
||||
const connectionName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
const canCreateConnection = permission.can(
|
||||
OrgPermissionAppConnectionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="App Connections can be created from the Organization Settings page."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label={`${connectionName} Connection`}
|
||||
helperText={
|
||||
isUpdate ? (
|
||||
"Cannot be updated"
|
||||
) : (
|
||||
<p>
|
||||
Check out{" "}
|
||||
<a
|
||||
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
|
||||
target="_blank"
|
||||
className="underline"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
our docs
|
||||
</a>{" "}
|
||||
to ensure your connection has the required permissions for secret scanning.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
if (callback) callback();
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={availableConnections}
|
||||
isDisabled={isUpdate}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="connection"
|
||||
/>
|
||||
{!isUpdate && 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 {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.`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "./schemas";
|
||||
|
||||
export const SecretScanningDataSourceDetailsFields = () => {
|
||||
const { control } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Provide a name and description for this Data Source.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText="Must be slug-friendly"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus value={value} onChange={onChange} placeholder="my-data-source" />
|
||||
</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="Provide a description for this data source..."
|
||||
className="!resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
TSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
import {
|
||||
useCreateSecretScanningDataSource,
|
||||
useUpdateSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2/mutations";
|
||||
|
||||
import { SecretScanningDataSourceSchema, TSecretScanningDataSourceForm } from "./schemas";
|
||||
import { SecretScanningDataSourceConfigFields } from "./SecretScanningDataSourceConfigFields";
|
||||
import { SecretScanningDataSourceDetailsFields } from "./SecretScanningDataSourceDetailsFields";
|
||||
import { SecretScanningDataSourceReviewFields } from "./SecretScanningDataSourceReviewFields";
|
||||
|
||||
type Props = {
|
||||
onComplete: (dataSource: TSecretScanningDataSource) => void;
|
||||
type: SecretScanningDataSource;
|
||||
onCancel: () => void;
|
||||
dataSource?: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
|
||||
[
|
||||
{ name: "Configuration", key: "config", fields: ["config", "isAutoScanEnabled", "connection"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
|
||||
const createDataSource = useCreateSecretScanningDataSource();
|
||||
const updateDataSource = useUpdateSecretScanningDataSource();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const formMethods = useForm<TSecretScanningDataSourceForm>({
|
||||
resolver: zodResolver(SecretScanningDataSourceSchema),
|
||||
defaultValues: dataSource ?? {
|
||||
type,
|
||||
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
|
||||
},
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ connection, ...formData }: TSecretScanningDataSourceForm) => {
|
||||
const mutation = dataSource
|
||||
? updateDataSource.mutateAsync({
|
||||
dataSourceId: dataSource.id,
|
||||
projectId: dataSource.projectId,
|
||||
...formData
|
||||
})
|
||||
: createDataSource.mutateAsync({
|
||||
...formData,
|
||||
connectionId: connection?.id,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
try {
|
||||
const source = await mutation;
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${source ? "updated" : "created"} ${sourceType} Data Source`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(source);
|
||||
} catch (err: any) {
|
||||
createNotification({
|
||||
title: `Failed to ${dataSource ? "update" : "create"} ${sourceType} Data Source`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (selectedTabIndex === 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
trigger,
|
||||
formState: { isSubmitting }
|
||||
} = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
handleSubmit(onSubmit)();
|
||||
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;
|
||||
};
|
||||
|
||||
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>
|
||||
<SecretScanningDataSourceConfigFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretScanningDataSourceDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretScanningDataSourceReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
{isFinalStep ? `${dataSource ? "Update" : "Create"} Data Source` : "Next"}
|
||||
</Button>
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
|
||||
|
||||
export const GitHubDataSourceReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.GitHub;
|
||||
}
|
||||
>();
|
||||
|
||||
const [{ includeRepos }, connection] = watch(["config", "connection"]);
|
||||
const shouldScanAll = includeRepos[0] === "*";
|
||||
|
||||
return (
|
||||
<SecretScanningDataSourceConfigReviewSection>
|
||||
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
|
||||
<GenericFieldLabel label="Scan Repositories">
|
||||
{shouldScanAll ? "All" : includeRepos.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
</SecretScanningDataSourceConfigReviewSection>
|
||||
);
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
const { type, name, description } = watch();
|
||||
|
||||
const Component = COMPONENT_MAP[type];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<Component />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceReviewFields";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConfigReviewSection = ({ children }: Props) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Configuration</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigReviewSection";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceForm";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type SchemaOptions = {
|
||||
isConnectionRequired: boolean;
|
||||
};
|
||||
|
||||
export const BaseSecretScanningDataSourceSchema = ({ isConnectionRequired }: SchemaOptions) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").nullish(),
|
||||
connection: isConnectionRequired
|
||||
? z.object({ name: z.string(), id: z.string().uuid() })
|
||||
: z.null().or(z.undefined()),
|
||||
isAutoScanEnabled: z.boolean(),
|
||||
id: z.string().uuid().optional()
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
|
||||
|
||||
export const GitHubDataSourceSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretScanningDataSource.GitHub),
|
||||
config: z.object({
|
||||
includeRepos: z
|
||||
.string()
|
||||
.array()
|
||||
.min(1, "One or more repositories required")
|
||||
.max(100, "Cannot configure more than 100 repositories")
|
||||
})
|
||||
})
|
||||
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitHubDataSourceSchema } from "./github-data-source-schema";
|
||||
|
||||
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
||||
GitHubDataSourceSchema
|
||||
]);
|
||||
|
||||
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./CreateSecretScanningDataSourceModal";
|
||||
export * from "./EditSecretScanningDataSourceModal";
|
||||
export * from "./SecretScanningScanStatus";
|
||||
@@ -52,7 +52,7 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteSecretSync}
|
||||
>
|
||||
|
||||
+25
-1
@@ -131,7 +131,27 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-md"
|
||||
tooltipText="When a secret is synced, its key will be injected into the key schema before it reaches the destination. This is useful for organization."
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<span>
|
||||
When a secret is synced, values will be injected into the key schema before it
|
||||
reaches the destination. This is useful for organization.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span>Available keys:</span>
|
||||
<ul className="list-disc pl-4 text-sm">
|
||||
<li>
|
||||
<code>{"{{secretKey}}"}</code> - The key of the secret
|
||||
</li>
|
||||
<li>
|
||||
<code>{"{{environment}}"}</code> - The environment which the secret is in
|
||||
(e.g. dev, staging, prod)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
@@ -146,11 +166,15 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Key Schema
|
||||
</a>{" "}
|
||||
to ensure that Infisical only manages the specific keys you intend, keeping
|
||||
everything else untouched.
|
||||
<br />
|
||||
<br />
|
||||
Destination secrets that do not match the schema will not be deleted or updated.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) =>
|
||||
!val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val),
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...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);
|
||||
|
||||
const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length;
|
||||
|
||||
return contentIsValid && secretKeyCount === 1;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
|
||||
"Key schema must include exactly one {{secretKey}} 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."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge";
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
@@ -23,9 +24,12 @@ const badgeVariants = cva(
|
||||
|
||||
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
|
||||
|
||||
export const Badge = ({ children, className, variant }: BadgeProps) => {
|
||||
export const Badge = ({ children, className, variant, ...props }: BadgeProps) => {
|
||||
return (
|
||||
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
|
||||
<div
|
||||
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ type Props = {
|
||||
isDisabled?: boolean;
|
||||
icon?: IconProp;
|
||||
isMulti?: boolean;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
|
||||
@@ -33,6 +34,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
dropdownContainerClassName,
|
||||
position,
|
||||
containerClassName,
|
||||
iconClassName,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -58,7 +60,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} />}
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
|
||||
<div className="flex-1 truncate">
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
</div>
|
||||
|
||||
@@ -27,10 +27,6 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/organization/sso",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
|
||||
),
|
||||
SecretScanning: setRoute(
|
||||
"/organization/secret-scanning",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||
),
|
||||
SecretSharing: setRoute(
|
||||
"/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
|
||||
@@ -334,6 +330,16 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
|
||||
)
|
||||
},
|
||||
SecretScanning: {
|
||||
DataSourceByIdPage: setRoute(
|
||||
"/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
|
||||
),
|
||||
FindingsPage: setRoute(
|
||||
"/secret-scanning/$projectId/findings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/findings"
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
|
||||
ViewSecretRequestByIDPage: setRoute(
|
||||
|
||||
@@ -2,6 +2,7 @@ export { useOrgPermission } from "./OrgPermissionContext";
|
||||
export type { TOrgPermission } from "./types";
|
||||
export {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionBillingActions,
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects
|
||||
|
||||
@@ -7,6 +7,11 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionBillingActions {
|
||||
Read = "read",
|
||||
ManageBilling = "manage-billing"
|
||||
}
|
||||
|
||||
export enum OrgGatewayPermissionActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
@@ -100,7 +105,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
||||
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
|
||||
@@ -2,7 +2,6 @@ export { useProjectPermission } from "./ProjectPermissionContext";
|
||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||
export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionApprovalActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
@@ -11,6 +10,7 @@ export {
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub
|
||||
} from "./types";
|
||||
|
||||
@@ -24,15 +24,6 @@ export enum ProjectPermissionSecretActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionApprovalActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
AllowChangeBypass = "allow-change-bypass",
|
||||
AllowAccessBypass = "allow-access-bypass"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionDynamicSecretActions {
|
||||
ReadRootCredential = "read-root-credential",
|
||||
CreateRootCredential = "create-root-credential",
|
||||
@@ -113,6 +104,15 @@ export enum ProjectPermissionPkiSubscriberActions {
|
||||
ListCerts = "list-certs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPkiTemplateActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
IssueCert = "issue-cert",
|
||||
ListCerts = "list-certs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadGeneratedCredentials = "read-generated-credentials",
|
||||
@@ -122,6 +122,26 @@ export enum ProjectPermissionSecretRotationActions {
|
||||
RotateSecrets = "rotate-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretScanningDataSourceActions {
|
||||
Read = "read-data-sources",
|
||||
Create = "create-data-sources",
|
||||
Edit = "edit-data-sources",
|
||||
Delete = "delete-data-sources",
|
||||
TriggerScans = "trigger-data-source-scans",
|
||||
ReadScans = "read-data-source-scans",
|
||||
ReadResources = "read-data-source-resources"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretScanningFindingActions {
|
||||
Read = "read-findings",
|
||||
Update = "update-findings"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretScanningConfigActions {
|
||||
Read = "read-configs",
|
||||
Update = "update-configs"
|
||||
}
|
||||
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$ALL = "$all",
|
||||
@@ -214,7 +234,10 @@ export enum ProjectPermissionSub {
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs",
|
||||
Kmip = "kmip",
|
||||
Commits = "commits"
|
||||
Commits = "commits",
|
||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||
SecretScanningFindings = "secret-scanning-findings",
|
||||
SecretScanningConfigs = "secret-scanning-configs"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -253,6 +276,11 @@ export type PkiSubscriberSubjectFields = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type PkiTemplateSubjectFields = {
|
||||
name: string;
|
||||
// (dangtony98): consider adding [commonName] as a subject field in the future
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -300,7 +328,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [
|
||||
ProjectPermissionIdentityActions,
|
||||
(
|
||||
@@ -310,7 +338,13 @@ export type ProjectPermissionSet =
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
(
|
||||
| ProjectPermissionSub.CertificateTemplates
|
||||
| (ForcedSubject<ProjectPermissionSub.CertificateTemplates> & PkiTemplateSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||
@@ -339,6 +373,12 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
|
||||
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits];
|
||||
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits]
|
||||
| [
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
ProjectPermissionSub.SecretScanningDataSources
|
||||
]
|
||||
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
||||
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -2,6 +2,7 @@ export { useOrganization } from "./OrganizationContext";
|
||||
export type { TOrgPermission } from "./OrgPermissionContext";
|
||||
export {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionBillingActions,
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -10,7 +11,6 @@ export {
|
||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||
export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionApprovalActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
@@ -19,6 +19,7 @@ export {
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faGithub, IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faBullseye,
|
||||
faKey,
|
||||
faLink,
|
||||
faLock,
|
||||
@@ -19,10 +20,12 @@ import {
|
||||
DatabricksConnectionMethod,
|
||||
GcpConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
GitHubRadarConnectionMethod,
|
||||
HCVaultConnectionMethod,
|
||||
HumanitecConnectionMethod,
|
||||
LdapConnectionMethod,
|
||||
MsSqlConnectionMethod,
|
||||
MySqlConnectionMethod,
|
||||
OnePassConnectionMethod,
|
||||
PostgresConnectionMethod,
|
||||
TAppConnection,
|
||||
@@ -35,10 +38,15 @@ import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-con
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<
|
||||
AppConnection,
|
||||
{ name: string; image: string; size?: number; enterprise?: boolean }
|
||||
{ name: string; image: string; size?: number; icon?: IconDefinition; enterprise?: boolean }
|
||||
> = {
|
||||
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
||||
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" },
|
||||
[AppConnection.GitHubRadar]: {
|
||||
name: "GitHub Radar",
|
||||
image: "GitHub.png",
|
||||
icon: faBullseye
|
||||
},
|
||||
[AppConnection.GCP]: {
|
||||
name: "GCP",
|
||||
image: "Google Cloud Platform.png"
|
||||
@@ -58,6 +66,7 @@ export const APP_CONNECTION_MAP: Record<
|
||||
[AppConnection.Vercel]: { name: "Vercel", image: "Vercel.png" },
|
||||
[AppConnection.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
|
||||
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" },
|
||||
[AppConnection.MySql]: { name: "MySQL", image: "MySql.png" },
|
||||
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" },
|
||||
[AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" },
|
||||
[AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 },
|
||||
@@ -71,6 +80,7 @@ export const APP_CONNECTION_MAP: Record<
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
case GitHubRadarConnectionMethod.App:
|
||||
return { name: "GitHub App", icon: faGithub };
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
@@ -95,6 +105,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "API Token", icon: faKey };
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
return { name: "Username & Password", icon: faLock };
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
|
||||
@@ -61,10 +61,14 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
|
||||
return project;
|
||||
};
|
||||
export const getProjectHomePage = (workspace: Workspace) => {
|
||||
if (workspace.type === ProjectType.CertificateManager) {
|
||||
return `/${workspace.type}/$projectId/subscribers` as const;
|
||||
switch (workspace.type) {
|
||||
case ProjectType.CertificateManager:
|
||||
return `/${workspace.type}/$projectId/subscribers` as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/${workspace.type}/$projectId/data-sources` as const;
|
||||
default:
|
||||
return `/${workspace.type}/$projectId/overview` as const;
|
||||
}
|
||||
return `/${workspace.type}/$projectId/overview` as const;
|
||||
};
|
||||
|
||||
export const getProjectTitle = (type: ProjectType) => {
|
||||
@@ -72,7 +76,8 @@ export const getProjectTitle = (type: ProjectType) => {
|
||||
[ProjectType.SecretManager]: "Secret Management",
|
||||
[ProjectType.KMS]: "Key Management",
|
||||
[ProjectType.CertificateManager]: "Cert Management",
|
||||
[ProjectType.SSH]: "SSH"
|
||||
[ProjectType.SSH]: "SSH",
|
||||
[ProjectType.SecretScanning]: "Secret Scanning"
|
||||
};
|
||||
return titleConvert[type];
|
||||
};
|
||||
|
||||
@@ -15,6 +15,11 @@ export const SECRET_ROTATION_MAP: Record<
|
||||
image: "MsSql.png",
|
||||
size: 50
|
||||
},
|
||||
[SecretRotation.MySqlCredentials]: {
|
||||
name: "MySQL Credentials",
|
||||
image: "MySql.png",
|
||||
size: 50
|
||||
},
|
||||
[SecretRotation.Auth0ClientSecret]: {
|
||||
name: "Auth0 Client Secret",
|
||||
image: "Auth0.png",
|
||||
@@ -40,6 +45,7 @@ export const SECRET_ROTATION_MAP: Record<
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.MySqlCredentials]: AppConnection.MySql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
@@ -50,6 +56,7 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
|
||||
export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
|
||||
[SecretRotation.PostgresCredentials]: true,
|
||||
[SecretRotation.MsSqlCredentials]: true,
|
||||
[SecretRotation.MySqlCredentials]: true,
|
||||
[SecretRotation.Auth0ClientSecret]: false,
|
||||
[SecretRotation.AzureClientSecret]: true,
|
||||
[SecretRotation.LdapPassword]: false,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
faBan,
|
||||
faCheck,
|
||||
faMagnifyingGlassMinus,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningFindingStatus
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
|
||||
SecretScanningDataSource,
|
||||
{ name: string; image: string; size: number }
|
||||
> = {
|
||||
[SecretScanningDataSource.GitHub]: {
|
||||
name: "GitHub",
|
||||
image: "GitHub.png",
|
||||
size: 45
|
||||
}
|
||||
};
|
||||
|
||||
export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
|
||||
SecretScanningDataSource,
|
||||
AppConnection
|
||||
> = {
|
||||
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar
|
||||
};
|
||||
|
||||
export const RESOURCE_DESCRIPTION_HELPER: Record<
|
||||
SecretScanningDataSource,
|
||||
{
|
||||
verb: string;
|
||||
pluralNoun: string;
|
||||
singularNoun: string;
|
||||
singularTitle: string;
|
||||
pluralTitle: string;
|
||||
}
|
||||
> = {
|
||||
[SecretScanningDataSource.GitHub]: {
|
||||
verb: "push",
|
||||
pluralNoun: "repositories",
|
||||
singularNoun: "repository",
|
||||
pluralTitle: "Repositories",
|
||||
singularTitle: "Repository"
|
||||
}
|
||||
};
|
||||
|
||||
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = {
|
||||
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" },
|
||||
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" },
|
||||
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" },
|
||||
[SecretScanningFindingStatus.FalsePositive]: {
|
||||
icon: faMagnifyingGlassMinus,
|
||||
className: "text-mineshaft-400"
|
||||
}
|
||||
};
|
||||
@@ -21,6 +21,7 @@ export const useCreateAccessApprovalPolicy = () => {
|
||||
projectSlug,
|
||||
approvals,
|
||||
approvers,
|
||||
bypassers,
|
||||
name,
|
||||
secretPath,
|
||||
enforcementLevel,
|
||||
@@ -30,6 +31,7 @@ export const useCreateAccessApprovalPolicy = () => {
|
||||
environment,
|
||||
projectSlug,
|
||||
approvals,
|
||||
bypassers,
|
||||
approvers,
|
||||
secretPath,
|
||||
name,
|
||||
@@ -53,6 +55,7 @@ export const useUpdateAccessApprovalPolicy = () => {
|
||||
mutationFn: async ({
|
||||
id,
|
||||
approvers,
|
||||
bypassers,
|
||||
approvals,
|
||||
name,
|
||||
secretPath,
|
||||
@@ -62,6 +65,7 @@ export const useUpdateAccessApprovalPolicy = () => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||
approvals,
|
||||
approvers,
|
||||
bypassers,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
|
||||
@@ -16,6 +16,7 @@ export type TAccessApprovalPolicy = {
|
||||
enforcementLevel: EnforcementLevel;
|
||||
updatedAt: Date;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
allowedSelfApprovals: boolean;
|
||||
};
|
||||
|
||||
@@ -24,11 +25,21 @@ export enum ApproverType {
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
export enum BypasserType {
|
||||
User = "user",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
export type Approver = {
|
||||
id: string;
|
||||
type: ApproverType;
|
||||
};
|
||||
|
||||
export type Bypasser = {
|
||||
id: string;
|
||||
type: BypasserType;
|
||||
};
|
||||
|
||||
export type TAccessApprovalRequest = {
|
||||
id: string;
|
||||
policyId: string;
|
||||
@@ -68,6 +79,7 @@ export type TAccessApprovalRequest = {
|
||||
name: string;
|
||||
approvals: number;
|
||||
approvers: string[];
|
||||
bypassers: string[];
|
||||
secretPath?: string | null;
|
||||
envId: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
@@ -146,6 +158,7 @@ export type TCreateAccessPolicyDTO = {
|
||||
name?: string;
|
||||
environment: string;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
secretPath?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
@@ -156,6 +169,7 @@ export type TUpdateAccessPolicyDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
secretPath?: string;
|
||||
environment?: string;
|
||||
approvals?: number;
|
||||
|
||||
@@ -1,16 +1,2 @@
|
||||
export {
|
||||
useAdminDeleteUser,
|
||||
useAdminGrantServerAdminAccess,
|
||||
useAdminRemoveIdentitySuperAdminAccess,
|
||||
useCreateAdminUser,
|
||||
useInvalidateCache,
|
||||
useRemoveUserServerAdminAccess,
|
||||
useUpdateServerConfig,
|
||||
useUpdateServerEncryptionStrategy
|
||||
} from "./mutation";
|
||||
export {
|
||||
useAdminGetUsers,
|
||||
useGetAdminIntegrationsConfig,
|
||||
useGetServerConfig,
|
||||
useGetServerRootKmsEncryptionDetails
|
||||
} from "./queries";
|
||||
export * from "./mutation";
|
||||
export * from "./queries";
|
||||
|
||||
@@ -63,6 +63,41 @@ export const useAdminDeleteUser = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [adminStandaloneKeys.getUsers]
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: adminStandaloneKeys.getOrganizations });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminDeleteOrganizationMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<object, object, { organizationId: string; membershipId: string }>({
|
||||
mutationFn: async ({ organizationId, membershipId }) => {
|
||||
await apiRequest.delete(
|
||||
`/api/v1/admin/organization-management/organizations/${organizationId}/memberships/${membershipId}`
|
||||
);
|
||||
|
||||
return {};
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [adminStandaloneKeys.getOrganizations]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminDeleteOrganization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (organizationId: string) => {
|
||||
await apiRequest.delete(
|
||||
`/api/v1/admin/organization-management/organizations/${organizationId}`
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [adminStandaloneKeys.getOrganizations]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,8 +6,10 @@ import { Identity } from "@app/hooks/api/identities/types";
|
||||
import { User } from "../types";
|
||||
import {
|
||||
AdminGetIdentitiesFilters,
|
||||
AdminGetOrganizationsFilters,
|
||||
AdminGetUsersFilters,
|
||||
AdminIntegrationsConfig,
|
||||
OrganizationWithProjects,
|
||||
TGetInvalidatingCacheStatus,
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
@@ -15,12 +17,15 @@ import {
|
||||
|
||||
export const adminStandaloneKeys = {
|
||||
getUsers: "get-users",
|
||||
getOrganizations: "get-organizations",
|
||||
getIdentities: "get-identities"
|
||||
};
|
||||
|
||||
export const adminQueryKeys = {
|
||||
serverConfig: () => ["server-config"] as const,
|
||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||
getOrganizations: (filters: AdminGetOrganizationsFilters) =>
|
||||
[adminStandaloneKeys.getOrganizations, { filters }] as const,
|
||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||
@@ -34,6 +39,28 @@ export const fetchServerConfig = async () => {
|
||||
return data.config;
|
||||
};
|
||||
|
||||
export const useAdminGetOrganizations = (filters: AdminGetOrganizationsFilters) => {
|
||||
return useInfiniteQuery({
|
||||
initialPageParam: 0,
|
||||
queryKey: adminQueryKeys.getOrganizations(filters),
|
||||
queryFn: async ({ pageParam }) => {
|
||||
const { data } = await apiRequest.get<{ organizations: OrganizationWithProjects[] }>(
|
||||
"/api/v1/admin/organization-management/organizations",
|
||||
{
|
||||
params: {
|
||||
...filters,
|
||||
offset: pageParam
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.organizations;
|
||||
},
|
||||
getNextPageParam: (lastPage, pages) =>
|
||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetServerConfig = ({
|
||||
options = {}
|
||||
}: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Organization } from "../types";
|
||||
|
||||
export enum LoginMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
@@ -8,6 +10,27 @@ export enum LoginMethod {
|
||||
OIDC = "oidc"
|
||||
}
|
||||
|
||||
export type OrganizationWithProjects = Organization & {
|
||||
members: {
|
||||
user: {
|
||||
id: string;
|
||||
email: string | null;
|
||||
username: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
};
|
||||
membershipId: string;
|
||||
role: string;
|
||||
roleId: string | null;
|
||||
}[];
|
||||
projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TServerConfig = {
|
||||
initialized: boolean;
|
||||
allowSignUp: boolean;
|
||||
@@ -51,6 +74,11 @@ export type TCreateAdminUserDTO = {
|
||||
salt: string;
|
||||
};
|
||||
|
||||
export type AdminGetOrganizationsFilters = {
|
||||
limit: number;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export type AdminGetUsersFilters = {
|
||||
limit: number;
|
||||
searchTerm: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum AppConnection {
|
||||
AWS = "aws",
|
||||
GitHub = "github",
|
||||
GitHubRadar = "github-radar",
|
||||
GCP = "gcp",
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
@@ -11,6 +12,7 @@ export enum AppConnection {
|
||||
Vercel = "vercel",
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql",
|
||||
MySql = "mysql",
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||
|
||||
import {
|
||||
TGitHubRadarConnectionListRepositoriesResponse,
|
||||
TGitHubRadarConnectionRepository
|
||||
} from "./types";
|
||||
|
||||
const githubRadarConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "github-radar"] as const,
|
||||
listRepositories: (connectionId: string) =>
|
||||
[...githubRadarConnectionKeys.all, "repositories", connectionId] as const
|
||||
};
|
||||
|
||||
export const useGitHubRadarConnectionListRepositories = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TGitHubRadarConnectionRepository[],
|
||||
unknown,
|
||||
TGitHubRadarConnectionRepository[],
|
||||
ReturnType<typeof githubRadarConnectionKeys.listRepositories>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: githubRadarConnectionKeys.listRepositories(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGitHubRadarConnectionListRepositoriesResponse>(
|
||||
`/api/v1/app-connections/github-radar/${connectionId}/repositories`
|
||||
);
|
||||
|
||||
return data.repositories;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export type TGitHubRadarConnectionOrganization = {
|
||||
login: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type TGitHubRadarConnectionRepository = {
|
||||
id: number;
|
||||
name: string;
|
||||
owner: TGitHubRadarConnectionOrganization;
|
||||
};
|
||||
|
||||
export type TGitHubRadarConnectionListRepositoriesResponse = {
|
||||
repositories: TGitHubRadarConnectionRepository[];
|
||||
};
|
||||
@@ -17,6 +17,11 @@ export type TGitHubConnectionOption = TAppConnectionOptionBase & {
|
||||
appClientSlug?: string;
|
||||
};
|
||||
|
||||
export type TGitHubRadarConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.GitHubRadar;
|
||||
appClientSlug?: string;
|
||||
};
|
||||
|
||||
export type TGcpConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.GCP;
|
||||
};
|
||||
@@ -60,6 +65,10 @@ export type TMsSqlConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.MsSql;
|
||||
};
|
||||
|
||||
export type TMySqlConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.MySql;
|
||||
};
|
||||
|
||||
export type TCamundaConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Camunda;
|
||||
};
|
||||
@@ -105,6 +114,7 @@ export type TAppConnectionOption =
|
||||
| TVercelConnectionOption
|
||||
| TPostgresConnectionOption
|
||||
| TMsSqlConnectionOption
|
||||
| TMySqlConnectionOption
|
||||
| TCamundaConnectionOption
|
||||
| TWindmillConnectionOption
|
||||
| TAuth0ConnectionOption
|
||||
@@ -116,6 +126,7 @@ export type TAppConnectionOption =
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
[AppConnection.GitHub]: TGitHubConnectionOption;
|
||||
[AppConnection.GitHubRadar]: TGitHubRadarConnectionOption;
|
||||
[AppConnection.GCP]: TGcpConnectionOption;
|
||||
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
|
||||
@@ -126,6 +137,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Vercel]: TVercelConnectionOption;
|
||||
[AppConnection.Postgres]: TPostgresConnectionOption;
|
||||
[AppConnection.MsSql]: TMsSqlConnectionOption;
|
||||
[AppConnection.MySql]: TMySqlConnectionOption;
|
||||
[AppConnection.Camunda]: TCamundaConnectionOption;
|
||||
[AppConnection.Windmill]: TWindmillConnectionOption;
|
||||
[AppConnection.Auth0]: TAuth0ConnectionOption;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum GitHubRadarConnectionMethod {
|
||||
App = "github-app"
|
||||
}
|
||||
|
||||
export type TGitHubRadarConnection = TRootAppConnection & { app: AppConnection.GitHubRadar } & {
|
||||
method: GitHubRadarConnectionMethod.App;
|
||||
credentials: {
|
||||
code: string;
|
||||
installationId: string;
|
||||
};
|
||||
};
|
||||
@@ -10,10 +10,12 @@ import { TCamundaConnection } from "./camunda-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
import { TGitHubConnection } from "./github-connection";
|
||||
import { TGitHubRadarConnection } from "./github-radar-connection";
|
||||
import { THCVaultConnection } from "./hc-vault-connection";
|
||||
import { THumanitecConnection } from "./humanitec-connection";
|
||||
import { TLdapConnection } from "./ldap-connection";
|
||||
import { TMsSqlConnection } from "./mssql-connection";
|
||||
import { TMySqlConnection } from "./mysql-connection";
|
||||
import { TOCIConnection } from "./oci-connection";
|
||||
import { TPostgresConnection } from "./postgres-connection";
|
||||
import { TTeamCityConnection } from "./teamcity-connection";
|
||||
@@ -31,10 +33,12 @@ export * from "./camunda-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./gcp-connection";
|
||||
export * from "./github-connection";
|
||||
export * from "./github-radar-connection";
|
||||
export * from "./hc-vault-connection";
|
||||
export * from "./humanitec-connection";
|
||||
export * from "./ldap-connection";
|
||||
export * from "./mssql-connection";
|
||||
export * from "./mysql-connection";
|
||||
export * from "./oci-connection";
|
||||
export * from "./postgres-connection";
|
||||
export * from "./teamcity-connection";
|
||||
@@ -45,6 +49,7 @@ export * from "./windmill-connection";
|
||||
export type TAppConnection =
|
||||
| TAwsConnection
|
||||
| TGitHubConnection
|
||||
| TGitHubRadarConnection
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
@@ -55,6 +60,7 @@ export type TAppConnection =
|
||||
| TVercelConnection
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection
|
||||
| TMySqlConnection
|
||||
| TCamundaConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
@@ -92,6 +98,7 @@ export type TDeleteAppConnectionDTO = {
|
||||
export type TAppConnectionMap = {
|
||||
[AppConnection.AWS]: TAwsConnection;
|
||||
[AppConnection.GitHub]: TGitHubConnection;
|
||||
[AppConnection.GitHubRadar]: TGitHubRadarConnection;
|
||||
[AppConnection.GCP]: TGcpConnection;
|
||||
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
|
||||
@@ -102,6 +109,7 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.Vercel]: TVercelConnection;
|
||||
[AppConnection.Postgres]: TPostgresConnection;
|
||||
[AppConnection.MsSql]: TMsSqlConnection;
|
||||
[AppConnection.MySql]: TMySqlConnection;
|
||||
[AppConnection.Camunda]: TCamundaConnection;
|
||||
[AppConnection.Windmill]: TWindmillConnection;
|
||||
[AppConnection.Auth0]: TAuth0Connection;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
import { TBaseSqlConnectionCredentials } from "./shared";
|
||||
|
||||
export enum MySqlConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
|
||||
export type TMySqlConnection = TRootAppConnection & { app: AppConnection.MySql } & {
|
||||
method: MySqlConnectionMethod.UsernameAndPassword;
|
||||
credentials: TBaseSqlConnectionCredentials;
|
||||
};
|
||||
@@ -191,6 +191,20 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity",
|
||||
[EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity",
|
||||
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_LIST]: "List Secret Scanning Data Sources",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_CREATE]: "Create Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_UPDATE]: "Update Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_DELETE]: "Delete Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_GET]: "Get Secret Scanning Data Source",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN]: "Trigger Secret Scanning Data Source Scan",
|
||||
[EventType.SECRET_SCANNING_DATA_SOURCE_SCAN]: "Secret Scanning Data Source Scan",
|
||||
[EventType.SECRET_SCANNING_RESOURCE_LIST]: "List Secret Scanning Resources",
|
||||
[EventType.SECRET_SCANNING_SCAN_LIST]: "List Secret Scanning Scans",
|
||||
[EventType.SECRET_SCANNING_FINDING_LIST]: "List Secret Scanning Findings",
|
||||
[EventType.SECRET_SCANNING_FINDING_UPDATE]: "Update Secret Scanning Finding Status",
|
||||
[EventType.SECRET_SCANNING_CONFIG_GET]: "Get Secret Scanning Configuration",
|
||||
[EventType.SECRET_SCANNING_CONFIG_UPDATE]: "Update Secret Scanning Configuration",
|
||||
|
||||
[EventType.CREATE_PKI_SUBSCRIBER]: "Create PKI subscriber",
|
||||
[EventType.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber",
|
||||
[EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber",
|
||||
|
||||
@@ -185,6 +185,20 @@ export enum EventType {
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get",
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
|
||||
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
|
||||
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
||||
SECRET_SCANNING_DATA_SOURCE_DELETE = "secret-scanning-data-source-delete",
|
||||
SECRET_SCANNING_DATA_SOURCE_GET = "secret-scanning-data-source-get",
|
||||
SECRET_SCANNING_DATA_SOURCE_TRIGGER_SCAN = "secret-scanning-data-source-trigger-scan",
|
||||
SECRET_SCANNING_DATA_SOURCE_SCAN = "secret-scanning-data-source-scan",
|
||||
SECRET_SCANNING_RESOURCE_LIST = "secret-scanning-resource-list",
|
||||
SECRET_SCANNING_SCAN_LIST = "secret-scanning-scan-list",
|
||||
SECRET_SCANNING_FINDING_LIST = "secret-scanning-finding-list",
|
||||
SECRET_SCANNING_FINDING_UPDATE = "secret-scanning-finding-update",
|
||||
SECRET_SCANNING_CONFIG_GET = "secret-scanning-config-get",
|
||||
SECRET_SCANNING_CONFIG_UPDATE = "secret-scanning-config-update",
|
||||
|
||||
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
|
||||
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
|
||||
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
|
||||
|
||||
@@ -31,10 +31,14 @@ export const useUpdateCa = () => {
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ projectId, type }) => {
|
||||
onSuccess: ({ projectId, type }, { caName }) => {
|
||||
caKeys.getCaByNameAndProjectId(caName, projectId);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.getCaByNameAndProjectId(caName, projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
export {
|
||||
useCreateCertTemplate,
|
||||
useCreateCertTemplateV2,
|
||||
useCreateEstConfig,
|
||||
useDeleteCertTemplate,
|
||||
useDeleteCertTemplateV2,
|
||||
useUpdateCertTemplate,
|
||||
useUpdateCertTemplateV2,
|
||||
useUpdateEstConfig
|
||||
} from "./mutations";
|
||||
export { useGetCertTemplate, useGetEstConfig } from "./queries";
|
||||
export { useGetCertTemplate, useGetEstConfig, useListCertificateTemplates } from "./queries";
|
||||
|
||||
@@ -8,9 +8,12 @@ import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TCreateCertificateTemplateV2DTO,
|
||||
TCreateEstConfigDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TDeleteCertificateTemplateV2DTO,
|
||||
TUpdateCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateV2DTO,
|
||||
TUpdateEstConfigDTO
|
||||
} from "./types";
|
||||
|
||||
@@ -73,6 +76,58 @@ export const useDeleteCertTemplate = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCertTemplateV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, object, TCreateCertificateTemplateV2DTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.post<{
|
||||
certificateTemplate: TCertificateTemplate;
|
||||
}>("/api/v2/pki/certificate-templates", dto);
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCertTemplateV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, object, TUpdateCertificateTemplateV2DTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.patch<{ certificateTemplate: TCertificateTemplate }>(
|
||||
`/api/v2/pki/certificate-templates/${dto.templateName}`,
|
||||
dto
|
||||
);
|
||||
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCertTemplateV2 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, object, TDeleteCertificateTemplateV2DTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.delete<{ certificateTemplate: TCertificateTemplate }>(
|
||||
`/api/v2/pki/certificate-templates/${dto.templateName}`,
|
||||
{
|
||||
data: {
|
||||
projectId: dto.projectId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateEstConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<object, object, TCreateEstConfigDTO>({
|
||||
|
||||
@@ -2,10 +2,20 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate, TEstConfig } from "./types";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCertificateTemplateV2,
|
||||
TEstConfig,
|
||||
TListCertificateTemplatesDTO
|
||||
} from "./types";
|
||||
|
||||
export const certTemplateKeys = {
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"],
|
||||
listTemplates: ({ projectId, ...el }: { limit?: number; offset?: number; projectId: string }) => [
|
||||
"list-template",
|
||||
projectId,
|
||||
el
|
||||
],
|
||||
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
|
||||
};
|
||||
|
||||
@@ -22,6 +32,29 @@ export const useGetCertTemplate = (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useListCertificateTemplates = ({
|
||||
limit = 100,
|
||||
offset = 0,
|
||||
projectId
|
||||
}: TListCertificateTemplatesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.listTemplates({ limit, offset, projectId }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateTemplates: TCertificateTemplateV2[];
|
||||
totalCount?: number;
|
||||
}>("/api/v2/pki/certificate-templates", {
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetEstConfig = (certificateTemplateId: string) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),
|
||||
|
||||
@@ -14,6 +14,26 @@ export type TCertificateTemplate = {
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2 = {
|
||||
id: string;
|
||||
caId: string;
|
||||
caName: string;
|
||||
projectId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
ca: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateDTO = {
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
@@ -44,6 +64,34 @@ export type TDeleteCertificateTemplateDTO = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateV2DTO = {
|
||||
caName: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
projectId: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateV2DTO = {
|
||||
templateName: string;
|
||||
caName?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
projectId: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateV2DTO = {
|
||||
templateName: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCreateEstConfigDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain?: string;
|
||||
@@ -67,3 +115,9 @@ export type TEstConfig = {
|
||||
isEnabled: boolean;
|
||||
disableBootstrapCertValidation: boolean;
|
||||
};
|
||||
|
||||
export type TListCertificateTemplatesDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ export type TDynamicSecret = {
|
||||
status?: DynamicSecretStatus;
|
||||
statusDetails?: string;
|
||||
maxTTL: string;
|
||||
usernameTemplate?: string | null;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
@@ -31,7 +32,9 @@ export enum DynamicSecretProviders {
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake",
|
||||
Totp = "totp",
|
||||
SapAse = "sap-ase"
|
||||
SapAse = "sap-ase",
|
||||
Kubernetes = "kubernetes",
|
||||
Vertica = "vertica"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@@ -261,6 +264,32 @@ export type TDynamicSecretProvider =
|
||||
algorithm?: string;
|
||||
digits?: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Kubernetes;
|
||||
inputs: {
|
||||
url: string;
|
||||
clusterToken: string;
|
||||
ca?: string;
|
||||
serviceAccountName: string;
|
||||
credentialType: "dynamic" | "static";
|
||||
namespace: string;
|
||||
gatewayId?: string;
|
||||
sslEnabled: boolean;
|
||||
audiences: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Vertica;
|
||||
inputs: {
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
@@ -272,6 +301,7 @@ export type TCreateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
name: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
usernameTemplate?: string;
|
||||
};
|
||||
|
||||
export type TUpdateDynamicSecretDTO = {
|
||||
@@ -285,6 +315,7 @@ export type TUpdateDynamicSecretDTO = {
|
||||
defaultTTL?: string;
|
||||
maxTTL?: string | null;
|
||||
inputs?: unknown;
|
||||
usernameTemplate?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -843,7 +843,8 @@ export const useAddIdentityKubernetesAuth = () => {
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
tokenReviewMode
|
||||
}) => {
|
||||
const {
|
||||
data: { identityKubernetesAuth }
|
||||
@@ -860,7 +861,8 @@ export const useAddIdentityKubernetesAuth = () => {
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
tokenReviewMode
|
||||
}
|
||||
);
|
||||
|
||||
@@ -950,7 +952,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
tokenReviewMode
|
||||
}) => {
|
||||
const {
|
||||
data: { identityKubernetesAuth }
|
||||
@@ -967,7 +970,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
tokenReviewMode
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -379,10 +379,16 @@ export type DeleteIdentityAzureAuthDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export enum IdentityKubernetesAuthTokenReviewMode {
|
||||
Api = "api",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
export type IdentityKubernetesAuth = {
|
||||
identityId: string;
|
||||
kubernetesHost: string;
|
||||
tokenReviewerJwt: string;
|
||||
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
||||
allowedNamespaces: string;
|
||||
allowedNames: string;
|
||||
allowedAudience: string;
|
||||
@@ -399,6 +405,7 @@ export type AddIdentityKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost: string;
|
||||
tokenReviewerJwt?: string;
|
||||
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
||||
allowedNamespaces: string;
|
||||
allowedNames: string;
|
||||
allowedAudience: string;
|
||||
@@ -417,6 +424,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost?: string;
|
||||
tokenReviewerJwt?: string | null;
|
||||
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
||||
allowedNamespaces?: string;
|
||||
allowedNames?: string;
|
||||
allowedAudience?: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { MutationCache, QueryClient } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
@@ -18,6 +19,39 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
|
||||
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
||||
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
||||
|
||||
function ValidationErrorModal({ serverResponse }: { serverResponse: TApiErrors }) {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
if (serverResponse.error !== ApiErrorTypes.ValidationError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={open} onOpenChange={setOpen}>
|
||||
<ModalContent title="Validation Error Details">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Field</Th>
|
||||
<Th>Issue</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{serverResponse.message?.map(({ message, path }) => (
|
||||
<Tr key={path.join(".")}>
|
||||
<Td>{path.join(".")}</Td>
|
||||
<Td>{message.toLowerCase()}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export const onRequestError = (error: unknown) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverResponse = error.response?.data as TApiErrors;
|
||||
@@ -27,35 +61,7 @@ export const onRequestError = (error: unknown) => {
|
||||
title: "Validation Error",
|
||||
type: "error",
|
||||
text: "Please check the input and try again.",
|
||||
callToAction: (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="Validation Error Details">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Field</Th>
|
||||
<Th>Issue</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{serverResponse.message?.map(({ message, path }) => (
|
||||
<Tr key={path.join(".")}>
|
||||
<Td>{path.join(".")}</Td>
|
||||
<Td>{message.toLowerCase()}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
),
|
||||
callToAction: <ValidationErrorModal serverResponse={serverResponse} />,
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
|
||||
@@ -96,7 +96,7 @@ export const useUpdateOrgRole = () => {
|
||||
data: { role }
|
||||
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
|
||||
...dto,
|
||||
permissions: permissions?.length ? packRules(permissions) : undefined
|
||||
permissions: permissions ? packRules(permissions) : undefined
|
||||
});
|
||||
|
||||
return role;
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
||||
workspaceId,
|
||||
approvals,
|
||||
approvers,
|
||||
bypassers,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
@@ -24,6 +25,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
||||
workspaceId,
|
||||
approvals,
|
||||
approvers,
|
||||
bypassers,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
@@ -46,6 +48,7 @@ export const useUpdateSecretApprovalPolicy = () => {
|
||||
mutationFn: async ({
|
||||
id,
|
||||
approvers,
|
||||
bypassers,
|
||||
approvals,
|
||||
secretPath,
|
||||
name,
|
||||
@@ -55,6 +58,7 @@ export const useUpdateSecretApprovalPolicy = () => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||
approvals,
|
||||
approvers,
|
||||
bypassers,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
|
||||
@@ -25,6 +25,16 @@ export type Approver = {
|
||||
type: ApproverType;
|
||||
};
|
||||
|
||||
export enum BypasserType {
|
||||
User = "user",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
export type Bypasser = {
|
||||
id: string;
|
||||
type: BypasserType;
|
||||
};
|
||||
|
||||
export type TGetSecretApprovalPoliciesDTO = {
|
||||
workspaceId: string;
|
||||
};
|
||||
@@ -41,6 +51,7 @@ export type TCreateSecretPolicyDTO = {
|
||||
environment: string;
|
||||
secretPath?: string | null;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
@@ -50,6 +61,7 @@ export type TUpdateSecretPolicyDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
secretPath?: string | null;
|
||||
approvals?: number;
|
||||
allowedSelfApprovals?: boolean;
|
||||
|
||||
@@ -58,7 +58,7 @@ export type TSecretApprovalRequest = {
|
||||
secretPath: string;
|
||||
hasMerged: boolean;
|
||||
status: "open" | "close";
|
||||
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
|
||||
policy: Omit<TSecretApprovalPolicy, "approvers" | "bypassers"> & {
|
||||
approvers: {
|
||||
userId: string;
|
||||
email: string;
|
||||
@@ -66,6 +66,13 @@ export type TSecretApprovalRequest = {
|
||||
lastName: string;
|
||||
username: string;
|
||||
}[];
|
||||
bypassers: {
|
||||
userId: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
}[];
|
||||
};
|
||||
statusChangedByUserId: string;
|
||||
statusChangedByUser?: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
MySqlCredentials = "mysql-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret",
|
||||
AzureClientSecret = "azure-client-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
|
||||
@@ -31,9 +31,15 @@ import { TSqlCredentialsRotationOption } from "@app/hooks/api/secretRotationsV2/
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import {
|
||||
TMySqlCredentialsRotation,
|
||||
TMySqlCredentialsRotationGeneratedCredentialsResponse
|
||||
} from "./mysql-credentials-rotation";
|
||||
|
||||
export type TSecretRotationV2 = (
|
||||
| TPostgresCredentialsRotation
|
||||
| TMsSqlCredentialsRotation
|
||||
| TMySqlCredentialsRotation
|
||||
| TAuth0ClientSecretRotation
|
||||
| TAzureClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
@@ -56,6 +62,7 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 };
|
||||
export type TViewSecretRotationGeneratedCredentialsResponse =
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMySqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
|
||||
| TAzureClientSecretRotationGeneratedCredentialsResponse
|
||||
| TLdapPasswordRotationGeneratedCredentialsResponse
|
||||
@@ -105,6 +112,7 @@ export type TViewSecretRotationV2GeneratedCredentialsDTO = {
|
||||
export type TSecretRotationOptionMap = {
|
||||
[SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption;
|
||||
[SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption;
|
||||
[SecretRotation.MySqlCredentials]: TSqlCredentialsRotationOption;
|
||||
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption;
|
||||
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationOption;
|
||||
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
|
||||
@@ -114,6 +122,7 @@ export type TSecretRotationOptionMap = {
|
||||
export type TSecretRotationGeneratedCredentialsResponseMap = {
|
||||
[SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.MySqlCredentials]: TMySqlCredentialsRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.AzureClientSecret]: TAzureClientSecretRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
TSecretRotationV2Base,
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase,
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationProperties
|
||||
} from "@app/hooks/api/secretRotationsV2/types/shared";
|
||||
|
||||
export type TMySqlCredentialsRotation = TSecretRotationV2Base & {
|
||||
type: SecretRotation.MySqlCredentials;
|
||||
} & TSqlCredentialsRotationProperties;
|
||||
|
||||
export type TMySqlCredentialsRotationGeneratedCredentialsResponse =
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase<
|
||||
SecretRotation.MySqlCredentials,
|
||||
TSqlCredentialsRotationGeneratedCredentials
|
||||
>;
|
||||
@@ -14,8 +14,11 @@ export type TSqlCredentialsRotationProperties = {
|
||||
|
||||
export type TSqlCredentialsRotationOption = {
|
||||
name: string;
|
||||
type: SecretRotation.PostgresCredentials | SecretRotation.MsSqlCredentials;
|
||||
connection: AppConnection.Postgres | AppConnection.MsSql;
|
||||
type:
|
||||
| SecretRotation.PostgresCredentials
|
||||
| SecretRotation.MsSqlCredentials
|
||||
| SecretRotation.MySqlCredentials;
|
||||
connection: AppConnection.Postgres | AppConnection.MsSql | AppConnection.MySql;
|
||||
template: {
|
||||
secretsMapping: TSqlCredentialsRotationProperties["secretsMapping"];
|
||||
createUserStatement: string;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export enum SecretScanningDataSource {
|
||||
GitHub = "github"
|
||||
}
|
||||
|
||||
export enum SecretScanningScanStatus {
|
||||
Completed = "completed",
|
||||
Failed = "failed",
|
||||
Queued = "queued",
|
||||
Scanning = "scanning"
|
||||
}
|
||||
|
||||
export enum SecretScanningScanType {
|
||||
FullScan = "full-scan",
|
||||
DiffScan = "diff-scan"
|
||||
}
|
||||
|
||||
export enum SecretScanningFindingStatus {
|
||||
Resolved = "resolved",
|
||||
Unresolved = "unresolved",
|
||||
FalsePositive = "false-positive",
|
||||
Ignore = "ignore"
|
||||
}
|
||||
|
||||
export enum SecretScanningResource {
|
||||
Repository = "repository",
|
||||
Project = "project"
|
||||
}
|
||||
|
||||
export enum SecretScanningFindingSeverity {
|
||||
High = "high",
|
||||
Medium = "medium",
|
||||
Low = "low"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./enums";
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretScanningV2Keys } from "./queries";
|
||||
import {
|
||||
TCreateSecretScanningDataSourceDTO,
|
||||
TDeleteSecretScanningDataSourceDTO,
|
||||
TGetSecretScanningConfigResponse,
|
||||
TSecretScanningConfig,
|
||||
TSecretScanningDataSourceResponse,
|
||||
TSecretScanningFindingResponse,
|
||||
TTriggerSecretScanningDataSourceDTO,
|
||||
TUpdateSecretScanningDataSourceDTO,
|
||||
TUpdateSecretScanningFinding
|
||||
} from "./types";
|
||||
|
||||
export const useCreateSecretScanningDataSource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, ...params }: TCreateSecretScanningDataSourceDTO) => {
|
||||
const { data } = await apiRequest.post<TSecretScanningDataSourceResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.dataSource;
|
||||
},
|
||||
onSuccess: (_, { projectId }) =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listDataSources(projectId)
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretScanningDataSource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, dataSourceId, ...params }: TUpdateSecretScanningDataSourceDTO) => {
|
||||
const { data } = await apiRequest.patch<TSecretScanningDataSourceResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.dataSource;
|
||||
},
|
||||
onSuccess: (_, { projectId, dataSourceId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listDataSources(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listResources(dataSourceId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listScans(dataSourceId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretScanningDataSource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, dataSourceId }: TDeleteSecretScanningDataSourceDTO) => {
|
||||
const { data } = await apiRequest.delete<TSecretScanningDataSourceResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`
|
||||
);
|
||||
|
||||
return data.dataSource;
|
||||
},
|
||||
onSuccess: (_, { projectId, dataSourceId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listDataSources(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listResources(dataSourceId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listScans(dataSourceId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useTriggerSecretScanningDataSource = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ type, dataSourceId, resourceId }: TTriggerSecretScanningDataSourceDTO) => {
|
||||
const { data } = await apiRequest.post<TSecretScanningDataSourceResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}${resourceId ? `/resources/${resourceId}` : ""}/scan`
|
||||
);
|
||||
|
||||
return data.dataSource;
|
||||
},
|
||||
onSuccess: (_, { projectId, dataSourceId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listDataSources(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listResources(dataSourceId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listScans(dataSourceId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretScanningFinding = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ findingId, ...params }: TUpdateSecretScanningFinding) => {
|
||||
const { data } = await apiRequest.patch<TSecretScanningFindingResponse>(
|
||||
`/api/v2/secret-scanning/findings/${findingId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.finding;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.listFindings(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.findingCount(projectId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.dataSource()
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretScanningConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ projectId, ...params }: TSecretScanningConfig) => {
|
||||
const { data } = await apiRequest.patch<TGetSecretScanningConfigResponse>(
|
||||
`/api/v2/secret-scanning/configs?projectId=${projectId}`,
|
||||
params
|
||||
);
|
||||
|
||||
return data.config;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretScanningV2Keys.configByProjectId(projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,248 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TGetSecretScanningConfigResponse,
|
||||
TGetSecretScanningDataSource,
|
||||
TGetSecretScanningUnresolvedFindingsResponse,
|
||||
TListSecretScanningDataSourceOptions,
|
||||
TListSecretScanningDataSources,
|
||||
TListSecretScanningFindingsResponse,
|
||||
TListSecretScanningResourcesResponse,
|
||||
TListSecretScanningScansResponse,
|
||||
TSecretScanningConfig,
|
||||
TSecretScanningDataSource,
|
||||
TSecretScanningDataSourceOption,
|
||||
TSecretScanningDataSourceResponse,
|
||||
TSecretScanningDataSourceWithDetails,
|
||||
TSecretScanningFinding,
|
||||
TSecretScanningResourceWithDetails,
|
||||
TSecretScanningScanWithDetails
|
||||
} from "./types";
|
||||
|
||||
export const secretScanningV2Keys = {
|
||||
all: ["secret-scanning-v2"] as const,
|
||||
dataSource: () => [...secretScanningV2Keys.all, "data-source"] as const,
|
||||
dataSourceOptions: () => [...secretScanningV2Keys.dataSource(), "options"] as const,
|
||||
dataSourceById: (dataSourceId: string) => [
|
||||
...secretScanningV2Keys.dataSource(),
|
||||
"byId",
|
||||
dataSourceId
|
||||
],
|
||||
listDataSources: (projectId: string) => [...secretScanningV2Keys.dataSource(), "list", projectId],
|
||||
listResources: (dataSourceId: string) => [
|
||||
...secretScanningV2Keys.dataSource(),
|
||||
"resources",
|
||||
dataSourceId
|
||||
],
|
||||
listScans: (dataSourceId: string) => [
|
||||
...secretScanningV2Keys.dataSource(),
|
||||
"scans",
|
||||
dataSourceId
|
||||
],
|
||||
finding: () => [...secretScanningV2Keys.all, "finding"] as const,
|
||||
findingCount: (projectId: string) =>
|
||||
[...secretScanningV2Keys.finding(), "count", projectId] as const,
|
||||
listFindings: (projectId: string) => [...secretScanningV2Keys.finding(), "list", projectId],
|
||||
configByProjectId: (projectId: string) =>
|
||||
[...secretScanningV2Keys.all, "config", projectId] as const
|
||||
};
|
||||
|
||||
export const useSecretScanningDataSourceOptions = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningDataSourceOption[],
|
||||
unknown,
|
||||
TSecretScanningDataSourceOption[],
|
||||
ReturnType<typeof secretScanningV2Keys.dataSourceOptions>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.dataSourceOptions(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListSecretScanningDataSourceOptions>(
|
||||
"/api/v2/secret-scanning/data-sources/options"
|
||||
);
|
||||
|
||||
return data.dataSourceOptions;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListSecretScanningDataSources = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningDataSourceWithDetails[],
|
||||
unknown,
|
||||
TSecretScanningDataSourceWithDetails[],
|
||||
ReturnType<typeof secretScanningV2Keys.listDataSources>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.listDataSources(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListSecretScanningDataSources>(
|
||||
"/api/v2/secret-scanning/data-sources-dashboard",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data.dataSources;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSecretScanningDataSource = (
|
||||
{ dataSourceId, type }: TGetSecretScanningDataSource,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningDataSource,
|
||||
unknown,
|
||||
TSecretScanningDataSource,
|
||||
ReturnType<typeof secretScanningV2Keys.dataSourceById>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.dataSourceById(dataSourceId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TSecretScanningDataSourceResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}`
|
||||
);
|
||||
|
||||
return data.dataSource;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListSecretScanningResources = (
|
||||
{ dataSourceId, type }: TGetSecretScanningDataSource,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningResourceWithDetails[],
|
||||
unknown,
|
||||
TSecretScanningResourceWithDetails[],
|
||||
ReturnType<typeof secretScanningV2Keys.listResources>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.listResources(dataSourceId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListSecretScanningResourcesResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}/resources-dashboard`
|
||||
);
|
||||
|
||||
return data.resources;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListSecretScanningScans = (
|
||||
{ dataSourceId, type }: TGetSecretScanningDataSource,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningScanWithDetails[],
|
||||
unknown,
|
||||
TSecretScanningScanWithDetails[],
|
||||
ReturnType<typeof secretScanningV2Keys.listScans>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.listScans(dataSourceId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListSecretScanningScansResponse>(
|
||||
`/api/v2/secret-scanning/data-sources/${type}/${dataSourceId}/scans-dashboard`
|
||||
);
|
||||
|
||||
return data.scans;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSecretScanningUnresolvedFindingCount = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<number, unknown, number, ReturnType<typeof secretScanningV2Keys.findingCount>>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.findingCount(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetSecretScanningUnresolvedFindingsResponse>(
|
||||
"/api/v2/secret-scanning/unresolved-findings-count",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data.unresolvedFindings;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useListSecretScanningFindings = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningFinding[],
|
||||
unknown,
|
||||
TSecretScanningFinding[],
|
||||
ReturnType<typeof secretScanningV2Keys.listFindings>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.listFindings(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TListSecretScanningFindingsResponse>(
|
||||
"/api/v2/secret-scanning/findings",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data.findings;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSecretScanningConfig = (
|
||||
projectId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSecretScanningConfig,
|
||||
unknown,
|
||||
TSecretScanningConfig,
|
||||
ReturnType<typeof secretScanningV2Keys.configByProjectId>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretScanningV2Keys.configByProjectId(projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetSecretScanningConfigResponse>(
|
||||
"/api/v2/secret-scanning/configs",
|
||||
{ params: { projectId } }
|
||||
);
|
||||
|
||||
return data.config;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { SecretScanningDataSource } from "../enums";
|
||||
import { TSecretScanningDataSourceBase } from "./shared";
|
||||
|
||||
export type TGitHubDataSource = TSecretScanningDataSourceBase & {
|
||||
type: SecretScanningDataSource.GitHub;
|
||||
config: {
|
||||
includeRepos: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TGitHubDataSourceOption = {
|
||||
name: string;
|
||||
type: SecretScanningDataSource.GitHub;
|
||||
connection: AppConnection.GitHub;
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
SecretScanningFindingSeverity,
|
||||
SecretScanningFindingStatus,
|
||||
SecretScanningResource,
|
||||
SecretScanningScanStatus,
|
||||
SecretScanningScanType
|
||||
} from "../enums";
|
||||
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
|
||||
|
||||
export type TSecretScanningDataSource = TGitHubDataSource;
|
||||
|
||||
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
|
||||
lastScannedAt: string | null;
|
||||
lastScanStatus: SecretScanningScanStatus | null;
|
||||
lastScanStatusMessage: string | null;
|
||||
unresolvedFindings: number | null;
|
||||
};
|
||||
|
||||
export type TListSecretScanningDataSources = {
|
||||
dataSources: TSecretScanningDataSourceWithDetails[];
|
||||
};
|
||||
|
||||
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption;
|
||||
|
||||
export type TListSecretScanningDataSourceOptions = {
|
||||
dataSourceOptions: TSecretScanningDataSourceOption[];
|
||||
};
|
||||
|
||||
export type TSecretScanningDataSourceResponse = { dataSource: TSecretScanningDataSource };
|
||||
|
||||
export type TCreateSecretScanningDataSourceDTO = DiscriminativePick<
|
||||
TSecretScanningDataSource,
|
||||
"name" | "config" | "description" | "connectionId" | "type" | "isAutoScanEnabled" | "projectId"
|
||||
>;
|
||||
|
||||
export type TUpdateSecretScanningDataSourceDTO = Partial<
|
||||
Omit<TCreateSecretScanningDataSourceDTO, "type" | "connectionId" | "projectId">
|
||||
> & {
|
||||
type: SecretScanningDataSource;
|
||||
dataSourceId: string;
|
||||
// required for query invalidation
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TDeleteSecretScanningDataSourceDTO = {
|
||||
type: SecretScanningDataSource;
|
||||
dataSourceId: string;
|
||||
// required for query invalidation
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TTriggerSecretScanningDataSourceDTO = {
|
||||
type: SecretScanningDataSource;
|
||||
dataSourceId: string;
|
||||
resourceId?: string;
|
||||
// required for query invalidation
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TUpdateSecretScanningFinding = {
|
||||
findingId: string;
|
||||
status: SecretScanningFindingStatus;
|
||||
remarks?: string | null;
|
||||
// required for query invalidation
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TSecretScanningFindingResponse = {
|
||||
finding: TSecretScanningFinding;
|
||||
};
|
||||
|
||||
export type TGetSecretScanningDataSource = {
|
||||
dataSourceId: string;
|
||||
type: SecretScanningDataSource;
|
||||
};
|
||||
|
||||
export type TSecretScanningResourceWithDetails = {
|
||||
id: string;
|
||||
dataSourceId: string;
|
||||
type: SecretScanningResource;
|
||||
externalId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastScannedAt: string;
|
||||
lastScanStatus: SecretScanningScanStatus;
|
||||
lastScanStatusMessage: string | null;
|
||||
unresolvedFindings: number;
|
||||
};
|
||||
|
||||
export type TListSecretScanningResourcesResponse = {
|
||||
resources: TSecretScanningResourceWithDetails[];
|
||||
};
|
||||
|
||||
export type TSecretScanningScanWithDetails = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
resourceId: string;
|
||||
type: SecretScanningScanType;
|
||||
status: SecretScanningScanStatus;
|
||||
statusMessage?: string | null;
|
||||
unresolvedFindings: number;
|
||||
resolvedFindings: number;
|
||||
resourceName: string;
|
||||
};
|
||||
|
||||
export type TListSecretScanningScansResponse = {
|
||||
scans: TSecretScanningScanWithDetails[];
|
||||
};
|
||||
|
||||
export type TGetSecretScanningUnresolvedFindingsResponse = {
|
||||
unresolvedFindings: number;
|
||||
};
|
||||
|
||||
export type TSecretScanningConfig = {
|
||||
projectId: string;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
export type TGetSecretScanningConfigResponse = {
|
||||
config: TSecretScanningConfig;
|
||||
};
|
||||
|
||||
export type TSecretScanningFinding = {
|
||||
id: string;
|
||||
dataSourceName: string;
|
||||
dataSourceType: SecretScanningDataSource;
|
||||
resourceName: string;
|
||||
resourceType: SecretScanningResource;
|
||||
rule: string;
|
||||
severity: SecretScanningFindingSeverity;
|
||||
status: SecretScanningFindingStatus;
|
||||
remarks?: string;
|
||||
fingerprint: string;
|
||||
// TODO scott: this will need to be type differentiated once we add other scan types
|
||||
details: {
|
||||
description: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
file: string;
|
||||
link: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
};
|
||||
projectId: string;
|
||||
scanId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TListSecretScanningFindingsResponse = {
|
||||
findings: TSecretScanningFinding[];
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./secret-scanning-data-source-base";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
export type TSecretScanningDataSourceBase = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
connectionId?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
projectId: string;
|
||||
connection?: {
|
||||
app: AppConnection;
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
isAutoScanEnabled: boolean;
|
||||
isDisconnected: boolean;
|
||||
};
|
||||
@@ -50,6 +50,7 @@ export type SubscriptionPlan = {
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: boolean;
|
||||
kmip: boolean;
|
||||
secretScanning: boolean;
|
||||
enterpriseSecretSyncs: boolean;
|
||||
enterpriseAppConnections: boolean;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,8 @@ export enum ProjectType {
|
||||
SecretManager = "secret-manager",
|
||||
CertificateManager = "cert-manager",
|
||||
KMS = "kms",
|
||||
SSH = "ssh"
|
||||
SSH = "ssh",
|
||||
SecretScanning = "secret-scanning"
|
||||
}
|
||||
|
||||
export enum ProjectUserMembershipTemporaryMode {
|
||||
|
||||
@@ -6,16 +6,18 @@ import { useDebounce } from "@app/hooks/useDebounce";
|
||||
export const usePagination = <T extends string>(
|
||||
initialOrderBy: T,
|
||||
{
|
||||
initPerPage = 100
|
||||
initPerPage = 100,
|
||||
initSearch = ""
|
||||
}: {
|
||||
initPerPage?: number;
|
||||
initSearch?: string;
|
||||
} = {}
|
||||
) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(initPerPage);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState<T>(initialOrderBy);
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState(initSearch);
|
||||
const [debouncedSearch] = useDebounce(search);
|
||||
|
||||
const offset = (page - 1) * perPage;
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faArrowLeft, faInfo, faMobile, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Outlet, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
|
||||
import { AdminSidebar } from "./Sidebar";
|
||||
|
||||
export const AdminLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useServerConfig();
|
||||
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
|
||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
return (
|
||||
@@ -30,54 +26,11 @@ export const AdminLayout = () => {
|
||||
<div className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden md:flex`}>
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="flex-grow">
|
||||
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{envConfig.PLATFORM_VERSION && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {envConfig.PLATFORM_VERSION}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||
<AdminSidebar />
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 px-4 pb-4 dark:[color-scheme:dark]">
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { faArrowLeft, faInfo, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Menu,
|
||||
MenuGroup,
|
||||
MenuItem
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar";
|
||||
|
||||
const generalTabs = [
|
||||
{
|
||||
label: "General",
|
||||
icon: "settings-cog",
|
||||
link: "/admin/"
|
||||
},
|
||||
{
|
||||
label: "Encryption",
|
||||
icon: "lock-closed",
|
||||
link: "/admin/encryption"
|
||||
},
|
||||
{
|
||||
label: "Authentication",
|
||||
icon: "check",
|
||||
link: "/admin/authentication"
|
||||
},
|
||||
{
|
||||
label: "Integrations",
|
||||
icon: "sliding-carousel",
|
||||
link: "/admin/integrations"
|
||||
},
|
||||
{
|
||||
label: "Caching",
|
||||
icon: "note",
|
||||
link: "/admin/caching"
|
||||
}
|
||||
];
|
||||
|
||||
const resourceTabs = [
|
||||
{
|
||||
label: "Organizations",
|
||||
icon: "groups",
|
||||
link: "/admin/resources/organizations"
|
||||
},
|
||||
{
|
||||
label: "User Identities",
|
||||
icon: "user",
|
||||
link: "/admin/resources/user-identities"
|
||||
},
|
||||
{
|
||||
label: "Machine Identities",
|
||||
icon: "key-user",
|
||||
link: "/admin/resources/machine-identities"
|
||||
}
|
||||
];
|
||||
|
||||
export const AdminSidebar = () => {
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
return (
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="flex-grow">
|
||||
<Link to={`/organization/${ProjectType.SecretManager}/overview` as const}>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
</div>
|
||||
</Link>
|
||||
<Menu>
|
||||
<MenuGroup title="General">
|
||||
{generalTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
|
||||
{tab.label}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Resources">
|
||||
{resourceTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)} icon={tab.icon}>
|
||||
{tab.label}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="relative mb-4 mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{envConfig.PLATFORM_VERSION && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {envConfig.PLATFORM_VERSION}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -55,16 +55,15 @@ export const OrganizationLayout = () => {
|
||||
linkOptions({ to: "/organization/kms/overview" }).to,
|
||||
linkOptions({ to: "/organization/kms/settings" }).to,
|
||||
linkOptions({ to: "/organization/ssh/overview" }).to,
|
||||
linkOptions({ to: "/organization/ssh/settings" }).to
|
||||
linkOptions({ to: "/organization/ssh/settings" }).to,
|
||||
linkOptions({ to: "/organization/secret-scanning/overview" }).to,
|
||||
linkOptions({ to: "/organization/secret-scanning/settings" }).to
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const shouldShowOrgSidebar =
|
||||
location.pathname.startsWith("/organization") &&
|
||||
(!isSecretSharingPage || shouldShowProductsSidebar) &&
|
||||
!([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes(
|
||||
location.pathname
|
||||
);
|
||||
(!isSecretSharingPage || shouldShowProductsSidebar);
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user