Merge branch 'main' into feat/point-in-time-revamp

This commit is contained in:
carlosmonastyrski
2025-06-05 18:31:27 -03:00
757 changed files with 31611 additions and 4112 deletions
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { AuthenticationPageForm } from "./components";
export const AuthenticationPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Authentication"
description="Manage authentication settings for your Infisical instance."
/>
<AuthenticationPageForm />
</div>
</div>
</div>
);
};
@@ -20,7 +20,7 @@ const formSchema = z.object({
type TAuthForm = z.infer<typeof formSchema>;
export const AuthPanel = () => {
export const AuthenticationPageForm = () => {
const { config } = useServerConfig();
const { enabledLoginMethods } = config;
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
@@ -0,0 +1 @@
export { AuthenticationPageForm } from "./AuthenticationPageForm";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { AuthenticationPage } from "./AuthenticationPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/authentication"
)({
component: AuthenticationPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Authentication",
link: linkOptions({
to: "/admin/authentication"
})
}
]
};
}
});
@@ -0,0 +1,24 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { CachingPageForm } from "./components";
export const CachingPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title="Caching" description="Manage caching for your Infisical instance." />
<CachingPageForm />
</div>
</div>
</div>
);
};
@@ -10,7 +10,7 @@ import { useInvalidateCache } from "@app/hooks/api";
import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries";
import { CacheType } from "@app/hooks/api/admin/types";
export const CachingPanel = () => {
export const CachingPageForm = () => {
const { mutateAsync: invalidateCache } = useInvalidateCache();
const { user } = useUser();
@@ -0,0 +1 @@
export { CachingPageForm } from "./CachingPageForm";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CachingPage } from "./CachingPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/caching"
)({
component: CachingPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Caching",
link: linkOptions({
to: "/admin/caching"
})
}
]
};
}
});
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { EncryptionPageForm } from "./components";
export const EncryptionPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Encryption"
description="Manage encryption settings for your Infisical instance."
/>
<EncryptionPageForm />
</div>
</div>
</div>
);
};
@@ -8,11 +8,11 @@ import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useUpdateServerEncryptionStrategy } from "@app/hooks/api";
import {
RootKeyEncryptionStrategy,
TGetServerRootKmsEncryptionDetails
} from "@app/hooks/api/admin/types";
useGetServerRootKmsEncryptionDetails,
useUpdateServerEncryptionStrategy
} from "@app/hooks/api";
import { RootKeyEncryptionStrategy } from "@app/hooks/api/admin/types";
const formSchema = z.object({
encryptionStrategy: z.nativeEnum(RootKeyEncryptionStrategy)
@@ -25,11 +25,9 @@ const strategies: Record<RootKeyEncryptionStrategy, string> = {
type TForm = z.infer<typeof formSchema>;
type Props = {
rootKmsDetails?: TGetServerRootKmsEncryptionDetails;
};
export const EncryptionPageForm = () => {
const { data: rootKmsDetails } = useGetServerRootKmsEncryptionDetails();
export const EncryptionPanel = ({ rootKmsDetails }: Props) => {
const { mutateAsync: updateEncryptionStrategy } = useUpdateServerEncryptionStrategy();
const { subscription } = useSubscription();
@@ -0,0 +1 @@
export { EncryptionPageForm } from "./EncryptionPageForm";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { EncryptionPage } from "./EncryptionPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/encryption"
)({
component: EncryptionPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Encryption",
link: linkOptions({
to: "/admin/encryption"
})
}
]
};
}
});
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { GeneralPageForm } from "./components";
export const GeneralPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="General"
description="Manage general settings for your Infisical instance."
/>
<GeneralPageForm />
</div>
</div>
</div>
);
};
@@ -0,0 +1,309 @@
import { Controller, useForm } from "react-hook-form";
import { faAt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Select,
SelectClear,
SelectItem,
Switch,
TextArea
} from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
enum SignUpModes {
Disabled = "disabled",
Anyone = "anyone"
}
const formSchema = z.object({
signUpMode: z.nativeEnum(SignUpModes),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean(),
trustLdapEmails: z.boolean(),
trustOidcEmails: z.boolean(),
defaultAuthOrgId: z.string(),
authConsentContent: z.string().optional().default(""),
pageFrameContent: z.string().optional().default("")
});
type TDashboardForm = z.infer<typeof formSchema>;
export const GeneralPageForm = () => {
const data = useServerConfig();
const { config } = data;
const {
control,
handleSubmit,
watch,
formState: { isSubmitting, isDirty }
} = useForm<TDashboardForm>({
resolver: zodResolver(formSchema),
values: {
// eslint-disable-next-line
signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled,
allowedSignUpDomain: config.allowedSignUpDomain,
trustSamlEmails: config.trustSamlEmails,
trustLdapEmails: config.trustLdapEmails,
trustOidcEmails: config.trustOidcEmails,
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
authConsentContent: config.authConsentContent ?? "",
pageFrameContent: config.pageFrameContent ?? ""
}
});
const signUpMode = watch("signUpMode");
const defaultAuthOrgId = watch("defaultAuthOrgId");
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
const organizations = useGetOrganizations();
const onFormSubmit = async (formData: TDashboardForm) => {
try {
const {
allowedSignUpDomain,
trustSamlEmails,
trustLdapEmails,
trustOidcEmails,
authConsentContent,
pageFrameContent
} = formData;
await updateServerConfig({
defaultAuthOrgId: defaultAuthOrgId || null,
allowSignUp: signUpMode !== SignUpModes.Disabled,
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
trustLdapEmails,
trustOidcEmails,
authConsentContent,
pageFrameContent
});
createNotification({
text: "Successfully changed sign up setting.",
type: "success"
});
} catch (e) {
console.error(e);
createNotification({
type: "error",
text: "Failed to update sign up setting."
});
}
};
return (
<form
className="space-y-8 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Allow user signups</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want users to be able to signup freely into your Infisical instance.
</div>
<Controller
control={control}
name="signUpMode"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl className="max-w-sm" errorText={error?.message} isError={Boolean(error)}>
<Select
className="w-full bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800"
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
{...field}
>
<SelectItem value={SignUpModes.Disabled}>Disabled</SelectItem>
<SelectItem value={SignUpModes.Anyone}>Anyone</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
{signUpMode === "anyone" && (
<div className="flex flex-col justify-start">
<div className="mb-4 text-xl font-semibold text-mineshaft-100">
Restrict signup by email domain(s)
</div>
<Controller
control={control}
defaultValue=""
name="allowedSignUpDomain"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Leave blank to allow any email domains"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
placeholder="gmail.com, aws.com, redhat.com"
leftIcon={<FontAwesomeIcon icon={faAt} />}
/>
</FormControl>
)}
/>
</div>
)}
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Default organization</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select the default organization you want to set for SAML/LDAP/OIDC/Github logins. When
selected, user logins will be automatically scoped to the selected organization.
</div>
<Controller
control={control}
name="defaultAuthOrgId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl className="max-w-sm" errorText={error?.message} isError={Boolean(error)}>
<Select
placeholder="Allow all organizations"
className="w-full bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800"
defaultValue={field.value ?? " "}
onValueChange={(e) => onChange(e)}
{...field}
>
<SelectClear
selectValue={defaultAuthOrgId}
onClear={() => {
onChange("");
}}
>
Allow all organizations
</SelectClear>
{organizations.data?.map((org) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Trust emails</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want Infisical to trust external emails from SAML/LDAP/OIDC identity
providers. If set to false, then Infisical will prompt SAML/LDAP/OIDC provisioned users to
verify their email upon their first login.
</div>
<Controller
control={control}
name="trustSamlEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-saml-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust SAML emails</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="trustLdapEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-ldap-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust LDAP emails</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="trustOidcEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-oidc-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust OIDC emails</p>
</Switch>
</FormControl>
);
}}
/>
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Notices</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-400">
Configure system-wide notification banners and security messages. These settings control
the text displayed to users during authentication and throughout their session
</div>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Auth Consent Content"
tooltipText="Formatting supported: HTML, Markdown, Plain text"
>
<TextArea
placeholder="**Auth Consent Message**"
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="authConsentContent"
/>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Page Frame Content"
tooltipText="Formatting supported: HTML, Markdown, Plain text"
>
<TextArea
placeholder='<div style="background-color: red">TOP SECRET</div>'
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="pageFrameContent"
/>
</div>
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting || !isDirty}>
Save
</Button>
</form>
);
};
@@ -0,0 +1 @@
export { GeneralPageForm } from "./GeneralPageForm";
@@ -0,0 +1,23 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GeneralPage } from "./GeneralPage";
export const Route = createFileRoute("/_authenticate/_inject-org-details/admin/_admin-layout/")({
component: GeneralPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "General",
link: linkOptions({
to: "/admin"
})
}
]
};
}
});
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { IntegrationsPageForm } from "./components";
export const IntegrationsPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Integrations"
description="Manage integrations for your Infisical instance."
/>
<IntegrationsPageForm />
</div>
</div>
</div>
);
};
@@ -3,7 +3,7 @@ import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm";
export const IntegrationPanel = () => {
export const IntegrationsPageForm = () => {
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
return (
@@ -0,0 +1 @@
export { IntegrationsPageForm } from "./IntegrationsPageForm";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { IntegrationsPage } from "./IntegrationsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations"
)({
component: IntegrationsPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Integrations",
link: linkOptions({
to: "/admin/integrations"
})
}
]
};
}
});
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { MachineIdentitiesTable } from "./components";
export const MachineIdentitiesResourcesPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Machine Identities"
description="Manage all machine identities within your Infisical instance."
/>
<MachineIdentitiesTable />
</div>
</div>
</div>
);
};
@@ -120,7 +120,7 @@ const IdentityPanelTable = ({
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="star"
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
@@ -133,7 +133,7 @@ const IdentityPanelTable = ({
);
};
export const IdentityPanel = () => {
export const MachineIdentitiesTable = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeServerAdmin"
] as const);
@@ -161,13 +161,10 @@ export const IdentityPanel = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
</div>
<IdentityPanelTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure want to remove Server Admin permissions from ${
title={`Are you sure you want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { name: string })?.name || ""
}?`}
subTitle=""
@@ -0,0 +1 @@
export { MachineIdentitiesTable } from "./MachineIdentitiesTable";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MachineIdentitiesResourcesPage } from "./MachineIdentitiesResourcesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities"
)({
component: MachineIdentitiesResourcesPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Machine Identities",
link: linkOptions({
to: "/admin/resources/machine-identities"
})
}
]
};
}
});
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { OrganizationsTable } from "./components";
export const OrganizationResourcesPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Organizations"
description="Manage all organizations within your Infisical instance."
/>
<OrganizationsTable />
</div>
</div>
</div>
);
};
@@ -0,0 +1,396 @@
import { useState } from "react";
import {
faBuilding,
faCircleQuestion,
faEllipsis,
faMagnifyingGlass
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useDebounce, usePopUp } from "@app/hooks";
import {
useAdminDeleteOrganization,
useAdminDeleteOrganizationMembership,
useAdminDeleteUser,
useAdminGetOrganizations
} from "@app/hooks/api";
import { OrganizationWithProjects } from "@app/hooks/api/admin/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const ViewMembersModalContent = ({
popUp,
handlePopUpOpen
}: {
popUp: UsePopUpState<["viewMembers"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>,
data?: {
username?: string;
membershipId?: string;
userId?: string;
orgName?: string;
orgId?: string;
organization?: OrganizationWithProjects;
}
) => void;
}) => {
const organization = popUp.viewMembers?.data?.organization as OrganizationWithProjects;
return (
<div className="space-y-2">
{organization?.members?.map((member) => (
<div className="flex items-center justify-between gap-2 rounded-md bg-mineshaft-700 px-4 py-2">
<div>
<div className="flex items-center gap-2">
<p className="text-xs text-mineshaft-100">
<div>
{member.user.firstName ? (
<div>
{member.user.firstName} {member.user.lastName}
</div>
) : (
<p className="text-mineshaft-400">Not set</p>
)}
</div>
<div className="flex gap-2 opacity-80">
<div>{member.user.username || member.user.email}</div>
<Badge variant="primary">
<div className="flex items-center gap-1">
<span className="capitalize">{member.role.replace("-", " ")}</span>
{Boolean(member.roleId) && (
<Tooltip content="This member has a custom role assigned.">
<FontAwesomeIcon icon={faCircleQuestion} className="text-xs" />
</Tooltip>
)}
</div>
</Badge>
</div>
</p>
</div>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<FontAwesomeIcon
icon={faEllipsis}
className="cursor-pointer text-sm text-mineshaft-400 transition-all hover:text-primary-500"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("deleteOrganizationMembership", {
membershipId: member.membershipId,
orgId: organization.id,
username: member.user.username,
orgName: organization.name
})
}
>
Remove From Organization
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteUser", { userId: member.user.id })}
>
Delete User
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
);
};
const ViewMembersModal = ({
isOpen,
onOpenChange,
popUp,
handlePopUpOpen
}: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
popUp: UsePopUpState<["viewMembers", "deleteOrganizationMembership", "deleteUser"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>
) => void;
}) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
onOpenAutoFocus={(event) => {
event.preventDefault();
}}
title="Organization Members"
subTitle="View the members of the organization."
>
<ViewMembersModalContent popUp={popUp} handlePopUpOpen={handlePopUpOpen} />
</ModalContent>
</Modal>
);
};
const OrganizationsPanelTable = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: {
popUp: UsePopUpState<
["deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser"]
>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser"]
>,
data?: {
orgName?: string;
orgId?: string;
message?: string;
organization?: OrganizationWithProjects;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["deleteOrganization", "viewMembers"]>,
isOpen?: boolean
) => void;
}) => {
const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500);
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
useAdminGetOrganizations({
limit: 20,
searchTerm: debouncedSearchTerm
});
const isEmpty = !isPending && !data?.pages?.[0].length;
return (
<>
<div className="flex gap-2">
<Input
value={searchOrganizationsFilter}
onChange={(e) => setSearchOrganizationsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organizations..."
className="flex-1"
/>
</div>
<div className="mt-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5/12">Name</Th>
<Th className="w-5/12">Members</Th>
<Th className="w-5/12">Projects</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
{!isPending &&
data?.pages?.map((orgs) =>
orgs.map((org) => {
return (
<Tr key={`org-${org.id}`} className="w-full">
<Td className="w-5/12">
{org.name ? (
org.name
) : (
<span className="text-mineshaft-400">Not set</span>
)}
</Td>
<Td className="w-5/12">
{org.members.length} {org.members.length === 1 ? "member" : "members"}
<Button
variant="outline_bg"
size="xs"
className="ml-2"
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
>
View Members
</Button>
</Td>
<Td className="w-5/12">
{org.projects.length} {org.projects.length === 1 ? "project" : "projects"}
</Td>
<Td>
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteOrganization", {
orgId: org.id,
orgName: org.name
});
}}
>
Delete Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})
)}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of list"}
</Button>
)}
</div>
<ViewMembersModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
isOpen={popUp.viewMembers.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("viewMembers", isOpen)}
/>
</>
);
};
export const OrganizationsTable = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteOrganization",
"deleteOrganizationMembership",
"deleteUser",
"viewMembers"
] as const);
const { mutateAsync: deleteOrganization } = useAdminDeleteOrganization();
const { mutateAsync: deleteOrganizationMembership } = useAdminDeleteOrganizationMembership();
const { mutateAsync: deleteUser } = useAdminDeleteUser();
const handleDeleteOrganization = async () => {
const { orgId } = popUp?.deleteOrganization?.data as { orgId: string };
await deleteOrganization(orgId);
createNotification({
type: "success",
text: "Successfully deleted organization"
});
handlePopUpClose("deleteOrganization");
};
const handleDeleteOrganizationMembership = async () => {
const { orgId, membershipId } = popUp?.deleteOrganizationMembership?.data as {
orgId: string;
membershipId: string;
};
if (!orgId || !membershipId) {
return;
}
await deleteOrganizationMembership({ organizationId: orgId, membershipId });
createNotification({
type: "success",
text: "Successfully removed user from organization"
});
handlePopUpClose("viewMembers");
handlePopUpClose("deleteOrganizationMembership");
};
const handleDeleteUser = async () => {
const { userId } = popUp?.deleteUser?.data as { userId: string };
if (!userId) {
return;
}
await deleteUser(userId);
createNotification({
type: "success",
text: "Successfully deleted user"
});
handlePopUpClose("viewMembers");
handlePopUpClose("deleteUser");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<OrganizationsPanelTable
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteOrganization.isOpen}
deleteKey="delete"
title={`Are you sure you want to delete organization ${
(popUp?.deleteOrganization?.data as { orgName: string })?.orgName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrganization", isOpen)}
onDeleteApproved={handleDeleteOrganization}
/>
<DeleteActionModal
isOpen={popUp.deleteOrganizationMembership.isOpen}
deleteKey="delete"
title={`Are you sure you want to remove ${
(popUp?.deleteOrganizationMembership?.data as { username: string })?.username || ""
} from organization ${
(popUp?.deleteOrganizationMembership?.data as { orgName: string })?.orgName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrganizationMembership", isOpen)}
onDeleteApproved={handleDeleteOrganizationMembership}
/>
<DeleteActionModal
isOpen={popUp.deleteUser.isOpen}
deleteKey="delete"
title={`Are you sure you want to delete user ${
(popUp?.deleteUser?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteUser", isOpen)}
onDeleteApproved={handleDeleteUser}
/>
</div>
);
};
@@ -0,0 +1 @@
export { OrganizationsTable } from "./OrganizationsTable";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { OrganizationResourcesPage } from "./OrganizationResourcesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations"
)({
component: OrganizationResourcesPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Organizations",
link: linkOptions({
to: "/admin/resources/organizations"
})
}
]
};
}
});
@@ -1,424 +0,0 @@
import { Helmet } from "react-helmet";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faAt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
ContentLoader,
FormControl,
Input,
Select,
SelectClear,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
Tabs,
TextArea
} from "@app/components/v2";
import { useServerConfig, useUser } from "@app/context";
import {
useGetOrganizations,
useGetServerRootKmsEncryptionDetails,
useUpdateServerConfig
} from "@app/hooks/api";
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
import { AuthPanel } from "./components/AuthPanel";
import { CachingPanel } from "./components/CachingPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { UserPanel } from "./components/UserPanel";
enum TabSections {
Settings = "settings",
Encryption = "encryption",
Auth = "auth",
Integrations = "integrations",
Users = "users",
Identities = "identities",
Kmip = "kmip",
Caching = "caching"
}
enum SignUpModes {
Disabled = "disabled",
Anyone = "anyone"
}
const formSchema = z.object({
signUpMode: z.nativeEnum(SignUpModes),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean(),
trustLdapEmails: z.boolean(),
trustOidcEmails: z.boolean(),
defaultAuthOrgId: z.string(),
authConsentContent: z.string().optional().default(""),
pageFrameContent: z.string().optional().default("")
});
type TDashboardForm = z.infer<typeof formSchema>;
export const OverviewPage = () => {
const { t } = useTranslation();
const data = useServerConfig();
const { data: serverRootKmsDetails } = useGetServerRootKmsEncryptionDetails();
const { config } = data;
const {
control,
handleSubmit,
watch,
formState: { isSubmitting, isDirty }
} = useForm<TDashboardForm>({
resolver: zodResolver(formSchema),
values: {
// eslint-disable-next-line
signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled,
allowedSignUpDomain: config.allowedSignUpDomain,
trustSamlEmails: config.trustSamlEmails,
trustLdapEmails: config.trustLdapEmails,
trustOidcEmails: config.trustOidcEmails,
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
authConsentContent: config.authConsentContent ?? "",
pageFrameContent: config.pageFrameContent ?? ""
}
});
const signUpMode = watch("signUpMode");
const defaultAuthOrgId = watch("defaultAuthOrgId");
const { user } = useUser();
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
const organizations = useGetOrganizations();
const isNotAllowed = !user?.superAdmin;
const onFormSubmit = async (formData: TDashboardForm) => {
try {
const {
allowedSignUpDomain,
trustSamlEmails,
trustLdapEmails,
trustOidcEmails,
authConsentContent,
pageFrameContent
} = formData;
await updateServerConfig({
defaultAuthOrgId: defaultAuthOrgId || null,
allowSignUp: signUpMode !== SignUpModes.Disabled,
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
trustSamlEmails,
trustLdapEmails,
trustOidcEmails,
authConsentContent,
pageFrameContent
});
createNotification({
text: "Successfully changed sign up setting.",
type: "success"
});
} catch (e) {
console.error(e);
createNotification({
type: "error",
text: "Failed to update sign up setting."
});
}
};
return (
<>
<Helmet>
<title>{t("common.head-title", { title: t("admin.dashboard") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("admin.dashboard.og-title") ?? ""} />
<meta name="og:description" content={t("admin.dashboard.og-description") ?? ""} />
</Helmet>
<div className="h-full">
<div className="container mx-auto max-w-7xl px-4 pb-12 text-white dark:[color-scheme:dark]">
<div className="mx-auto mb-6 w-full max-w-7xl pt-6">
<div className="mb-8 flex flex-col items-start justify-between text-xl">
<h1 className="text-3xl font-semibold">Server Admin Console</h1>
<p className="text-base text-bunker-300">
Manage your instance level configurations.
</p>
</div>
</div>
{isNotAllowed ? (
<ContentLoader text={isNotAllowed ? "Redirecting to org page..." : undefined} />
) : (
<div>
<Tabs defaultValue={TabSections.Settings}>
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Encryption}>Encryption</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>User Identities</Tab>
<Tab value={TabSections.Identities}>Machine Identities</Tab>
<Tab value={TabSections.Caching}>Caching</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>
<form
className="mb-6 space-y-8 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
Allow user signups
</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want users to be able to signup freely into your Infisical
instance.
</div>
<Controller
control={control}
name="signUpMode"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="max-w-sm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800"
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
{...field}
>
<SelectItem value={SignUpModes.Disabled}>Disabled</SelectItem>
<SelectItem value={SignUpModes.Anyone}>Anyone</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
{signUpMode === "anyone" && (
<div className="flex flex-col justify-start">
<div className="mb-4 text-xl font-semibold text-mineshaft-100">
Restrict signup by email domain(s)
</div>
<Controller
control={control}
defaultValue=""
name="allowedSignUpDomain"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Leave blank to allow any email domains"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
placeholder="gmail.com, aws.com, redhat.com"
leftIcon={<FontAwesomeIcon icon={faAt} />}
/>
</FormControl>
)}
/>
</div>
)}
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
Default organization
</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select the default organization you want to set for SAML/LDAP/OIDC/Github
logins. When selected, user logins will be automatically scoped to the
selected organization.
</div>
<Controller
control={control}
name="defaultAuthOrgId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="max-w-sm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
placeholder="Allow all organizations"
className="w-full bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-800"
defaultValue={field.value ?? " "}
onValueChange={(e) => onChange(e)}
{...field}
>
<SelectClear
selectValue={defaultAuthOrgId}
onClear={() => {
onChange("");
}}
>
Allow all organizations
</SelectClear>
{organizations.data?.map((org) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">
Trust emails
</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select if you want Infisical to trust external emails from SAML/LDAP/OIDC
identity providers. If set to false, then Infisical will prompt
SAML/LDAP/OIDC provisioned users to verify their email upon their first
login.
</div>
<Controller
control={control}
name="trustSamlEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-saml-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust SAML emails</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="trustLdapEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-ldap-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust LDAP emails</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="trustOidcEmails"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="trust-oidc-emails"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-full">Trust OIDC emails</p>
</Switch>
</FormControl>
);
}}
/>
</div>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Notices</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-400">
Configure system-wide notification banners and security messages. These
settings control the text displayed to users during authentication and
throughout their session
</div>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Auth Consent Content"
tooltipText="Formatting supported: HTML, Markdown, Plain text"
>
<TextArea
placeholder="**Auth Consent Message**"
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="authConsentContent"
/>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Page Frame Content"
tooltipText="Formatting supported: HTML, Markdown, Plain text"
>
<TextArea
placeholder='<div style="background-color: red">TOP SECRET</div>'
{...field}
rows={3}
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="pageFrameContent"
/>
</div>
<Button
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</form>
</TabPanel>
<TabPanel value={TabSections.Encryption}>
<EncryptionPanel rootKmsDetails={serverRootKmsDetails} />
</TabPanel>
<TabPanel value={TabSections.Auth}>
<AuthPanel />
</TabPanel>
<TabPanel value={TabSections.Integrations}>
<IntegrationPanel />
</TabPanel>
<TabPanel value={TabSections.Users}>
<UserPanel />
</TabPanel>
<TabPanel value={TabSections.Identities}>
<IdentityPanel />
</TabPanel>
<TabPanel value={TabSections.Caching}>
<CachingPanel />
</TabPanel>
</Tabs>
</div>
)}
</div>
</div>
</>
);
};
@@ -1,7 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute("/_authenticate/_inject-org-details/admin/_admin-layout/")({
component: OverviewPage
});
@@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { UserIdentitiesTable } from "./components";
export const UserIdentitiesResourcesPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="User Identities"
description="Manage all user identities within your Infisical instance."
/>
<UserIdentitiesTable />
</div>
</div>
</div>
);
};
@@ -213,7 +213,7 @@ const UserPanelTable = ({
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="star"
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
@@ -226,7 +226,7 @@ const UserPanelTable = ({
);
};
export const UserPanel = () => {
export const UserIdentitiesTable = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser",
"upgradePlan",
@@ -297,9 +297,6 @@ export const UserPanel = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4">
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
</div>
<UserPanelTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
@@ -312,7 +309,7 @@ export const UserPanel = () => {
/>
<DeleteActionModal
isOpen={popUp.upgradeToServerAdmin.isOpen}
title={`Are you sure want to grant Server Admin permissions to ${
title={`Are you sure you want to grant Server Admin permissions to ${
(popUp?.upgradeToServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
@@ -323,7 +320,7 @@ export const UserPanel = () => {
/>
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure want to remove Server Admin permissions from ${
title={`Are you sure you want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
@@ -0,0 +1 @@
export { UserIdentitiesTable } from "./UserIdentitiesTable";
@@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserIdentitiesResourcesPage } from "./UserIdentitiesResourcesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities"
)({
component: UserIdentitiesResourcesPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "User Identities",
link: linkOptions({
to: "/admin/resources/user-identities"
})
}
]
};
}
});
@@ -22,8 +22,12 @@ export const VerifyEmailPage = () => {
*/
const sendVerificationEmail = async () => {
if (email) {
await mutateAsync({ email });
setStep(2);
try {
await mutateAsync({ email });
setStep(2);
} catch {
setLoading(false);
}
}
};
@@ -70,7 +70,7 @@ export const PkiAlertsSection = () => {
<PkiAlertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiAlert.isOpen}
title={`Are you sure want to remove the alert ${
title={`Are you sure you want to remove the alert ${
(popUp?.deletePkiAlert?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiAlert", isOpen)}
@@ -70,7 +70,7 @@ export const PkiCollectionSection = () => {
<PkiCollectionModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiCollection.isOpen}
title={`Are you sure want to remove the alert ${
title={`Are you sure you want to remove the alert ${
(popUp?.deletePkiCollection?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollection", isOpen)}
@@ -23,7 +23,6 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
import { CaModal } from "../CertificateAuthoritiesPage/components/CaModal";
import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificateTemplatesSection";
import {
CaCertificatesSection,
CaCrlsSection,
@@ -126,7 +125,6 @@ const Page = () => {
</div>
<div className="w-full">
<CaCertificatesSection caId={data.id} />
<CertificateTemplatesSection caId={data.id} />
<CaCrlsSection caId={data.id} />
</div>
</div>
@@ -137,7 +135,7 @@ const Page = () => {
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
title={`Are you sure you want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
@@ -188,11 +188,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
name,
type: CaType.INTERNAL,
status,
enableDirectIssuance,
configuration: {
...configuration,
maxPathLength: Number(configuration.maxPathLength)
}
enableDirectIssuance
});
} else {
// create
@@ -96,7 +96,7 @@ export const CaSection = () => {
<CaTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
title={`Are you sure you want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
@@ -108,7 +108,7 @@ export const CaSection = () => {
/>
<DeleteActionModal
isOpen={popUp.caStatus.isOpen}
title={`Are you sure want to ${
title={`Are you sure you want to ${
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
? "enable"
: "disable"
@@ -97,7 +97,7 @@ export const ExternalCaSection = () => {
<ExternalCaTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.deleteCa.isOpen}
title={`Are you sure want to remove the CA ${
title={`Are you sure you want to remove the CA ${
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
} from the project?`}
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
@@ -112,7 +112,7 @@ export const ExternalCaSection = () => {
/>
<DeleteActionModal
isOpen={popUp.caStatus.isOpen}
title={`Are you sure want to ${
title={`Are you sure you want to ${
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
? "enable"
: "disable"
@@ -9,7 +9,11 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplate } from "@app/hooks/api";
@@ -63,7 +67,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
@@ -86,7 +90,7 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCertificateTemplate.isOpen}
title={`Are you sure want to delete the certificate template ${
title={`Are you sure you want to delete the certificate template ${
(popUp?.deleteCertificateTemplate?.data as { name: string })?.name || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteCertificateTemplate", isOpen)}
@@ -19,7 +19,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useSubscription
} from "@app/context";
import { useGetCaCertTemplates } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -79,7 +83,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
Manage Policies
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
@@ -105,7 +109,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionPkiTemplateActions.Delete}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
@@ -89,7 +89,7 @@ export const CertificatesSection = () => {
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteCertificate.isOpen}
title={`Are you sure want to remove the certificate ${
title={`Are you sure you want to remove the certificate ${
(popUp?.deleteCertificate?.data as { commonName: string })?.commonName || ""
} from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)}
@@ -152,7 +152,7 @@ export const PkiCollectionPage = () => {
<PkiCollectionModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deletePkiCollection.isOpen}
title={`Are you sure want to delete the PKI collection ${data?.name ?? ""}?`}
title={`Are you sure you want to delete the PKI collection ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollection", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
@@ -74,7 +74,7 @@ export const PkiCollectionItemsSection = ({ collectionId, type }: Props) => {
/>
<DeleteActionModal
isOpen={popUp.deletePkiCollectionItem.isOpen}
title={`Are you sure want to remove the item from the collection ${pkiCollection.name}?`}
title={`Are you sure you want to remove the item from the collection ${pkiCollection.name}?`}
onChange={(isOpen) => handlePopUpToggle("deletePkiCollectionItem", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
@@ -0,0 +1,257 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import {
faCertificate,
faEllipsis,
faPencil,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Modal,
ModalContent,
PageHeader,
Pagination,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { PkiTemplateForm } from "./components/PkiTemplateForm";
const PER_PAGE_INIT = 25;
export const PkiTemplateListPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"certificateTemplate",
"deleteTemplate"
] as const);
const { data, isPending } = useListCertificateTemplates({
projectId: currentWorkspace.id,
offset: (page - 1) * perPage,
limit: perPage
});
const deleteCertTemplate = useDeleteCertTemplateV2();
const onRemovePkiSubscriberSubmit = async () => {
try {
const pkiTemplate = await deleteCertTemplate.mutateAsync({
projectId: currentWorkspace.id,
templateName: popUp?.deleteTemplate?.data?.name
});
createNotification({
text: `Successfully deleted PKI template: ${pkiTemplate.name}`,
type: "success"
});
handlePopUpClose("deleteTemplate");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete PKI subscriber",
type: "error"
});
}
};
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Certificate Templates"
description="Manage certificate template to request and issue dynamic certificates following a strict format."
/>
</div>
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Templates</p>
<div className="flex w-full justify-end">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("certificateTemplate")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Template
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Issuing CA</Th>
<Th className="w-64">Last Updated At</Th>
<Th />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="project-cert-templates" />}
{!isPending &&
data?.certificateTemplates?.map((template) => {
return (
<Tr className="h-10" key={`certificate-template-${template.id}`}>
<Td>{template.name}</Td>
<Td>
<Tag size="xs">{template.ca.name}</Tag>
</Td>
<Td>{format(new Date(template.updatedAt), "yyyy-MM-dd | HH:mm:ss")}</Td>
<Td className="text-right align-middle">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("certificateTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit Template
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Delete}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
{!isPending && !data?.certificateTemplates?.length && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No certificate templates found" icon={faCertificate} />
</Td>
</Tr>
)}
</TBody>
</Table>
{!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteTemplate.isOpen}
title="Are you sure you want to remove the PKI Template?"
onChange={(isOpen) => handlePopUpToggle("deleteTemplate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onRemovePkiSubscriberSubmit()}
/>
</div>
<div className="container mx-auto max-w-7xl" />
</div>
<Modal
isOpen={popUp?.certificateTemplate?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("certificateTemplate", isOpen)}
>
<ModalContent
title={
popUp.certificateTemplate?.data
? "Certificate Template"
: "Create Certificate Template"
}
>
<PkiTemplateForm
certTemplate={popUp?.certificateTemplate?.data}
handlePopUpToggle={(isOpen) => handlePopUpToggle("certificateTemplate", isOpen)}
/>
</ModalContent>
</Modal>
</div>
</>
);
};
@@ -0,0 +1,407 @@
import { Controller, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Checkbox,
FilterableSelect,
FormControl,
FormLabel,
Input,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateCertTemplateV2,
useListCasByProjectId,
useUpdateCertTemplateV2
} from "@app/hooks/api";
import {
EXTENDED_KEY_USAGES_OPTIONS,
KEY_USAGES_OPTIONS
} from "@app/hooks/api/certificates/constants";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { TCertificateTemplateV2 } from "@app/hooks/api/certificateTemplates/types";
import { slugSchema } from "@app/lib/schemas";
const validateTemplateRegexField = z.string().trim().min(1).max(100);
const schema = z.object({
ca: z.object({
name: z.string(),
id: z.string()
}),
name: slugSchema(),
commonName: validateTemplateRegexField,
subjectAlternativeName: validateTemplateRegexField,
ttl: z.string().trim().min(1),
keyUsages: z.object({
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
}),
extendedKeyUsages: z.object({
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
})
});
export type FormData = z.infer<typeof schema>;
type Props = {
certTemplate?: TCertificateTemplateV2;
handlePopUpToggle: (state?: boolean) => void;
};
export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas, isPending: isCaLoading } = useListCasByProjectId(currentWorkspace.id);
const { mutateAsync: createCertTemplate } = useCreateCertTemplateV2();
const { mutateAsync: updateCertTemplate } = useUpdateCertTemplateV2();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: async () => {
if (certTemplate) {
return {
ca: certTemplate.ca,
name: certTemplate.name,
commonName: certTemplate.commonName,
subjectAlternativeName: certTemplate.subjectAlternativeName,
ttl: certTemplate.ttl,
keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true]) ?? []),
extendedKeyUsages: Object.fromEntries(
certTemplate.extendedKeyUsages.map((name) => [name, true]) ?? []
)
};
}
return {
ca: { name: "", id: "" },
name: "",
subjectAlternativeName: "",
commonName: "",
ttl: "",
keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
},
extendedKeyUsages: {}
};
}
});
const onFormSubmit = async ({
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages,
extendedKeyUsages,
ca
}: FormData) => {
if (!currentWorkspace?.id) {
return;
}
try {
if (certTemplate) {
await updateCertTemplate({
templateName: certTemplate.name,
projectId: currentWorkspace.id,
caName: ca.name,
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
});
createNotification({
text: "Successfully updated certificate template",
type: "success"
});
} else {
await createCertTemplate({
projectId: currentWorkspace.id,
caName: ca.name,
name,
commonName,
subjectAlternativeName,
ttl,
keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage),
extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage)
});
createNotification({
text: "Successfully created certificate template",
type: "success"
});
}
reset();
handlePopUpToggle(false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to save changes",
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
{certTemplate && (
<FormControl label="Certificate Template ID">
<Input value={certTemplate.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Template Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="my-template" />
</FormControl>
)}
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<FilterableSelect
options={cas || []}
isLoading={isCaLoading}
placeholder="Select CA..."
onChange={onChange}
value={value}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
control={control}
name="ca"
/>
<Controller
control={control}
defaultValue=""
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label={
<div>
<FormLabel
isRequired
label="Common Name (CN)"
icon={
<Tooltip
className="text-center"
content={
<span>
This field accepts limited regular expressions: spaces, *, ., @, -, \ (for
escaping), and alphanumeric characters only
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
/>
</div>
}
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder=".*\.acme.com" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="subjectAlternativeName"
render={({ field, fieldState: { error } }) => (
<FormControl
label={
<div>
<FormLabel
isRequired
label="Alternative Names (SAN)"
icon={
<Tooltip
className="text-center"
content={
<span>
This field accepts limited regular expressions: spaces, *, ., @, -, \ (for
escaping), and alphanumeric characters only
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
/>
</div>
}
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="service\.acme.\..*" />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Max TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Key Usage</div>
</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="keyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
<Controller
control={control}
name="extendedKeyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Extended Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="mt-4 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Save
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => handlePopUpToggle(false)}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { PkiTemplateListPage } from "./PkiTemplateListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates/"
)({
component: PkiTemplateListPage
});
@@ -79,14 +79,6 @@ export const DeleteProjectSection = () => {
};
const handleLeaveWorkspaceSubmit = async () => {
console.log({
currentWorkspace,
currentOrg,
members,
isNoAccessMember,
membership
});
try {
setIsLeaving.on();
@@ -170,7 +162,7 @@ export const DeleteProjectSection = () => {
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
title="Are you sure you want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
@@ -180,8 +172,8 @@ export const DeleteProjectSection = () => {
<LeaveProjectModal
isOpen={popUp.leaveWorkspace.isOpen}
title="Are you sure want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
title="Are you sure you want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and its contents.`}
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Leave Project"
@@ -44,7 +44,7 @@ export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Prop
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteKmipClient}
@@ -18,7 +18,7 @@ export const OverviewPage = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="OverviewPage"
title="Overview Page"
description="Manage keys and perform cryptographic operations."
/>
<ProjectPermissionCan
@@ -43,7 +43,7 @@ export const DeleteCmekModal = ({ isOpen, onOpenChange, cmek }: Props) => {
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteCmek}
@@ -79,14 +79,6 @@ export const DeleteProjectSection = () => {
};
const handleLeaveWorkspaceSubmit = async () => {
console.log({
currentWorkspace,
currentOrg,
members,
isNoAccessMember,
membership
});
try {
setIsLeaving.on();
@@ -170,7 +162,7 @@ export const DeleteProjectSection = () => {
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
title="Are you sure you want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
@@ -180,8 +172,8 @@ export const DeleteProjectSection = () => {
<LeaveProjectModal
isOpen={popUp.leaveWorkspace.isOpen}
title="Are you sure want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
title="Are you sure you want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and its contents.`}
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Leave Project"
@@ -5,6 +5,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
@@ -72,6 +73,8 @@ export const AccessManagementPage = () => {
}
];
const hasNoAccess = tabSections.every((tab) => tab.isHidden);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<Helmet>
@@ -126,6 +129,7 @@ export const AccessManagementPage = () => {
))}
</Tabs>
</div>
{hasNoAccess && <OrgPermissionGuardBanner />}
</div>
);
};
@@ -80,7 +80,7 @@ export const OrgGroupsSection = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
title={`Are you sure you want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
@@ -33,13 +33,19 @@ import {
useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth
} from "@app/hooks/api";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import {
IdentityKubernetesAuthTokenReviewMode,
IdentityTrustedIp
} from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
tokenReviewMode: z
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
.default(IdentityKubernetesAuthTokenReviewMode.Api),
kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string().optional(),
gatewayId: z.string().optional().nullable(),
@@ -62,7 +68,15 @@ const schema = z
)
.min(1)
})
.required();
.superRefine((data, ctx) => {
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
ctx.addIssue({
path: ["gatewayId"],
code: z.ZodIssueCode.custom,
message: "When token review mode is set to Gateway, a gateway must be selected"
});
}
});
export type FormData = z.infer<typeof schema>;
@@ -100,11 +114,14 @@ export const IdentityKubernetesAuthForm = ({
control,
handleSubmit,
reset,
watch,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api,
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
@@ -128,6 +145,7 @@ export const IdentityKubernetesAuthForm = ({
useEffect(() => {
if (data) {
reset({
tokenReviewMode: data.tokenReviewMode,
kubernetesHost: data.kubernetesHost,
tokenReviewerJwt: data.tokenReviewerJwt,
allowedNames: data.allowedNames,
@@ -148,6 +166,7 @@ export const IdentityKubernetesAuthForm = ({
});
} else {
reset({
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode.Api,
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
@@ -173,6 +192,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenMaxTTL,
accessTokenNumUsesLimit,
gatewayId,
tokenReviewMode,
accessTokenTrustedIps
}: FormData) => {
try {
@@ -189,6 +209,7 @@ export const IdentityKubernetesAuthForm = ({
caCert,
identityId,
gatewayId: gatewayId || null,
tokenReviewMode,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@@ -205,6 +226,7 @@ export const IdentityKubernetesAuthForm = ({
allowedAudience: allowedAudience || "",
gatewayId: gatewayId || null,
caCert: caCert || "",
tokenReviewMode,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@@ -228,6 +250,8 @@ export const IdentityKubernetesAuthForm = ({
}
};
const tokenReviewMode = watch("tokenReviewMode");
return (
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
@@ -235,6 +259,7 @@ export const IdentityKubernetesAuthForm = ({
[
"kubernetesHost",
"tokenReviewerJwt",
"tokenReviewMode",
"gatewayId",
"accessTokenTTL",
"accessTokenMaxTTL",
@@ -269,21 +294,113 @@ export const IdentityKubernetesAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
<div className="flex w-full items-center gap-2">
<div className="w-full flex-1">
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
if (v === null) {
setValue(
"tokenReviewMode",
IdentityKubernetesAuthTokenReviewMode.Api,
{
shouldDirty: true,
shouldTouch: true
}
);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => {
onChange(null);
}}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
</div>
<Controller
control={control}
name="tokenReviewMode"
render={({ field }) => (
<FormControl
tooltipClassName="max-w-md"
tooltipText="The method of which tokens are reviewed. If you select Gateway as Reviewer, the selected gateway will be used to review tokens with. If this option is enabled, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding."
label="Review Mode"
>
<Select value={field.value} onValueChange={field.onChange}>
<SelectItem value="gateway">Gateway as Reviewer</SelectItem>
<SelectItem value="api">Manual Token Reviewer JWT (API)</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
{tokenReviewMode === "api" && (
<Controller
control={control}
name="tokenReviewerJwt"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipClassName="max-w-md"
label="Token Reviewer JWT"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
>
<Input {...field} placeholder="" type="password" />
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue=""
@@ -300,61 +417,6 @@ export const IdentityKubernetesAuthForm = ({
)}
/>
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(null)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller
control={control}
name="allowedNames"
@@ -127,7 +127,7 @@ export const IdentitySection = withPermission(
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
@@ -159,6 +159,7 @@ export const AddOrgMemberModal = ({
text: "Failed to invite user to org",
type: "error"
});
return;
}
if (serverDetails?.emailConfigured) {
@@ -143,7 +143,7 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
title={`Are you sure want to remove member with username ${
title={`Are you sure you want to remove member with username ${
(popUp?.removeMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
@@ -156,7 +156,7 @@ export const OrgMembersSection = () => {
/>
<DeleteActionModal
isOpen={popUp.deactivateMember.isOpen}
title={`Are you sure want to deactivate member with username ${
title={`Are you sure you want to deactivate member with username ${
(popUp?.deactivateMember?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deactivateMember", isOpen)}
@@ -1,8 +1,11 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faChevronRight,
faEllipsis,
faFilter,
faMagnifyingGlass,
faSearch,
faUsers
@@ -19,7 +22,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
EmptyState,
IconButton,
Input,
@@ -75,6 +82,10 @@ enum OrgMembersOrderBy {
Email = "email"
}
type Filter = {
roles: string[];
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
const navigate = useNavigate();
const { subscription } = useSubscription();
@@ -184,17 +195,29 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setUserTablePreference("orgMembersTable", PreferenceKey.PerPage, newPerPage);
};
const [filter, setFilter] = useState<Filter>({
roles: []
});
const filteredUsers = useMemo(
() =>
members
?.filter(
({ user: u, inviteEmail }) =>
?.filter(({ user: u, inviteEmail, role, roleId }) => {
if (
filter.roles.length &&
!filter.roles.includes(role === "custom" ? findRoleFromId(roleId)!.slug : role)
) {
return false;
}
return (
u?.firstName?.toLowerCase().includes(search.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(search.toLowerCase()) ||
u?.username?.toLowerCase().includes(search.toLowerCase()) ||
u?.email?.toLowerCase().includes(search.toLowerCase()) ||
inviteEmail?.toLowerCase().includes(search.toLowerCase())
)
);
})
.sort((a, b) => {
const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
@@ -217,7 +240,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase());
}),
[members, search, orderDirection, orderBy]
[members, search, orderDirection, orderBy, filter]
);
const handleSort = (column: OrgMembersOrderBy) => {
@@ -236,14 +259,81 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
setPage
});
const handleRoleToggle = useCallback(
(roleSlug: string) =>
setFilter((state) => {
const currentRoles = state.roles || [];
if (currentRoles.includes(roleSlug)) {
return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) };
}
return { ...state, roles: [...currentRoles, roleSlug] };
}),
[]
);
const isTableFiltered = Boolean(filter.roles.length);
return (
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Users"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-0">
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Roles
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Users
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleRoleToggle(slug);
}}
key={id}
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
</DropdownMenuContent>
</DropdownMenu>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
@@ -280,7 +280,7 @@ export const OrgRoleTable = () => {
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteRole?.data as TOrgRole)?.name || " "
} role?`}
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
@@ -67,6 +67,15 @@ export const OrgAdminProjects = withPermission(
});
return;
}
if (type === ProjectType.SecretScanning) {
await navigate({
to: "/secret-scanning/$projectId/data-sources" as const,
params: {
projectId
}
});
return;
}
await navigate({
to: `/${type}/$projectId/overview` as const,
params: {
@@ -19,10 +19,12 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { GitHubRadarConnectionForm } from "./GitHubRadarConnectionForm";
import { HCVaultConnectionForm } from "./HCVaultConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
@@ -71,6 +73,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
@@ -89,6 +93,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <PostgresConnectionForm onSubmit={onSubmit} />;
case AppConnection.MsSql:
return <MsSqlConnectionForm onSubmit={onSubmit} />;
case AppConnection.MySql:
return <MySqlConnectionForm onSubmit={onSubmit} />;
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureClientSecrets:
@@ -147,6 +153,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm appConnection={appConnection} />;
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
@@ -165,6 +173,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <PostgresConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MsSql:
return <MsSqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MySql:
return <MySqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.AzureClientSecrets:
@@ -183,7 +193,6 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <OCIConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OnePass:
return <OnePassConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,157 @@
import crypto from "crypto";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubRadarConnectionMethod,
TGitHubRadarConnection,
useGetAppConnectionOption
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TGitHubRadarConnection;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHubRadar),
method: z.nativeEnum(GitHubRadarConnectionMethod)
});
type FormData = z.infer<typeof formSchema>;
export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
const {
option: { appClientSlug },
isLoading
} = useGetAppConnectionOption(AppConnection.GitHubRadar);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.GitHubRadar,
method: GitHubRadarConnectionMethod.App
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
);
switch (formData.method) {
case GitHubRadarConnectionMethod.App:
window.location.assign(
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}`
);
break;
default:
throw new Error(
`Unhandled GitHub Radar Connection method: ${(formData as FormData).method}`
);
}
};
let isMissingConfig: boolean;
switch (selectedMethod) {
case GitHubRadarConnectionMethod.App:
isMissingConfig = !appClientSlug;
break;
default:
throw new Error(`Unhandled GitHub Radar Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.GitHubRadar].name
}. This field cannot be changed after creation.`}
errorText={
!isLoading && isMissingConfig
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See Docs to configure GitHub Radar ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(GitHubRadarConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,155 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { MySqlConnectionMethod, TMySqlConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
import {
BaseSqlUsernameAndPasswordConnectionSchema,
PlatformManagedNoticeBanner,
SqlConnectionFields
} from "./shared";
type Props = {
appConnection?: TMySqlConnection;
onSubmit: (formData: FormData) => Promise<void>;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.MySql),
isPlatformManagedCredentials: z.boolean().optional()
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(MySqlConnectionMethod.UsernameAndPassword),
credentials: BaseSqlUsernameAndPasswordConnectionSchema
})
]);
type FormData = z.infer<typeof formSchema>;
export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [showConfirmation, setShowConfirmation] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.MySql,
method: MySqlConnectionMethod.UsernameAndPassword,
credentials: {
host: "",
port: 3306,
database: "default",
username: "",
password: "",
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: undefined
}
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
const confirmSubmit = async (formData: FormData) => {
if (formData.isPlatformManagedCredentials) {
setShowConfirmation(true);
return;
}
await onSubmit(formData);
};
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
setSelectedTabIndex(0);
handleSubmit(confirmSubmit)(e);
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.MySql].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(MySqlConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<SqlConnectionFields
isPlatformManagedCredentials={isPlatformManagedCredentials}
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
/>
{isPlatformManagedCredentials ? (
<PlatformManagedNoticeBanner />
) : (
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Database"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
)}
</form>
<PlatformManagedConfirmationModal
onConfirm={() => handleSubmit(onSubmit)()}
onOpenChange={setShowConfirmation}
isOpen={showConfirmation}
/>
</FormProvider>
);
};
@@ -15,11 +15,19 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
return (
<div className="mb-4 flex w-full items-start gap-2 border-b border-mineshaft-500 pb-4">
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div className="relative">
<img
alt={`${appDetails.name} logo`}
src={`/images/integrations/${appDetails.image}`}
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
{appDetails.icon && (
<FontAwesomeIcon
icon={appDetails.icon}
className="absolute bottom-1 right-1 text-primary-700"
/>
)}
</div>
<div>
<div className="flex items-center text-mineshaft-300">
{appDetails.name}
@@ -31,7 +31,7 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
return (
<div className="grid grid-cols-4 gap-2">
{appConnectionOptions?.map((option) => {
const { image, name, size = 50, enterprise = false } = APP_CONNECTION_MAP[option.app];
const { image, name, size = 50, enterprise = false, icon } = APP_CONNECTION_MAP[option.app];
return (
<button
@@ -43,14 +43,23 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
>
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
{icon && (
<FontAwesomeIcon
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
size="xl"
icon={icon}
/>
)}
</div>
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
@@ -65,6 +65,8 @@ export const AppConnectionRow = ({
const methodDetails = getAppConnectionMethodDetails(method);
const connectionDetails = APP_CONNECTION_MAP[app];
return (
<Tr
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
@@ -72,12 +74,21 @@ export const AppConnectionRow = ({
>
<Td>
<div className="flex items-center gap-2">
<img
alt={`${APP_CONNECTION_MAP[app].name} integration`}
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
className="mr-0.5 w-5"
/>
<span className="hidden lg:inline">{APP_CONNECTION_MAP[app].name}</span>
<div className="relative">
<img
alt={`${connectionDetails.name} integration`}
src={`/images/integrations/${connectionDetails.image}`}
className="mr-0.5 w-5"
/>
{connectionDetails.icon && (
<FontAwesomeIcon
icon={connectionDetails.icon}
size="xs"
className="absolute -bottom-0.5 -right-0.5 text-primary-700"
/>
)}
</div>
<span className="hidden lg:inline">{connectionDetails.name}</span>
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">
@@ -44,7 +44,7 @@ export const DeleteAppConnectionModal = ({ isOpen, onOpenChange, appConnection }
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title={`Are you sure want to delete ${name}?`}
title={`Are you sure you want to delete ${name}?`}
deleteKey={name}
onDeleteApproved={handleDeleteAppConnection}
>
@@ -14,6 +14,7 @@ import {
TAzureClientSecretsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
@@ -26,6 +27,9 @@ type BaseFormData = {
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
@@ -40,6 +44,7 @@ type AzureClientSecretsFormData = BaseFormData &
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
@@ -51,6 +56,7 @@ type FormDataMap = {
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData"
@@ -318,6 +324,54 @@ export const OAuthCallbackPage = () => {
};
}, []);
const handleGithubRadar = useCallback(async () => {
const formData = getFormData(AppConnection.GitHubRadar);
if (formData === null) return null;
clearState(AppConnection.GitHubRadar);
const { connectionId, name, description, returnUrl } = formData;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
code: code as string,
installationId: installationId as string
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
credentials: {
code: code as string,
installationId: installationId as string
}
});
}
} catch (e: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} GitHub Radar Connection`,
text: e.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
}, []);
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
useEffect(() => {
if (!isReady) {
@@ -334,6 +388,8 @@ export const OAuthCallbackPage = () => {
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
} else if (appConnection === AppConnection.GitHubRadar) {
data = await handleGithubRadar();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();
} else if (appConnection === AppConnection.AzureAppConfiguration) {
@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { BillingTabGroup } from "./components";
@@ -24,7 +24,7 @@ export const BillingPage = () => {
</div>
<OrgPermissionCan
passThrough={false}
I={OrgPermissionActions.Read}
I={OrgPermissionBillingActions.Read}
a={OrgPermissionSubjects.Billing}
>
<BillingTabGroup />
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionBillingActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
@@ -112,7 +112,10 @@ export const PreviewSection = () => {
Get unlimited members, projects, RBAC, smart alerts, and so much more.
</p>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button
onClick={() => handleUpgradeBtnClick()}
@@ -156,7 +159,10 @@ export const PreviewSection = () => {
}`}
</p>
{isInfisicalCloud() && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<button
type="button"
@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
const schema = z
@@ -74,7 +74,10 @@ export const CompanyNameSection = () => {
name="name"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button
type="submit"
@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
const schema = z
@@ -75,7 +75,10 @@ export const InvoiceEmailSection = () => {
name="email"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button
type="submit"
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useAddOrgPmtMethod } from "@app/hooks/api";
import { PmtMethodsTable } from "./PmtMethodsTable";
@@ -27,7 +27,10 @@ export const PmtMethodsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Payment methods</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button
onClick={handleAddPmtMethodBtnClick}
@@ -16,7 +16,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
@@ -75,7 +75,7 @@ export const PmtMethodsTable = () => {
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { TaxIDModal } from "./TaxIDModal";
@@ -18,7 +18,10 @@ export const TaxIDSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Tax ID</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<Button
onClick={() => handlePopUpOpen("addTaxID")}
@@ -14,7 +14,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = {
@@ -103,7 +103,7 @@ export const TaxIDTable = () => {
<Td>{value}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
@@ -1,5 +1,5 @@
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { withPermission } from "@app/hoc";
@@ -47,5 +47,5 @@ export const BillingTabGroup = withPermission(
</Tabs>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
{ action: OrgPermissionBillingActions.Read, subject: OrgPermissionSubjects.Billing }
);
@@ -229,7 +229,7 @@ export const GatewayListPage = withPermission(
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure want to delete gateway ${
title={`Are you sure you want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
@@ -157,7 +157,7 @@ const Page = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${
title={`Are you sure you want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
@@ -94,7 +94,7 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
<AddGroupMembersModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.removeMemberFromGroup.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeMemberFromGroup?.data as { username: string })?.username || ""
} from the group?`}
onChange={(isOpen) => handlePopUpToggle("removeMemberFromGroup", isOpen)}
@@ -101,7 +101,7 @@ const Page = () => {
/>
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
title={`Are you sure you want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
@@ -66,7 +66,7 @@ export const IdentityProjectsSection = ({ identityId }: Props) => {
</div>
<DeleteActionModal
isOpen={popUp.removeIdentityFromProject.isOpen}
title={`Are you sure want to remove ${
title={`Are you sure you want to remove ${
(popUp?.removeIdentityFromProject?.data as { identityName: string })?.identityName || ""
} from ${
(popUp?.removeIdentityFromProject?.data as { projectName: string })?.projectName || ""
@@ -218,7 +218,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
title={`Are you sure you want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
@@ -175,7 +175,7 @@ export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identit
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
title={`Are you sure you want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
@@ -151,7 +151,7 @@ export const Content = ({
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
title={`Are you sure you want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
@@ -155,7 +155,7 @@ export const Page = () => {
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteOrgRole.isOpen}
title={`Are you sure want to delete the organization role ${data?.name ?? ""}?`}
title={`Are you sure you want to delete the organization role ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrgRole", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => onDeleteOrgRoleSubmit()}
@@ -5,6 +5,7 @@ import { OrgPermissionSubjects } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionAppConnectionActions,
OrgPermissionBillingActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionKmipActions,
@@ -21,6 +22,13 @@ const generalPermissionSchema = z
})
.optional();
const billingPermissionSchema = z
.object({
[OrgPermissionBillingActions.Read]: z.boolean().optional(),
[OrgPermissionBillingActions.ManageBilling]: z.boolean().optional()
})
.optional();
const appConnectionsPermissionSchema = z
.object({
[OrgPermissionAppConnectionActions.Read]: z.boolean().optional(),
@@ -113,7 +121,7 @@ export const formSchema = z.object({
scim: generalPermissionSchema,
[OrgPermissionSubjects.GithubOrgSync]: generalPermissionSchema,
ldap: generalPermissionSchema,
billing: generalPermissionSchema,
billing: billingPermissionSchema,
identity: identityPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
@@ -0,0 +1,174 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionBillingActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
const PERMISSION_ACTIONS = [
{ action: OrgPermissionBillingActions.Read, label: "View bills" },
{ action: OrgPermissionBillingActions.ManageBilling, label: "Manage billing" }
] as const;
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.billing"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.[OrgPermissionBillingActions.Read]) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: false,
[OrgPermissionBillingActions.ManageBilling]: false
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: true,
[OrgPermissionBillingActions.ManageBilling]: false
},
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: true,
[OrgPermissionBillingActions.ManageBilling]: true
},
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: false,
[OrgPermissionBillingActions.ManageBilling]: false
},
{ shouldDirty: true }
);
break;
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Billing</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.billing.${action}`}
key={`permissions.billing.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.billing.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -38,13 +38,6 @@ const MEMBERS_PERMISSIONS = [
{ action: "delete", label: "Remove members" }
] as const;
const BILLING_PERMISSIONS = [
{ action: "read", label: "View bills" },
{ action: "create", label: "Add payment methods" },
{ action: "edit", label: "Edit payments" },
{ action: "delete", label: "Remove payments" }
] as const;
const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "read", label: "View & Apply" },
{ action: "create", label: "Create" },
@@ -52,18 +45,16 @@ const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "delete", label: "Remove" }
] as const;
const getPermissionList = (option: string) => {
switch (option) {
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
case "billing":
return BILLING_PERMISSIONS;
case "incident-contact":
return INCIDENT_CONTACTS_PERMISSIONS;
const getPermissionList = (formName: Props["formName"]) => {
switch (formName) {
case "member":
return MEMBERS_PERMISSIONS;
case OrgPermissionSubjects.ProjectTemplates:
return PROJECT_TEMPLATES_PERMISSIONS;
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
case "incident-contact":
return INCIDENT_CONTACTS_PERMISSIONS;
default:
return PERMISSIONS;
}
@@ -74,7 +65,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share"
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share" | "billing"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
@@ -14,6 +14,7 @@ import {
TFormSchema
} from "../OrgRoleModifySection.utils";
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgPermissionBillingRow } from "./OrgPermissionBillingRow";
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
@@ -27,10 +28,6 @@ const SIMPLE_PERMISSION_OPTIONS = [
title: "User Management",
formName: "member"
},
{
title: "Usage & Billing",
formName: "billing"
},
{
title: "Role Management",
formName: "role"
@@ -186,6 +183,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionBillingRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionSecretShareRow
control={control}
setValue={setValue}
@@ -24,6 +24,8 @@ const formatDescription = (type: ProjectType) => {
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
if (type === ProjectType.SecretScanning)
return "Connect and monitor data sources to prevent secret leaks.";
return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
};
@@ -0,0 +1,7 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const SecretScanningOverviewPage = () => (
<ProductOverviewPage type={ProjectType.SecretScanning} />
);
@@ -0,0 +1,23 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretScanningOverviewPage } from "./SecretScanningOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning/overview"
)({
component: SecretScanningOverviewPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Scanning",
link: linkOptions({ to: "/organization/secret-scanning/overview" })
}
]
})
});
@@ -1,227 +0,0 @@
import { useEffect } from "react";
import { Helmet } from "react-helmet";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner, PageHeader, Pagination } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useServerConfig
} from "@app/context";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { withPermission } from "@app/hoc";
import { usePagination, usePopUp } from "@app/hooks";
import {
useCreateNewInstallationSession,
useGetSecretScanningInstallationStatus,
useGetSecretScanningRisks,
useLinkGitAppInstallationWithOrg
} from "@app/hooks/api/secretScanning";
import { SecretScanningOrderBy } from "@app/hooks/api/secretScanning/types";
import { ExportSecretScansModal } from "./components/ExportSecretScansModal";
import { SecretScanningFilter } from "./components/SecretScanningFilters";
import { SecretScanningFilterFormData, secretScanningFilterFormSchema } from "./components/types";
import { SecretScanningLogsTable } from "./components";
export const SecretScanningPage = withPermission(
() => {
const queryParams = useSearch({
from: ROUTE_PATHS.Organization.SecretScanning.id
});
const navigate = useNavigate();
const { control, watch } = useForm<SecretScanningFilterFormData>({
resolver: zodResolver(secretScanningFilterFormSchema),
defaultValues: {}
});
const { config } = useServerConfig();
const { currentOrg } = useOrganization();
const organizationId = currentOrg.id;
const { offset, limit, orderBy, setPage, perPage, page, setPerPage } = usePagination(
SecretScanningOrderBy.CreatedAt,
{ initPerPage: getUserTablePreference("secretScanningTable", PreferenceKey.PerPage, 20) }
);
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("secretScanningTable", PreferenceKey.PerPage, newPerPage);
};
const repositoryNames = watch("repositoryNames");
const resolvedStatus = watch("resolved");
const { isPending, data: risksData } = useGetSecretScanningRisks(
organizationId,
{
offset,
limit,
orderBy
},
{
repositoryNames:
repositoryNames?.length > 0 ? repositoryNames.map((repo) => repo.name) : undefined,
resolvedStatus
}
);
const { mutateAsync: linkGitAppInstallationWithOrganization } =
useLinkGitAppInstallationWithOrg();
const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
const {
data: installationStatus,
isPending: isSecretScanningInstatllationStatusLoading,
refetch: refetchSecretScanningInstallationStatus
} = useGetSecretScanningInstallationStatus(organizationId);
const integrationEnabled =
!isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
const { handlePopUpToggle, popUp } = usePopUp(["exportSecretScans"]);
useEffect(() => {
const linkInstallation = async () => {
if (queryParams.state && queryParams.installation_id) {
try {
const isLinked = await linkGitAppInstallationWithOrganization({
installationId: String(queryParams.installation_id),
sessionId: String(queryParams.state)
});
if (isLinked) {
await navigate({
to: "/organization/secret-scanning"
});
refetchSecretScanningInstallationStatus();
}
console.log("installation verification complete");
} catch (e) {
console.log("app installation is stale, start new session", e);
}
}
};
linkInstallation();
}, [queryParams.state, queryParams.installation_id]);
const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession({ organizationId });
window.location.href = `https://github.com/apps/${session.gitAppSlug}/installations/new?state=${session.sessionId}`;
};
return (
<div className="py-2">
<Helmet>
<title>Secret scanning</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
title="Secret Scanning"
description="Automatically monitor your GitHub activity and prevent secret leaks"
/>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased
usage.
</NoticeBanner>
)}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex flex-col items-start">
<div className="mb-1 flex flex-row">
Secret Scanning Status:{" "}
{integrationEnabled ? (
<p className="ml-1.5 font-semibold text-green">Enabled</p>
) : (
<p className="ml-1.5 font-semibold text-red">Not enabled</p>
)}
</div>
<div>
{integrationEnabled ? (
<p className="text-mineshaft-300">
Your GitHub organization is connected to Infisical, and is being continuously
monitored for secret leaks.
</p>
) : (
<p className="text-mineshaft-300">
Connect your GitHub organization to Infisical.
</p>
)}
</div>
</div>
{integrationEnabled ? (
<div>
<div className="absolute right-[2.5rem] top-[2.5rem] flex h-6 w-6 animate-ping items-center justify-center rounded-full bg-green" />
<div className="absolute right-[2.63rem] top-[2.63rem] flex h-5 w-5 animate-ping items-center justify-center rounded-full bg-green" />
<div className="absolute right-[2.82rem] top-[2.82rem] flex h-3.5 w-3.5 animate-ping items-center justify-center rounded-full bg-green" />
</div>
) : (
<div className="flex h-[3.25rem] items-center">
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.SecretScanning}
>
{(isAllowed) => (
<Button
variant="solid"
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="h-min py-2"
isDisabled={!isAllowed}
>
Integrate with GitHub
</Button>
)}
</OrgPermissionCan>
</div>
)}
</div>
<div className="mt-8 space-y-2">
{integrationEnabled && (
<div className="flex w-full items-center justify-end">
<SecretScanningFilter
repositories={risksData?.repos || []}
handlePopUpToggle={handlePopUpToggle}
control={control}
/>
</div>
)}
<SecretScanningLogsTable gitRisks={risksData?.risks} isPending={isPending} />
{!isPending && risksData?.totalCount !== undefined && risksData.totalCount >= 10 && (
<Pagination
className="rounded-md"
count={risksData.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
</div>
</div>
</div>
<ExportSecretScansModal
repositories={risksData?.repos || []}
handlePopUpToggle={handlePopUpToggle}
popUp={popUp}
/>
</div>
);
},
{
action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.SecretScanning
}
);
@@ -1,200 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useExportSecretScanningRisks } from "@app/hooks/api/secretScanning";
import { SecretScanningResolvedStatus } from "@app/hooks/api/secretScanning/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { convertJsonToCsv } from "@app/lib/fn/csv";
type Props = {
popUp: UsePopUpState<["exportSecretScans"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["exportSecretScans"]>,
state?: boolean
) => void;
repositories: string[];
};
enum ExportFormat {
Json = "json",
Csv = "csv"
}
const formSchema = z.object({
githubRepository: z.string().trim(),
status: z.nativeEnum(SecretScanningResolvedStatus),
exportFormat: z.nativeEnum(ExportFormat)
});
type TFormSchema = z.infer<typeof formSchema>;
export const ExportSecretScansModal = ({ popUp, handlePopUpToggle, repositories }: Props) => {
const { currentOrg } = useOrganization();
const { mutateAsync } = useExportSecretScanningRisks();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
defaultValues: {
exportFormat: ExportFormat.Json,
status: SecretScanningResolvedStatus.All,
githubRepository: "all"
}
});
const onFormSubmit = async (data: TFormSchema) => {
const gitRisks = await mutateAsync({
orgId: currentOrg.id,
filter: {
repositoryNames: data.githubRepository !== "all" ? [data.githubRepository] : undefined,
resolvedStatus: data.status
}
});
const filteredRisks = gitRisks
.filter((risk) =>
// eslint-disable-next-line no-nested-ternary
data.status === SecretScanningResolvedStatus.All
? true
: data.status === SecretScanningResolvedStatus.Resolved
? risk.isResolved
: !risk.isResolved
)
.filter((risk) => {
if (data.githubRepository === "all") return true;
return risk.repositoryFullName === data.githubRepository;
});
const formattedRisks = filteredRisks.map((risk) => {
return {
repository: risk.repositoryFullName,
fileName: risk.file,
isResolved: risk.isResolved ? "Resolved" : "Needs Attention",
status: risk.status || "UNRESOLVED",
entropy: risk.entropy,
secretType: risk.ruleID,
exposedSecretLink: `https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`,
commitAuthor: risk.author,
commitAuthorEmail: risk.email,
commitDate: risk.createdAt,
commitMessage: risk.message,
commitFingerprint: risk.fingerprint
};
});
const fileName = `infisical-secret-scans-${new Date().toISOString()}`;
if (data.exportFormat === ExportFormat.Csv) {
const csvBlob = convertJsonToCsv(formattedRisks);
FileSaver.saveAs(csvBlob, `${fileName}.csv`);
} else if (data.exportFormat === ExportFormat.Json) {
const json = JSON.stringify(formattedRisks, null, 2);
const blob = new Blob([json], { type: "application/json" });
FileSaver.saveAs(blob, `${fileName}.json`);
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Modal
isOpen={popUp?.exportSecretScans?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("exportSecretScans", isOpen);
}}
>
<ModalContent
title="Export Secret Scans"
footerContent={[
<Button
onClick={() => handleSubmit(onFormSubmit)()}
isLoading={isSubmitting}
type="submit"
colorSchema="primary"
>
Export secret scans
</Button>,
<Button
key="keep-old-btn"
className="ml-4"
onClick={() => handlePopUpToggle("exportSecretScans", false)}
variant="outline_bg"
isDisabled={isSubmitting}
>
Cancel
</Button>
]}
>
<Controller
control={control}
name="exportFormat"
render={({ field: { value, onChange } }) => (
<FormControl label="Export Format">
<Select
defaultValue={ExportFormat.Json}
value={value}
onValueChange={onChange}
className="w-full"
>
{Object.values(ExportFormat).map((format) => (
<SelectItem key={format} value={format}>
{format.toUpperCase()}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="status"
render={({ field: { value, onChange } }) => (
<FormControl label="Risk Status">
<Select
defaultValue="all"
value={value}
onValueChange={onChange}
className="w-full"
>
{Object.values(SecretScanningResolvedStatus).map((status) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="githubRepository"
render={({ field: { value, onChange } }) => (
<FormControl label="GitHub Repository">
<Select
defaultValue="all"
value={value}
onValueChange={onChange}
className="w-full"
>
<SelectItem value="all">All Repositories</SelectItem>
{repositories.map((repoName) => (
<SelectItem key={repoName} value={repoName}>
{repoName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</ModalContent>
</Modal>
</form>
);
};

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