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