mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 04:14:38 +00:00
Merge branch 'main' into feat/remindersImprovement
This commit is contained in:
@@ -5,9 +5,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { initProjectHelper } from "@app/helpers/project";
|
||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
@@ -16,7 +15,7 @@ import { onRequestError } from "@app/hooks/api/reactQuery";
|
||||
import InputField from "../basic/InputField";
|
||||
import checkPassword from "../utilities/checks/password/checkPassword";
|
||||
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "../utilities/cryptography/crypto";
|
||||
import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto";
|
||||
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
|
||||
import SecurityClient from "../utilities/SecurityClient";
|
||||
import { Button, Input } from "../v2";
|
||||
@@ -77,6 +76,7 @@ export default function UserInfoStep({
|
||||
}: UserInfoStepProps): JSX.Element {
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [organizationNameError, setOrganizationNameError] = useState(false);
|
||||
const { config } = useServerConfig();
|
||||
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
|
||||
@@ -109,12 +109,9 @@ export default function UserInfoStep({
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
const pair = await generateKeyPair(config.fipsEnabled);
|
||||
|
||||
localStorage.setItem("PRIVATE_KEY", pair.privateKey);
|
||||
|
||||
client.init(
|
||||
{
|
||||
@@ -145,7 +142,7 @@ export default function UserInfoStep({
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
text: pair.privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
@@ -168,7 +165,7 @@ export default function UserInfoStep({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
publicKey: pair.publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
@@ -189,11 +186,11 @@ export default function UserInfoStep({
|
||||
}
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
publicKey: pair.publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
privateKey
|
||||
privateKey: pair.privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function NavHeader({
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all",
|
||||
"text-sm transition-all",
|
||||
isHoveringCopyButton ? "text-bunker-200" : "text-bunker-300"
|
||||
)}
|
||||
>
|
||||
@@ -198,7 +198,7 @@ export default function NavHeader({
|
||||
}}
|
||||
search={(query) => ({ ...query, secretPath: newSecretPath })}
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all hover:text-primary",
|
||||
"text-sm transition-all hover:text-primary",
|
||||
isHoveringCopyButton ? "text-primary" : "text-primary/80"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const SecretDashboardPathBreadcrumb = ({
|
||||
<div className="group flex items-center space-x-2">
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all",
|
||||
"text-sm transition-all",
|
||||
isCopying ? "text-bunker-200" : "text-bunker-300"
|
||||
)}
|
||||
>
|
||||
@@ -77,7 +77,7 @@ export const SecretDashboardPathBreadcrumb = ({
|
||||
}}
|
||||
search={(query) => ({ ...query, secretPath: newSecretPath })}
|
||||
className={twMerge(
|
||||
"text-sm font-semibold transition-all hover:text-primary",
|
||||
"text-sm transition-all hover:text-primary",
|
||||
isCopying && "text-primary"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -63,6 +63,7 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
const { image, name } = SECRET_SYNC_MAP[destination];
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseSecretSyncs
|
||||
|
||||
@@ -60,7 +60,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
|
||||
? undefined
|
||||
: SecretSyncInitialSyncBehavior.OverwriteDestination
|
||||
}
|
||||
},
|
||||
} as Partial<TSecretSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import {
|
||||
TChecklyAccount,
|
||||
useChecklyConnectionListAccounts
|
||||
} from "@app/hooks/api/appConnections/checkly";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const ChecklySyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.Checkly }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
|
||||
const { data: accounts = [], isPending: isAccountsLoading } = useChecklyConnectionListAccounts(
|
||||
connectionId,
|
||||
{
|
||||
enabled: Boolean(connectionId)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.accountId", "");
|
||||
setValue("destinationConfig.accountName", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.accountId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Select an account"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isAccountsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={accounts.find((p) => p.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TChecklyAccount>;
|
||||
onChange(v?.id ?? null);
|
||||
setValue("destinationConfig.accountName", v?.name ?? "");
|
||||
}}
|
||||
options={accounts}
|
||||
placeholder="Select an account..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { SingleValue } from "react-select";
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
TCloudflareProject,
|
||||
TCloudflarePagesProject,
|
||||
useCloudflareConnectionListPagesProjects
|
||||
} from "@app/hooks/api/appConnections/cloudflare";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
@@ -52,7 +52,7 @@ export const CloudflarePagesSyncFields = () => {
|
||||
isDisabled={!connectionId}
|
||||
value={projects ? (projects.find((project) => project.name === value) ?? []) : []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TCloudflareProject>)?.name ?? null);
|
||||
onChange((option as SingleValue<TCloudflarePagesProject>)?.name ?? null);
|
||||
}}
|
||||
options={projects}
|
||||
placeholder="Select a project..."
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import {
|
||||
TCloudflareWorkersScript,
|
||||
useCloudflareConnectionListWorkersScripts
|
||||
} from "@app/hooks/api/appConnections/cloudflare";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const CloudflareWorkersSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.CloudflareWorkers }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
|
||||
const { data: scripts = [], isPending: isScriptsPending } =
|
||||
useCloudflareConnectionListWorkersScripts(connectionId, {
|
||||
enabled: Boolean(connectionId)
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.scriptId", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.scriptId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Worker Script"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isScriptsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={scripts?.find((script) => script.id === value) || []}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TCloudflareWorkersScript>)?.id ?? null);
|
||||
}}
|
||||
options={scripts}
|
||||
placeholder="Select a worker script..."
|
||||
getOptionLabel={(option) => option.id}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+9
@@ -10,7 +10,9 @@ import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFiel
|
||||
import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields";
|
||||
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
|
||||
import { CamundaSyncFields } from "./CamundaSyncFields";
|
||||
import { ChecklySyncFields } from "./ChecklySyncFields";
|
||||
import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields";
|
||||
import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields";
|
||||
import { DatabricksSyncFields } from "./DatabricksSyncFields";
|
||||
import { FlyioSyncFields } from "./FlyioSyncFields";
|
||||
import { GcpSyncFields } from "./GcpSyncFields";
|
||||
@@ -22,6 +24,7 @@ import { HumanitecSyncFields } from "./HumanitecSyncFields";
|
||||
import { OCIVaultSyncFields } from "./OCIVaultSyncFields";
|
||||
import { RailwaySyncFields } from "./RailwaySyncFields";
|
||||
import { RenderSyncFields } from "./RenderSyncFields";
|
||||
import { SupabaseSyncFields } from "./SupabaseSyncFields";
|
||||
import { TeamCitySyncFields } from "./TeamCitySyncFields";
|
||||
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
|
||||
import { VercelSyncFields } from "./VercelSyncFields";
|
||||
@@ -78,10 +81,16 @@ export const SecretSyncDestinationFields = () => {
|
||||
return <GitLabSyncFields />;
|
||||
case SecretSync.CloudflarePages:
|
||||
return <CloudflarePagesSyncFields />;
|
||||
case SecretSync.CloudflareWorkers:
|
||||
return <CloudflareWorkersSyncFields />;
|
||||
case SecretSync.Zabbix:
|
||||
return <ZabbixSyncFields />;
|
||||
case SecretSync.Railway:
|
||||
return <RailwaySyncFields />;
|
||||
case SecretSync.Checkly:
|
||||
return <ChecklySyncFields />;
|
||||
case SecretSync.Supabase:
|
||||
return <SupabaseSyncFields />;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import {
|
||||
TSupabaseProject,
|
||||
useSupabaseConnectionListProjects
|
||||
} from "@app/hooks/api/appConnections/supabase";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const SupabaseSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.Supabase }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
|
||||
const { data: projects = [], isPending: isProjectsLoading } = useSupabaseConnectionListProjects(
|
||||
connectionId,
|
||||
{
|
||||
enabled: Boolean(connectionId)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.projectName", "");
|
||||
setValue("destinationConfig.projectId", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.projectId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Select a project"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isProjectsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={projects.find((p) => p.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TSupabaseProject>;
|
||||
onChange(v?.id ?? null);
|
||||
setValue("destinationConfig.projectName", v?.name ?? "");
|
||||
}}
|
||||
options={projects}
|
||||
placeholder="Select project..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
@@ -58,8 +58,11 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.Flyio:
|
||||
case SecretSync.GitLab:
|
||||
case SecretSync.CloudflarePages:
|
||||
case SecretSync.CloudflareWorkers:
|
||||
case SecretSync.Zabbix:
|
||||
case SecretSync.Railway:
|
||||
case SecretSync.Checkly:
|
||||
case SecretSync.Supabase:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
+3
-1
@@ -11,7 +11,9 @@ export const AzureDevOpsSyncReviewFields = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
|
||||
{devopsProjectName && (
|
||||
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
|
||||
)}
|
||||
<GenericFieldLabel label="Project ID">{devopsProjectId}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const ChecklySyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Checkly }>();
|
||||
const accountName = watch("destinationConfig.accountName");
|
||||
|
||||
return <GenericFieldLabel label="Account">{accountName}</GenericFieldLabel>;
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const CloudflareWorkersSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.CloudflareWorkers }
|
||||
>();
|
||||
const scriptId = watch("destinationConfig.scriptId");
|
||||
|
||||
return <GenericFieldLabel label="Script">{scriptId}</GenericFieldLabel>;
|
||||
};
|
||||
+12
@@ -19,7 +19,9 @@ import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSy
|
||||
import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields";
|
||||
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
|
||||
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
|
||||
import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields";
|
||||
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
|
||||
import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields";
|
||||
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
|
||||
import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields";
|
||||
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
|
||||
@@ -32,6 +34,7 @@ import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
|
||||
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
|
||||
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
|
||||
import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
|
||||
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
|
||||
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
|
||||
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
|
||||
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
|
||||
@@ -126,12 +129,21 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.CloudflarePages:
|
||||
DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.CloudflareWorkers:
|
||||
DestinationFieldsComponent = <CloudflareWorkersSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Zabbix:
|
||||
DestinationFieldsComponent = <ZabbixSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Railway:
|
||||
DestinationFieldsComponent = <RailwaySyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Checkly:
|
||||
DestinationFieldsComponent = <ChecklySyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Supabase:
|
||||
DestinationFieldsComponent = <SupabaseSyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const SupabaseSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Supabase }>();
|
||||
const projectName = watch("destinationConfig.projectName");
|
||||
|
||||
return <GenericFieldLabel label="Project">{projectName}</GenericFieldLabel>;
|
||||
};
|
||||
+1
-4
@@ -8,10 +8,7 @@ export const AzureDevOpsSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
destination: z.literal(SecretSync.AzureDevOps),
|
||||
destinationConfig: z.object({
|
||||
devopsProjectId: z.string().trim().min(1, { message: "Azure DevOps Project ID is required" }),
|
||||
devopsProjectName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Azure DevOps Project Name is required" })
|
||||
devopsProjectName: z.string().trim().optional()
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const ChecklySyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Checkly),
|
||||
destinationConfig: z.object({
|
||||
accountId: z.string(),
|
||||
accountName: z.string()
|
||||
})
|
||||
})
|
||||
);
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const CloudflareWorkersSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.CloudflareWorkers),
|
||||
destinationConfig: z.object({
|
||||
scriptId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Script ID is required")
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Invalid script ID format")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -7,7 +7,9 @@ import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configur
|
||||
import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema";
|
||||
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
|
||||
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
|
||||
import { ChecklySyncDestinationSchema } from "./checkly-sync-destination-schema";
|
||||
import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema";
|
||||
import { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema";
|
||||
import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
|
||||
import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema";
|
||||
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
|
||||
@@ -19,6 +21,7 @@ import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-sch
|
||||
import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema";
|
||||
import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema";
|
||||
import { RenderSyncDestinationSchema } from "./render-sync-destination-schema";
|
||||
import { SupabaseSyncDestinationSchema } from "./supabase-sync-destination-schema";
|
||||
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
|
||||
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
|
||||
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
|
||||
@@ -48,8 +51,11 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
FlyioSyncDestinationSchema,
|
||||
GitlabSyncDestinationSchema,
|
||||
CloudflarePagesSyncDestinationSchema,
|
||||
CloudflareWorkersSyncDestinationSchema,
|
||||
SupabaseSyncDestinationSchema,
|
||||
ZabbixSyncDestinationSchema,
|
||||
RailwaySyncDestinationSchema
|
||||
RailwaySyncDestinationSchema,
|
||||
ChecklySyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const SupabaseSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Supabase),
|
||||
destinationConfig: z.object({
|
||||
projectId: z.string().max(255).min(1, "Project ID is required"),
|
||||
projectName: z.string().max(255).min(1, "Project Name is required")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -2,48 +2,55 @@
|
||||
// eslint-disable-next-line
|
||||
import argon2 from "argon2-browser/dist/argon2-bundled.min.js";
|
||||
import nacl from "tweetnacl";
|
||||
import { decodeBase64, decodeUTF8, encodeBase64, encodeUTF8 } from "tweetnacl-util";
|
||||
|
||||
import { encodeBase64 as naclEncodeBase64 } from "tweetnacl-util";
|
||||
import aes from "./aes-256-gcm";
|
||||
|
||||
/**
|
||||
* Return new base64, NaCl, public-private key pair.
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||
*/
|
||||
const generateKeyPair = () => {
|
||||
const encodeBase64 = (uint8Array: Uint8Array) => btoa(String.fromCharCode(...uint8Array));
|
||||
const decodeBase64 = (base64String: string) =>
|
||||
new Uint8Array([...atob(base64String)].map((c) => c.charCodeAt(0)));
|
||||
|
||||
const generateKeyPair = async (fipsEnabled: boolean) => {
|
||||
if (fipsEnabled) {
|
||||
if (!crypto || !crypto.subtle) {
|
||||
throw new Error("Web Crypto API not available");
|
||||
}
|
||||
|
||||
// browser version of how the key format we expect on the backend for asymmetric encryption
|
||||
const result = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: "X25519"
|
||||
},
|
||||
true, // extractable
|
||||
["deriveKey", "deriveBits"]
|
||||
);
|
||||
|
||||
// Type guard
|
||||
if (!("publicKey" in result)) {
|
||||
throw new Error("Expected CryptoKeyPair but got CryptoKey");
|
||||
}
|
||||
|
||||
const keyPair = result as CryptoKeyPair;
|
||||
const publicKeyBytes = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
||||
const privateKeyBytes = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
|
||||
|
||||
return {
|
||||
publicKey: btoa(String.fromCharCode(...new Uint8Array(publicKeyBytes))),
|
||||
privateKey: btoa(String.fromCharCode(...new Uint8Array(privateKeyBytes)))
|
||||
};
|
||||
}
|
||||
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = naclEncodeBase64(secretKeyUint8Array);
|
||||
const publicKey = naclEncodeBase64(publicKeyUint8Array);
|
||||
|
||||
return {
|
||||
publicKey: encodeBase64(pair.publicKey),
|
||||
privateKey: encodeBase64(pair.secretKey)
|
||||
publicKey,
|
||||
privateKey
|
||||
};
|
||||
};
|
||||
|
||||
type EncryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify that private key [privateKey] is the one that corresponds to
|
||||
* the public key [publicKey]
|
||||
* @param {Object}
|
||||
* @param {String} - base64-encoded Nacl private key
|
||||
* @param {String} - base64-encoded Nacl public key
|
||||
*/
|
||||
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
|
||||
const derivedPublicKey = encodeBase64(
|
||||
nacl.box.keyPair.fromSecretKey(decodeBase64(privateKey)).publicKey
|
||||
);
|
||||
|
||||
if (derivedPublicKey !== publicKey) {
|
||||
throw new Error("Failed to verify private key");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive a key from password [password] and salt [salt] using Argon2id
|
||||
* @param {Object} obj
|
||||
@@ -88,71 +95,6 @@ const deriveArgonKey = async ({
|
||||
return derivedKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
* [publicKey] likely belongs to the recipient.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
* @param {String} obj.publicKey - public key of the recipient
|
||||
* @param {String} obj.privateKey - private key of the sender (current user)
|
||||
* @returns {Object} obj
|
||||
* @returns {String} ciphertext - base64-encoded ciphertext
|
||||
* @returns {String} nonce - base64-encoded nonce
|
||||
*/
|
||||
const encryptAssymmetric = ({
|
||||
plaintext,
|
||||
publicKey,
|
||||
privateKey
|
||||
}: EncryptAsymmetricProps): {
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
} => {
|
||||
const nonce = nacl.randomBytes(24);
|
||||
const ciphertext = nacl.box(
|
||||
decodeUTF8(plaintext),
|
||||
nonce,
|
||||
decodeBase64(publicKey),
|
||||
decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
return {
|
||||
ciphertext: encodeBase64(ciphertext),
|
||||
nonce: encodeBase64(nonce)
|
||||
};
|
||||
};
|
||||
|
||||
type DecryptAsymmetricProps = {
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
|
||||
* [privateKey] likely belongs to the recipient.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.nonce - nonce
|
||||
* @param {String} obj.publicKey - base64-encoded public key of the sender
|
||||
* @param {String} obj.privateKey - base64-encoded private key of the receiver (current user)
|
||||
*/
|
||||
const decryptAssymmetric = ({
|
||||
ciphertext,
|
||||
nonce,
|
||||
publicKey,
|
||||
privateKey
|
||||
}: DecryptAsymmetricProps): string => {
|
||||
const plaintext = nacl.box.open(
|
||||
decodeBase64(ciphertext),
|
||||
decodeBase64(nonce),
|
||||
decodeBase64(publicKey),
|
||||
decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
return encodeUTF8(plaintext!);
|
||||
};
|
||||
|
||||
type EncryptSymmetricProps = {
|
||||
plaintext: string;
|
||||
key: string;
|
||||
@@ -226,11 +168,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
|
||||
};
|
||||
|
||||
export {
|
||||
decryptAssymmetric,
|
||||
decodeBase64,
|
||||
decryptSymmetric,
|
||||
deriveArgonKey,
|
||||
encryptAssymmetric,
|
||||
encodeBase64,
|
||||
encryptSymmetric,
|
||||
generateKeyPair,
|
||||
verifyPrivateKey
|
||||
generateKeyPair
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from "react";
|
||||
import { faEllipsis, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, ReactNode } from "@tanstack/react-router";
|
||||
import { LinkComponentProps } from "node_modules/@tanstack/react-router/dist/esm/link";
|
||||
@@ -27,7 +27,7 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWi
|
||||
<ol
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-bunker-100 sm:gap-2.5",
|
||||
"flex flex-wrap items-center break-words text-sm text-bunker-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -56,7 +56,7 @@ const BreadcrumbLink = React.forwardRef<
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={twMerge("transition-colors hover:text-primary-400", className)}
|
||||
className={twMerge("transition-colors hover:text-primary", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -79,7 +79,7 @@ BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
||||
<li role="presentation" aria-hidden="true" className={twMerge("", className)} {...props}>
|
||||
{children ?? <p className="px-2 text-lg text-mineshaft-400/70">/</p>}
|
||||
{children ?? <p className="px-3 text-lg text-mineshaft-400/70">/</p>}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
@@ -121,7 +121,7 @@ export type TBreadcrumbFormat =
|
||||
};
|
||||
|
||||
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
|
||||
<div className="mx-auto max-w-7xl py-4 text-white">
|
||||
<div className="mx-auto max-w-7xl text-white">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
|
||||
@@ -134,12 +134,17 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbSegment className="rounded-md px-2 py-1 py-2 hover:bg-mineshaft-600">
|
||||
{el.label} <FontAwesomeIcon icon={faSort} size="sm" />
|
||||
<BreadcrumbSegment className="rounded-md py-1 py-2">
|
||||
{el.label}{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faCaretDown}
|
||||
size="sm"
|
||||
className="ml-2 text-bunker-300"
|
||||
/>
|
||||
</BreadcrumbSegment>
|
||||
</BreadcrumbItem>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
<DropdownMenuContent side="bottom" sideOffset={8} align="start">
|
||||
{el?.dropdownTitle && <DropdownMenuLabel>{el.dropdownTitle}</DropdownMenuLabel>}
|
||||
{el.links.map((i, dropIndex) => (
|
||||
<Link
|
||||
|
||||
@@ -42,7 +42,7 @@ export const Checkbox = ({
|
||||
className={twMerge(
|
||||
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
|
||||
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
||||
isChecked && "border-primary/30 bg-primary/10",
|
||||
isChecked && "border-primary/50 bg-primary/30",
|
||||
Boolean(children) && "mr-3",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -29,10 +29,6 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
|
||||
)
|
||||
},
|
||||
SsoPage: setRoute(
|
||||
"/organization/sso",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
|
||||
),
|
||||
SecretSharing: setRoute(
|
||||
"/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
|
||||
|
||||
@@ -41,10 +41,12 @@ import {
|
||||
ZabbixConnectionMethod
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
|
||||
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
|
||||
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
||||
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
|
||||
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
|
||||
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
|
||||
import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<
|
||||
AppConnection,
|
||||
@@ -94,7 +96,9 @@ export const APP_CONNECTION_MAP: Record<
|
||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
||||
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }
|
||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
||||
[AppConnection.Checkly]: { name: "Checkly", image: "Checkly.png" },
|
||||
[AppConnection.Supabase]: { name: "Supabase", image: "Supabase.png" }
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
@@ -149,13 +153,16 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case HerokuConnectionMethod.AuthToken:
|
||||
return { name: "Auth Token", icon: faKey };
|
||||
case RailwayConnectionMethod.AccountToken:
|
||||
case SupabaseConnectionMethod.AccessToken:
|
||||
return { name: "Account Token", icon: faKey };
|
||||
case RailwayConnectionMethod.TeamToken:
|
||||
return { name: "Team Token", icon: faKey };
|
||||
case RailwayConnectionMethod.ProjectToken:
|
||||
return { name: "Project Token", icon: faKey };
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return { name: "API Key", icon: faKey };
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
||||
name: "Cloudflare Pages",
|
||||
image: "Cloudflare.png"
|
||||
},
|
||||
[SecretSync.CloudflareWorkers]: {
|
||||
name: "Cloudflare Workers",
|
||||
image: "Cloudflare.png"
|
||||
},
|
||||
[SecretSync.Zabbix]: {
|
||||
name: "Zabbix",
|
||||
image: "Zabbix.png"
|
||||
@@ -89,6 +93,14 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
||||
[SecretSync.Railway]: {
|
||||
name: "Railway",
|
||||
image: "Railway.png"
|
||||
},
|
||||
[SecretSync.Checkly]: {
|
||||
name: "Checkly",
|
||||
image: "Checkly.png"
|
||||
},
|
||||
[SecretSync.Supabase]: {
|
||||
name: "Supabase",
|
||||
image: "Supabase.png"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,8 +127,11 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.GitLab]: AppConnection.Gitlab,
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
|
||||
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
|
||||
[SecretSync.Supabase]: AppConnection.Supabase,
|
||||
[SecretSync.Zabbix]: AppConnection.Zabbix,
|
||||
[SecretSync.Railway]: AppConnection.Railway
|
||||
[SecretSync.Railway]: AppConnection.Railway,
|
||||
[SecretSync.Checkly]: AppConnection.Checkly
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||
|
||||
@@ -49,6 +49,7 @@ export type TServerConfig = {
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
invalidatingCache: boolean;
|
||||
fipsEnabled: boolean;
|
||||
envOverrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TChecklyAccount } from "./types";
|
||||
|
||||
const checklyConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "checkly"] as const,
|
||||
listAccounts: (connectionId: string) =>
|
||||
[...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const
|
||||
};
|
||||
|
||||
export const useChecklyConnectionListAccounts = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TChecklyAccount[],
|
||||
unknown,
|
||||
TChecklyAccount[],
|
||||
ReturnType<typeof checklyConnectionKeys.listAccounts>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: checklyConnectionKeys.listAccounts(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ accounts: TChecklyAccount[] }>(
|
||||
`/api/v1/app-connections/checkly/${connectionId}/accounts`
|
||||
);
|
||||
|
||||
return data.accounts;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TChecklyAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
runtimeId: string;
|
||||
};
|
||||
@@ -3,21 +3,23 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TCloudflareProject } from "./types";
|
||||
import { TCloudflarePagesProject, TCloudflareWorkersScript } from "./types";
|
||||
|
||||
const cloudflareConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "cloudflare"] as const,
|
||||
listPagesProjects: (connectionId: string) =>
|
||||
[...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const
|
||||
[...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const,
|
||||
listWorkersScripts: (connectionId: string) =>
|
||||
[...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const
|
||||
};
|
||||
|
||||
export const useCloudflareConnectionListPagesProjects = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TCloudflareProject[],
|
||||
TCloudflarePagesProject[],
|
||||
unknown,
|
||||
TCloudflareProject[],
|
||||
TCloudflarePagesProject[],
|
||||
ReturnType<typeof cloudflareConnectionKeys.listPagesProjects>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
@@ -26,7 +28,7 @@ export const useCloudflareConnectionListPagesProjects = (
|
||||
return useQuery({
|
||||
queryKey: cloudflareConnectionKeys.listPagesProjects(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TCloudflareProject[]>(
|
||||
const { data } = await apiRequest.get<TCloudflarePagesProject[]>(
|
||||
`/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-pages-projects`
|
||||
);
|
||||
|
||||
@@ -35,3 +37,28 @@ export const useCloudflareConnectionListPagesProjects = (
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useCloudflareConnectionListWorkersScripts = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TCloudflareWorkersScript[],
|
||||
unknown,
|
||||
TCloudflareWorkersScript[],
|
||||
ReturnType<typeof cloudflareConnectionKeys.listWorkersScripts>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: cloudflareConnectionKeys.listWorkersScripts(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TCloudflareWorkersScript[]>(
|
||||
`/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-workers-scripts`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export type TCloudflareProject = {
|
||||
export type TCloudflarePagesProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TCloudflareWorkersScript = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -30,5 +30,7 @@ export enum AppConnection {
|
||||
Cloudflare = "cloudflare",
|
||||
Bitbucket = "bitbucket",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway"
|
||||
Railway = "railway",
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TSupabaseProject } from "./types";
|
||||
|
||||
const supabaseConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "supabase"] as const,
|
||||
listProjects: (connectionId: string) =>
|
||||
[...supabaseConnectionKeys.all, "workspace-scopes", connectionId] as const
|
||||
};
|
||||
|
||||
export const useSupabaseConnectionListProjects = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TSupabaseProject[],
|
||||
unknown,
|
||||
TSupabaseProject[],
|
||||
ReturnType<typeof supabaseConnectionKeys.listProjects>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: supabaseConnectionKeys.listProjects(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ projects: TSupabaseProject[] }>(
|
||||
`/api/v1/app-connections/supabase/${connectionId}/projects`
|
||||
);
|
||||
|
||||
return data.projects;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type TSupabaseProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -144,6 +144,14 @@ export type TRailwayConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Railway;
|
||||
};
|
||||
|
||||
export type TChecklyConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Checkly;
|
||||
};
|
||||
|
||||
export type TSupabaseConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Supabase;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
@@ -174,7 +182,8 @@ export type TAppConnectionOption =
|
||||
| TCloudflareConnectionOption
|
||||
| TBitbucketConnectionOption
|
||||
| TZabbixConnectionOption
|
||||
| TRailwayConnectionOption;
|
||||
| TRailwayConnectionOption
|
||||
| TChecklyConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -209,4 +218,6 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
||||
[AppConnection.Checkly]: TChecklyConnectionOption;
|
||||
[AppConnection.Supabase]: TSupabaseConnectionOption;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum ChecklyConnectionMethod {
|
||||
ApiKey = "api-key"
|
||||
}
|
||||
|
||||
export type TChecklyConnection = TRootAppConnection & {
|
||||
app: AppConnection.Checkly;
|
||||
method: ChecklyConnectionMethod.ApiKey;
|
||||
credentials: {
|
||||
apiKey: string;
|
||||
};
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { TAzureDevOpsConnection } from "./azure-devops-connection";
|
||||
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
||||
import { TBitbucketConnection } from "./bitbucket-connection";
|
||||
import { TCamundaConnection } from "./camunda-connection";
|
||||
import { TChecklyConnection } from "./checkly-connection";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TFlyioConnection } from "./flyio-connection";
|
||||
@@ -27,6 +28,7 @@ import { TOracleDBConnection } from "./oracledb-connection";
|
||||
import { TPostgresConnection } from "./postgres-connection";
|
||||
import { TRailwayConnection } from "./railway-connection";
|
||||
import { TRenderConnection } from "./render-connection";
|
||||
import { TSupabaseConnection } from "./supabase-connection";
|
||||
import { TTeamCityConnection } from "./teamcity-connection";
|
||||
import { TTerraformCloudConnection } from "./terraform-cloud-connection";
|
||||
import { TVercelConnection } from "./vercel-connection";
|
||||
@@ -97,7 +99,9 @@ export type TAppConnection =
|
||||
| TCloudflareConnection
|
||||
| TBitbucketConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection;
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
|
||||
|
||||
@@ -157,4 +161,6 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.Bitbucket]: TBitbucketConnection;
|
||||
[AppConnection.Zabbix]: TZabbixConnection;
|
||||
[AppConnection.Railway]: TRailwayConnection;
|
||||
[AppConnection.Checkly]: TChecklyConnection;
|
||||
[AppConnection.Supabase]: TSupabaseConnection;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum SupabaseConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
|
||||
export type TSupabaseConnection = TRootAppConnection & {
|
||||
app: AppConnection.Supabase;
|
||||
method: SupabaseConnectionMethod.AccessToken;
|
||||
credentials: {
|
||||
instanceUrl?: string;
|
||||
accessKey: string;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { encodeBase64 } from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { cmekKeys } from "@app/hooks/api/cmeks/queries";
|
||||
import {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { TReactQueryOptions } from "@app/types/reactQuery";
|
||||
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { EncryptedSecret, SecretType, SecretV3RawSanitized } from "../secrets/types";
|
||||
import {
|
||||
TGetSecretApprovalRequestCount,
|
||||
TGetSecretApprovalRequestDetails,
|
||||
@@ -41,79 +35,6 @@ export const secretApprovalRequestKeys = {
|
||||
]
|
||||
};
|
||||
|
||||
export const decryptSecrets = (
|
||||
encryptedSecrets: EncryptedSecret[],
|
||||
decryptFileKey: UserWsKeyPair
|
||||
) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: decryptFileKey.encryptedKey,
|
||||
nonce: decryptFileKey.nonce,
|
||||
publicKey: decryptFileKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
const secrets: SecretV3RawSanitized[] = [];
|
||||
encryptedSecrets.forEach((encSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
const decryptedSecret: SecretV3RawSanitized = {
|
||||
id: encSecret.id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
secretValueHidden: encSecret.secretValueHidden,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
reminderRepeatDays: encSecret.secretReminderRepeatDays,
|
||||
reminderNote: encSecret.secretReminderNote,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt,
|
||||
version: encSecret.version,
|
||||
skipMultilineEncoding: encSecret.skipMultilineEncoding
|
||||
};
|
||||
|
||||
if (encSecret.type === SecretType.Personal) {
|
||||
personalSecrets[decryptedSecret.key] = {
|
||||
id: encSecret.id,
|
||||
value: secretValue
|
||||
};
|
||||
} else {
|
||||
secrets.push(decryptedSecret);
|
||||
}
|
||||
});
|
||||
|
||||
secrets.forEach((sec) => {
|
||||
if (personalSecrets?.[sec.key]) {
|
||||
sec.idOverride = personalSecrets[sec.key].id;
|
||||
sec.valueOverride = personalSecrets[sec.key].value;
|
||||
sec.overrideAction = "modified";
|
||||
}
|
||||
});
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const fetchSecretApprovalRequestList = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
|
||||
@@ -43,6 +43,7 @@ export type TSecretApprovalRequest = {
|
||||
isReplicated?: boolean;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
committerUserId: string;
|
||||
reviewers: {
|
||||
userId: string;
|
||||
|
||||
@@ -21,8 +21,11 @@ export enum SecretSync {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
CloudflarePages = "cloudflare-pages",
|
||||
CloudflareWorkers = "cloudflare-workers",
|
||||
Supabase = "supabase",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway"
|
||||
Railway = "railway",
|
||||
Checkly = "checkly"
|
||||
}
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
|
||||
@@ -6,7 +6,7 @@ export type TAzureDevOpsSync = TRootSecretSync & {
|
||||
destination: SecretSync.AzureDevOps;
|
||||
destinationConfig: {
|
||||
devopsProjectId: string;
|
||||
devopsProjectName: string;
|
||||
devopsProjectName?: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.AzureDevOps;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TChecklySync = TRootSecretSync & {
|
||||
destination: SecretSync.Checkly;
|
||||
destinationConfig: {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Checkly;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TCloudflareWorkersSync = TRootSecretSync & {
|
||||
destination: SecretSync.CloudflareWorkers;
|
||||
destinationConfig: {
|
||||
scriptId: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Cloudflare;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,9 @@ import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
|
||||
import { TAzureDevOpsSync } from "./azure-devops-sync";
|
||||
import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
|
||||
import { TCamundaSync } from "./camunda-sync";
|
||||
import { TChecklySync } from "./checkly-sync";
|
||||
import { TCloudflarePagesSync } from "./cloudflare-pages-sync";
|
||||
import { TCloudflareWorkersSync } from "./cloudflare-workers-sync";
|
||||
import { TDatabricksSync } from "./databricks-sync";
|
||||
import { TFlyioSync } from "./flyio-sync";
|
||||
import { TGcpSync } from "./gcp-sync";
|
||||
@@ -20,6 +22,7 @@ import { THerokuSync } from "./heroku-sync";
|
||||
import { THumanitecSync } from "./humanitec-sync";
|
||||
import { TOCIVaultSync } from "./oci-vault-sync";
|
||||
import { TRailwaySync } from "./railway-sync";
|
||||
import { TSupabaseSync } from "./supabase";
|
||||
import { TTeamCitySync } from "./teamcity-sync";
|
||||
import { TTerraformCloudSync } from "./terraform-cloud-sync";
|
||||
import { TVercelSync } from "./vercel-sync";
|
||||
@@ -56,8 +59,11 @@ export type TSecretSync =
|
||||
| TFlyioSync
|
||||
| TGitLabSync
|
||||
| TCloudflarePagesSync
|
||||
| TCloudflareWorkersSync
|
||||
| TZabbixSync
|
||||
| TRailwaySync;
|
||||
| TRailwaySync
|
||||
| TChecklySync
|
||||
| TSupabaseSync;
|
||||
|
||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TSupabaseSync = TRootSecretSync & {
|
||||
destination: SecretSync.Supabase;
|
||||
destinationConfig: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Supabase;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -17,8 +17,8 @@ export type SubscriptionPlan = {
|
||||
rbac: boolean;
|
||||
secretVersioning: boolean;
|
||||
slug: string;
|
||||
secretApproval: string;
|
||||
secretRotation: string;
|
||||
secretApproval: boolean;
|
||||
secretRotation: boolean;
|
||||
tier: number;
|
||||
workspaceLimit: number;
|
||||
workspacesUsed: number;
|
||||
|
||||
@@ -27,7 +27,7 @@ export * from "./secrets/types";
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||
export type { SubscriptionPlan } from "./subscriptions/types";
|
||||
export type { WsTag } from "./tags/types";
|
||||
export type { AddUserToWsDTOE2EE, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
|
||||
export type { OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
|
||||
export type { TWebhook } from "./webhooks/types";
|
||||
export type {
|
||||
CreateEnvironmentDTO,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export {
|
||||
useAddUserToWsE2EE,
|
||||
useAddUserToWsNonE2EE,
|
||||
useRemoveMyDuplicateAccounts,
|
||||
useRevokeMySessionById,
|
||||
|
||||
@@ -1,51 +1,10 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { userKeys } from "./query-keys";
|
||||
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
||||
|
||||
export const useAddUserToWsE2EE = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<object, object, AddUserToWsDTOE2EE>({
|
||||
mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: decryptKey.encryptedKey,
|
||||
nonce: decryptKey.nonce,
|
||||
publicKey: decryptKey.sender.publicKey,
|
||||
privateKey: userPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey }) => {
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: userPublicKey,
|
||||
privateKey: userPrivateKey
|
||||
});
|
||||
|
||||
return {
|
||||
orgMembershipId,
|
||||
workspaceEncryptedKey: inviteeCipherText,
|
||||
workspaceEncryptedNonce: inviteeNonce
|
||||
};
|
||||
});
|
||||
const { data } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/memberships`, {
|
||||
members: newWsMembers
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceUsers(workspaceId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
import { AddUserToWsDTONonE2EE } from "./types";
|
||||
|
||||
export const useAddUserToWsNonE2EE = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MfaMethod } from "../auth/types";
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { ProjectType, ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||
|
||||
export enum AuthMethod {
|
||||
@@ -129,16 +128,6 @@ export type TWorkspaceUser = {
|
||||
deniedPermissions: any[];
|
||||
};
|
||||
|
||||
export type AddUserToWsDTOE2EE = {
|
||||
workspaceId: string;
|
||||
decryptKey: UserWsKeyPair;
|
||||
userPrivateKey: string;
|
||||
members: {
|
||||
orgMembershipId: string;
|
||||
userPublicKey: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type AddUserToWsDTONonE2EE = {
|
||||
projectId: string;
|
||||
usernames: string[];
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useSubscription, useWorkspace } from "@app/context";
|
||||
import { useGetAccessApprovalPolicies } from "@app/hooks/api";
|
||||
|
||||
const matchesPath = (folderPath: string, pattern: string) => {
|
||||
const normalizedPath = folderPath === "/" ? "/" : folderPath.replace(/\/$/, "");
|
||||
const normalizedPattern = pattern === "/" ? "/" : pattern.replace(/\/$/, "");
|
||||
|
||||
if (normalizedPath === normalizedPattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedPattern.endsWith("/**")) {
|
||||
const basePattern = normalizedPattern.slice(0, -3); // Remove "/**"
|
||||
|
||||
// Handle root wildcard "/**"
|
||||
if (basePattern === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if path starts with the base pattern
|
||||
if (normalizedPath === basePattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if path is a subdirectory of the base pattern
|
||||
return normalizedPath.startsWith(`${basePattern}/`);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const usePathAccessPolicies = ({ secretPath, environment }: Params) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { subscription } = useSubscription();
|
||||
const { data: policies } = useGetAccessApprovalPolicies({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
options: {
|
||||
enabled: subscription.secretApproval
|
||||
}
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
const pathPolicies = policies?.filter(
|
||||
(policy) =>
|
||||
policy.environment.slug === environment && matchesPath(secretPath, policy.secretPath)
|
||||
);
|
||||
|
||||
return {
|
||||
hasPathPolicies: subscription.secretApproval && Boolean(pathPolicies?.length),
|
||||
pathPolicies
|
||||
};
|
||||
}, [secretPath, environment, policies, subscription.secretApproval]);
|
||||
};
|
||||
@@ -4,13 +4,13 @@ import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-ic
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faBuilding,
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faCubes,
|
||||
faEnvelope,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faSignOut,
|
||||
faSort,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -180,22 +180,22 @@ export const Navbar = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-10 flex h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2">
|
||||
<div className="z-10 flex min-h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4">
|
||||
<div>
|
||||
<Link to="/organization/projects">
|
||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<p className="pl-1 pr-3 text-lg text-mineshaft-400/70">/</p>
|
||||
<div className="flex items-center">
|
||||
<p className="pl-2 pr-3 text-lg text-mineshaft-400/70">/</p>
|
||||
<DropdownMenu modal={false}>
|
||||
<Link to="/organization/projects">
|
||||
<div className="flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary-400">
|
||||
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faCubes} className="pr-1 text-xs" />
|
||||
<FontAwesomeIcon icon={faBuilding} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<div className="max-w-32 overflow-hidden text-ellipsis">{currentOrg?.name}</div>
|
||||
<div className="mr-2 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300">
|
||||
<div className="whitespace-nowrap">{currentOrg?.name}</div>
|
||||
<div className="mr-1 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300 !no-underline">
|
||||
{getPlan(subscription)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,9 +206,9 @@ export const Navbar = () => {
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="rounded-md px-2 py-2 hover:bg-mineshaft-600"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -265,13 +265,11 @@ export const Navbar = () => {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<p className="px-2 text-lg text-mineshaft-400/70">/</p>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="pl-1 pr-3 text-lg text-mineshaft-400/70">/</p>
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
<div className="flex-grow" />
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
faBook,
|
||||
faCheckCircle,
|
||||
faCog,
|
||||
faCubes,
|
||||
faDoorClosed,
|
||||
@@ -100,18 +99,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/sso">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="mr-4" />
|
||||
</div>
|
||||
SSO Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faCube,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSort,
|
||||
faStar as faSolidStar
|
||||
faStar as faSolidStar,
|
||||
faTable
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, linkOptions } from "@tanstack/react-router";
|
||||
@@ -99,7 +99,7 @@ export const ProjectSelect = () => {
|
||||
}, [workspaces, projectFavorites, currentWorkspace]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="-mr-2 flex w-full items-center gap-1">
|
||||
<DropdownMenu modal={false}>
|
||||
<Link
|
||||
to={getProjectHomePage(
|
||||
@@ -109,12 +109,12 @@ export const ProjectSelect = () => {
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary-400">
|
||||
<div className="flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faCube} className="text-xs" />
|
||||
<FontAwesomeIcon icon={faTable} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<Tooltip content={currentWorkspace.name} className="max-w-96">
|
||||
<div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div className="max-w-44 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{currentWorkspace?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -125,10 +125,10 @@ export const ProjectSelect = () => {
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
ariaLabel="switch-project"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSort} className="text-xs text-bunker-300" />
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
|
||||
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
|
||||
|
||||
export const generateUserBackupKey = async (email: string, password: string) => {
|
||||
@@ -54,15 +52,15 @@ export const generateUserBackupKey = async (email: string, password: string) =>
|
||||
return generatedKey;
|
||||
};
|
||||
|
||||
export const generateUserPassKey = async (email: string, password: string) => {
|
||||
export const generateUserPassKey = async (
|
||||
email: string,
|
||||
password: string,
|
||||
fipsEnabled: boolean
|
||||
) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
const { publicKey, privateKey } = await generateKeyPair(fipsEnabled);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
client.init({ username: email, password }, () => resolve(null));
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useCallback } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { Badge, Button, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import { useServerConfig, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useGetServerRootKmsEncryptionDetails,
|
||||
@@ -29,6 +31,7 @@ export const EncryptionPageForm = () => {
|
||||
const { data: rootKmsDetails } = useGetServerRootKmsEncryptionDetails();
|
||||
|
||||
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
|
||||
const { config } = useServerConfig();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
@@ -117,14 +120,27 @@ export const EncryptionPageForm = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
{config.fipsEnabled && (
|
||||
<Tooltip content="FIPS mode of operation is enabled for your instance. All cryptographic operations within the FIPS boundaries are validated to be FIPS compliant.">
|
||||
<div>
|
||||
<Badge className="flex items-center gap-2" variant="primary">
|
||||
FIPS Mode: Enabled
|
||||
<FontAwesomeIcon icon={faInfoCircle} />
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useUpdateServerConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
const gitHubAppFormSchema = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
appSlug: z.string(),
|
||||
appId: z.string(),
|
||||
privateKey: z.string()
|
||||
});
|
||||
|
||||
type TGitHubAppConnectionForm = z.infer<typeof gitHubAppFormSchema>;
|
||||
|
||||
type Props = {
|
||||
adminIntegrationsConfig?: AdminIntegrationsConfig;
|
||||
};
|
||||
|
||||
export const GitHubAppConnectionForm = ({ adminIntegrationsConfig }: Props) => {
|
||||
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
|
||||
const [isGitHubAppClientSecretFocused, setIsGitHubAppClientSecretFocused] = useToggle();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TGitHubAppConnectionForm>({
|
||||
resolver: zodResolver(gitHubAppFormSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TGitHubAppConnectionForm) => {
|
||||
await updateAdminServerConfig({
|
||||
gitHubAppConnectionClientId: data.clientId,
|
||||
gitHubAppConnectionClientSecret: data.clientSecret,
|
||||
gitHubAppConnectionSlug: data.appSlug,
|
||||
gitHubAppConnectionId: data.appId,
|
||||
gitHubAppConnectionPrivateKey: data.privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Updated GitHub app connection configuration. It can take up to 5 minutes to take effect.",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (adminIntegrationsConfig) {
|
||||
setValue("clientId", adminIntegrationsConfig.gitHubAppConnection.clientId);
|
||||
setValue("clientSecret", adminIntegrationsConfig.gitHubAppConnection.clientSecret);
|
||||
setValue("appSlug", adminIntegrationsConfig.gitHubAppConnection.appSlug);
|
||||
setValue("appId", adminIntegrationsConfig.gitHubAppConnection.appId);
|
||||
setValue("privateKey", adminIntegrationsConfig.gitHubAppConnection.privateKey);
|
||||
}
|
||||
}, [adminIntegrationsConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="github-app-integration" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
|
||||
<div className="text-md group order-1 ml-3 flex items-center gap-2">
|
||||
<FaGithub className="text-lg group-hover:text-primary-400" />
|
||||
<div className="text-[15px] font-semibold">GitHub App</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent childrenClassName="px-0 py-0">
|
||||
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
|
||||
<div className="mb-2 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 1: Create and configure GitHub App. Please refer to the documentation below for
|
||||
more information.
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/app-connections/github#self-hosted-instance"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button colorSchema="secondary">Documentation</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
|
||||
Step 2: Configure your instance-wide settings to enable GitHub App connections. Copy
|
||||
the credentials from your GitHub App's settings page.
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client ID"
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="clientSecret"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client Secret"
|
||||
tooltipText="You can find your Client Secret in the GitHub App's settings under 'Client secrets'."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type={isGitHubAppClientSecretFocused ? "text" : "password"}
|
||||
onFocus={() => setIsGitHubAppClientSecretFocused.on()}
|
||||
onBlur={() => setIsGitHubAppClientSecretFocused.off()}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="appSlug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="App Slug"
|
||||
tooltipText="The GitHub App slug from the app's URL (e.g., 'my-app' from github.com/apps/my-app)."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="appId"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="App ID"
|
||||
tooltipText="The numeric App ID found in your GitHub App's settings."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
type="text"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="privateKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Private Key"
|
||||
tooltipText="The private key generated for your GitHub App (PEM format)."
|
||||
className="w-96"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
className="min-h-32"
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -5,23 +5,17 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
|
||||
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
|
||||
|
||||
import { GitHubAppConnectionForm } from "./GitHubAppConnectionForm";
|
||||
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
|
||||
import { SlackIntegrationForm } from "./SlackIntegrationForm";
|
||||
|
||||
enum IntegrationTabSections {
|
||||
Workflow = "workflow",
|
||||
AppConnections = "app-connections"
|
||||
Workflow = "workflow"
|
||||
}
|
||||
|
||||
interface WorkflowTabProps {
|
||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||
}
|
||||
|
||||
interface AppConnectionsTabProps {
|
||||
adminIntegrationsConfig: AdminIntegrationsConfig;
|
||||
}
|
||||
|
||||
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
@@ -29,12 +23,6 @@ const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppConnectionsTab = ({ adminIntegrationsConfig }: AppConnectionsTabProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<GitHubAppConnectionForm adminIntegrationsConfig={adminIntegrationsConfig} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IntegrationsPageForm = () => {
|
||||
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
|
||||
|
||||
@@ -59,11 +47,6 @@ export const IntegrationsPageForm = () => {
|
||||
key: IntegrationTabSections.Workflow,
|
||||
label: "Workflows",
|
||||
component: WorkflowTab
|
||||
},
|
||||
{
|
||||
key: IntegrationTabSections.AppConnections,
|
||||
label: "App Connections",
|
||||
component: AppConnectionsTab
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -49,7 +49,11 @@ export const SignUpPage = () => {
|
||||
// avoid multi submission
|
||||
if (isSubmitting) return;
|
||||
try {
|
||||
const { privateKey, ...userPass } = await generateUserPassKey(email, password);
|
||||
const { privateKey, ...userPass } = await generateUserPassKey(
|
||||
email,
|
||||
password,
|
||||
config.fipsEnabled
|
||||
);
|
||||
const res = await createAdminUser({
|
||||
email,
|
||||
password,
|
||||
|
||||
@@ -8,17 +8,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import InputField from "@app/components/basic/InputField";
|
||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
|
||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
completeAccountSignupInvite,
|
||||
@@ -68,6 +67,7 @@ export const SignupInvitePage = () => {
|
||||
const metadata = queryParams.get("metadata") || undefined;
|
||||
|
||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||
const { config } = useServerConfig();
|
||||
|
||||
const loggedIn = isLoggedIn();
|
||||
|
||||
@@ -95,11 +95,7 @@ export const SignupInvitePage = () => {
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
|
||||
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
|
||||
|
||||
+4
-8
@@ -4,15 +4,14 @@ import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
|
||||
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
|
||||
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { initProjectHelper } from "@app/helpers/project";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
@@ -65,6 +64,7 @@ export const UserInfoSSOStep = ({
|
||||
const { mutateAsync: selectOrganization } = useSelectOrganization();
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
const navigate = useNavigate();
|
||||
const { config } = useServerConfig();
|
||||
|
||||
useEffect(() => {
|
||||
const randomPassword = crypto.randomBytes(32).toString("hex");
|
||||
@@ -94,11 +94,7 @@ export const UserInfoSSOStep = ({
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
|
||||
client.init(
|
||||
|
||||
@@ -31,10 +31,10 @@ enum TimeUnit {
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
pkiCollectionId: z.string(),
|
||||
alertBefore: z.string(),
|
||||
pkiCollectionId: z.string().min(1),
|
||||
alertBefore: z.string().min(1),
|
||||
alertUnit: z.nativeEnum(TimeUnit),
|
||||
emails: z.string().trim()
|
||||
emails: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
const convertToDays = (unit: TimeUnit, value: number) => {
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export const CaCertificatesTable = ({ caId }: Props) => {
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="ca-certificates" />}
|
||||
{!isPending &&
|
||||
caCerts?.map((caCert, index) => {
|
||||
caCerts?.map?.((caCert, index) => {
|
||||
const isLastItem = index === caCerts.length - 1;
|
||||
const caCertObj = new x509.X509Certificate(caCert.certificate);
|
||||
return (
|
||||
|
||||
+1
@@ -76,6 +76,7 @@ export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }:
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
method: EnrollmentMethod.EST,
|
||||
caChain: data.caChain,
|
||||
isEnabled: data.isEnabled,
|
||||
disableBootstrapCertValidation: data.disableBootstrapCertValidation
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
faCertificate,
|
||||
faCog,
|
||||
faEllipsis,
|
||||
faPencil,
|
||||
faPlus,
|
||||
@@ -12,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
@@ -40,12 +42,14 @@ import {
|
||||
import {
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSub,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
|
||||
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
|
||||
|
||||
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
|
||||
import { PkiTemplateForm } from "./components/PkiTemplateForm";
|
||||
|
||||
const PER_PAGE_INIT = 25;
|
||||
@@ -56,9 +60,13 @@ export const PkiTemplateListPage = () => {
|
||||
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"certificateTemplate",
|
||||
"deleteTemplate"
|
||||
"deleteTemplate",
|
||||
"enrollmentOptions",
|
||||
"estUpgradePlan"
|
||||
] as const);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data, isPending } = useListCertificateTemplates({
|
||||
projectId: currentWorkspace.id,
|
||||
offset: (page - 1) * perPage,
|
||||
@@ -92,7 +100,7 @@ export const PkiTemplateListPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
|
||||
<title>{t("common.head-title", { title: "PKI Templates" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
@@ -177,7 +185,33 @@ export const PkiTemplateListPage = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Edit}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription.pkiEst) {
|
||||
handlePopUpOpen("estUpgradePlan");
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("enrollmentOptions", {
|
||||
id: template.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
>
|
||||
Manage Enrollment
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
@@ -251,7 +285,13 @@ export const PkiTemplateListPage = () => {
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.estUpgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("estUpgradePlan", isOpen)}
|
||||
text="You can only configure template enrollment methods if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useForm } from "react-hook-form";
|
||||
import { faCheckCircle, faCopy, faInfoCircle, faLockOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { decodeBase64 } from "tweetnacl-util";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { decodeBase64 } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
|
||||
@@ -2,10 +2,10 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faFileSignature, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { decodeBase64 } from "tweetnacl-util";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { decodeBase64 } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
|
||||
+10
@@ -18,6 +18,7 @@ import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
|
||||
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { ChecklyConnectionForm } from "./ChecklyConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
||||
@@ -36,6 +37,7 @@ import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
|
||||
import { PostgresConnectionForm } from "./PostgresConnectionForm";
|
||||
import { RailwayConnectionForm } from "./RailwayConnectionForm";
|
||||
import { RenderConnectionForm } from "./RenderConnectionForm";
|
||||
import { SupabaseConnectionForm } from "./SupabaseConnectionForm";
|
||||
import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
|
||||
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
|
||||
import { VercelConnectionForm } from "./VercelConnectionForm";
|
||||
@@ -143,6 +145,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <ZabbixConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Railway:
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Checkly:
|
||||
return <ChecklyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Supabase:
|
||||
return <SupabaseConnectionForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${app}`);
|
||||
}
|
||||
@@ -243,6 +249,10 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <ZabbixConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Railway:
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Checkly:
|
||||
return <ChecklyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Supabase:
|
||||
return <SupabaseConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||
}
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
ChecklyConnectionMethod,
|
||||
TChecklyConnection
|
||||
} from "@app/hooks/api/appConnections/types/checkly-connection";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TChecklyConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Checkly)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(ChecklyConnectionMethod.ApiKey),
|
||||
credentials: z.object({
|
||||
apiKey: z.string().trim().min(1, "Service API Key required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ChecklyConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Checkly,
|
||||
method: ChecklyConnectionMethod.ApiKey
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The type of token you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Checkly].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Token Type"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(ChecklyConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="API Key Value"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Checkly"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
SupabaseConnectionMethod,
|
||||
TSupabaseConnection
|
||||
} from "@app/hooks/api/appConnections/types/supabase-connection";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TSupabaseConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Supabase)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(SupabaseConnectionMethod.AccessToken),
|
||||
credentials: z.object({
|
||||
accessKey: z.string().trim().min(1, "Access Key required"),
|
||||
instanceUrl: z.string().url().optional()
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const SupabaseConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Supabase,
|
||||
method: SupabaseConnectionMethod.AccessToken
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="credentials.instanceUrl"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Instance URL"
|
||||
tooltipClassName="max-w-sm"
|
||||
tooltipText="Will default to Supabase Cloud if not specified."
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="https://api.supabase.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The type of token you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Supabase].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Token Type"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(SupabaseConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.accessKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Access Key Value"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Supabase"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
-18
@@ -1,6 +1,3 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
@@ -11,21 +8,6 @@ export const OrgSecurityTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<>
|
||||
<NoticeBannerV2
|
||||
className="mx-auto mb-4"
|
||||
titleClassName="text-base"
|
||||
title="Single Sign-On (SSO) Settings"
|
||||
>
|
||||
<p className="mt-1 text-mineshaft-300">
|
||||
SSO Settings have been relocated:{" "}
|
||||
<Link
|
||||
className="text-mineshaft-200 underline underline-offset-2"
|
||||
to="/organization/sso"
|
||||
>
|
||||
Click here to view SSO Settings
|
||||
</Link>
|
||||
</p>
|
||||
</NoticeBannerV2>
|
||||
<OrgGenericAuthSection />
|
||||
<OrgUserAccessTokenLimitSection />
|
||||
</>
|
||||
|
||||
+13
-1
@@ -9,8 +9,10 @@ import { ImportTab } from "../ImportTab";
|
||||
import { KmipTab } from "../KmipTab/OrgKmipTab";
|
||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
import { OrgProvisioningTab } from "../OrgProvisioningTab";
|
||||
import { OrgSecurityTab } from "../OrgSecurityTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||
import { OrgSsoTab } from "../OrgSsoTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab";
|
||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
||||
|
||||
export const OrgTabGroup = () => {
|
||||
@@ -19,6 +21,16 @@ export const OrgTabGroup = () => {
|
||||
});
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
|
||||
{
|
||||
name: "SSO",
|
||||
key: "sso-settings",
|
||||
component: OrgSsoTab
|
||||
},
|
||||
{
|
||||
name: "Provisioning",
|
||||
key: "provisioning-settings",
|
||||
component: OrgProvisioningTab
|
||||
},
|
||||
{ name: "Security", key: "tab-org-security", component: OrgSecurityTab },
|
||||
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
|
||||
{
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./OrgWorkflowIntegrationTab";
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { SsoTabGroup } from "./components/SsoTabGroup";
|
||||
|
||||
export const SsoPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Single Sign-On (SSO)</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Single Sign-On (SSO)" />
|
||||
<SsoTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
|
||||
import { OrgProvisioningTab } from "../OrgProvisioningTab";
|
||||
import { OrgSsoTab } from "../OrgSsoTab";
|
||||
|
||||
export const SsoTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SsoPage.id
|
||||
});
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-sso-auth", component: OrgSsoTab },
|
||||
{ name: "Provisioning", key: "tab-sso-identity", component: OrgProvisioningTab }
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||
|
||||
return (
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { SsoTabGroup } from "./SsoTabGroup";
|
||||
@@ -1,26 +0,0 @@
|
||||
import { createFileRoute, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SsoPage } from "./SsoPage";
|
||||
|
||||
const SettingsPageQueryParams = z.object({
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
|
||||
)({
|
||||
component: SsoPage,
|
||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Single Sign-On (SSO)"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user