mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 10:29:17 +00:00
Merge branch 'main' into feature/mongodb-secret-rotation
This commit is contained in:
@@ -76,8 +76,8 @@ export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) =
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-medium text-mineshaft-100">Project Overview</h2>
|
||||
<div className="justify-betweens mb-8 flex flex-wrap gap-2">
|
||||
<h2 className="flex-1 text-xl font-medium text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
|
||||
@@ -59,6 +59,8 @@ export type TViewSharedSecretResponse = {
|
||||
tag: string;
|
||||
accessType: SecretSharingAccessType;
|
||||
orgName?: string;
|
||||
expiresAt?: Date | string;
|
||||
expiresAfterViews?: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
@@ -15,7 +12,6 @@ import { InsecureConnectionBanner } from "../OrganizationLayout/components/Insec
|
||||
import { AdminNavBar } from "./AdminNavBar";
|
||||
|
||||
export const AdminLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useServerConfig();
|
||||
const { data: serverDetails, isLoading } = useFetchServerStatus();
|
||||
const { subscription } = useSubscription();
|
||||
@@ -26,7 +22,7 @@ export const AdminLayout = () => {
|
||||
<>
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
className={`dark ${containerHeight} flex w-full flex-col overflow-x-hidden bg-bunker-800 transition-all`}
|
||||
>
|
||||
<Navbar />
|
||||
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />}
|
||||
@@ -40,12 +36,6 @@ export const AdminLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const KmsLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Outlet, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -27,8 +24,6 @@ export const OrganizationLayout = () => {
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
const { data: serverDetails, isLoading } = useFetchServerStatus();
|
||||
@@ -38,7 +33,7 @@ export const OrganizationLayout = () => {
|
||||
<>
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
className={`dark ${containerHeight} flex w-full flex-col overflow-x-hidden bg-bunker-800 transition-all`}
|
||||
>
|
||||
<Navbar />
|
||||
<div className="flex grow flex-col overflow-y-hidden">
|
||||
@@ -61,12 +56,6 @@ export const OrganizationLayout = () => {
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,15 +16,19 @@ import {
|
||||
faSignOut,
|
||||
faToolbox,
|
||||
faUser,
|
||||
faUserCog,
|
||||
faUserPlus,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { UserPlusIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
BreadcrumbContainer,
|
||||
@@ -44,7 +48,13 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
@@ -294,7 +304,7 @@ export const Navbar = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center overflow-hidden">
|
||||
<div className="flex min-w-12 items-center overflow-hidden">
|
||||
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
||||
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
<Badge
|
||||
@@ -324,7 +334,7 @@ export const Navbar = () => {
|
||||
<span>{currentOrg?.name}</span>
|
||||
</button>
|
||||
</Badge>
|
||||
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
|
||||
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
|
||||
{getPlan(subscription)}
|
||||
</div>
|
||||
{subscription.cardDeclined && (
|
||||
@@ -468,7 +478,7 @@ export const Navbar = () => {
|
||||
className={twMerge(
|
||||
"gap-x-1.5 text-sm",
|
||||
!isOrgScope &&
|
||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||
"min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||
)}
|
||||
>
|
||||
<Link to="/organization/projects">
|
||||
@@ -567,15 +577,36 @@ export const Navbar = () => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{user.superAdmin && !location.pathname.startsWith("/admin") && (
|
||||
<Link
|
||||
className="mr-2 flex items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
to="/admin"
|
||||
>
|
||||
<InstanceIcon className="mr-2 inline-block size-3.5" />
|
||||
Server Console
|
||||
</Link>
|
||||
)}
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{!location.pathname.startsWith("/admin") ? (
|
||||
user.superAdmin ? (
|
||||
<Link
|
||||
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
to="/admin"
|
||||
>
|
||||
<InstanceIcon className="inline-block size-3.5" />
|
||||
<span className="ml-2 hidden md:inline-block">Server Console</span>
|
||||
</Link>
|
||||
) : (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Link
|
||||
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
to="/organization/access-management"
|
||||
search={{
|
||||
selectedTab: "members",
|
||||
action: "invite-members"
|
||||
}}
|
||||
>
|
||||
<UserPlusIcon className="inline-block size-3.5" />
|
||||
<span className="ml-2 hidden md:inline-block">Invite Members</span>
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
)
|
||||
) : null}
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger>
|
||||
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">
|
||||
@@ -657,8 +688,27 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/personal-settings">
|
||||
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserCog} />}>
|
||||
Personal Settings
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Link
|
||||
to="/organization/access-management"
|
||||
search={{
|
||||
selectedTab: "members",
|
||||
action: "invite-members"
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
||||
Invite Members
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
|
||||
@@ -21,7 +21,7 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{!isHidden && (
|
||||
<div className="dark hidden w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4 md:flex">
|
||||
<div className="dark flex w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4">
|
||||
<motion.div
|
||||
key="menu-org-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PamLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
|
||||
export const PersonalSettingsLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 md:flex">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<div className="dark flex h-screen w-full flex-col overflow-x-hidden bg-bunker-800">
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
@@ -17,7 +14,6 @@ export const PkiManagerLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: subscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
||||
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||
@@ -32,147 +28,137 @@ export const PkiManagerLayout = () => {
|
||||
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/policies"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Certificates</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}>
|
||||
Certificate Authorities
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/alerting"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Alerting</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/integrations"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
|
||||
)}
|
||||
</Link>
|
||||
{showLegacySection && (
|
||||
<>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Subscribers (Legacy)</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-templates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Certificate Templates (Legacy)
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/policies"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Certificates</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}>
|
||||
Certificate Authorities
|
||||
</Tab>
|
||||
)}
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/alerting"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Alerting</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/integrations"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>App Connections</Tab>}
|
||||
</Link>
|
||||
{showLegacySection && (
|
||||
<>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Subscribers (Legacy)</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-templates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Certificate Templates (Legacy)
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -106,7 +106,9 @@ export const ProjectSelect = () => {
|
||||
</p>
|
||||
<Badge variant="project">
|
||||
<ProjectIcon />
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
<span className="hidden sm:inline-block">
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
</span>
|
||||
</Badge>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
@@ -20,7 +17,6 @@ export const SecretManagerLayout = () => {
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const location = useLocation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const projectSlug = currentProject?.slug || "";
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
|
||||
@@ -42,141 +38,131 @@ export const SecretManagerLayout = () => {
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id,
|
||||
...(currentProject.environments.length
|
||||
? { envSlug: currentProject.environments[0]?.slug }
|
||||
: {})
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive || location.pathname.match(/\/secrets\/|\/commits\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Overview
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/approval"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="warning" isSquare className="ml-1.5">
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/integrations"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
|
||||
</Link>
|
||||
{Boolean(secretRotations?.length) && (
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/secret-rotation"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id,
|
||||
...(currentProject.environments.length
|
||||
? { envSlug: currentProject.environments[0]?.slug }
|
||||
: {})
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive || location.pathname.match(/\/secrets\/|\/commits\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
|
||||
)}
|
||||
</Link>
|
||||
Overview
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/approval"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="warning" isSquare className="ml-1.5">
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/integrations"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
|
||||
</Link>
|
||||
{Boolean(secretRotations?.length) && (
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/app-connections"
|
||||
to="/projects/secret-management/$projectId/secret-rotation"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
|
||||
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>App Connections</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive ||
|
||||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SecretScanningLayout = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SshLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
key="menu-project-items"
|
||||
|
||||
+1
-1
@@ -368,7 +368,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name="type"
|
||||
defaultValue={CaType.ACME}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
|
||||
+7
-2
@@ -10,7 +10,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDeleteCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/mutations";
|
||||
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
|
||||
import { type TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
|
||||
|
||||
import { CreateTemplateModal } from "./CreateTemplateModal";
|
||||
import { TemplateList } from "./TemplateList";
|
||||
@@ -84,7 +84,12 @@ export const CertificateTemplatesV2Tab = () => {
|
||||
|
||||
<TemplateList onEditTemplate={handleEditTemplate} onDeleteTemplate={handleDeleteTemplate} />
|
||||
|
||||
<CreateTemplateModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
|
||||
<CreateTemplateModal
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedTemplate && (
|
||||
<>
|
||||
|
||||
+157
-32
@@ -36,13 +36,18 @@ import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
CertSanInclude,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeInclude,
|
||||
CertSubjectAttributeType,
|
||||
SAN_INCLUDE_OPTIONS,
|
||||
SAN_TYPE_OPTIONS,
|
||||
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
|
||||
SUBJECT_ATTRIBUTE_TYPE_OPTIONS
|
||||
SUBJECT_ATTRIBUTE_TYPE_OPTIONS,
|
||||
TEMPLATE_PRESET_IDS,
|
||||
type TemplatePresetId
|
||||
} from "./shared/certificate-constants";
|
||||
import { CERTIFICATE_TEMPLATE_PRESETS } from "./shared/template-presets";
|
||||
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
|
||||
|
||||
export type FormData = TemplateFormData;
|
||||
@@ -91,7 +96,7 @@ const SIGNATURE_ALGORITHMS = [
|
||||
"SHA256-ECDSA",
|
||||
"SHA384-ECDSA",
|
||||
"SHA512-ECDSA"
|
||||
];
|
||||
] as const;
|
||||
|
||||
const KEY_ALGORITHMS = [
|
||||
"RSA-2048",
|
||||
@@ -100,7 +105,7 @@ const KEY_ALGORITHMS = [
|
||||
"ECDSA-P256",
|
||||
"ECDSA-P384",
|
||||
"ECDSA-P521"
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => {
|
||||
const { currentProject } = useProject();
|
||||
@@ -117,7 +122,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
subj.allowed.forEach((allowedValue) => {
|
||||
attributes.push({
|
||||
type: subj.type as CertSubjectAttributeType,
|
||||
include: "optional",
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: [allowedValue]
|
||||
});
|
||||
});
|
||||
@@ -126,7 +131,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
subj.denied.forEach((deniedValue) => {
|
||||
attributes.push({
|
||||
type: subj.type as CertSubjectAttributeType,
|
||||
include: "prohibit",
|
||||
include: CertSubjectAttributeInclude.PROHIBIT,
|
||||
value: [deniedValue]
|
||||
});
|
||||
});
|
||||
@@ -141,7 +146,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
san.required.forEach((requiredValue) => {
|
||||
subjectAlternativeNames.push({
|
||||
type: san.type as CertSubjectAlternativeNameType,
|
||||
include: "mandatory",
|
||||
include: CertSanInclude.MANDATORY,
|
||||
value: [requiredValue]
|
||||
});
|
||||
});
|
||||
@@ -150,7 +155,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
san.allowed.forEach((allowedValue) => {
|
||||
subjectAlternativeNames.push({
|
||||
type: san.type as CertSubjectAlternativeNameType,
|
||||
include: "optional",
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: [allowedValue]
|
||||
});
|
||||
});
|
||||
@@ -159,7 +164,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
san.denied.forEach((deniedValue) => {
|
||||
subjectAlternativeNames.push({
|
||||
type: san.type as CertSubjectAlternativeNameType,
|
||||
include: "prohibit",
|
||||
include: CertSanInclude.PROHIBIT,
|
||||
value: [deniedValue]
|
||||
});
|
||||
});
|
||||
@@ -219,6 +224,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
};
|
||||
|
||||
return {
|
||||
preset: TEMPLATE_PRESET_IDS.CUSTOM,
|
||||
name: templateData.name || "",
|
||||
description: templateData.description || "",
|
||||
attributes,
|
||||
@@ -231,25 +237,30 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
};
|
||||
};
|
||||
|
||||
const getDefaultValues = (): FormData => ({
|
||||
name: "",
|
||||
description: "",
|
||||
attributes: [],
|
||||
keyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||
subjectAlternativeNames: [],
|
||||
validity: {
|
||||
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
|
||||
},
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: []
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: []
|
||||
}
|
||||
});
|
||||
const getDefaultValues = (): FormData & { preset: TemplatePresetId } => {
|
||||
return {
|
||||
preset: TEMPLATE_PRESET_IDS.CUSTOM,
|
||||
name: "",
|
||||
description: "",
|
||||
attributes: [],
|
||||
keyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||
subjectAlternativeNames: [],
|
||||
validity: {
|
||||
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
|
||||
},
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: []
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: []
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<
|
||||
FormData & { preset: TemplatePresetId }
|
||||
>({
|
||||
resolver: zodResolver(templateSchema),
|
||||
defaultValues: getDefaultValues(),
|
||||
mode: "onChange",
|
||||
@@ -260,7 +271,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
useEffect(() => {
|
||||
if (isEdit && template) {
|
||||
const convertedData = convertApiToUiFormat(template);
|
||||
reset(convertedData);
|
||||
reset({ ...convertedData, preset: TEMPLATE_PRESET_IDS.CUSTOM });
|
||||
} else if (!isEdit) {
|
||||
reset(getDefaultValues());
|
||||
}
|
||||
@@ -273,6 +284,37 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
requiredUsages: [],
|
||||
optionalUsages: []
|
||||
};
|
||||
const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM;
|
||||
|
||||
const handlePresetChange = (presetId: TemplatePresetId) => {
|
||||
setValue("preset", presetId);
|
||||
|
||||
if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedPreset = CERTIFICATE_TEMPLATE_PRESETS.find((p) => p.id === presetId);
|
||||
if (selectedPreset) {
|
||||
if (selectedPreset.formData.keyUsages) {
|
||||
setValue("keyUsages", selectedPreset.formData.keyUsages);
|
||||
}
|
||||
if (selectedPreset.formData.extendedKeyUsages) {
|
||||
setValue("extendedKeyUsages", selectedPreset.formData.extendedKeyUsages);
|
||||
}
|
||||
if (selectedPreset.formData.attributes) {
|
||||
setValue("attributes", selectedPreset.formData.attributes);
|
||||
}
|
||||
if (selectedPreset.formData.subjectAlternativeNames) {
|
||||
setValue("subjectAlternativeNames", selectedPreset.formData.subjectAlternativeNames);
|
||||
}
|
||||
if (selectedPreset.formData.signatureAlgorithm) {
|
||||
setValue("signatureAlgorithm", selectedPreset.formData.signatureAlgorithm);
|
||||
}
|
||||
if (selectedPreset.formData.keyAlgorithm) {
|
||||
setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const consolidateByType = <
|
||||
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
|
||||
@@ -309,9 +351,17 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
data.attributes?.map((attr) => {
|
||||
const result: AttributeTransform = { type: attr.type };
|
||||
|
||||
if (attr.include === "optional" && attr.value && attr.value.length > 0) {
|
||||
if (
|
||||
attr.include === CertSubjectAttributeInclude.OPTIONAL &&
|
||||
attr.value &&
|
||||
attr.value.length > 0
|
||||
) {
|
||||
result.allowed = attr.value;
|
||||
} else if (attr.include === "prohibit" && attr.value && attr.value.length > 0) {
|
||||
} else if (
|
||||
attr.include === CertSubjectAttributeInclude.PROHIBIT &&
|
||||
attr.value &&
|
||||
attr.value.length > 0
|
||||
) {
|
||||
result.denied = attr.value;
|
||||
}
|
||||
|
||||
@@ -322,11 +372,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
data.subjectAlternativeNames?.map((san) => {
|
||||
const result: SanTransform = { type: san.type };
|
||||
|
||||
if (san.include === "mandatory" && san.value && san.value.length > 0) {
|
||||
if (san.include === CertSanInclude.MANDATORY && san.value && san.value.length > 0) {
|
||||
result.required = san.value;
|
||||
} else if (san.include === "optional" && san.value && san.value.length > 0) {
|
||||
} else if (san.include === CertSanInclude.OPTIONAL && san.value && san.value.length > 0) {
|
||||
result.allowed = san.value;
|
||||
} else if (san.include === "prohibit" && san.value && san.value.length > 0) {
|
||||
} else if (san.include === CertSanInclude.PROHIBIT && san.value && san.value.length > 0) {
|
||||
result.denied = san.value;
|
||||
}
|
||||
|
||||
@@ -464,11 +514,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
value: ["*"]
|
||||
};
|
||||
setValue("attributes", [...watchedAttributes, newAttribute]);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
};
|
||||
|
||||
const removeAttribute = (index: number) => {
|
||||
const newAttributes = watchedAttributes.filter((_, i) => i !== index);
|
||||
setValue("attributes", newAttributes);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
};
|
||||
|
||||
const addSan = () => {
|
||||
@@ -478,11 +536,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
value: ["*"]
|
||||
};
|
||||
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSan = (index: number) => {
|
||||
const newSans = watchedSans.filter((_, i) => i !== index);
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUsagesChange = (usages: {
|
||||
@@ -493,6 +559,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
requiredUsages: usages.requiredUsages,
|
||||
optionalUsages: usages.optionalUsages
|
||||
});
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtendedKeyUsagesChange = (usages: {
|
||||
@@ -503,6 +573,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
requiredUsages: usages.requiredUsages,
|
||||
optionalUsages: usages.optionalUsages
|
||||
});
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -555,6 +629,33 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="preset"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Preset"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={handlePresetChange}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
<SelectItem value={TEMPLATE_PRESET_IDS.CUSTOM}>Custom</SelectItem>
|
||||
{CERTIFICATE_TEMPLATE_PRESETS.map((preset) => (
|
||||
<SelectItem key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<AccordionItem value="attributes">
|
||||
<AccordionTrigger>Subject Attributes</AccordionTrigger>
|
||||
@@ -595,6 +696,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
type: value as CertSubjectAttributeType
|
||||
};
|
||||
setValue("attributes", newAttributes);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
}}
|
||||
className="w-48"
|
||||
>
|
||||
@@ -615,6 +720,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
value as (typeof SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS)[number]
|
||||
};
|
||||
setValue("attributes", newAttributes);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
}}
|
||||
className="w-32"
|
||||
>
|
||||
@@ -635,6 +744,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||
};
|
||||
setValue("attributes", newAttributes);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
}}
|
||||
className={`flex-1 ${
|
||||
attr.value && attr.value.length > 0 && attr.value[0] === ""
|
||||
@@ -701,6 +814,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
type: value as CertSubjectAlternativeNameType
|
||||
};
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
}}
|
||||
className="w-36"
|
||||
>
|
||||
@@ -720,6 +837,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
include: value as (typeof SAN_INCLUDE_OPTIONS)[number]
|
||||
};
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
}}
|
||||
className="w-32"
|
||||
>
|
||||
@@ -740,6 +861,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||
};
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
|
||||
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
|
||||
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
}
|
||||
}}
|
||||
className={`flex-1 ${
|
||||
san.value && san.value.length > 0 && san.value[0] === ""
|
||||
|
||||
+37
-2
@@ -150,8 +150,19 @@ export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttribute
|
||||
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
|
||||
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
|
||||
|
||||
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const;
|
||||
export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
|
||||
export enum CertSubjectAttributeInclude {
|
||||
OPTIONAL = "optional",
|
||||
PROHIBIT = "prohibit"
|
||||
}
|
||||
|
||||
export enum CertSanInclude {
|
||||
MANDATORY = "mandatory",
|
||||
OPTIONAL = "optional",
|
||||
PROHIBIT = "prohibit"
|
||||
}
|
||||
|
||||
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = Object.values(CertSubjectAttributeInclude);
|
||||
export const SAN_INCLUDE_OPTIONS = Object.values(CertSanInclude);
|
||||
|
||||
export const USAGE_STATES = {
|
||||
REQUIRED: "required",
|
||||
@@ -239,3 +250,27 @@ export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string =>
|
||||
};
|
||||
return mapping[templateFormat] || templateFormat;
|
||||
};
|
||||
|
||||
export const TEMPLATE_PRESET_IDS = {
|
||||
CUSTOM: "custom",
|
||||
TLS_SERVER: "tls-server",
|
||||
TLS_CLIENT: "tls-client",
|
||||
CODE_SIGNING: "code-signing",
|
||||
DEVICE: "device",
|
||||
USER: "user",
|
||||
EMAIL_PROTECTION: "email-protection",
|
||||
DUAL_PURPOSE_SERVER: "dual-purpose-server"
|
||||
} as const;
|
||||
|
||||
export type TemplatePresetId = (typeof TEMPLATE_PRESET_IDS)[keyof typeof TEMPLATE_PRESET_IDS];
|
||||
|
||||
export const ALGORITHM_FAMILIES = {
|
||||
ECDSA: {
|
||||
signature: ["SHA256-ECDSA", "SHA384-ECDSA", "SHA512-ECDSA"] as const,
|
||||
key: ["ECDSA-P256", "ECDSA-P384", "ECDSA-P521"] as const
|
||||
},
|
||||
RSA: {
|
||||
signature: ["SHA256-RSA", "SHA384-RSA", "SHA512-RSA"] as const,
|
||||
key: ["RSA-2048", "RSA-3072", "RSA-4096"] as const
|
||||
}
|
||||
} as const;
|
||||
|
||||
+19
-4
@@ -4,21 +4,22 @@ import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
CertSanInclude,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeInclude,
|
||||
CertSubjectAttributeType,
|
||||
SAN_INCLUDE_OPTIONS,
|
||||
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS
|
||||
TEMPLATE_PRESET_IDS
|
||||
} from "./certificate-constants";
|
||||
|
||||
export const uiAttributeSchema = z.object({
|
||||
type: z.nativeEnum(CertSubjectAttributeType),
|
||||
include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS),
|
||||
include: z.nativeEnum(CertSubjectAttributeInclude),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty"))
|
||||
});
|
||||
|
||||
export const uiSanSchema = z.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
include: z.enum(SAN_INCLUDE_OPTIONS),
|
||||
include: z.nativeEnum(CertSanInclude),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty"))
|
||||
});
|
||||
|
||||
@@ -51,7 +52,21 @@ export const uiKeyAlgorithmSchema = z.object({
|
||||
.min(1, "At least one key type must be selected")
|
||||
});
|
||||
|
||||
export const uiPresetSchema = z
|
||||
.enum([
|
||||
TEMPLATE_PRESET_IDS.CUSTOM,
|
||||
TEMPLATE_PRESET_IDS.TLS_SERVER,
|
||||
TEMPLATE_PRESET_IDS.TLS_CLIENT,
|
||||
TEMPLATE_PRESET_IDS.CODE_SIGNING,
|
||||
TEMPLATE_PRESET_IDS.DEVICE,
|
||||
TEMPLATE_PRESET_IDS.USER,
|
||||
TEMPLATE_PRESET_IDS.EMAIL_PROTECTION,
|
||||
TEMPLATE_PRESET_IDS.DUAL_PURPOSE_SERVER
|
||||
])
|
||||
.default(TEMPLATE_PRESET_IDS.CUSTOM);
|
||||
|
||||
export const templateSchema = z.object({
|
||||
preset: uiPresetSchema,
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
import {
|
||||
ALGORITHM_FAMILIES,
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
CertSanInclude,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeInclude,
|
||||
CertSubjectAttributeType,
|
||||
TEMPLATE_PRESET_IDS,
|
||||
type TemplatePresetId
|
||||
} from "./certificate-constants";
|
||||
import { TemplateFormData } from ".";
|
||||
|
||||
export interface CertificateTemplatePreset {
|
||||
readonly id: TemplatePresetId;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly useCase: string;
|
||||
readonly formData: Omit<TemplateFormData, "preset">;
|
||||
}
|
||||
|
||||
export const CERTIFICATE_TEMPLATE_PRESETS: CertificateTemplatePreset[] = [
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.TLS_SERVER,
|
||||
name: "TLS Server Certificate",
|
||||
description: "Standard TLS/SSL server certificate for HTTPS services and API endpoints.",
|
||||
useCase: "Web servers, API endpoints, HTTPS services",
|
||||
formData: {
|
||||
name: "TLS Server Certificate",
|
||||
description: "Standard TLS/SSL server certificate for HTTPS services and API endpoints.",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
|
||||
optionalUsages: [CertExtendedKeyUsageType.SERVER_AUTH]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.DNS_NAME,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
},
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.TLS_CLIENT,
|
||||
name: "TLS Client Certificate",
|
||||
description: "Client certificate for mutual TLS authentication and API access.",
|
||||
useCase: "Client authentication, mTLS, API authentication",
|
||||
formData: {
|
||||
name: "TLS Client Certificate",
|
||||
description: "Client certificate for mutual TLS authentication and API access.",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_AGREEMENT]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [CertExtendedKeyUsageType.CLIENT_AUTH],
|
||||
optionalUsages: [CertExtendedKeyUsageType.CLIENT_AUTH]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.EMAIL,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
},
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.DNS_NAME,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.CODE_SIGNING,
|
||||
name: "Code Signing Certificate",
|
||||
description:
|
||||
"Certificate for signing software, executables, and packages. Requires hardware security modules.",
|
||||
useCase: "Software signing, executable authentication, package validation",
|
||||
formData: {
|
||||
name: "Code Signing Certificate",
|
||||
description:
|
||||
"Certificate for signing software, executables, and packages. Requires hardware security modules.",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.NON_REPUDIATION],
|
||||
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.NON_REPUDIATION]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [CertExtendedKeyUsageType.CODE_SIGNING],
|
||||
optionalUsages: [
|
||||
CertExtendedKeyUsageType.CODE_SIGNING,
|
||||
CertExtendedKeyUsageType.TIME_STAMPING
|
||||
]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.EMAIL,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.RSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.RSA.key]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.DEVICE,
|
||||
name: "Device Certificate",
|
||||
description:
|
||||
"Certificate for IoT devices and embedded systems authentication. IEEE 802.1AR compliant.",
|
||||
useCase: "Device authentication, IoT security, embedded systems",
|
||||
formData: {
|
||||
name: "Device Certificate",
|
||||
description:
|
||||
"Certificate for IoT devices and embedded systems authentication. IEEE 802.1AR compliant.",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_AGREEMENT]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [CertExtendedKeyUsageType.CLIENT_AUTH],
|
||||
optionalUsages: [CertExtendedKeyUsageType.CLIENT_AUTH, CertExtendedKeyUsageType.SERVER_AUTH]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.DNS_NAME,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
},
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.USER,
|
||||
name: "User Certificate",
|
||||
description:
|
||||
"Personal certificate for user authentication and email signing. FIPS 201 PIV compliant.",
|
||||
useCase: "Personal authentication, smart cards, email protection",
|
||||
formData: {
|
||||
name: "User Certificate",
|
||||
description:
|
||||
"Personal certificate for user authentication and email signing. FIPS 201 PIV compliant.",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
optionalUsages: [
|
||||
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||
CertKeyUsageType.KEY_ENCIPHERMENT,
|
||||
CertKeyUsageType.KEY_AGREEMENT
|
||||
]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [
|
||||
CertExtendedKeyUsageType.CLIENT_AUTH,
|
||||
CertExtendedKeyUsageType.EMAIL_PROTECTION
|
||||
],
|
||||
optionalUsages: [
|
||||
CertExtendedKeyUsageType.CLIENT_AUTH,
|
||||
CertExtendedKeyUsageType.EMAIL_PROTECTION
|
||||
]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.EMAIL,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.EMAIL_PROTECTION,
|
||||
name: "Email Protection Certificate",
|
||||
description: "S/MIME certificate for email encryption and digital signing. RFC 8550 compliant.",
|
||||
useCase: "Email encryption, digital signing, secure messaging",
|
||||
formData: {
|
||||
name: "Email Protection Certificate",
|
||||
description:
|
||||
"S/MIME certificate for email encryption and digital signing. RFC 8550 compliant.",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
optionalUsages: [
|
||||
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||
CertKeyUsageType.KEY_ENCIPHERMENT,
|
||||
CertKeyUsageType.KEY_AGREEMENT
|
||||
]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [CertExtendedKeyUsageType.EMAIL_PROTECTION],
|
||||
optionalUsages: [CertExtendedKeyUsageType.EMAIL_PROTECTION]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.EMAIL,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.RSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.RSA.key]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: TEMPLATE_PRESET_IDS.DUAL_PURPOSE_SERVER,
|
||||
name: "Dual-Purpose Server Certificate",
|
||||
description:
|
||||
"Certificate for services requiring both server and client authentication capabilities",
|
||||
useCase: "Microservices, service mesh, dual authentication",
|
||||
formData: {
|
||||
name: "Dual-Purpose Server Certificate",
|
||||
description:
|
||||
"Certificate for services requiring both server and client authentication capabilities",
|
||||
keyUsages: {
|
||||
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||
optionalUsages: [
|
||||
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||
CertKeyUsageType.KEY_ENCIPHERMENT,
|
||||
CertKeyUsageType.KEY_AGREEMENT
|
||||
]
|
||||
},
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: [
|
||||
CertExtendedKeyUsageType.SERVER_AUTH,
|
||||
CertExtendedKeyUsageType.CLIENT_AUTH
|
||||
],
|
||||
optionalUsages: [CertExtendedKeyUsageType.SERVER_AUTH, CertExtendedKeyUsageType.CLIENT_AUTH]
|
||||
},
|
||||
validity: {
|
||||
maxDuration: {
|
||||
value: 365,
|
||||
unit: CertDurationUnit.DAYS
|
||||
}
|
||||
},
|
||||
attributes: [
|
||||
{
|
||||
type: CertSubjectAttributeType.COMMON_NAME,
|
||||
include: CertSubjectAttributeInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.DNS_NAME,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
},
|
||||
{
|
||||
type: CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||
include: CertSanInclude.OPTIONAL,
|
||||
value: ["*"]
|
||||
}
|
||||
],
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
|
||||
},
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const OrgGroupsSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ export const IdentitySection = withPermission(
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex w-full items-center gap-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||
@@ -139,7 +139,7 @@ export const IdentitySection = withPermission(
|
||||
</div>
|
||||
{/* Identity Auth Templates Section */}
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identity Auth Templates</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/auth-templates" />
|
||||
|
||||
+25
-6
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { BanIcon } from "lucide-react";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { BanIcon, UserPlusIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge, DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -35,6 +37,7 @@ import { OrgMembersTable } from "./OrgMembersTable";
|
||||
export const OrgMembersSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const navigate = useNavigate();
|
||||
const orgId = currentOrg?.id ?? "";
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
@@ -55,6 +58,22 @@ export const OrgMembersSection = () => {
|
||||
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
|
||||
const urlAction = useSearch({
|
||||
from: ROUTE_PATHS.Organization.AccessControlPage.id,
|
||||
select: (el) => el.action,
|
||||
structuralSharing: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (urlAction === "invite-members") {
|
||||
handlePopUpOpen("addMember");
|
||||
navigate({
|
||||
to: ".",
|
||||
search: ({ action, ...search }) => search
|
||||
});
|
||||
}
|
||||
}, [urlAction]);
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||
const { mutateAsync: deleteBatchMutateAsync } = useDeleteOrgMembershipBatch();
|
||||
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
||||
@@ -184,7 +203,7 @@ export const OrgMembersSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Users</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
||||
@@ -194,13 +213,13 @@ export const OrgMembersSection = () => {
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
leftIcon={<UserPlusIcon size={16} />}
|
||||
onClick={() =>
|
||||
isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal()
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Member
|
||||
{isSubOrganization ? "Add Members" : "Invite Members"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
||||
+2
-2
@@ -392,7 +392,7 @@ export const OrgMembersTable = ({
|
||||
}}
|
||||
/>
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<Th className="min-w-40 md:w-1/3 md:min-w-0">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -505,7 +505,7 @@ export const OrgMembersTable = ({
|
||||
<Td
|
||||
className={twMerge("group max-w-0", isActive ? "" : "text-mineshaft-400")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
<p className="truncate">
|
||||
{name ?? <span className="text-mineshaft-400">Not Set</span>}
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ export const OrgRoleTable = () => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Roles
|
||||
|
||||
+59
@@ -4,8 +4,10 @@ import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
@@ -18,8 +20,11 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string";
|
||||
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||
import {
|
||||
LdapConnectionMethod,
|
||||
LdapConnectionProvider,
|
||||
@@ -84,6 +89,7 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.LDAP,
|
||||
method: LdapConnectionMethod.SimpleBind,
|
||||
gatewayId: null,
|
||||
credentials: {
|
||||
provider: LdapConnectionProvider.ActiveDirectory,
|
||||
url: "",
|
||||
@@ -104,6 +110,8 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
|
||||
const selectedProvider = watch("credentials.provider");
|
||||
const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false;
|
||||
const { subscription } = useSubscription();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
@@ -114,6 +122,57 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
}}
|
||||
>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
{subscription.gateway && (
|
||||
<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"
|
||||
>
|
||||
<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={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isGatewaysLoading}
|
||||
placeholder="Default: Internet Gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value={null as unknown as string}
|
||||
onClick={() => onChange(undefined)}
|
||||
>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{gateways?.map((el) => (
|
||||
<SelectItem value={el.id} key={el.id}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<div className="grid grid-cols-2 items-center gap-2">
|
||||
<Controller
|
||||
name="method"
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">App Connections</p>
|
||||
|
||||
@@ -150,8 +150,8 @@ const Page = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<GroupMembersSection groupId={groupId} groupSlug={data.group.slug} />
|
||||
|
||||
@@ -116,8 +116,8 @@ const Page = () => {
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<IdentityDetailsSection
|
||||
isOrgIdentity={data.identity.orgId === currentOrg.id}
|
||||
identityId={identityId}
|
||||
|
||||
+1
-1
@@ -301,7 +301,7 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your relay will use for authentication."
|
||||
tooltipText="The identity token that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FilterableSelect,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
ModalClose,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission
|
||||
} from "@app/context";
|
||||
import {
|
||||
useAddIdentityTokenAuth,
|
||||
useCreateTokenIdentityTokenAuth,
|
||||
useGetIdentityMembershipOrgs,
|
||||
useGetIdentityTokenAuth,
|
||||
useGetRelays
|
||||
} from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { RelayOption } from "./RelayOption";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: slugSchema({ field: "name" }),
|
||||
relay: z
|
||||
.object(
|
||||
{
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Relay is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Relay is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
identity: z
|
||||
.object(
|
||||
{
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
identityToken: z.string().min(1, "Token is required")
|
||||
});
|
||||
|
||||
export const GatewayCliSystemdDeploymentMethod = () => {
|
||||
const { protocol, hostname, port } = window.location;
|
||||
const portSuffix = port && port !== "80" ? `:${port}` : "";
|
||||
const siteURL = `${protocol}//${hostname}${portSuffix}`;
|
||||
|
||||
const navigate = useNavigate({
|
||||
from: ROUTE_PATHS.Organization.NetworkingPage.path
|
||||
});
|
||||
|
||||
const [autogenerateToken, setAutogenerateToken] = useState(true);
|
||||
const [step, setStep] = useState<"form" | "command">("form");
|
||||
const [name, setName] = useState("");
|
||||
const [relay, setRelay] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>({ id: "_auto", name: "Auto Select Relay" });
|
||||
const [identity, setIdentity] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(null);
|
||||
const [identityToken, setIdentityToken] = useState("");
|
||||
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
|
||||
|
||||
const errors = useMemo(() => {
|
||||
const errorMap: Record<string, string | undefined> = {};
|
||||
formErrors.forEach((issue) => {
|
||||
if (issue.path.length > 0) {
|
||||
errorMap[String(issue.path[0])] = issue.message;
|
||||
}
|
||||
});
|
||||
return errorMap;
|
||||
}, [formErrors]);
|
||||
|
||||
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { permission } = useOrgPermission();
|
||||
const canCreateToken = permission.can(
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
|
||||
useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000
|
||||
});
|
||||
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
|
||||
|
||||
const { mutateAsync: createToken, isPending: isCreatingToken } =
|
||||
useCreateTokenIdentityTokenAuth();
|
||||
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
|
||||
useAddIdentityTokenAuth();
|
||||
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
|
||||
|
||||
const handleGenerateCommand = async () => {
|
||||
setFormErrors([]);
|
||||
|
||||
if (canCreateToken && autogenerateToken) {
|
||||
const validation = formSchemaWithIdentity.safeParse({
|
||||
name,
|
||||
relay,
|
||||
identity
|
||||
});
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedIdentity = validation.data.identity;
|
||||
|
||||
try {
|
||||
const { data: identityTokenAuth } = await refetch();
|
||||
if (!identityTokenAuth) {
|
||||
await addIdentityTokenAuth({
|
||||
identityId: validatedIdentity.id,
|
||||
organizationId,
|
||||
accessTokenTTL: 2592000,
|
||||
accessTokenMaxTTL: 2592000,
|
||||
accessTokenNumUsesLimit: 0,
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
createNotification({
|
||||
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
|
||||
type: "warning"
|
||||
});
|
||||
}
|
||||
|
||||
const token = await createToken({
|
||||
identityId: validatedIdentity.id,
|
||||
name: `gateway token for ${name} (autogenerated)`
|
||||
});
|
||||
setIdentityToken(token.accessToken);
|
||||
createNotification({
|
||||
text: "Automatically generated a token for the selected identity.",
|
||||
type: "info"
|
||||
});
|
||||
setStep("command");
|
||||
} catch {
|
||||
setIdentityToken("");
|
||||
}
|
||||
} else {
|
||||
const validation = formSchemaWithToken.safeParse({
|
||||
name,
|
||||
relay,
|
||||
identityToken
|
||||
});
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
}
|
||||
setStep("command");
|
||||
}
|
||||
};
|
||||
|
||||
const installCommand = useMemo(() => {
|
||||
const relayPart = relay?.id !== "_auto" ? ` --relay=${relay?.name || ""}` : "";
|
||||
return `sudo infisical gateway systemd install --name=${name}${relayPart} --domain=${siteURL} --token=${identityToken}`;
|
||||
}, [name, relay, identityToken, siteURL]);
|
||||
|
||||
const startServiceCommand = "sudo systemctl start infisical-gateway";
|
||||
|
||||
if (step === "command") {
|
||||
return (
|
||||
<>
|
||||
<FormLabel label="Installation Command" />
|
||||
<div className="flex gap-2">
|
||||
<Input value={installCommand} isDisabled />
|
||||
<IconButton
|
||||
ariaLabel="copy install command"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(installCommand);
|
||||
createNotification({
|
||||
text: "Installation command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<FormLabel label="Start the Gateway Service" className="mt-4" />
|
||||
<div className="mb-2 flex gap-2">
|
||||
<Input value={startServiceCommand} isDisabled />
|
||||
<IconButton
|
||||
ariaLabel="copy start service command"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(startServiceCommand);
|
||||
createNotification({
|
||||
text: "Start service command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<a
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span>Install the Infisical CLI</span>
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
|
||||
</a>
|
||||
<div className="mt-6 flex items-center">
|
||||
<ModalClose asChild>
|
||||
<Button className="mr-4" size="sm" colorSchema="secondary">
|
||||
Done
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormLabel label="Name" tooltipText="The name for your gateway." />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter gateway name..."
|
||||
isError={Boolean(errors.name)}
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
|
||||
|
||||
<FormLabel label="Relay" tooltipText="The relay to use with your gateway." className="mt-4" />
|
||||
<FilterableSelect
|
||||
value={relay}
|
||||
onChange={(newValue) => {
|
||||
if ((newValue as SingleValue<{ id: string }>)?.id === "_create") {
|
||||
navigate({
|
||||
search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" })
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setRelay(newValue as SingleValue<{ id: string; name: string }>);
|
||||
}}
|
||||
isLoading={isRelaysLoading}
|
||||
options={[
|
||||
{
|
||||
id: "_auto",
|
||||
name: "Auto Select Relay"
|
||||
},
|
||||
{
|
||||
id: "_create",
|
||||
name: "Deploy New Relay"
|
||||
},
|
||||
...(relays || [])
|
||||
]}
|
||||
placeholder="Select relay..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
components={{ Option: RelayOption }}
|
||||
/>
|
||||
{errors.relay && <p className="mt-1 text-sm text-red">{errors.relay}</p>}
|
||||
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
setIdentity(
|
||||
e as SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canCreateToken && (
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
isChecked={autogenerateToken}
|
||||
onCheckedChange={(e) => {
|
||||
setAutogenerateToken(Boolean(e));
|
||||
}}
|
||||
id="autogenerate-token"
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
colorSchema="secondary"
|
||||
onClick={handleGenerateCommand}
|
||||
isLoading={isCreatingToken || isAddingTokenAuth}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { GatewayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect";
|
||||
|
||||
import { GatewayCliDeploymentMethod } from "./GatewayCliDeploymentMethod";
|
||||
import { GatewayCliSystemdDeploymentMethod } from "./GatewayCliSystemdDeploymentMethod";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -11,7 +12,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const GatewayDeploymentInfoMap = {
|
||||
cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod }
|
||||
cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod },
|
||||
systemd: { name: "CLI (systemd)", image: "SSH.png", component: GatewayCliSystemdDeploymentMethod }
|
||||
} as const;
|
||||
|
||||
export type GatewayDeploymentMethod = keyof typeof GatewayDeploymentInfoMap;
|
||||
|
||||
+1
-10
@@ -156,15 +156,6 @@ export const RelayCliDeploymentMethod = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIdentityChange = (
|
||||
selectedIdentity: SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>
|
||||
) => {
|
||||
setIdentity(selectedIdentity);
|
||||
};
|
||||
|
||||
const command = useMemo(() => {
|
||||
return `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`;
|
||||
}, [name, siteURL, host, identityToken]);
|
||||
@@ -245,7 +236,7 @@ export const RelayCliDeploymentMethod = () => {
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
handleIdentityChange(
|
||||
setIdentity(
|
||||
e as SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FilterableSelect,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
ModalClose,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission
|
||||
} from "@app/context";
|
||||
import {
|
||||
useAddIdentityTokenAuth,
|
||||
useCreateTokenIdentityTokenAuth,
|
||||
useGetIdentityMembershipOrgs,
|
||||
useGetIdentityTokenAuth
|
||||
} from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const baseFormSchema = z.object({
|
||||
name: slugSchema({ field: "name" }),
|
||||
host: z.string().min(1, "Host is required")
|
||||
});
|
||||
|
||||
const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
identity: z
|
||||
.object(
|
||||
{
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
identityToken: z.string().min(1, "Token is required")
|
||||
});
|
||||
|
||||
export const RelayCliSystemdDeploymentMethod = () => {
|
||||
const { protocol, hostname, port } = window.location;
|
||||
const portSuffix = port && port !== "80" ? `:${port}` : "";
|
||||
const siteURL = `${protocol}//${hostname}${portSuffix}`;
|
||||
|
||||
const [autogenerateToken, setAutogenerateToken] = useState(true);
|
||||
const [step, setStep] = useState<"form" | "command">("form");
|
||||
const [name, setName] = useState("");
|
||||
const [host, setHost] = useState("");
|
||||
|
||||
const [identity, setIdentity] = useState<null | {
|
||||
id: string;
|
||||
name: string;
|
||||
}>(null);
|
||||
const [identityToken, setIdentityToken] = useState("");
|
||||
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
|
||||
|
||||
const errors = useMemo(() => {
|
||||
const errorMap: Record<string, string | undefined> = {};
|
||||
formErrors.forEach((issue) => {
|
||||
if (issue.path.length > 0) {
|
||||
errorMap[String(issue.path[0])] = issue.message;
|
||||
}
|
||||
});
|
||||
return errorMap;
|
||||
}, [formErrors]);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { permission } = useOrgPermission();
|
||||
const canCreateToken = permission.can(
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
);
|
||||
|
||||
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
|
||||
useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000
|
||||
});
|
||||
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
|
||||
|
||||
const { mutateAsync: createToken, isPending: isCreatingToken } =
|
||||
useCreateTokenIdentityTokenAuth();
|
||||
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
|
||||
useAddIdentityTokenAuth();
|
||||
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
|
||||
|
||||
const handleGenerateCommand = async () => {
|
||||
setFormErrors([]);
|
||||
|
||||
if (canCreateToken && autogenerateToken) {
|
||||
const validation = formSchemaWithIdentity.safeParse({ name, host, identity });
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedIdentity = validation.data.identity;
|
||||
|
||||
try {
|
||||
const { data: identityTokenAuth } = await refetch();
|
||||
if (!identityTokenAuth) {
|
||||
await addIdentityTokenAuth({
|
||||
identityId: validatedIdentity.id,
|
||||
organizationId,
|
||||
accessTokenTTL: 2592000,
|
||||
accessTokenMaxTTL: 2592000,
|
||||
accessTokenNumUsesLimit: 0,
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
createNotification({
|
||||
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
|
||||
type: "warning"
|
||||
});
|
||||
}
|
||||
|
||||
const token = await createToken({
|
||||
identityId: validatedIdentity.id,
|
||||
name: `relay token for ${name} (autogenerated)`
|
||||
});
|
||||
setIdentityToken(token.accessToken);
|
||||
createNotification({
|
||||
text: "Automatically generated a token for the selected identity.",
|
||||
type: "info"
|
||||
});
|
||||
setStep("command");
|
||||
} catch {
|
||||
setIdentityToken("");
|
||||
}
|
||||
} else {
|
||||
const validation = formSchemaWithToken.safeParse({
|
||||
name,
|
||||
host,
|
||||
identityToken
|
||||
});
|
||||
if (!validation.success) {
|
||||
setFormErrors(validation.error.issues);
|
||||
return;
|
||||
}
|
||||
setStep("command");
|
||||
}
|
||||
};
|
||||
|
||||
const installCommand = useMemo(() => {
|
||||
return `sudo infisical relay systemd install --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`;
|
||||
}, [name, siteURL, host, identityToken]);
|
||||
|
||||
const startServiceCommand = "sudo systemctl start infisical-relay";
|
||||
const enableServiceCommand = "sudo systemctl enable infisical-relay";
|
||||
|
||||
if (step === "command") {
|
||||
return (
|
||||
<>
|
||||
<FormLabel label="Installation Command" />
|
||||
<div className="flex gap-2">
|
||||
<Input value={installCommand} isDisabled />
|
||||
<IconButton
|
||||
ariaLabel="copy install command"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(installCommand);
|
||||
createNotification({
|
||||
text: "Installation command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<FormLabel label="Start the Relay Service" className="mt-4" />
|
||||
<div className="mb-2 flex gap-2">
|
||||
<Input value={startServiceCommand} isDisabled />
|
||||
<IconButton
|
||||
ariaLabel="copy start service command"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(startServiceCommand);
|
||||
createNotification({
|
||||
text: "Start service command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input value={enableServiceCommand} isDisabled />
|
||||
<IconButton
|
||||
ariaLabel="copy enable service command"
|
||||
variant="outline_bg"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(enableServiceCommand);
|
||||
createNotification({
|
||||
text: "Enable service command copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
className="w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<a
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span>Install the Infisical CLI</span>
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
|
||||
</a>
|
||||
<div className="mt-6 flex items-center">
|
||||
<ModalClose asChild>
|
||||
<Button className="mr-4" size="sm" colorSchema="secondary">
|
||||
Done
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormLabel label="Name" tooltipText="The name for your relay." />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter relay name..."
|
||||
isError={Boolean(errors.name)}
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
|
||||
|
||||
<FormLabel
|
||||
label="Host"
|
||||
tooltipText="The public IP address of the system you're deploying the relay to."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={host}
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
placeholder="0.0.0.0"
|
||||
isError={Boolean(errors.host)}
|
||||
/>
|
||||
{errors.host && <p className="mt-1 text-sm text-red">{errors.host}</p>}
|
||||
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
setIdentity(
|
||||
e as SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canCreateToken && (
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
isChecked={autogenerateToken}
|
||||
onCheckedChange={(e) => {
|
||||
setAutogenerateToken(Boolean(e));
|
||||
}}
|
||||
id="autogenerate-token"
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
colorSchema="secondary"
|
||||
onClick={handleGenerateCommand}
|
||||
isLoading={isCreatingToken || isAddingTokenAuth}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
@@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect";
|
||||
|
||||
import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod";
|
||||
import { RelayCliSystemdDeploymentMethod } from "./RelayCliSystemdDeploymentMethod";
|
||||
import { RelayTerraformDeploymentMethod } from "./RelayTerraformDeploymentMethod";
|
||||
|
||||
type Props = {
|
||||
@@ -13,6 +14,7 @@ type Props = {
|
||||
|
||||
export const RelayDeploymentInfoMap = {
|
||||
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod },
|
||||
systemd: { name: "CLI (systemd)", image: "SSH.png", component: RelayCliSystemdDeploymentMethod },
|
||||
terraform: {
|
||||
name: "Terraform",
|
||||
image: "Terraform.png",
|
||||
|
||||
+1
-10
@@ -190,15 +190,6 @@ export const RelayTerraformDeploymentMethod = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIdentityChange = (
|
||||
selectedIdentity: SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>
|
||||
) => {
|
||||
setIdentity(selectedIdentity);
|
||||
};
|
||||
|
||||
const terraformCommand = useMemo(() => {
|
||||
return `terraform {
|
||||
required_providers {
|
||||
@@ -365,7 +356,7 @@ resource "aws_eip_association" "eip_assoc" {
|
||||
<FilterableSelect
|
||||
value={identity}
|
||||
onChange={(e) =>
|
||||
handleIdentityChange(
|
||||
setIdentity(
|
||||
e as SingleValue<{
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -125,7 +125,7 @@ export const AllProjectView = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full flex-row flex-wrap gap-2 md:flex-nowrap md:gap-0">
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
|
||||
@@ -360,7 +360,7 @@ export const MyProjectView = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full flex-row flex-wrap gap-2 md:flex-nowrap md:gap-0">
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50/60 duration-200 focus:bg-mineshaft-700/80"
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ export const ExternalMigrationsTab = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-base font-medium text-mineshaft-100">
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ export const VaultConnectionSection = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
|
||||
+6
-4
@@ -34,9 +34,11 @@ export const ProjectTemplatesSection = () => {
|
||||
project setup
|
||||
</p>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/project-templates" />
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/project-templates" />
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
@@ -57,7 +59,7 @@ export const ProjectTemplatesSection = () => {
|
||||
handlePopUpOpen("addTemplate");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-auto"
|
||||
// className="ml-auto"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
|
||||
@@ -242,8 +242,8 @@ const Page = withPermission(
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="w-full md:w-96">
|
||||
<UserDetailsSection membershipId={membershipId} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<div className="w-full space-y-2">
|
||||
|
||||
@@ -13,6 +13,8 @@ import { Button, IconButton } from "@app/components/v2";
|
||||
import { useTimedReset, useToggle } from "@app/hooks";
|
||||
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { SecretShareInfo } from "./SecretShareInfo";
|
||||
|
||||
type Props = {
|
||||
secret: TViewSharedSecretResponse["secret"];
|
||||
secretKey: string | null;
|
||||
@@ -71,6 +73,7 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<SecretShareInfo secret={secret} />
|
||||
<Button
|
||||
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { faClock, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||
|
||||
type Props = {
|
||||
secret: TViewSharedSecretResponse["secret"];
|
||||
};
|
||||
|
||||
export const SecretShareInfo = ({ secret }: Props) => {
|
||||
let timeRemaining: string | null = null;
|
||||
|
||||
if (secret.expiresAt) {
|
||||
try {
|
||||
timeRemaining = format(new Date(secret.expiresAt), "yyyy-MM-dd 'at' HH:mm a");
|
||||
} catch {
|
||||
timeRemaining = null;
|
||||
}
|
||||
}
|
||||
|
||||
let viewsRemaining: number | null = null;
|
||||
|
||||
if (secret.expiresAfterViews) {
|
||||
viewsRemaining = secret.expiresAfterViews - 1;
|
||||
}
|
||||
|
||||
if (!timeRemaining && viewsRemaining === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col gap-2 rounded-md border border-mineshaft-600 bg-mineshaft-700/50 p-3 text-sm text-gray-300">
|
||||
{timeRemaining && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faClock} className="text-mineshaft-400" />
|
||||
<span>Expires on {timeRemaining}</span>
|
||||
</div>
|
||||
)}
|
||||
{viewsRemaining !== null && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faEye} className="text-mineshaft-400" />
|
||||
<span>
|
||||
{viewsRemaining === 0
|
||||
? "This is the last time you can view this secret"
|
||||
: `${viewsRemaining} more view${viewsRemaining === 1 ? "" : "s"} remaining`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -39,7 +39,7 @@ const CommitItem = ({
|
||||
<div className="flex gap-2 px-4 py-3 transition-colors duration-200 hover:bg-zinc-800">
|
||||
<div className="flex min-w-0 flex-1 flex-col items-start">
|
||||
<p className="block w-full truncate text-left text-sm text-mineshaft-100">
|
||||
{commit.message}
|
||||
{commit.message || <span className="text-mineshaft-400 italic">No message</span>}
|
||||
</p>
|
||||
<p className="text-left text-xs text-mineshaft-300">
|
||||
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}{" "}
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export const SecretSyncsTab = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-lg font-medium text-mineshaft-100">Secret Syncs</p>
|
||||
|
||||
@@ -959,9 +959,9 @@ export const OverviewPage = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||
<div className="flex flex-row items-center justify-center space-x-2">
|
||||
<div className="flex flex-row flex-wrap items-center gap-2">
|
||||
{isTableFiltered && (
|
||||
<Button
|
||||
variant="plain"
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ export const AccessApprovalRequest = ({
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Access Requests</p>
|
||||
|
||||
+1
-1
@@ -240,7 +240,7 @@ export const ApprovalPolicyList = ({ projectId }: IProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Policies</p>
|
||||
|
||||
+1
-1
@@ -690,7 +690,7 @@ export const ActionBar = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<SecretSearchInput
|
||||
isSingleEnv
|
||||
className="w-2/5"
|
||||
|
||||
+2
-6
@@ -293,9 +293,6 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
}
|
||||
|
||||
const handleCommit = async () => {
|
||||
if (!commitMessage.trim()) {
|
||||
return;
|
||||
}
|
||||
await onCommit(pendingChanges, commitMessage);
|
||||
clearAllPendingChanges({
|
||||
projectId,
|
||||
@@ -432,7 +429,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
{/* Commit Message */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-mineshaft-200">
|
||||
Commit Message <span className="text-red-400">*</span>
|
||||
Commit Message
|
||||
</label>
|
||||
<Input
|
||||
value={commitMessage}
|
||||
@@ -440,7 +437,6 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
placeholder="Describe your changes..."
|
||||
className="w-full"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -457,7 +453,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
|
||||
<Button
|
||||
onClick={handleCommit}
|
||||
isLoading={isCommitting}
|
||||
isDisabled={isCommitting || !commitMessage.trim()}
|
||||
isDisabled={isCommitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
|
||||
+2
-1
@@ -40,12 +40,13 @@ export const SecretSharingSection = () => {
|
||||
<p className="mb-3 text-xl font-medium">Allow Secret Sharing</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<div>
|
||||
<Checkbox
|
||||
id="secretSharing"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentProject?.secretSharing ?? true}
|
||||
onCheckedChange={(state) => handleToggle(state as boolean)}
|
||||
allowMultilineLabel
|
||||
>
|
||||
This feature enables your project members to securely share secrets.
|
||||
</Checkbox>
|
||||
|
||||
+2
-1
@@ -40,12 +40,13 @@ export const SecretSnapshotsLegacySection = () => {
|
||||
<p className="mb-3 text-xl font-medium">Show Secret Snapshots ( legacy )</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<div>
|
||||
<Checkbox
|
||||
id="showSnapshotsLegacy"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentProject?.showSnapshotsLegacy ?? false}
|
||||
onCheckedChange={(state) => handleToggle(state as boolean)}
|
||||
allowMultilineLabel
|
||||
>
|
||||
This feature enables your project members to view secret snapshots in the legacy
|
||||
format.
|
||||
|
||||
Reference in New Issue
Block a user