mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 05:28:51 +00:00
Merge branch 'main' into ENG-3959
This commit is contained in:
@@ -63,14 +63,14 @@ export const WishForm = () => {
|
||||
open={isOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="text-md mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<div className="mb-3 w-full cursor-pointer pl-5 text-sm whitespace-nowrap text-mineshaft-400 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faRocketchat} className="mr-2" />
|
||||
Request a feature
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
hideCloseBtn
|
||||
align="start"
|
||||
align="end"
|
||||
alignOffset={20}
|
||||
className="mb-1 w-auto border border-mineshaft-600 bg-mineshaft-900 p-4 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
|
||||
@@ -20,11 +20,11 @@ const badgeVariants = cva(
|
||||
success: "bg-green/20 text-green",
|
||||
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
|
||||
namespace:
|
||||
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1 w-min whitespace-nowrap",
|
||||
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1.5 w-min whitespace-nowrap",
|
||||
project:
|
||||
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
|
||||
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1.5 whitespace-nowrap",
|
||||
instance:
|
||||
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap"
|
||||
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1.5 w-min whitespace-nowrap"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +120,16 @@ export type TBreadcrumbFormat =
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
|
||||
<div className="mx-auto max-w-7xl text-white">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
const BreadcrumbContainer = ({
|
||||
breadcrumbs,
|
||||
className
|
||||
}: {
|
||||
breadcrumbs: TBreadcrumbFormat[];
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={twMerge("mx-auto max-w-8xl overflow-hidden text-white", className)}>
|
||||
<Breadcrumb className="overflow-hidden">
|
||||
<BreadcrumbList className="overflow-hidden">
|
||||
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
|
||||
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
|
||||
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
|
||||
@@ -165,8 +171,8 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
|
||||
const Component = el.component;
|
||||
return (
|
||||
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbSegment>
|
||||
<BreadcrumbItem className="overflow-hidden">
|
||||
<BreadcrumbSegment className="overflow-hidden">
|
||||
<Component />
|
||||
</BreadcrumbSegment>
|
||||
</BreadcrumbItem>
|
||||
|
||||
@@ -5,29 +5,48 @@ import { ReactNode } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { BadgeProps } from "@app/components/v2/Badge/Badge";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
type Props = {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
scope: "org" | "project" | "namespace" | "instance";
|
||||
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
||||
};
|
||||
|
||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
|
||||
org: { label: "Organization", icon: faGlobe },
|
||||
project: { label: "Project", icon: faCube },
|
||||
namespace: { label: "Namespace", icon: faCubes },
|
||||
[ProjectType.SecretManager]: { label: "Project", icon: faCube },
|
||||
[ProjectType.CertificateManager]: { label: "Project", icon: faCube },
|
||||
[ProjectType.SSH]: { label: "Project", icon: faCube },
|
||||
[ProjectType.KMS]: { label: "Project", icon: faCube },
|
||||
[ProjectType.PAM]: { label: "Project", icon: faCube },
|
||||
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
|
||||
namespace: { label: "Sub-Organization", icon: faCubes },
|
||||
instance: { label: "Server", icon: faServer }
|
||||
};
|
||||
|
||||
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]> = {
|
||||
org: "org",
|
||||
[ProjectType.SecretManager]: "project",
|
||||
[ProjectType.CertificateManager]: "project",
|
||||
[ProjectType.SSH]: "project",
|
||||
[ProjectType.KMS]: "project",
|
||||
[ProjectType.PAM]: "project",
|
||||
[ProjectType.SecretScanning]: "project",
|
||||
namespace: "namespace",
|
||||
instance: "instance"
|
||||
};
|
||||
|
||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||
<div className={twMerge("mb-4 w-full", className)}>
|
||||
<div className={twMerge("mb-10 w-full", className)}>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
|
||||
{scope && (
|
||||
<Badge variant={scope} className="mt-1 ml-2.5">
|
||||
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
|
||||
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
|
||||
{SCOPE_NAME[scope].label}
|
||||
</Badge>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type TabsProps = TabsPrimitive.TabsProps;
|
||||
|
||||
export const Tabs = ({ className, children, ...props }: TabsProps) => (
|
||||
<TabsPrimitive.Root className={twMerge("flex flex-col", className)} {...props}>
|
||||
<TabsPrimitive.Root
|
||||
className={twMerge(
|
||||
"flex",
|
||||
className,
|
||||
props.orientation === "vertical" ? "flex-col xl:flex-row xl:gap-x-12" : "flex-col"
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabsPrimitive.Root>
|
||||
);
|
||||
@@ -13,7 +22,11 @@ export type TabListProps = TabsPrimitive.TabsListProps;
|
||||
|
||||
export const TabList = ({ className, children, ...props }: TabListProps) => (
|
||||
<TabsPrimitive.List
|
||||
className={twMerge("flex shrink-0 border-b-2 border-mineshaft-800", className)}
|
||||
className={twMerge(
|
||||
"no-scrollbar flex shrink-0 overflow-auto border-b-2 border-mineshaft-800",
|
||||
"data-[orientation=vertical]:xl:flex-col data-[orientation=vertical]:xl:items-start data-[orientation=vertical]:xl:gap-y-6 data-[orientation=vertical]:xl:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -26,18 +39,29 @@ export const Tab = ({
|
||||
className,
|
||||
children,
|
||||
variant = "project",
|
||||
icon,
|
||||
...props
|
||||
}: TabProps & { variant?: "project" | "namespace" | "org" }) => (
|
||||
}: TabProps & {
|
||||
icon?: IconDefinition;
|
||||
variant?: "project" | "namespace" | "org" | "instance";
|
||||
}) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"flex h-10 items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:text-white",
|
||||
"flex h-10 cursor-pointer items-center justify-center border-transparent",
|
||||
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
|
||||
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
|
||||
"border-b hover:text-mineshaft-200",
|
||||
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
|
||||
"hover:border-mineshaft-400",
|
||||
variant === "project" && "data-[state=active]:border-primary",
|
||||
variant === "namespace" && "data-[state=active]:border-namespace-v1",
|
||||
variant === "org" && "data-[state=active]:border-org-v1",
|
||||
variant === "instance" && "data-[state=active]:border-mineshaft-300",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="mr-2" size="xs" />}
|
||||
{children}
|
||||
</TabsPrimitive.Trigger>
|
||||
);
|
||||
@@ -46,7 +70,10 @@ export type TabPanelProps = TabsPrimitive.TabsContentProps;
|
||||
|
||||
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
|
||||
<TabsPrimitive.Content
|
||||
className={twMerge("grow rounded-br-md rounded-bl-md py-5 outline-hidden", className)}
|
||||
className={twMerge(
|
||||
"grow rounded-br-md rounded-bl-md py-5 outline-hidden data-[orientation=vertical]:xl:overflow-x-hidden data-[orientation=vertical]:xl:py-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -24,6 +24,8 @@ apiRequest.interceptors.request.use((config) => {
|
||||
const token = getAuthToken();
|
||||
const providerAuthToken = SecurityClient.getProviderAuthToken();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (config.headers) {
|
||||
if (signupTempToken) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -38,6 +40,17 @@ apiRequest.interceptors.request.use((config) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${providerAuthToken}`;
|
||||
}
|
||||
|
||||
const rootOrgHeader = config.headers.get("x-root-org");
|
||||
|
||||
if (rootOrgHeader) {
|
||||
config.headers.delete("x-root-org");
|
||||
} else {
|
||||
const subOrganization = params.get("subOrganization");
|
||||
if (subOrganization) {
|
||||
config.headers.set("x-infisical-org", subOrganization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -33,10 +33,6 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
|
||||
),
|
||||
SecretSharingSettings: setRoute(
|
||||
"/organization/secret-sharing/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
|
||||
),
|
||||
SettingsPage: setRoute(
|
||||
"/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||
@@ -66,7 +62,11 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/organization/app-connections/$appConnection/oauth/callback",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
|
||||
)
|
||||
}
|
||||
},
|
||||
NetworkingPage: setRoute(
|
||||
"/organization/networking",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/networking"
|
||||
)
|
||||
},
|
||||
SecretManager: {
|
||||
ApprovalPage: setRoute(
|
||||
|
||||
@@ -62,7 +62,8 @@ export enum OrgPermissionSubjects {
|
||||
SecretShare = "secret-share",
|
||||
GithubOrgSync = "github-org-sync",
|
||||
GithubOrgSyncManual = "github-org-sync-manual",
|
||||
MachineIdentityAuthTemplate = "machine-identity-auth-template"
|
||||
MachineIdentityAuthTemplate = "machine-identity-auth-template",
|
||||
SubOrganization = "sub-organization"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@@ -112,6 +113,11 @@ export enum OrgPermissionGroupActions {
|
||||
RemoveMembers = "remove-members"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubOrgActions {
|
||||
Create = "create",
|
||||
DirectAccess = "direct-access"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
connectionId: string;
|
||||
};
|
||||
@@ -151,6 +157,7 @@ export type OrgPermissionSet =
|
||||
| OrgPermissionSubjects.AppConnections
|
||||
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
|
||||
)
|
||||
];
|
||||
]
|
||||
| [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization];
|
||||
|
||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { useRouteContext, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
||||
|
||||
@@ -9,11 +10,29 @@ export const useOrganization = () => {
|
||||
select: (el) => el.organizationId
|
||||
});
|
||||
|
||||
const subOrganization = useSearch({
|
||||
strict: false,
|
||||
select: (el) => el?.subOrganization
|
||||
});
|
||||
|
||||
const { data: currentOrg } = useSuspenseQuery({
|
||||
queryKey: organizationKeys.getOrgById(organizationId),
|
||||
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"),
|
||||
queryFn: () => fetchOrganizationById(organizationId),
|
||||
staleTime: Infinity
|
||||
});
|
||||
|
||||
return { currentOrg };
|
||||
const org = useMemo(
|
||||
() => ({
|
||||
currentOrg: {
|
||||
...currentOrg,
|
||||
id: currentOrg?.subOrganization?.id || currentOrg?.id,
|
||||
parentOrgId: currentOrg.id
|
||||
},
|
||||
isSubOrganization: Boolean(currentOrg.subOrganization),
|
||||
isRootOrganization: !currentOrg.subOrganization
|
||||
}),
|
||||
[currentOrg, subOrganization]
|
||||
);
|
||||
|
||||
return org;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
|
||||
@@ -124,6 +124,14 @@ export enum ProjectPermissionPkiTemplateActions {
|
||||
ListCerts = "list-certs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateProfileActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
IssueCert = "issue-cert"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadGeneratedCredentials = "read-generated-credentials",
|
||||
@@ -293,6 +301,7 @@ export enum ProjectPermissionSub {
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
PkiSubscribers = "pki-subscribers",
|
||||
CertificateProfiles = "certificate-profiles",
|
||||
Kms = "kms",
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs",
|
||||
@@ -470,6 +479,7 @@ export type ProjectPermissionSet =
|
||||
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
|
||||
@@ -15,6 +15,7 @@ export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
|
||||
@@ -79,7 +79,7 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]
|
||||
case ProjectType.SecretManager:
|
||||
return "/projects/secret-management/$projectId/overview" as const;
|
||||
case ProjectType.CertificateManager:
|
||||
return "/projects/cert-management/$projectId/subscribers" as const;
|
||||
return "/projects/cert-management/$projectId/policies" as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/projects/${type}/$projectId/data-sources` as const;
|
||||
case ProjectType.PAM:
|
||||
|
||||
@@ -24,7 +24,7 @@ export const withPermission = <T extends object, J extends TOrgPermission>(
|
||||
|
||||
// akhilmhdh: Set as any due to casl/react ts type bug
|
||||
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
|
||||
if (permission.cannot(action as any, subject)) {
|
||||
if (permission.cannot(action as any, subject as any)) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
|
||||
@@ -2,8 +2,10 @@ export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
useCreateCertificateV3,
|
||||
useDeleteCa,
|
||||
useImportCaCertificate,
|
||||
useOrderCertificateWithProfile,
|
||||
useRenewCa,
|
||||
useSignIntermediate,
|
||||
useUpdateCa
|
||||
@@ -21,3 +23,4 @@ export {
|
||||
useListCasByTypeAndProjectId,
|
||||
useListExternalCasByProjectId
|
||||
} from "./queries";
|
||||
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
|
||||
|
||||
@@ -9,9 +9,13 @@ import {
|
||||
TCreateCertificateAuthorityDTO,
|
||||
TCreateCertificateDTO,
|
||||
TCreateCertificateResponse,
|
||||
TCreateCertificateV3DTO,
|
||||
TCreateCertificateV3Response,
|
||||
TDeleteCertificateAuthorityDTO,
|
||||
TImportCaCertificateDTO,
|
||||
TImportCaCertificateResponse,
|
||||
TOrderCertificateDTO,
|
||||
TOrderCertificateResponse,
|
||||
TRenewCaDTO,
|
||||
TRenewCaResponse,
|
||||
TSignIntermediateDTO,
|
||||
@@ -148,6 +152,46 @@ export const useCreateCertificate = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCertificateV3 = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateV3Response, object, TCreateCertificateV3DTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TCreateCertificateV3Response>(
|
||||
"/api/v3/certificates/issue-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["certificate-profiles"]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useOrderCertificateWithProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TOrderCertificateResponse, object, TOrderCertificateDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TOrderCertificateResponse>(
|
||||
"/api/v3/certificates/order-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRenewCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TRenewCaResponse, object, TRenewCaDTO>({
|
||||
|
||||
@@ -155,7 +155,7 @@ export type TCreateCertificateDTO = {
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string; // sans
|
||||
subjectAltNames: string; // sans
|
||||
ttl: string; // string compatible with ms
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
@@ -171,6 +171,84 @@ export type TCreateCertificateResponse = {
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateV3DTO = {
|
||||
projectSlug: string;
|
||||
profileId: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
organizationUnit?: string;
|
||||
locality?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
email?: string;
|
||||
streetAddress?: string;
|
||||
postalCode?: string;
|
||||
subjectAltNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateV3Response = TCreateCertificateResponse & {
|
||||
projectId: string;
|
||||
profileName: string;
|
||||
certificateId: string;
|
||||
};
|
||||
|
||||
export type TOrderCertificateDTO = {
|
||||
projectSlug: string;
|
||||
profileId: string;
|
||||
subjectAlternativeNames: Array<{
|
||||
type: "dns" | "ip";
|
||||
value: string;
|
||||
}>;
|
||||
ttl: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
commonName?: string;
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
};
|
||||
|
||||
export type TOrderCertificateResponse = {
|
||||
orderId: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
subjectAlternativeNames: Array<{
|
||||
type: "dns" | "ip";
|
||||
value: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
}>;
|
||||
authorizations: Array<{
|
||||
identifier: {
|
||||
type: "dns" | "ip";
|
||||
value: string;
|
||||
};
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
expires?: string;
|
||||
challenges: Array<{
|
||||
type: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
url: string;
|
||||
token: string;
|
||||
validated?: string;
|
||||
error?: string | Error;
|
||||
}>;
|
||||
}>;
|
||||
certificate?: string;
|
||||
privateKey?: string;
|
||||
expires: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
};
|
||||
|
||||
export type TRenewCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export {
|
||||
useCreateCertificateProfile,
|
||||
useDeleteCertificateProfile,
|
||||
useUpdateCertificateProfile
|
||||
} from "./mutations";
|
||||
export {
|
||||
certificateProfileKeys,
|
||||
useGetCertificateProfileById,
|
||||
useGetCertificateProfileBySlug,
|
||||
useGetProfileCertificates,
|
||||
useGetProfileMetrics,
|
||||
useListCertificateProfiles
|
||||
} from "./queries";
|
||||
export type * from "./types";
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { certificateProfileKeys } from "./queries";
|
||||
import {
|
||||
TCertificateProfile,
|
||||
TCreateCertificateProfileDTO,
|
||||
TDeleteCertificateProfileDTO,
|
||||
TUpdateCertificateProfileDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCertificateProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TCertificateProfile, object, TCreateCertificateProfileDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: response } = await apiRequest.post<{
|
||||
certificateProfile: TCertificateProfile;
|
||||
}>("/api/v1/pki/certificate-profiles", data);
|
||||
return response.certificateProfile;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certificateProfileKeys.list({ projectId })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCertificateProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TCertificateProfile, object, TUpdateCertificateProfileDTO>({
|
||||
mutationFn: async ({ profileId, ...data }) => {
|
||||
const { data: response } = await apiRequest.patch<{
|
||||
certificateProfile: TCertificateProfile;
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}`, data);
|
||||
return response.certificateProfile;
|
||||
},
|
||||
onSuccess: (profile, { profileId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certificateProfileKeys.list({ projectId: profile.projectId })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certificateProfileKeys.getById(profileId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCertificateProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TCertificateProfile, object, TDeleteCertificateProfileDTO>({
|
||||
mutationFn: async ({ profileId }) => {
|
||||
const { data: response } = await apiRequest.delete<{
|
||||
certificateProfile: TCertificateProfile;
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}`);
|
||||
return response.certificateProfile;
|
||||
},
|
||||
onSuccess: (profile, { profileId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certificateProfileKeys.list({ projectId: profile.projectId })
|
||||
});
|
||||
queryClient.removeQueries({
|
||||
queryKey: certificateProfileKeys.getById(profileId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileWithDetails,
|
||||
TGetCertificateProfileByIdDTO,
|
||||
TGetCertificateProfileBySlugDTO,
|
||||
TGetProfileCertificatesDTO,
|
||||
TGetProfileMetricsDTO,
|
||||
TListCertificateProfilesDTO,
|
||||
TProfileCertificate
|
||||
} from "./types";
|
||||
|
||||
export const certificateProfileKeys = {
|
||||
list: (params: {
|
||||
projectId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
includeMetrics?: boolean;
|
||||
includeConfigs?: boolean;
|
||||
enrollmentType?: string;
|
||||
expiringDays?: number;
|
||||
}) => ["certificate-profiles", "list", params],
|
||||
getById: (profileId: string) => ["certificate-profiles", "get-by-id", profileId],
|
||||
getBySlug: (projectId: string, slug: string) => [
|
||||
"certificate-profiles",
|
||||
"get-by-slug",
|
||||
projectId,
|
||||
slug
|
||||
],
|
||||
getCertificates: (profileId: string, params?: Omit<TGetProfileCertificatesDTO, "profileId">) => [
|
||||
"certificate-profiles",
|
||||
"certificates",
|
||||
profileId,
|
||||
params
|
||||
],
|
||||
getMetrics: (profileId: string, params?: Omit<TGetProfileMetricsDTO, "profileId">) => [
|
||||
"certificate-profiles",
|
||||
"metrics",
|
||||
profileId,
|
||||
params
|
||||
]
|
||||
};
|
||||
|
||||
export const useListCertificateProfiles = ({
|
||||
projectId,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
search,
|
||||
includeMetrics = false,
|
||||
includeConfigs = false,
|
||||
enrollmentType,
|
||||
expiringDays = 7
|
||||
}: TListCertificateProfilesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.list({
|
||||
projectId,
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
includeMetrics,
|
||||
includeConfigs,
|
||||
enrollmentType,
|
||||
expiringDays
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateProfiles: TCertificateProfile[];
|
||||
totalCount: number;
|
||||
}>("/api/v1/pki/certificate-profiles", {
|
||||
params: {
|
||||
projectId,
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
includeMetrics,
|
||||
includeConfigs,
|
||||
enrollmentType,
|
||||
expiringDays
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(projectId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCertificateProfileById = ({ profileId }: TGetCertificateProfileByIdDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.getById(profileId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateProfile: TCertificateProfileWithDetails;
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}`);
|
||||
return data.certificateProfile;
|
||||
},
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCertificateProfileBySlug = ({
|
||||
projectId,
|
||||
slug
|
||||
}: TGetCertificateProfileBySlugDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.getBySlug(projectId, slug),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateProfile: TCertificateProfile;
|
||||
}>(`/api/v1/pki/certificate-profiles/slug/${slug}`, {
|
||||
params: { projectId }
|
||||
});
|
||||
return data.certificateProfile;
|
||||
},
|
||||
enabled: Boolean(projectId && slug)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProfileCertificates = ({
|
||||
profileId,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
status,
|
||||
search
|
||||
}: TGetProfileCertificatesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.getCertificates(profileId, { offset, limit, status, search }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificates: TProfileCertificate[];
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}/certificates`, {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
status,
|
||||
search
|
||||
}
|
||||
});
|
||||
return data.certificates;
|
||||
},
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProfileMetrics = ({ profileId, expiringDays = 7 }: TGetProfileMetricsDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certificateProfileKeys.getMetrics(profileId, { expiringDays }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
metrics: TCertificateProfileMetrics;
|
||||
}>(`/api/v1/pki/certificate-profiles/${profileId}/metrics`, {
|
||||
params: { expiringDays }
|
||||
});
|
||||
return data.metrics;
|
||||
},
|
||||
enabled: Boolean(profileId)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
export type TCertificateProfile = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
caId: string;
|
||||
certificateTemplateId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
enrollmentType: "api" | "est";
|
||||
estConfigId?: string;
|
||||
apiConfigId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metrics?: TCertificateProfileMetrics;
|
||||
};
|
||||
|
||||
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||
certificateAuthority?: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
status: string;
|
||||
name: string;
|
||||
};
|
||||
certificateTemplate?: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
estConfig?: {
|
||||
id: string;
|
||||
disableBootstrapCaValidation: boolean;
|
||||
passphrase: string;
|
||||
caChain: string;
|
||||
};
|
||||
apiConfig?: {
|
||||
id: string;
|
||||
autoRenew: boolean;
|
||||
autoRenewDays?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateCertificateProfileDTO = {
|
||||
projectId: string;
|
||||
caId: string;
|
||||
certificateTemplateId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
enrollmentType: "api" | "est";
|
||||
estConfig?: {
|
||||
disableBootstrapCaValidation?: boolean;
|
||||
passphrase: string;
|
||||
caChain?: string;
|
||||
};
|
||||
apiConfig?: {
|
||||
autoRenew?: boolean;
|
||||
autoRenewDays?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateCertificateProfileDTO = {
|
||||
profileId: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
estConfig?: {
|
||||
disableBootstrapCaValidation?: boolean;
|
||||
passphrase?: string;
|
||||
caChain?: string;
|
||||
};
|
||||
apiConfig?: {
|
||||
autoRenew?: boolean;
|
||||
autoRenewDays?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TDeleteCertificateProfileDTO = {
|
||||
profileId: string;
|
||||
};
|
||||
|
||||
export type TListCertificateProfilesDTO = {
|
||||
projectId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
includeMetrics?: boolean;
|
||||
includeConfigs?: boolean;
|
||||
enrollmentType?: "api" | "est";
|
||||
expiringDays?: number;
|
||||
};
|
||||
|
||||
export type TGetCertificateProfileByIdDTO = {
|
||||
profileId: string;
|
||||
};
|
||||
|
||||
export type TGetCertificateProfileBySlugDTO = {
|
||||
projectId: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type TCertificateProfileMetrics = {
|
||||
profileId: string;
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiredCertificates: number;
|
||||
expiringCertificates: number;
|
||||
revokedCertificates: number;
|
||||
};
|
||||
|
||||
export type TProfileCertificate = {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
cn: string;
|
||||
status: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
isRevoked: boolean;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type TGetProfileCertificatesDTO = {
|
||||
profileId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
status?: "active" | "expired" | "revoked";
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TGetProfileMetricsDTO = {
|
||||
profileId: string;
|
||||
expiringDays?: number;
|
||||
};
|
||||
@@ -7,13 +7,17 @@ import { projectKeys } from "../projects";
|
||||
import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TCreateCertificateTemplateV2DTO,
|
||||
TCreateCertificateTemplateV2WithPoliciesDTO,
|
||||
TCreateEstConfigDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TDeleteCertificateTemplateV2DTO,
|
||||
TDeleteCertificateTemplateV2WithPoliciesDTO,
|
||||
TUpdateCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateV2DTO,
|
||||
TUpdateCertificateTemplateV2WithPoliciesDTO,
|
||||
TUpdateEstConfigDTO
|
||||
} from "./types";
|
||||
|
||||
@@ -163,3 +167,72 @@ export const useUpdateEstConfig = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCertificateTemplateV2WithPolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
object,
|
||||
TCreateCertificateTemplateV2WithPoliciesDTO
|
||||
>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: response } = await apiRequest.post<{
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>("/api/v2/certificate-templates", data);
|
||||
return response.certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certTemplateKeys.listTemplatesV2({ projectId })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCertificateTemplateV2WithPolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
object,
|
||||
TUpdateCertificateTemplateV2WithPoliciesDTO
|
||||
>({
|
||||
mutationFn: async ({ templateId, ...data }) => {
|
||||
const { data: response } = await apiRequest.patch<{
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>(`/api/v2/certificate-templates/${templateId}`, data);
|
||||
return response.certificateTemplate;
|
||||
},
|
||||
onSuccess: (template, { templateId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certTemplateKeys.listTemplatesV2({ projectId: template.projectId })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certTemplateKeys.getTemplateV2ById(templateId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCertificateTemplateV2WithPolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
object,
|
||||
TDeleteCertificateTemplateV2WithPoliciesDTO
|
||||
>({
|
||||
mutationFn: async ({ templateId }) => {
|
||||
const { data: response } = await apiRequest.delete<{
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||
return response.certificateTemplate;
|
||||
},
|
||||
onSuccess: (template, { templateId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: certTemplateKeys.listTemplatesV2({ projectId: template.projectId })
|
||||
});
|
||||
queryClient.removeQueries({
|
||||
queryKey: certTemplateKeys.getTemplateV2ById(templateId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,8 +5,11 @@ import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCertificateTemplateV2,
|
||||
TCertificateTemplateV2WithPolicies,
|
||||
TEstConfig,
|
||||
TListCertificateTemplatesDTO
|
||||
TGetCertificateTemplateV2ByIdDTO,
|
||||
TListCertificateTemplatesDTO,
|
||||
TListCertificateTemplatesV2DTO
|
||||
} from "./types";
|
||||
|
||||
export const certTemplateKeys = {
|
||||
@@ -16,7 +19,16 @@ export const certTemplateKeys = {
|
||||
projectId,
|
||||
el
|
||||
],
|
||||
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
|
||||
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"],
|
||||
listTemplatesV2: ({
|
||||
projectId,
|
||||
...el
|
||||
}: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
projectId: string;
|
||||
}) => ["list-templates-v2", projectId, el],
|
||||
getTemplateV2ById: (id: string) => ["cert-template-v2", id]
|
||||
};
|
||||
|
||||
export const useGetCertTemplate = (id: string) => {
|
||||
@@ -68,3 +80,42 @@ export const useGetEstConfig = (certificateTemplateId: string) => {
|
||||
enabled: Boolean(certificateTemplateId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListCertificateTemplatesV2 = ({
|
||||
projectId,
|
||||
limit = 20,
|
||||
offset = 0
|
||||
}: TListCertificateTemplatesV2DTO) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.listTemplatesV2({ projectId, limit, offset }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateTemplates: TCertificateTemplateV2WithPolicies[];
|
||||
totalCount: number;
|
||||
}>("/api/v2/certificate-templates", {
|
||||
params: {
|
||||
projectId,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(projectId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCertificateTemplateV2ById = ({
|
||||
templateId
|
||||
}: TGetCertificateTemplateV2ByIdDTO) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.getTemplateV2ById(templateId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificateTemplate: TCertificateTemplateV2WithPolicies;
|
||||
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||
return data.certificateTemplate;
|
||||
},
|
||||
enabled: Boolean(templateId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -121,3 +121,90 @@ export type TListCertificateTemplatesDTO = {
|
||||
offset?: number;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2Policy = {
|
||||
subject?: Array<{
|
||||
type: "common_name" | "organization" | "country";
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>;
|
||||
sans?: Array<{
|
||||
type: "dns_name" | "ip_address" | "email" | "uri";
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>;
|
||||
keyUsages?: {
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
};
|
||||
extendedKeyUsages?: {
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
};
|
||||
algorithms?: {
|
||||
signature?: Array<
|
||||
"SHA256-RSA" | "SHA384-RSA" | "SHA512-RSA" | "SHA256-ECDSA" | "SHA384-ECDSA" | "SHA512-ECDSA"
|
||||
>;
|
||||
keyAlgorithm?: Array<"RSA-2048" | "RSA-3072" | "RSA-4096" | "ECDSA-P256" | "ECDSA-P384">;
|
||||
};
|
||||
validity?: {
|
||||
max?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2WithPolicies = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
subject?: TCertificateTemplateV2Policy["subject"];
|
||||
sans?: TCertificateTemplateV2Policy["sans"];
|
||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
algorithms?: TCertificateTemplateV2Policy["algorithms"];
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateV2WithPoliciesDTO = {
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
subject?: TCertificateTemplateV2Policy["subject"];
|
||||
sans?: TCertificateTemplateV2Policy["sans"];
|
||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
algorithms?: TCertificateTemplateV2Policy["algorithms"];
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateV2WithPoliciesDTO = {
|
||||
templateId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
subject?: TCertificateTemplateV2Policy["subject"];
|
||||
sans?: TCertificateTemplateV2Policy["sans"];
|
||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
algorithms?: TCertificateTemplateV2Policy["algorithms"];
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateV2WithPoliciesDTO = {
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
export type TListCertificateTemplatesV2DTO = {
|
||||
projectId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type TGetCertificateTemplateV2ByIdDTO = {
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export const getCertStatusBadgeVariant = (status: CertStatus) => {
|
||||
|
||||
export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
||||
[CertKeyAlgorithm.RSA_2048]: "RSA 2048",
|
||||
[CertKeyAlgorithm.RSA_3072]: "RSA 3072",
|
||||
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
|
||||
[CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
|
||||
[CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384"
|
||||
@@ -31,6 +32,7 @@ export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
||||
|
||||
export const certKeyAlgorithms = [
|
||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
|
||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_3072], value: CertKeyAlgorithm.RSA_3072 },
|
||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_4096], value: CertKeyAlgorithm.RSA_4096 },
|
||||
{
|
||||
label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P256],
|
||||
@@ -96,3 +98,12 @@ export const EXTENDED_KEY_USAGES_OPTIONS = [
|
||||
{ value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" },
|
||||
{ value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" }
|
||||
] as const;
|
||||
|
||||
export const SIGNATURE_ALGORITHMS_OPTIONS = [
|
||||
{ value: "RSA-SHA256", label: "RSA-SHA256" },
|
||||
{ value: "RSA-SHA384", label: "RSA-SHA384" },
|
||||
{ value: "RSA-SHA512", label: "RSA-SHA512" },
|
||||
{ value: "ECDSA-SHA256", label: "ECDSA-SHA256" },
|
||||
{ value: "ECDSA-SHA384", label: "ECDSA-SHA384" },
|
||||
{ value: "ECDSA-SHA512", label: "ECDSA-SHA512" }
|
||||
] as const;
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum CertStatus {
|
||||
|
||||
export enum CertKeyAlgorithm {
|
||||
RSA_2048 = "RSA_2048",
|
||||
RSA_3072 = "RSA_3072",
|
||||
RSA_4096 = "RSA_4096",
|
||||
ECDSA_P256 = "EC_prime256v1",
|
||||
ECDSA_P384 = "EC_secp384r1"
|
||||
@@ -24,22 +25,22 @@ export enum CrlReason {
|
||||
}
|
||||
|
||||
export enum CertKeyUsage {
|
||||
DIGITAL_SIGNATURE = "digitalSignature",
|
||||
KEY_ENCIPHERMENT = "keyEncipherment",
|
||||
NON_REPUDIATION = "nonRepudiation",
|
||||
DATA_ENCIPHERMENT = "dataEncipherment",
|
||||
KEY_AGREEMENT = "keyAgreement",
|
||||
KEY_CERT_SIGN = "keyCertSign",
|
||||
CRL_SIGN = "cRLSign",
|
||||
ENCIPHER_ONLY = "encipherOnly",
|
||||
DECIPHER_ONLY = "decipherOnly"
|
||||
DIGITAL_SIGNATURE = "digital_signature",
|
||||
KEY_ENCIPHERMENT = "key_encipherment",
|
||||
NON_REPUDIATION = "non_repudiation",
|
||||
DATA_ENCIPHERMENT = "data_encipherment",
|
||||
KEY_AGREEMENT = "key_agreement",
|
||||
KEY_CERT_SIGN = "key_cert_sign",
|
||||
CRL_SIGN = "crl_sign",
|
||||
ENCIPHER_ONLY = "encipher_only",
|
||||
DECIPHER_ONLY = "decipher_only"
|
||||
}
|
||||
|
||||
export enum CertExtendedKeyUsage {
|
||||
CLIENT_AUTH = "clientAuth",
|
||||
SERVER_AUTH = "serverAuth",
|
||||
CODE_SIGNING = "codeSigning",
|
||||
EMAIL_PROTECTION = "emailProtection",
|
||||
TIMESTAMPING = "timeStamping",
|
||||
OCSP_SIGNING = "ocspSigning"
|
||||
CLIENT_AUTH = "client_auth",
|
||||
SERVER_AUTH = "server_auth",
|
||||
CODE_SIGNING = "code_signing",
|
||||
EMAIL_PROTECTION = "email_protection",
|
||||
TIMESTAMPING = "time_stamping",
|
||||
OCSP_SIGNING = "ocsp_signing"
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ export const useRevokeCert = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["certificate-profiles", "list"]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export type TCertificate = {
|
||||
status: CertStatus;
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
subjectAltNames: string;
|
||||
serialNumber: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
|
||||
@@ -20,6 +20,7 @@ export type Identity = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isInstanceAdmin?: boolean;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type IdentityAccessToken = {
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from "./pkiSubscriber";
|
||||
export * from "./projects";
|
||||
export * from "./projectUserAdditionalPrivilege";
|
||||
export * from "./rateLimit";
|
||||
export * from "./relays";
|
||||
export * from "./roles";
|
||||
export * from "./scim";
|
||||
export * from "./secretApproval";
|
||||
@@ -48,6 +49,7 @@ export * from "./sshCertificateTemplates";
|
||||
export * from "./sshHost";
|
||||
export * from "./sshHostGroup";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./subOrganizations";
|
||||
export * from "./subscriptions";
|
||||
export * from "./tags";
|
||||
export * from "./trustedIps";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation";
|
||||
export type {
|
||||
TCreateOrgIdentityMembershipDTO,
|
||||
TDeleteOrgIdentityMembershipDTO,
|
||||
TOrgIdentityMembership
|
||||
} from "./types";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { identitiesKeys } from "../identities";
|
||||
import {
|
||||
TCreateOrgIdentityMembershipDTO,
|
||||
TDeleteOrgIdentityMembershipDTO,
|
||||
TOrgIdentityMembership
|
||||
} from "./types";
|
||||
|
||||
export const useCreateOrgIdentityMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => {
|
||||
const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>(
|
||||
`/api/v1/organization/identity-memberships/${identityId}`,
|
||||
{ roles }
|
||||
);
|
||||
return data.identityMembership;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteOrgIdentityMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => {
|
||||
const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>(
|
||||
`/api/v1/organization/identity-memberships/${identityId}`
|
||||
);
|
||||
return data.identityMembership;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export enum TemporaryPermissionMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
|
||||
export type TOrgIdentityMembership = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
identityId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateOrgIdentityMembershipDTO = {
|
||||
identityId: string;
|
||||
roles: Array<
|
||||
| {
|
||||
role: string;
|
||||
isTemporary?: false;
|
||||
}
|
||||
| {
|
||||
role: string;
|
||||
isTemporary: true;
|
||||
temporaryMode: TemporaryPermissionMode;
|
||||
temporaryRange: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export type TDeleteOrgIdentityMembershipDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ export {
|
||||
useDeleteOrgById,
|
||||
useDeleteOrgPmtMethod,
|
||||
useDeleteOrgTaxId,
|
||||
useGetAvailableOrgIdentities,
|
||||
useGetIdentityMembershipOrgs,
|
||||
useGetOrganizationGroups,
|
||||
useGetOrganizations,
|
||||
|
||||
@@ -42,7 +42,9 @@ export const organizationKeys = {
|
||||
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
||||
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
|
||||
getOrgById: (orgId: string) => ["organization", { orgId }]
|
||||
getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }],
|
||||
getAvailableIdentities: () => ["available-identities"],
|
||||
getAvailableUsers: () => ["available-users"]
|
||||
};
|
||||
|
||||
export const fetchOrganizations = async () => {
|
||||
@@ -64,7 +66,9 @@ export const useGetOrganizations = () => {
|
||||
export const fetchOrganizationById = async (id: string) => {
|
||||
const {
|
||||
data: { organization }
|
||||
} = await apiRequest.get<{ organization: Organization }>(`/api/v1/organization/${id}`);
|
||||
} = await apiRequest.get<{
|
||||
organization: Organization & { subOrganization?: { id: string; name: string } };
|
||||
}>(`/api/v1/organization/${id}`);
|
||||
return organization;
|
||||
};
|
||||
|
||||
@@ -572,3 +576,29 @@ export const useGetOrgIntegrationAuths = <TData = IntegrationAuth[],>(
|
||||
select
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAvailableOrgIdentities = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: organizationKeys.getAvailableIdentities(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>(
|
||||
"/api/v1/organization/identities/available"
|
||||
);
|
||||
|
||||
return data.identities;
|
||||
},
|
||||
enabled
|
||||
});
|
||||
|
||||
export const useGetAvailableOrgUsers = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: organizationKeys.getAvailableUsers(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
users: { username: string; id: string; firstName: string; lastName: string }[];
|
||||
}>("/api/v1/organization/users/available");
|
||||
|
||||
return data.users;
|
||||
},
|
||||
enabled
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const UNCHANGED_PASSWORD_SENTINEL = "__INFISICAL_UNCHANGED__";
|
||||
@@ -3,6 +3,7 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TPamResourceOption } from "./types/resource-options";
|
||||
import { PamResourceType } from "./enums";
|
||||
import { TPamAccount, TPamFolder, TPamResource, TPamSession } from "./types";
|
||||
|
||||
export const pamKeys = {
|
||||
@@ -12,6 +13,12 @@ export const pamKeys = {
|
||||
session: () => [...pamKeys.all, "session"] as const,
|
||||
listResourceOptions: () => [...pamKeys.resource(), "options"] as const,
|
||||
listResources: (projectId: string) => [...pamKeys.resource(), "list", projectId],
|
||||
getResource: (resourceType: string, resourceId: string) => [
|
||||
...pamKeys.resource(),
|
||||
"get",
|
||||
resourceType,
|
||||
resourceId
|
||||
],
|
||||
listAccounts: (projectId: string) => [...pamKeys.account(), "list", projectId],
|
||||
getSession: (sessionId: string) => [...pamKeys.session(), "get", sessionId],
|
||||
listSessions: (projectId: string) => [...pamKeys.session(), "list", projectId]
|
||||
@@ -68,6 +75,28 @@ export const useListPamResources = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPamResourceById = (
|
||||
resourceType?: PamResourceType,
|
||||
resourceId?: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<TPamResource, unknown, TPamResource, ReturnType<typeof pamKeys.getResource>>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: pamKeys.getResource(resourceType || "", resourceId || ""),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ resource: TPamResource }>(
|
||||
`/api/v1/pam/resources/${resourceType}/${resourceId}`
|
||||
);
|
||||
|
||||
return data.resource;
|
||||
},
|
||||
enabled: !!resourceId && !!resourceType && (options?.enabled ?? true),
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
// Accounts
|
||||
export const useListPamAccounts = (
|
||||
projectId: string,
|
||||
|
||||
@@ -9,9 +9,13 @@ export interface TBasePamAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
resourceType: PamResourceType;
|
||||
rotationCredentialsConfigured: boolean;
|
||||
};
|
||||
name: string;
|
||||
description?: string | null;
|
||||
rotationEnabled: boolean;
|
||||
rotationIntervalSeconds?: number | null;
|
||||
lastRotatedAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TBasePamResource } from "./base-resource";
|
||||
// Resources
|
||||
export type TPostgresResource = TBasePamResource & { resourceType: PamResourceType.Postgres } & {
|
||||
connectionDetails: TBaseSqlConnectionDetails;
|
||||
rotationAccountCredentials?: TBaseSqlCredentials | null;
|
||||
};
|
||||
|
||||
// Accounts
|
||||
|
||||
@@ -2,6 +2,7 @@ export {
|
||||
useCreateFolder,
|
||||
useDeleteFolder,
|
||||
useGetFoldersByEnv,
|
||||
useGetOrCreateFolder,
|
||||
useGetProjectFolders,
|
||||
useUpdateFolder
|
||||
} from "./queries";
|
||||
|
||||
@@ -140,6 +140,59 @@ export const useGetFoldersByEnv = ({
|
||||
return { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv };
|
||||
};
|
||||
|
||||
export const useGetOrCreateFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TSecretFolder, object, TCreateFolderDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data: existingFolder } = await apiRequest.get<{ folders: TSecretFolder[] }>(
|
||||
"/api/v2/folders",
|
||||
{
|
||||
params: {
|
||||
projectId: dto.projectId,
|
||||
environment: dto.environment,
|
||||
path: dto.path || "/"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const folder = existingFolder.folders.find((f) => f.name === dto.name);
|
||||
|
||||
if (folder) return folder;
|
||||
|
||||
const { data } = await apiRequest.post("/api/v2/folders", {
|
||||
...dto,
|
||||
projectId: dto.projectId
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId, environment, path }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({
|
||||
projectId,
|
||||
secretPath: path ?? "/"
|
||||
})
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: commitKeys.count({ projectId, environment, directory: path })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: commitKeys.history({ projectId, environment, directory: path })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -170,6 +223,9 @@ export const useCreateFolder = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: commitKeys.count({ projectId, environment, directory: path })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: commitKeys.history({ projectId, environment, directory: path })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export { useCreateSubOrganization, useUpdateSubOrganization } from "./mutations";
|
||||
export { subOrganizationsQuery } from "./queries";
|
||||
export type {
|
||||
TCreateSubOrganizationDTO,
|
||||
TListSubOrganizationsDTO,
|
||||
TSubOrganization,
|
||||
TUpdateSubOrganizationDTO
|
||||
} from "./types";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { subOrganizationsQuery } from "./queries";
|
||||
import { TCreateSubOrganizationDTO, TSubOrganization, TUpdateSubOrganizationDTO } from "./types";
|
||||
|
||||
export const useCreateSubOrganization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (dto: TCreateSubOrganizationDTO) => {
|
||||
const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
|
||||
"/api/v1/sub-organizations",
|
||||
dto,
|
||||
{
|
||||
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSubOrganization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ subOrgId, name }: TUpdateSubOrganizationDTO) => {
|
||||
const { data } = await apiRequest.patch<{ organization: TSubOrganization }>(
|
||||
`/api/v1/sub-organizations/${subOrgId}`,
|
||||
{ name }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TListSubOrganizationsDTO, TSubOrganization } from "./types";
|
||||
|
||||
export const subOrganizationsQuery = {
|
||||
allKey: () => ["sub-organizations"] as const,
|
||||
listKey: (params?: TListSubOrganizationsDTO) =>
|
||||
[...subOrganizationsQuery.allKey(), "list", params] as const,
|
||||
list: (params: TListSubOrganizationsDTO) =>
|
||||
queryOptions({
|
||||
queryKey: subOrganizationsQuery.listKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ organizations: TSubOrganization[] }>(
|
||||
"/api/v1/sub-organizations",
|
||||
{
|
||||
params: {
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
isAccessible: params.isAccessible
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.organizations;
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export type TSubOrganization = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
parentOrgId: string;
|
||||
};
|
||||
|
||||
export type TCreateSubOrganizationDTO = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TListSubOrganizationsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
isAccessible?: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateSubOrganizationDTO = {
|
||||
subOrgId: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export type SubscriptionPlan = {
|
||||
customRateLimits: boolean;
|
||||
pitRecovery: boolean;
|
||||
githubOrgSync: boolean;
|
||||
subOrganization?: boolean;
|
||||
ipAllowlisting: boolean;
|
||||
rbac: boolean;
|
||||
secretVersioning: boolean;
|
||||
@@ -47,6 +48,7 @@ export type SubscriptionPlan = {
|
||||
gateway: boolean;
|
||||
externalKms: boolean;
|
||||
pkiEst: boolean;
|
||||
pkiLegacyTemplates: boolean;
|
||||
enforceMfa: boolean;
|
||||
enforceGoogleSSO: boolean;
|
||||
projectTemplates: boolean;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
--font-inter: "Inter", sans-serif;
|
||||
--color-org-v1: #30B3FF;
|
||||
--color-namespace-v1: #96ff59;
|
||||
|
||||
--max-width-8xl: 88rem; /* 1408px */
|
||||
/* Primary */
|
||||
--color-primary-50: #fffff5;
|
||||
--color-primary-100: #fcfce8;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RedisBanner } from "@app/layouts/OrganizationLayout/components/RedisBan
|
||||
import { SmtpBanner } from "@app/layouts/OrganizationLayout/components/SmtpBanner";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { AdminSidebar } from "./Sidebar";
|
||||
import { AdminNavBar } from "./AdminNavBar";
|
||||
|
||||
export const AdminLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -33,9 +33,9 @@ export const AdminLayout = () => {
|
||||
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
|
||||
{!isLoading && subscription.auditLogs && <AuditLogBanner />}
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<AdminSidebar />
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark">
|
||||
<div className="flex grow flex-col overflow-y-hidden">
|
||||
<AdminNavBar />
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faBuilding,
|
||||
faCog,
|
||||
faDatabase,
|
||||
faKey,
|
||||
faLock,
|
||||
faPlug,
|
||||
faUserTie
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Tab, TabList, Tabs, Tooltip } from "@app/components/v2";
|
||||
|
||||
const generalTabs = [
|
||||
{
|
||||
label: "General",
|
||||
icon: faCog,
|
||||
link: "/admin/"
|
||||
},
|
||||
{
|
||||
label: "Resource Overview",
|
||||
icon: faBuilding,
|
||||
link: "/admin/resources/overview"
|
||||
},
|
||||
{
|
||||
label: "Access Control",
|
||||
icon: faUserTie,
|
||||
link: "/admin/access-management"
|
||||
},
|
||||
{
|
||||
label: "Encryption",
|
||||
icon: faLock,
|
||||
link: "/admin/encryption"
|
||||
},
|
||||
{
|
||||
label: "Authentication",
|
||||
icon: faCheckCircle,
|
||||
link: "/admin/authentication"
|
||||
},
|
||||
{
|
||||
label: "Integrations",
|
||||
icon: faPlug,
|
||||
link: "/admin/integrations"
|
||||
},
|
||||
{
|
||||
label: "Caching",
|
||||
icon: faDatabase,
|
||||
link: "/admin/caching"
|
||||
},
|
||||
{
|
||||
label: "Environment Variables",
|
||||
icon: faKey,
|
||||
link: "/admin/environment"
|
||||
}
|
||||
];
|
||||
|
||||
export const AdminNavBar = () => {
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
return (
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<motion.div
|
||||
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">
|
||||
<Tooltip position="bottom" content="Back to organization">
|
||||
<Link to="/organization/projects">
|
||||
<Tab value="back">
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</Tab>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
{generalTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<Tab variant="instance" value={isActive ? "selected" : ""}>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faBuilding,
|
||||
faChevronLeft,
|
||||
faCog,
|
||||
faDatabase,
|
||||
faKey,
|
||||
faLock,
|
||||
faPlug,
|
||||
faUserTie
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
|
||||
const generalTabs = [
|
||||
{
|
||||
label: "General",
|
||||
icon: faCog,
|
||||
link: "/admin/"
|
||||
},
|
||||
{
|
||||
label: "Encryption",
|
||||
icon: faLock,
|
||||
link: "/admin/encryption"
|
||||
},
|
||||
{
|
||||
label: "Authentication",
|
||||
icon: faCheckCircle,
|
||||
link: "/admin/authentication"
|
||||
},
|
||||
{
|
||||
label: "Integrations",
|
||||
icon: faPlug,
|
||||
link: "/admin/integrations"
|
||||
},
|
||||
{
|
||||
label: "Caching",
|
||||
icon: faDatabase,
|
||||
link: "/admin/caching"
|
||||
},
|
||||
{
|
||||
label: "Environment Variables",
|
||||
icon: faKey,
|
||||
link: "/admin/environment"
|
||||
}
|
||||
];
|
||||
|
||||
const othersTabs = [
|
||||
{
|
||||
label: "Access Controls",
|
||||
icon: faUserTie,
|
||||
link: "/admin/access-management"
|
||||
},
|
||||
{
|
||||
label: "Resource Overview",
|
||||
icon: faBuilding,
|
||||
link: "/admin/resources/overview"
|
||||
}
|
||||
];
|
||||
|
||||
export const AdminSidebar = () => {
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
return (
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Configuration">
|
||||
{generalTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={tab.icon} />
|
||||
</div>
|
||||
{tab.label}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
{othersTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={tab.icon} />
|
||||
</div>
|
||||
{tab.label}
|
||||
</div>
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faChevronLeft}
|
||||
flip="vertical"
|
||||
/>
|
||||
}
|
||||
>
|
||||
Back to Organization
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Menu>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { faBook, faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission } from "@app/context";
|
||||
|
||||
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
|
||||
@@ -12,138 +10,81 @@ export const KmsLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="unlock" />
|
||||
KMS
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/kms/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
</div>
|
||||
Overview
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/kms/$projectId/kmip"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faLock} />
|
||||
</div>
|
||||
KMIP
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/kms/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/kms/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/kms/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/kms/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Overview</Tab>}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
<Link
|
||||
to="/projects/kms/$projectId/kmip"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>KMIP</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/kms/$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/kms/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/kms/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useFetchServerStatus } from "@app/hooks/api";
|
||||
import { AuditLogBanner } from "./components/AuditLogBanner";
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
import { Navbar } from "./components/NavBar";
|
||||
import { OrgSidebar } from "./components/OrgSidebar";
|
||||
import { OrgNavBar } from "./components/OrgNavBar";
|
||||
import { RedisBanner } from "./components/RedisBanner";
|
||||
import { SmtpBanner } from "./components/SmtpBanner";
|
||||
|
||||
@@ -41,16 +41,16 @@ export const OrganizationLayout = () => {
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
>
|
||||
<Navbar />
|
||||
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />}
|
||||
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
|
||||
{!isLoading && subscription.auditLogs && <AuditLogBanner />}
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<OrgSidebar isHidden={isInsideProject} />
|
||||
<div className="flex grow flex-col overflow-y-hidden">
|
||||
<OrgNavBar isHidden={isInsideProject} />
|
||||
{!isLoading && !isInsideProject && !serverDetails?.redisConfigured && <RedisBanner />}
|
||||
{!isLoading && !isInsideProject && !serverDetails?.emailConfigured && <SmtpBanner />}
|
||||
{!isLoading && !isInsideProject && subscription.auditLogs && <AuditLogBanner />}
|
||||
{!window.isSecureContext && !isInsideProject && <InsecureConnectionBanner />}
|
||||
<main
|
||||
className={twMerge(
|
||||
"flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark",
|
||||
isInsideProject && "p-0"
|
||||
"flex-1 overflow-x-hidden bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark",
|
||||
isInsideProject ? "overflow-y-hidden p-0" : "overflow-y-auto"
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
@@ -6,11 +6,15 @@ import {
|
||||
faBook,
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faChevronRight,
|
||||
faCubes,
|
||||
faEnvelope,
|
||||
faExclamationTriangle,
|
||||
faGlobe,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faPlus,
|
||||
faServer,
|
||||
faSignOut,
|
||||
faToolbox,
|
||||
@@ -18,7 +22,7 @@ import {
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -33,6 +37,9 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownSubMenu,
|
||||
DropdownSubMenuContent,
|
||||
DropdownSubMenuTrigger,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -43,15 +50,22 @@ import { envConfig } from "@app/config/env";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { projectKeys, useGetOrganizations, useLogoutUser } from "@app/hooks/api";
|
||||
import {
|
||||
projectKeys,
|
||||
subOrganizationsQuery,
|
||||
useGetOrganizations,
|
||||
useGetOrgTrialUrl,
|
||||
useLogoutUser
|
||||
} from "@app/hooks/api";
|
||||
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
||||
import { SubscriptionPlan } from "@app/hooks/api/types";
|
||||
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||
|
||||
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||
import { NewSubOrganizationForm } from "./NewSubOrganizationForm";
|
||||
import { NotificationDropdown } from "./NotificationDropdown";
|
||||
|
||||
const getPlan = (subscription: SubscriptionPlan) => {
|
||||
@@ -118,10 +132,18 @@ export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
export const Navbar = () => {
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
const [showAdminsModal, setShowAdminsModal] = useState(false);
|
||||
const [showSubOrgForm, setShowSubOrgForm] = useState(false);
|
||||
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
|
||||
|
||||
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
|
||||
const { data: subOrganizations = [] } = useQuery({
|
||||
...subOrgQuery,
|
||||
enabled: Boolean(subscription.subOrganization)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
|
||||
sessionStorage.setItem("paymentFailed", "true");
|
||||
@@ -136,6 +158,7 @@ export const Navbar = () => {
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
@@ -160,8 +183,11 @@ export const Navbar = () => {
|
||||
}
|
||||
await router.invalidate();
|
||||
await navigateUserToOrg(navigate, orgId);
|
||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||
};
|
||||
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
const logOutUser = async () => {
|
||||
try {
|
||||
@@ -201,45 +227,85 @@ export const Navbar = () => {
|
||||
|
||||
const isServerAdminPanel = location.pathname.startsWith("/admin");
|
||||
|
||||
const isOrgScope = breadcrumbs?.length === 1; // TODO: scott/akhil is this adequate?
|
||||
const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate?
|
||||
|
||||
const handleOrgNav = async (org: Organization) => {
|
||||
if (currentOrg?.id === org.id) return;
|
||||
|
||||
if (org.authEnforced) {
|
||||
// org has an org-level auth method enabled (e.g. SAML)
|
||||
// -> logout + redirect to SAML SSO
|
||||
|
||||
await logout.mutateAsync();
|
||||
if (org.orgAuthMethod === AuthMethod.OIDC) {
|
||||
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
|
||||
} else {
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
|
||||
}
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (org.googleSsoAuthEnforced) {
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleOrgChange(org?.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-10 flex min-h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4">
|
||||
<div>
|
||||
<Link to="/organization/projects">
|
||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
{isServerAdminPanel ? (
|
||||
<>
|
||||
<Link
|
||||
to="/admin"
|
||||
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<div className="whitespace-nowrap">Server Console</div>
|
||||
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
|
||||
<div className="mr-auto flex items-center overflow-hidden">
|
||||
<div className="shrink-0">
|
||||
<Link to="/organization/projects">
|
||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||
</Link>
|
||||
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
|
||||
{breadcrumbs ? (
|
||||
// scott: remove /admin as we show server console above
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<DropdownMenu modal={false}>
|
||||
<Link to="/organization/projects">
|
||||
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
</div>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
{isServerAdminPanel ? (
|
||||
<>
|
||||
<Link
|
||||
to="/admin"
|
||||
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<div className="whitespace-nowrap">Server Console</div>
|
||||
</Link>
|
||||
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
|
||||
{breadcrumbs ? (
|
||||
// scott: remove /admin as we show server console above
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex 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
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organization/projects",
|
||||
search: (search) => ({ ...search, subOrganization: undefined })
|
||||
});
|
||||
if (isSubOrganization) {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}
|
||||
}}
|
||||
variant="org"
|
||||
className={twMerge("text-sm", !isOrgScope && "bg-transparent opacity-75")}
|
||||
className={twMerge(
|
||||
"max-w-full min-w-0 cursor-pointer text-sm",
|
||||
(!isOrgScope || isSubOrganization) &&
|
||||
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
{currentOrg?.name}
|
||||
<p className="truncate">{currentOrg?.name}</p>
|
||||
</Badge>
|
||||
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
|
||||
{getPlan(subscription)}
|
||||
@@ -258,87 +324,238 @@ export const Navbar = () => {
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="px-2 py-1"
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="center"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
|
||||
Organizations
|
||||
</div>
|
||||
{orgs?.map((org) => {
|
||||
if (
|
||||
subscription.subOrganization &&
|
||||
(org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId)
|
||||
) {
|
||||
return (
|
||||
<DropdownSubMenu key={`${org.id}-sub-orgs`}>
|
||||
<DropdownSubMenuTrigger
|
||||
onClick={() => {
|
||||
setIsOrgSelectOpen(false);
|
||||
handleOrgNav(org);
|
||||
}}
|
||||
className="cursor-pointer font-normal"
|
||||
>
|
||||
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
|
||||
{currentOrg?.id === org.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
|
||||
)}
|
||||
<p className="truncate">{org.name}</p>
|
||||
<FontAwesomeIcon className="ml-auto shrink-0" icon={faChevronRight} />
|
||||
</div>
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent
|
||||
sideOffset={8}
|
||||
alignOffset={-24}
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
|
||||
Sub-Organizations
|
||||
</div>
|
||||
{subOrganizations.map((subOrg) => (
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organization/projects",
|
||||
search: (prev) => ({ ...prev, subOrganization: subOrg.name })
|
||||
});
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}}
|
||||
className="cursor-pointer font-normal"
|
||||
key={subOrg.id}
|
||||
>
|
||||
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
|
||||
{currentOrg?.id === subOrg.id && (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="shrink-0 text-primary"
|
||||
/>
|
||||
)}
|
||||
<p className="truncate">{subOrg.name}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Boolean(subOrganizations.length) && (
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => setShowSubOrgForm(true)}
|
||||
>
|
||||
New Sub-Organization
|
||||
</DropdownMenuItem>
|
||||
</DropdownSubMenuContent>
|
||||
</DropdownSubMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleOrgNav(org)}
|
||||
className="cursor-pointer font-normal"
|
||||
key={org.id}
|
||||
>
|
||||
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
|
||||
{currentOrg?.id === org.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
|
||||
)}
|
||||
<p className="truncate">{org.name}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faSignOut} />}
|
||||
onClick={logOutUser}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">organizations</div>
|
||||
{orgs?.map((org) => {
|
||||
return (
|
||||
<DropdownMenuItem key={org.id}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (currentOrg?.id === org.id) return;
|
||||
|
||||
if (org.authEnforced) {
|
||||
// org has an org-level auth method enabled (e.g. SAML)
|
||||
// -> logout + redirect to SAML SSO
|
||||
|
||||
await logout.mutateAsync();
|
||||
if (org.orgAuthMethod === AuthMethod.OIDC) {
|
||||
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
|
||||
} else {
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
|
||||
}
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (org.googleSsoAuthEnforced) {
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleOrgChange(org?.id);
|
||||
}}
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{currentOrg.subOrganization && (
|
||||
<>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
<DropdownMenu modal={false}>
|
||||
<Link className="overflow-hidden" to="/organization/projects">
|
||||
<Badge
|
||||
variant="namespace"
|
||||
className={twMerge(
|
||||
"flex max-w-full min-w-0 cursor-pointer text-sm",
|
||||
!isOrgScope &&
|
||||
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCubes} />
|
||||
<p className="truncate">{currentOrg.subOrganization.name}</p>
|
||||
</Badge>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
className="flex w-full items-center justify-start p-0 font-normal"
|
||||
leftIcon={
|
||||
currentOrg?.id === org.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
|
||||
)
|
||||
}
|
||||
ariaLabel="switch-org"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
|
||||
{org.name}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="center"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
|
||||
Sub-Organizations
|
||||
</div>
|
||||
{subOrganizations.map((subOrg) => (
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organization/projects",
|
||||
search: (prev) => ({ ...prev, subOrganization: subOrg.name })
|
||||
});
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}}
|
||||
className="cursor-pointer font-normal"
|
||||
key={subOrg.id}
|
||||
>
|
||||
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
|
||||
{currentOrg?.id === subOrg.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
|
||||
)}
|
||||
<p className="truncate">{subOrg.name}</p>
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Boolean(subOrganizations.length) && (
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => setShowSubOrgForm(true)}
|
||||
>
|
||||
New Sub-Organization
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faSignOut} />} onClick={logOutUser}>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
|
||||
) : null}
|
||||
</>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
{!isOrgScope && (
|
||||
<>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer
|
||||
className="min-w-[15rem] flex-1"
|
||||
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<Tooltip content="Start Free Pro Trial">
|
||||
<Button
|
||||
variant="plain"
|
||||
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
>
|
||||
Free Pro Trial
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{user.superAdmin && !location.pathname.startsWith("/admin") && (
|
||||
<Link
|
||||
className="mr-2 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"
|
||||
>
|
||||
<FontAwesomeIcon icon={faServer} className="mr-2" />
|
||||
Server Console
|
||||
</Link>
|
||||
)}
|
||||
<div className="grow" />
|
||||
<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">
|
||||
@@ -466,6 +683,7 @@ export const Navbar = () => {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
|
||||
<ModalContent
|
||||
title={
|
||||
@@ -509,6 +727,20 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal isOpen={showSubOrgForm} onOpenChange={setShowSubOrgForm}>
|
||||
<ModalContent
|
||||
title="Create Sub-Organizations"
|
||||
subTitle="Define a new sub-organization under your current organization."
|
||||
>
|
||||
<div className="mb-2">
|
||||
<NewSubOrganizationForm
|
||||
onClose={() => {
|
||||
setShowSubOrgForm(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useCreateSubOrganization } from "@app/hooks/api";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const AddOrgSchema = z.object({
|
||||
name: slugSchema()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof AddOrgSchema>;
|
||||
|
||||
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||
const createSubOrg = useCreateSubOrganization();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting }
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
},
|
||||
resolver: zodResolver(AddOrgSchema)
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
|
||||
const onSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
const { organization } = await createSubOrg.mutateAsync({
|
||||
name
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created sub organization"
|
||||
});
|
||||
onClose();
|
||||
|
||||
navigate({
|
||||
to: "/organization/projects",
|
||||
search: (prev) => ({ ...prev, subOrganization: organization.name })
|
||||
});
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to create sub organization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
helperText="Must be slug-friendly"
|
||||
errorText={error?.message}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus value={value} onChange={onChange} placeholder="example-team" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
<div className="flex w-full gap-4 pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Add Sub-Organization
|
||||
</Button>
|
||||
<Button onClick={() => onClose()} variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -16,15 +16,15 @@ export const Notification = ({ notification, onDelete }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"group relative flex cursor-pointer items-start border-b border-mineshaft-600 p-3 transition-colors",
|
||||
"group relative flex cursor-pointer items-start border-b border-mineshaft-600 p-2 transition-colors",
|
||||
notification.link ? "hover:bg-mineshaft-700" : "cursor-default",
|
||||
!notification.isRead && "bg-mineshaft-800"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full min-w-0 flex-col">
|
||||
<div className="flex w-full min-w-0 flex-col p-1">
|
||||
<div className="flex gap-2">
|
||||
{!notification.isRead && (
|
||||
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
|
||||
<FontAwesomeIcon icon={faCircle} className="mt-0.5 size-2 text-yellow-400" />
|
||||
)}
|
||||
<Tooltip
|
||||
content={<Markdown>{notification.title}</Markdown>}
|
||||
@@ -45,7 +45,7 @@ export const Notification = ({ notification, onDelete }: Props) => {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-0 shrink-0 justify-end opacity-0 transition-all group-hover:w-[24px] group-hover:opacity-100">
|
||||
<div className="mt-0.5 flex w-0 shrink-0 justify-end opacity-0 transition-all group-hover:w-[24px] group-hover:opacity-100">
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
variant="plain"
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Link, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
isHidden?: boolean;
|
||||
};
|
||||
|
||||
export const OrgNavBar = ({ isHidden }: Props) => {
|
||||
const { isRootOrganization } = useOrganization();
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const variant = isRootOrganization ? "org" : "namespace";
|
||||
|
||||
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">
|
||||
<motion.div
|
||||
key="menu-org-items"
|
||||
initial={{ x: -150 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link to="/organization/projects">
|
||||
{({ isActive }) => (
|
||||
<Tab variant={variant} value={isActive ? "selected" : ""}>
|
||||
Overview
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/app-connections">
|
||||
{({ isActive }) => (
|
||||
<Tab variant={variant} value={isActive ? "selected" : ""}>
|
||||
App Connections
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/networking">
|
||||
{({ isActive }) => (
|
||||
<Tab variant={variant} value={isActive ? "selected" : ""}>
|
||||
Networking
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/secret-sharing">
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""} variant={variant}>
|
||||
Secret Sharing
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
variant={variant}
|
||||
value={
|
||||
isActive ||
|
||||
pathname.match(
|
||||
/organization\/members|organization\/identities|organization\/groups|organization\/roles/
|
||||
)
|
||||
? "selected"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Access Control
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<Tab variant={variant} value={isActive ? "selected" : ""}>
|
||||
Audit Logs
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
{isRootOrganization && (
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<Tab variant={variant} value={isActive ? "selected" : ""}>
|
||||
Usage & Billing
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<Tab variant={variant} value={isActive ? "selected" : ""}>
|
||||
Settings
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
<CreateOrgModal
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgNavBar } from "./OrgNavBar";
|
||||
@@ -1,214 +0,0 @@
|
||||
import {
|
||||
faBook,
|
||||
faCog,
|
||||
faInfinity,
|
||||
faMoneyBill,
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faShare,
|
||||
faTable,
|
||||
faUsers,
|
||||
faUserTie
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Menu, MenuGroup, MenuItem, Tooltip } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetOrgTrialUrl } from "@app/hooks/api";
|
||||
|
||||
type Props = {
|
||||
isHidden?: boolean;
|
||||
};
|
||||
|
||||
export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { user } = useUser();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{!isHidden && (
|
||||
<motion.aside
|
||||
key="org-sidebar"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: -240 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -240 }}
|
||||
layout
|
||||
className="dark z-10 w-60 border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-800 to-mineshaft-900"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<Menu>
|
||||
<MenuGroup title="Overview">
|
||||
<Link to="/organization/projects">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faTable} />
|
||||
</div>
|
||||
Overview
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Organization Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
|
||||
</div>
|
||||
Usage & Billing
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} className="mr-4" />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} className="mr-4" />
|
||||
</div>
|
||||
Organization Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Resources">
|
||||
<Link to="/organization/app-connections">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} className="mr-4" />
|
||||
</div>
|
||||
App Connections
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/networking">
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
|
||||
</div>
|
||||
Networking
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
<div className="grow" />
|
||||
<Menu>
|
||||
{subscription &&
|
||||
subscription.slug === "starter" &&
|
||||
!subscription.has_used_trial && (
|
||||
<Tooltip content="Start Free Pro Trial">
|
||||
<MenuItem
|
||||
variant="org"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faInfinity}
|
||||
/>
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
>
|
||||
Pro Trial
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Link to="/organization/secret-sharing">
|
||||
<MenuItem
|
||||
variant="org"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faShare} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Share Secret
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{user.superAdmin && (
|
||||
<Link to="/admin">
|
||||
<MenuItem
|
||||
variant="org"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon
|
||||
className="mx-1 inline-block shrink-0"
|
||||
icon={faUserTie}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Server Console
|
||||
</MenuItem>
|
||||
</Link>
|
||||
)}
|
||||
</Menu>
|
||||
</nav>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<CreateOrgModal
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { OrgSidebar } from "./OrgSidebar";
|
||||
@@ -1,19 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
faBook,
|
||||
faBoxOpen,
|
||||
faCog,
|
||||
faDisplay,
|
||||
faHome,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
@@ -23,7 +13,7 @@ export const PamLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
|
||||
const location = useLocation();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,152 +25,85 @@ export const PamLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="groups" />
|
||||
PAM
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/pam/$projectId/accounts"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</div>
|
||||
Accounts
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/resources"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBoxOpen} />
|
||||
</div>
|
||||
Resources
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/sessions"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDisplay} />
|
||||
</div>
|
||||
Sessions
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/pam/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/pam/$projectId/accounts"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Accounts</Tab>}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/resources"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Resources</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/sessions"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Sessions</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$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/pam/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/pam/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</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>
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faArrowLeft, faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
|
||||
@@ -12,27 +10,10 @@ export const PersonalSettingsLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
<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">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
|
||||
<div className="grow">
|
||||
<Link to="/organization/projects">
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
|
||||
</div>
|
||||
)
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 dark:scheme-dark">
|
||||
<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>
|
||||
|
||||
@@ -1,249 +1,188 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
faBell,
|
||||
faBook,
|
||||
faCertificate,
|
||||
faCog,
|
||||
faFileLines,
|
||||
faHome,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faPuzzlePiece,
|
||||
faSitemap,
|
||||
faStamp,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission } from "@app/context";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission, useSubscription } from "@app/context";
|
||||
import {
|
||||
useListWorkspaceCertificateTemplates,
|
||||
useListWorkspacePkiSubscribers
|
||||
} from "@app/hooks/api";
|
||||
|
||||
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
|
||||
|
||||
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({
|
||||
projectId: currentProject?.id || ""
|
||||
});
|
||||
const templates = templatesData?.certificateTemplates || [];
|
||||
|
||||
const hasExistingSubscribers = subscribers.length > 0;
|
||||
const hasExistingTemplates = templates.length > 0;
|
||||
const showLegacySection =
|
||||
subscription.pkiLegacyTemplates || hasExistingSubscribers || hasExistingTemplates;
|
||||
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
|
||||
PKI Manager
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faSitemap} />
|
||||
</div>
|
||||
Subscribers
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-templates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faFileLines} />
|
||||
</div>
|
||||
Certificate Templates
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCertificate} />
|
||||
</div>
|
||||
Certificates
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faStamp} />
|
||||
</div>
|
||||
Certificates Authority
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/alerting"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
</div>
|
||||
Alerting
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/integrations"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} />
|
||||
</div>
|
||||
Integrations
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} />
|
||||
</div>
|
||||
App Connections
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
<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" : ""}>Policies</Tab>}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificates"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive || location.pathname.match(/\/pki-collections\//)
|
||||
? "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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<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 className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</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>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ export const AssumePrivilegeModeBanner = () => {
|
||||
if (!assumedPrivilegeDetails) return null;
|
||||
|
||||
return (
|
||||
<div className="z-10 -mx-4 flex items-center justify-center gap-2 rounded-sm border border-mineshaft-600 bg-primary-400 p-2 text-mineshaft-800 shadow-sm">
|
||||
<div className="flex w-full items-center border-b border-yellow/50 bg-yellow/30 px-4 py-2 text-sm text-yellow-200">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mr-2" />
|
||||
You are currently viewing the project with privileges of{" "}
|
||||
@@ -24,7 +24,7 @@ export const AssumePrivilegeModeBanner = () => {
|
||||
{assumedPrivilegeDetails?.actorName}
|
||||
</b>
|
||||
</div>
|
||||
<div>
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
|
||||
@@ -35,10 +35,19 @@ import {
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetUserProjects } from "@app/hooks/api";
|
||||
import { Project } from "@app/hooks/api/projects/types";
|
||||
import { Project, ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
|
||||
const PROJECT_TYPE_NAME: Record<ProjectType, string> = {
|
||||
[ProjectType.SecretManager]: "Secrets Management",
|
||||
[ProjectType.CertificateManager]: "PKI",
|
||||
[ProjectType.SSH]: "SSH",
|
||||
[ProjectType.KMS]: "KMS",
|
||||
[ProjectType.PAM]: "PAM",
|
||||
[ProjectType.SecretScanning]: "Secret Scanning"
|
||||
};
|
||||
|
||||
export const ProjectSelect = () => {
|
||||
const [searchProject, setSearchProject] = useState("");
|
||||
const { currentProject: currentWorkspace } = useProject();
|
||||
@@ -99,26 +108,24 @@ export const ProjectSelect = () => {
|
||||
}, [projects, projectFavorites, currentWorkspace]);
|
||||
|
||||
return (
|
||||
<div className="-mr-2 flex w-full items-center gap-1">
|
||||
<div className="mr-2 flex items-center gap-1 overflow-hidden">
|
||||
<DropdownMenu modal={false}>
|
||||
<Link
|
||||
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white"
|
||||
>
|
||||
<div className="relative flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary">
|
||||
<Tooltip content={currentWorkspace.name} className="max-w-96 break-words">
|
||||
<Badge
|
||||
variant="project"
|
||||
className="max-w-44 overflow-hidden text-sm text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
|
||||
{currentWorkspace?.name}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
|
||||
{currentWorkspace?.name}
|
||||
</p>
|
||||
<Badge variant="project" className="cursor-pointer">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
<span>
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
</span>
|
||||
</Badge>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
@@ -164,9 +171,21 @@ export const ProjectSelect = () => {
|
||||
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||
params: {
|
||||
projectId: workspace.id
|
||||
},
|
||||
search: {
|
||||
subOrganization: currentOrg?.subOrganization?.name
|
||||
}
|
||||
});
|
||||
window.location.assign(url.to.replaceAll("$projectId", workspace.id));
|
||||
const urlInstance = new URL(
|
||||
`${window.location.origin}/${url.to.replaceAll("$projectId", workspace.id)}`
|
||||
);
|
||||
if (currentOrg?.subOrganization) {
|
||||
urlInstance.searchParams.set(
|
||||
"subOrganization",
|
||||
currentOrg.subOrganization.name
|
||||
);
|
||||
}
|
||||
window.location.assign(urlInstance);
|
||||
}}
|
||||
icon={
|
||||
currentWorkspace?.id === workspace.id && (
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
faArrowsSpin,
|
||||
faBook,
|
||||
faCheckToSlot,
|
||||
faCog,
|
||||
faHome,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faPuzzlePiece,
|
||||
faUsers,
|
||||
faVault
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
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";
|
||||
|
||||
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission } from "@app/context";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
@@ -28,10 +17,10 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
|
||||
export const SecretManagerLayout = () => {
|
||||
const { currentProject, projectId } = useProject();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const location = useLocation();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const projectSlug = currentProject?.slug || "";
|
||||
const location = useLocation();
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
|
||||
projectId
|
||||
@@ -54,208 +43,131 @@ export const SecretManagerLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="vault" />
|
||||
Secrets Manager
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id,
|
||||
...(currentProject.environments.length
|
||||
? { envSlug: currentProject.environments[0]?.slug }
|
||||
: {})
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
variant="project"
|
||||
isSelected={
|
||||
isActive ||
|
||||
location.pathname.startsWith(
|
||||
`/projects/secret-management/${currentProject.id}/overview`
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faVault} />
|
||||
</div>
|
||||
Secrets
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/integrations"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} />
|
||||
</div>
|
||||
Integrations
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
{Boolean(secretRotations?.length) && (
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/secret-rotation"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
<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 }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faArrowsSpin} />
|
||||
</div>
|
||||
Secret Rotations
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
Overview
|
||||
</Tab>
|
||||
)}
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/approval"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCheckToSlot} />
|
||||
</div>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open ||
|
||||
accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="primary" className="ml-1.5">
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} />
|
||||
</div>
|
||||
App Connections
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
<Link
|
||||
to="/projects/secret-management/$projectId/approval"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<Badge variant="primary" 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
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
<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 className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</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>
|
||||
</div>
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import {
|
||||
faBook,
|
||||
faCog,
|
||||
faDatabase,
|
||||
faHome,
|
||||
faMagnifyingGlass,
|
||||
faPlug,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionSub,
|
||||
useProject,
|
||||
@@ -29,6 +19,7 @@ export const SecretScanningLayout = () => {
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const location = useLocation();
|
||||
|
||||
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(
|
||||
currentProject.id,
|
||||
@@ -45,158 +36,94 @@ export const SecretScanningLayout = () => {
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="secret-scan" />
|
||||
Secret Scanning
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/data-sources"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDatabase} />
|
||||
</div>
|
||||
Data Sources
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/findings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex w-full gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
</div>
|
||||
<span>Findings</span>
|
||||
{Boolean(unresolvedFindings) && (
|
||||
<Badge variant="primary" className="mr-2 ml-auto h-min">
|
||||
{unresolvedFindings}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} />
|
||||
</div>
|
||||
App Connections
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/data-sources"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Data Sources</Tab>}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/findings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab value={isActive ? "selected" : ""}>
|
||||
Findings
|
||||
{Boolean(unresolvedFindings) && (
|
||||
<Badge variant="primary" className="ml-2 h-min">
|
||||
{unresolvedFindings}
|
||||
</Badge>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/app-connections"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>App Connections</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$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-scanning/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/secret-scanning/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import {
|
||||
faBook,
|
||||
faCog,
|
||||
faHome,
|
||||
faServer,
|
||||
faStamp,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -24,149 +15,104 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
|
||||
export const SshLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
|
||||
<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="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
className="px-4"
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="terminal" />
|
||||
SSH
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Menu>
|
||||
<MenuGroup title="Resources">
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faServer} />
|
||||
</div>
|
||||
Hosts
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/cas"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faStamp} />
|
||||
</div>
|
||||
Certificates Authority
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
</MenuGroup>
|
||||
|
||||
<MenuGroup title="Others">
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/access-management"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</div>
|
||||
Audit Logs
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Home
|
||||
</MenuItem>
|
||||
<nav className="w-full">
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/overview"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive || location.pathname.match(/\/ssh-host-groups\//) ? "selected" : ""
|
||||
}
|
||||
>
|
||||
Hosts
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
</Menu>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/cas"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}
|
||||
>
|
||||
Certificate Authorities
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</ProjectPermissionCan>
|
||||
<Link
|
||||
to="/projects/ssh/$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/ssh/$projectId/audit-logs"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects/ssh/$projectId/settings"
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
|
||||
</Link>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,8 @@ export const AccessManagementPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Access Control" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Access Control"
|
||||
|
||||
@@ -13,8 +13,8 @@ export const AuthenticationPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Authentication"
|
||||
|
||||
@@ -13,8 +13,8 @@ export const CachingPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Caching"
|
||||
|
||||
@@ -13,8 +13,8 @@ export const EncryptionPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Encryption"
|
||||
|
||||
@@ -13,8 +13,8 @@ export const EnvironmentPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Environment Variables"
|
||||
|
||||
@@ -15,8 +15,8 @@ export const GeneralPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="General"
|
||||
|
||||
@@ -13,8 +13,8 @@ export const IntegrationsPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Integrations"
|
||||
|
||||
@@ -13,18 +13,24 @@ export const ResourceOverviewPage = () => {
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Resource Overview" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Resource Overview"
|
||||
description="Manage resources within your Infisical instance."
|
||||
/>
|
||||
<Tabs defaultValue="tab-organizations">
|
||||
<Tabs orientation="vertical" defaultValue="tab-organizations">
|
||||
<TabList>
|
||||
<Tab value="tab-organizations">Organizations</Tab>
|
||||
<Tab value="tab-users">Users</Tab>
|
||||
<Tab value="tab-identities">Identities</Tab>
|
||||
<Tab variant="instance" value="tab-organizations">
|
||||
Organizations
|
||||
</Tab>
|
||||
<Tab variant="instance" value="tab-users">
|
||||
Users
|
||||
</Tab>
|
||||
<Tab variant="instance" value="tab-identities">
|
||||
Identities
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value="tab-organizations">
|
||||
<OrganizationsTable />
|
||||
|
||||
@@ -138,7 +138,7 @@ export const SignupInvitePage = () => {
|
||||
|
||||
// Step 4 of the sign up process (download the emergency kit pdf)
|
||||
const stepConfirmEmail = (
|
||||
<div className="mx-1 mb-36 flex h-7/12 w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 py-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
|
||||
<div className="mx-1 mt-14 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 py-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
|
||||
<p className="mb-2 flex justify-center text-center text-4xl font-medium text-primary-100">
|
||||
Confirm your email
|
||||
</p>
|
||||
@@ -179,7 +179,7 @@ export const SignupInvitePage = () => {
|
||||
|
||||
// Because this is the invite signup - we directly go to the last step of signup (email is already verified)
|
||||
const main = (
|
||||
<div className="mx-auto mb-32 h-7/12 w-max rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-8 py-10 drop-shadow-xl md:mb-16">
|
||||
<div className="mx-auto mt-14 mb-32 w-max rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-8 py-10 drop-shadow-xl md:mb-16">
|
||||
<p className="mx-8 mb-6 flex justify-center bg-linear-to-tr from-mineshaft-300 to-white bg-clip-text text-4xl font-bold text-transparent md:mx-16">
|
||||
Almost there!
|
||||
</p>
|
||||
|
||||
@@ -4,19 +4,20 @@ import { useTranslation } from "react-i18next";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { PkiAlertsSection } from "./components";
|
||||
|
||||
export const AlertingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Alerting" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Alerting"
|
||||
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
|
||||
/>
|
||||
|
||||
+21
-4
@@ -1,5 +1,7 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -18,6 +20,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api";
|
||||
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
|
||||
@@ -84,10 +87,24 @@ const Page = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||
params={{
|
||||
projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Certificate Authorities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
description="Manage certificate authority"
|
||||
title={data.name}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
+4
-3
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { ExternalCaSection } from "./components/ExternalCaSection";
|
||||
import { CaSection } from "./components";
|
||||
@@ -11,13 +12,13 @@ import { CaSection } from "./components";
|
||||
export const CertificateAuthoritiesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Authorities"
|
||||
description="Manage certificate authorities for issuing and signing certificates"
|
||||
/>
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
SelectItem
|
||||
// DatePicker
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
@@ -60,12 +59,7 @@ const schema = z
|
||||
commonName: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string(),
|
||||
keyAlgorithm: z.enum([
|
||||
CertKeyAlgorithm.RSA_2048,
|
||||
CertKeyAlgorithm.RSA_4096,
|
||||
CertKeyAlgorithm.ECDSA_P256,
|
||||
CertKeyAlgorithm.ECDSA_P384
|
||||
])
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||
})
|
||||
.required()
|
||||
})
|
||||
@@ -145,7 +139,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
maxPathLength: ca.configuration.maxPathLength
|
||||
? String(ca.configuration.maxPathLength)
|
||||
: "",
|
||||
keyAlgorithm: ca.configuration.keyAlgorithm
|
||||
keyAlgorithm: Object.values(CertKeyAlgorithm).includes(
|
||||
ca.configuration.keyAlgorithm as CertKeyAlgorithm
|
||||
)
|
||||
? ca.configuration.keyAlgorithm
|
||||
: CertKeyAlgorithm.RSA_2048
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -153,7 +151,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
type: CaType.INTERNAL,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
enableDirectIssuance: false,
|
||||
configuration: {
|
||||
type: InternalCaType.ROOT,
|
||||
organization: "",
|
||||
@@ -457,23 +455,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="enableDirectIssuance"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||
<Switch
|
||||
id="enable-direct-issuance"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Enable Direct Issuance</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission
|
||||
} from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { PkiCollectionSection } from "../AlertingPage/components";
|
||||
import { CertificatesSection } from "./components";
|
||||
@@ -27,13 +28,13 @@ export const CertificatesPage = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificates"
|
||||
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
type AlgorithmOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type AlgorithmSelectorsProps = {
|
||||
control: Control<any>;
|
||||
availableSignatureAlgorithms: AlgorithmOption[];
|
||||
availableKeyAlgorithms: AlgorithmOption[];
|
||||
signatureError?: string;
|
||||
keyError?: string;
|
||||
};
|
||||
|
||||
export const AlgorithmSelectors = ({
|
||||
control,
|
||||
availableSignatureAlgorithms,
|
||||
availableKeyAlgorithms,
|
||||
signatureError,
|
||||
keyError
|
||||
}: AlgorithmSelectorsProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="signatureAlgorithm"
|
||||
render={({ field: { onChange, ...field } }) => (
|
||||
<FormControl
|
||||
label="Signature Algorithm"
|
||||
errorText={signatureError}
|
||||
isError={Boolean(signatureError)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder={
|
||||
availableSignatureAlgorithms.length > 0
|
||||
? "Select signature algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{availableSignatureAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
render={({ field: { onChange, ...field } }) => (
|
||||
<FormControl
|
||||
label="Key Algorithm"
|
||||
errorText={keyError}
|
||||
isError={Boolean(keyError)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder={
|
||||
availableKeyAlgorithms.length > 0
|
||||
? "Select key algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{availableKeyAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-20
@@ -7,7 +7,6 @@ import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
@@ -24,8 +23,6 @@ const schema = z.object({
|
||||
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
||||
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
||||
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
||||
|
||||
friendlyName: z.string(),
|
||||
collectionId: z.string().optional()
|
||||
});
|
||||
|
||||
@@ -72,7 +69,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
friendlyName,
|
||||
collectionId
|
||||
}: FormData) => {
|
||||
try {
|
||||
@@ -84,8 +80,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
|
||||
friendlyName,
|
||||
pkiCollectionId: collectionId
|
||||
});
|
||||
|
||||
@@ -150,20 +144,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useGetCert } from "@app/hooks/api";
|
||||
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
|
||||
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
import { AlgorithmSelectors } from "./AlgorithmSelectors";
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
import { filterUsages, formatSubjectAltNames, getAttributeValue } from "./certificateUtils";
|
||||
import { KeyUsageSection } from "./KeyUsageSection";
|
||||
import { SubjectAltNamesField } from "./SubjectAltNamesField";
|
||||
import { useCertificateTemplate } from "./useCertificateTemplate";
|
||||
|
||||
const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||
return z.object({
|
||||
profileId: z.string().min(1, "Profile is required"),
|
||||
subjectAttributes: shouldShowSubjectSection
|
||||
? z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(["common_name"]),
|
||||
value: z.string().min(1, "Value is required")
|
||||
})
|
||||
)
|
||||
.min(1, "At least one subject attribute is required")
|
||||
: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum(["common_name"]),
|
||||
value: z.string().min(1, "Value is required")
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
subjectAltNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
value: z.string().min(1, "Value is required")
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
ttl: z.string().trim().min(1, "TTL is required"),
|
||||
signatureAlgorithm: z.string().min(1, "Signature algorithm is required"),
|
||||
keyAlgorithm: z.string().min(1, "Key algorithm is required"),
|
||||
keyUsages: z
|
||||
.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||
})
|
||||
.default({}),
|
||||
extendedKeyUsages: z
|
||||
.object({
|
||||
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||
})
|
||||
.default({})
|
||||
});
|
||||
};
|
||||
|
||||
export type FormData = z.infer<ReturnType<typeof createSchema>>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateIssuance"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateIssuance"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
profileId?: string;
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const inputSerialNumber =
|
||||
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "";
|
||||
const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
|
||||
|
||||
const { data: cert } = useGetCert(sanitizedSerialNumber);
|
||||
|
||||
const { data: profilesData } = useListCertificateProfiles({
|
||||
projectId: currentProject?.id || "",
|
||||
includeMetrics: false,
|
||||
enrollmentType: "api"
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||
|
||||
const selectedProfileId = useMemo(() => {
|
||||
const form = document.querySelector('select[name="profileId"]') as HTMLSelectElement;
|
||||
return form?.value || profileId || "";
|
||||
}, [profileId]);
|
||||
|
||||
const selectedProfile = useMemo(
|
||||
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
|
||||
[profilesData?.certificateProfiles, selectedProfileId]
|
||||
);
|
||||
|
||||
const { data: templateData } = useGetCertificateTemplateV2ById({
|
||||
templateId: selectedProfile?.certificateTemplateId || ""
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(createSchema((templateData?.subject?.length || 0) > 0)),
|
||||
defaultValues: {
|
||||
profileId: profileId || "",
|
||||
subjectAttributes: [],
|
||||
subjectAltNames: [],
|
||||
ttl: "30d",
|
||||
signatureAlgorithm: "",
|
||||
keyAlgorithm: "",
|
||||
keyUsages: {},
|
||||
extendedKeyUsages: {}
|
||||
}
|
||||
});
|
||||
|
||||
const actualSelectedProfileId = watch("profileId");
|
||||
const actualSelectedProfile = useMemo(
|
||||
() => profilesData?.certificateProfiles?.find((p) => p.id === actualSelectedProfileId),
|
||||
[profilesData?.certificateProfiles, actualSelectedProfileId]
|
||||
);
|
||||
|
||||
const {
|
||||
constraints,
|
||||
filteredKeyUsages,
|
||||
filteredExtendedKeyUsages,
|
||||
availableSignatureAlgorithms,
|
||||
availableKeyAlgorithms,
|
||||
resetConstraints
|
||||
} = useCertificateTemplate(
|
||||
templateData,
|
||||
actualSelectedProfile,
|
||||
popUp?.certificateIssuance?.isOpen || false,
|
||||
setValue,
|
||||
watch
|
||||
);
|
||||
|
||||
const resetAllState = useCallback(() => {
|
||||
setCertificateDetails(null);
|
||||
resetConstraints();
|
||||
reset();
|
||||
}, [reset, resetConstraints]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
const subjectAttrs: Array<{ type: "common_name"; value: string }> = [];
|
||||
if (cert.commonName)
|
||||
subjectAttrs.push({ type: "common_name" as const, value: cert.commonName });
|
||||
|
||||
reset({
|
||||
profileId: "",
|
||||
subjectAttributes:
|
||||
subjectAttrs.length > 0 ? subjectAttrs : [{ type: "common_name" as const, value: "" }],
|
||||
subjectAltNames: cert.subjectAltNames
|
||||
? cert.subjectAltNames.split(",").map((name) => {
|
||||
const trimmed = name.trim();
|
||||
if (trimmed.includes("@"))
|
||||
return { type: CertSubjectAlternativeNameType.EMAIL, value: trimmed };
|
||||
if (trimmed.match(/^\d+\.\d+\.\d+\.\d+$/))
|
||||
return { type: CertSubjectAlternativeNameType.IP_ADDRESS, value: trimmed };
|
||||
if (trimmed.startsWith("http"))
|
||||
return { type: CertSubjectAlternativeNameType.URI, value: trimmed };
|
||||
return { type: CertSubjectAlternativeNameType.DNS_NAME, value: trimmed };
|
||||
})
|
||||
: [],
|
||||
ttl: "",
|
||||
signatureAlgorithm: "",
|
||||
keyAlgorithm: "",
|
||||
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||
extendedKeyUsages: Object.fromEntries(
|
||||
(cert.extendedKeyUsages || []).map((name) => [name, true])
|
||||
)
|
||||
});
|
||||
}
|
||||
}, [cert, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (popUp?.certificateIssuance?.isOpen && profileId && !cert) {
|
||||
setValue("profileId", profileId);
|
||||
}
|
||||
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
async ({
|
||||
profileId: formProfileId,
|
||||
subjectAttributes,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentProject?.slug) {
|
||||
createNotification({
|
||||
text: "Project not found. Please refresh and try again.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formProfileId) {
|
||||
createNotification({
|
||||
text: "Please select a certificate profile.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let commonName = "";
|
||||
if (
|
||||
constraints.shouldShowSubjectSection &&
|
||||
subjectAttributes &&
|
||||
subjectAttributes.length > 0
|
||||
) {
|
||||
commonName = getAttributeValue(subjectAttributes, "common_name");
|
||||
if (!commonName.trim()) {
|
||||
createNotification({
|
||||
text: "Common name is required.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const certificateRequest: any = {
|
||||
profileId: formProfileId,
|
||||
projectSlug: currentProject.slug,
|
||||
ttl,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||
};
|
||||
|
||||
if (constraints.shouldShowSubjectSection && commonName) {
|
||||
certificateRequest.commonName = commonName;
|
||||
}
|
||||
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
||||
if (formattedSans && formattedSans.length > 0) {
|
||||
certificateRequest.altNames = formattedSans;
|
||||
}
|
||||
}
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } =
|
||||
await createCertificate(certificateRequest);
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Certificate creation failed:", err);
|
||||
const errorMessage =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "An unexpected error occurred while creating the certificate";
|
||||
createNotification({
|
||||
text: `Failed to create certificate: ${errorMessage}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
currentProject?.slug,
|
||||
createCertificate,
|
||||
constraints.shouldShowSubjectSection,
|
||||
constraints.shouldShowSanSection
|
||||
]
|
||||
);
|
||||
|
||||
const getModalTitle = () => {
|
||||
if (certificateDetails) return "Certificate Created Successfully";
|
||||
if (cert) return "Certificate Details";
|
||||
return "Issue New Certificate";
|
||||
};
|
||||
|
||||
const getModalSubTitle = () => {
|
||||
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
|
||||
if (cert) return "View certificate information";
|
||||
return "Issue a new certificate using a certificate profile";
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateIssuance?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateIssuance", isOpen);
|
||||
if (!isOpen) {
|
||||
resetAllState();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
|
||||
{certificateDetails && (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
certificate={certificateDetails.certificate}
|
||||
certificateChain={certificateDetails.certificateChain}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
)}
|
||||
{cert && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-mineshaft-300">Certificate Details</h4>
|
||||
<p className="text-sm text-mineshaft-400">Serial Number: {cert.serialNumber}</p>
|
||||
<p className="text-sm text-mineshaft-400">Common Name: {cert.commonName}</p>
|
||||
<p className="text-sm text-mineshaft-400">Status: {cert.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!cert && !certificateDetails && (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{!profileId && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="profileId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Certificate Profile"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
Certificate profiles define the policies and enrollment methods
|
||||
for certificate issuance. The selected profile will enforce
|
||||
validation rules and determine the CA used for signing.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder="Select a certificate profile"
|
||||
position="popper"
|
||||
>
|
||||
{profilesData?.certificateProfiles?.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
{profile.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(actualSelectedProfile || profileId) && (
|
||||
<>
|
||||
{constraints.shouldShowSubjectSection && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAttributes"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name"
|
||||
isRequired
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input
|
||||
value={value?.[0]?.value || ""}
|
||||
onChange={(e) => {
|
||||
onChange([{ type: "common_name", value: e.target.value }]);
|
||||
}}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{constraints.shouldShowSanSection && (
|
||||
<SubjectAltNamesField
|
||||
control={control}
|
||||
allowedSanTypes={constraints.allowedSanTypes}
|
||||
error={formState.errors.subjectAltNames?.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Time to Live (TTL)"
|
||||
isRequired
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input {...field} placeholder="30d, 1y, 8760h" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AlgorithmSelectors
|
||||
control={control}
|
||||
availableSignatureAlgorithms={availableSignatureAlgorithms}
|
||||
availableKeyAlgorithms={availableKeyAlgorithms}
|
||||
signatureError={formState.errors.signatureAlgorithm?.message}
|
||||
keyError={formState.errors.keyAlgorithm?.message}
|
||||
/>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<KeyUsageSection
|
||||
control={control}
|
||||
title="Key Usages"
|
||||
accordionValue="key-usages"
|
||||
namePrefix="keyUsages"
|
||||
options={filteredKeyUsages}
|
||||
requiredUsages={constraints.requiredKeyUsages}
|
||||
/>
|
||||
<KeyUsageSection
|
||||
control={control}
|
||||
title="Extended Key Usages"
|
||||
accordionValue="extended-key-usages"
|
||||
namePrefix="extendedKeyUsages"
|
||||
options={filteredExtendedKeyUsages}
|
||||
requiredUsages={constraints.requiredExtendedKeyUsages}
|
||||
/>
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
|
||||
>
|
||||
{cert ? "Update" : "Issue Certificate"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
handlePopUpToggle("certificateIssuance", false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -46,9 +46,8 @@ const schema = z.object({
|
||||
certificateTemplateId: z.string().optional(),
|
||||
caId: z.string(),
|
||||
collectionId: z.string().optional(),
|
||||
friendlyName: z.string(),
|
||||
commonName: z.string().trim().min(1),
|
||||
altNames: z.string(),
|
||||
subjectAltNames: z.string(),
|
||||
ttl: z.string().trim(),
|
||||
keyUsages: z.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
@@ -139,9 +138,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (cert) {
|
||||
reset({
|
||||
caId: cert.caId,
|
||||
friendlyName: cert.friendlyName,
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames,
|
||||
subjectAltNames: cert.subjectAltNames,
|
||||
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||
ttl: "",
|
||||
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||
@@ -152,9 +150,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} else {
|
||||
reset({
|
||||
caId: "",
|
||||
friendlyName: "",
|
||||
commonName: "",
|
||||
altNames: "",
|
||||
subjectAltNames: "",
|
||||
ttl: "",
|
||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
|
||||
keyUsages: {
|
||||
@@ -182,10 +179,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
friendlyName,
|
||||
collectionId,
|
||||
commonName,
|
||||
altNames,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
@@ -198,9 +194,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||
projectSlug: currentProject.slug,
|
||||
pkiCollectionId: collectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
@@ -359,20 +354,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
@@ -391,7 +372,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="altNames"
|
||||
name="subjectAltNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Alternative Names (SANs)"
|
||||
|
||||
+15
-3
@@ -7,22 +7,28 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionSub,
|
||||
useProject
|
||||
useProject,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
|
||||
export const CertificatesSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||
|
||||
const isLegacyTemplatesEnabled = subscription.pkiLegacyTemplates;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificateIssuance",
|
||||
"certificate",
|
||||
"certificateImport",
|
||||
"certificateCert",
|
||||
@@ -73,7 +79,9 @@ export const CertificatesSection = () => {
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
onClick={() =>
|
||||
handlePopUpOpen(isLegacyTemplatesEnabled ? "certificate" : "certificateIssuance")
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue
|
||||
@@ -83,7 +91,11 @@ export const CertificatesSection = () => {
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
{isLegacyTemplatesEnabled ? (
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
) : (
|
||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
)}
|
||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Checkbox,
|
||||
FormLabel
|
||||
} from "@app/components/v2";
|
||||
|
||||
type KeyUsageOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type KeyUsageSectionProps = {
|
||||
control: Control<any>;
|
||||
title: string;
|
||||
accordionValue: string;
|
||||
namePrefix: "keyUsages" | "extendedKeyUsages";
|
||||
options: KeyUsageOption[];
|
||||
requiredUsages: string[];
|
||||
};
|
||||
|
||||
export const KeyUsageSection = ({
|
||||
control,
|
||||
title,
|
||||
accordionValue,
|
||||
namePrefix,
|
||||
options,
|
||||
requiredUsages
|
||||
}: KeyUsageSectionProps) => {
|
||||
if (options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<AccordionItem value={accordionValue}>
|
||||
<AccordionTrigger>{title}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{options.map(({ label, value }) => {
|
||||
const isRequired = requiredUsages.includes(value);
|
||||
return (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={`${namePrefix}.${value}` as any}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`${namePrefix}-${value}`}
|
||||
isChecked={field.value || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!isRequired) {
|
||||
field.onChange(checked);
|
||||
}
|
||||
}}
|
||||
isDisabled={isRequired}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel
|
||||
id={`${namePrefix}-${value}`}
|
||||
className={`text-sm ${
|
||||
isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"
|
||||
}`}
|
||||
label={label}
|
||||
/>
|
||||
{isRequired && <span className="text-xs">(Required)</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
import { getSanPlaceholder, getSanTypeLabels, SubjectAltName } from "./certificateUtils";
|
||||
|
||||
type SubjectAltNamesFieldProps = {
|
||||
control: Control<any>;
|
||||
allowedSanTypes: CertSubjectAlternativeNameType[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const SubjectAltNamesField = ({
|
||||
control,
|
||||
allowedSanTypes,
|
||||
error
|
||||
}: SubjectAltNamesFieldProps) => {
|
||||
const sanTypeLabels = getSanTypeLabels();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAltNames"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormControl
|
||||
label="Subject Alternative Names (SANs)"
|
||||
errorText={error}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{value.map((san: SubjectAltName, index: number) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={`subject-alt-name-${index}`} className="flex items-center gap-2">
|
||||
<Select
|
||||
value={san.type}
|
||||
onValueChange={(newType) => {
|
||||
const newValue = [...value];
|
||||
newValue[index] = {
|
||||
...san,
|
||||
type: newType as CertSubjectAlternativeNameType
|
||||
};
|
||||
onChange(newValue);
|
||||
}}
|
||||
className="w-24"
|
||||
>
|
||||
{allowedSanTypes.map((sanType) => (
|
||||
<SelectItem key={sanType} value={sanType}>
|
||||
{sanTypeLabels[sanType]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
value={san.value}
|
||||
onChange={(e) => {
|
||||
const newValue = [...value];
|
||||
newValue[index] = { ...san, value: e.target.value };
|
||||
onChange(newValue);
|
||||
}}
|
||||
placeholder={getSanPlaceholder(san.type)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<IconButton
|
||||
ariaLabel="Remove SAN"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newValue = value.filter((_: any, i: number) => i !== index);
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
const defaultType =
|
||||
allowedSanTypes.length > 0
|
||||
? allowedSanTypes[0]
|
||||
: CertSubjectAlternativeNameType.DNS_NAME;
|
||||
onChange([...value, { type: defaultType, value: "" }]);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Add SAN
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
export const getSanPlaceholder = (sanType: CertSubjectAlternativeNameType): string => {
|
||||
switch (sanType) {
|
||||
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||
return "example.com or *.example.com";
|
||||
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||
return "192.168.1.1";
|
||||
case CertSubjectAlternativeNameType.EMAIL:
|
||||
return "[email protected]";
|
||||
case CertSubjectAlternativeNameType.URI:
|
||||
return "https://example.com";
|
||||
default:
|
||||
return "Enter value";
|
||||
}
|
||||
};
|
||||
|
||||
export const getSanTypeLabels = () => ({
|
||||
[CertSubjectAlternativeNameType.DNS_NAME]: "DNS",
|
||||
[CertSubjectAlternativeNameType.IP_ADDRESS]: "IP",
|
||||
[CertSubjectAlternativeNameType.EMAIL]: "Email",
|
||||
[CertSubjectAlternativeNameType.URI]: "URI"
|
||||
});
|
||||
|
||||
export type SubjectAltName = {
|
||||
type: CertSubjectAlternativeNameType;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const formatSubjectAltNames = (subjectAltNames: SubjectAltName[]) => {
|
||||
return subjectAltNames
|
||||
.filter((san) => san.value.trim())
|
||||
.map((san) => ({
|
||||
type: san.type,
|
||||
value: san.value.trim()
|
||||
}));
|
||||
};
|
||||
|
||||
export const filterUsages = <T extends Record<string, boolean>>(usages: T): string[] => {
|
||||
return Object.entries(usages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
export const getAttributeValue = (
|
||||
subjectAttributes: Array<{ type: string; value: string }> | undefined,
|
||||
type: string
|
||||
): string => {
|
||||
const foundAttr = subjectAttributes?.find((attr) => attr.type === type);
|
||||
return foundAttr?.value || "";
|
||||
};
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
|
||||
import {
|
||||
EXTENDED_KEY_USAGES_OPTIONS,
|
||||
KEY_USAGES_OPTIONS
|
||||
} from "@app/hooks/api/certificates/constants";
|
||||
import {
|
||||
CertSubjectAlternativeNameType,
|
||||
mapTemplateKeyAlgorithmToApi,
|
||||
mapTemplateSignatureAlgorithmToApi
|
||||
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
export type TemplateConstraints = {
|
||||
allowedKeyUsages: string[];
|
||||
allowedExtendedKeyUsages: string[];
|
||||
requiredKeyUsages: string[];
|
||||
requiredExtendedKeyUsages: string[];
|
||||
allowedSignatureAlgorithms: string[];
|
||||
allowedKeyAlgorithms: string[];
|
||||
allowedSanTypes: CertSubjectAlternativeNameType[];
|
||||
shouldShowSanSection: boolean;
|
||||
shouldShowSubjectSection: boolean;
|
||||
};
|
||||
|
||||
export const useCertificateTemplate = (
|
||||
templateData: any,
|
||||
selectedProfile: any,
|
||||
isModalOpen: boolean,
|
||||
setValue: UseFormSetValue<any>,
|
||||
watch: UseFormWatch<any>
|
||||
) => {
|
||||
const [constraints, setConstraints] = useState<TemplateConstraints>({
|
||||
allowedKeyUsages: [],
|
||||
allowedExtendedKeyUsages: [],
|
||||
requiredKeyUsages: [],
|
||||
requiredExtendedKeyUsages: [],
|
||||
allowedSignatureAlgorithms: [],
|
||||
allowedKeyAlgorithms: [],
|
||||
allowedSanTypes: [
|
||||
CertSubjectAlternativeNameType.DNS_NAME,
|
||||
CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||
CertSubjectAlternativeNameType.EMAIL,
|
||||
CertSubjectAlternativeNameType.URI
|
||||
],
|
||||
shouldShowSanSection: true,
|
||||
shouldShowSubjectSection: true
|
||||
});
|
||||
|
||||
const filteredKeyUsages = useMemo(() => {
|
||||
return KEY_USAGES_OPTIONS.filter(({ value }) => constraints.allowedKeyUsages.includes(value));
|
||||
}, [constraints.allowedKeyUsages]);
|
||||
|
||||
const filteredExtendedKeyUsages = useMemo(() => {
|
||||
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
||||
constraints.allowedExtendedKeyUsages.includes(value)
|
||||
);
|
||||
}, [constraints.allowedExtendedKeyUsages]);
|
||||
|
||||
const availableSignatureAlgorithms = useMemo(() => {
|
||||
return constraints.allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [constraints.allowedSignatureAlgorithms]);
|
||||
|
||||
const availableKeyAlgorithms = useMemo(() => {
|
||||
return constraints.allowedKeyAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [constraints.allowedKeyAlgorithms]);
|
||||
|
||||
const resetConstraints = () => {
|
||||
setConstraints({
|
||||
allowedKeyUsages: [],
|
||||
allowedExtendedKeyUsages: [],
|
||||
requiredKeyUsages: [],
|
||||
requiredExtendedKeyUsages: [],
|
||||
allowedSignatureAlgorithms: [],
|
||||
allowedKeyAlgorithms: [],
|
||||
allowedSanTypes: [
|
||||
CertSubjectAlternativeNameType.DNS_NAME,
|
||||
CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||
CertSubjectAlternativeNameType.EMAIL,
|
||||
CertSubjectAlternativeNameType.URI
|
||||
],
|
||||
shouldShowSanSection: true,
|
||||
shouldShowSubjectSection: true
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (templateData && selectedProfile && isModalOpen) {
|
||||
const newConstraints: TemplateConstraints = {
|
||||
allowedSignatureAlgorithms: templateData.algorithms?.signature || [],
|
||||
allowedKeyAlgorithms: templateData.algorithms?.keyAlgorithm || [],
|
||||
allowedKeyUsages: [
|
||||
...(templateData.keyUsages?.required || []),
|
||||
...(templateData.keyUsages?.allowed || [])
|
||||
],
|
||||
allowedExtendedKeyUsages: [
|
||||
...(templateData.extendedKeyUsages?.required || []),
|
||||
...(templateData.extendedKeyUsages?.allowed || [])
|
||||
],
|
||||
requiredKeyUsages: templateData.keyUsages?.required || [],
|
||||
requiredExtendedKeyUsages: templateData.extendedKeyUsages?.required || [],
|
||||
allowedSanTypes: [],
|
||||
shouldShowSanSection: true,
|
||||
shouldShowSubjectSection: true
|
||||
};
|
||||
|
||||
// Set TTL if available
|
||||
if (templateData.validity?.max) {
|
||||
setValue("ttl", templateData.validity.max);
|
||||
}
|
||||
|
||||
// Handle SAN types
|
||||
if (templateData.sans && templateData.sans.length > 0) {
|
||||
const sanTypes: CertSubjectAlternativeNameType[] = [];
|
||||
templateData.sans.forEach((sanPolicy: any) => {
|
||||
if (!sanTypes.includes(sanPolicy.type)) {
|
||||
sanTypes.push(sanPolicy.type);
|
||||
}
|
||||
});
|
||||
newConstraints.allowedSanTypes = sanTypes;
|
||||
newConstraints.shouldShowSanSection = true;
|
||||
} else {
|
||||
newConstraints.allowedSanTypes = [];
|
||||
newConstraints.shouldShowSanSection = false;
|
||||
setValue("subjectAltNames", []);
|
||||
}
|
||||
|
||||
// Handle subject section
|
||||
if (templateData.subject && templateData.subject.length > 0) {
|
||||
newConstraints.shouldShowSubjectSection = true;
|
||||
const currentSubjectAttrs = watch("subjectAttributes");
|
||||
if (!currentSubjectAttrs || currentSubjectAttrs.length === 0) {
|
||||
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
||||
}
|
||||
} else {
|
||||
newConstraints.shouldShowSubjectSection = false;
|
||||
setValue("subjectAttributes", undefined);
|
||||
}
|
||||
|
||||
setConstraints(newConstraints);
|
||||
|
||||
// Set initial required usages
|
||||
const initialKeyUsages: Record<string, boolean> = {};
|
||||
const initialExtendedKeyUsages: Record<string, boolean> = {};
|
||||
|
||||
(templateData.keyUsages?.required || []).forEach((usage: string) => {
|
||||
initialKeyUsages[usage] = true;
|
||||
});
|
||||
|
||||
(templateData.extendedKeyUsages?.required || []).forEach((usage: string) => {
|
||||
initialExtendedKeyUsages[usage] = true;
|
||||
});
|
||||
|
||||
setValue("keyUsages", initialKeyUsages);
|
||||
setValue("extendedKeyUsages", initialExtendedKeyUsages);
|
||||
}
|
||||
}, [templateData, selectedProfile, setValue, watch, isModalOpen]);
|
||||
|
||||
return {
|
||||
constraints,
|
||||
filteredKeyUsages,
|
||||
filteredExtendedKeyUsages,
|
||||
availableSignatureAlgorithms,
|
||||
availableKeyAlgorithms,
|
||||
resetConstraints
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
import { PkiSyncsTab } from "./components";
|
||||
@@ -42,16 +43,18 @@ export const IntegrationsListPage = () => {
|
||||
<meta property="og:title" content="Manage your certificates in seconds" />
|
||||
<meta name="og:description" content="Sync and manage PKI certificates across services" />
|
||||
</Helmet>
|
||||
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
|
||||
<div className="relative mx-auto max-w-8xl pb-12 text-white">
|
||||
<div className="mb-8">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Integrations"
|
||||
description="Manage integrations with third-party certificate services."
|
||||
/>
|
||||
<Tabs value={currentTab} onValueChange={updateSelectedTab}>
|
||||
<Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
<Tab value={IntegrationsListPageTabs.PkiSyncs}>Certificate Syncs</Tab>
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.PkiSyncs}>
|
||||
Certificate Syncs
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IntegrationsListPageTabs.PkiSyncs}>
|
||||
<ProjectPermissionCan
|
||||
|
||||
+21
-4
@@ -1,6 +1,8 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -19,6 +21,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api";
|
||||
import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { PkiCollectionModal } from "../AlertingPage/components/PkiCollectionModal";
|
||||
@@ -70,10 +73,24 @@ export const PkiCollectionPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificates"
|
||||
params={{
|
||||
projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Certificates
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title={data.name}
|
||||
description="Manage certificate collection"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
+21
-4
@@ -1,6 +1,8 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal";
|
||||
@@ -75,10 +78,24 @@ const Page = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Subscribers
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title={data.name}
|
||||
description="Manage PKI subscriber"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { PkiSubscriberSection } from "./components";
|
||||
|
||||
@@ -13,10 +14,10 @@ export const PkiSubscribersPage = () => {
|
||||
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Subscribers"
|
||||
description="Manage subscribers that request and receive certificates, including user devices, servers, and services."
|
||||
/>
|
||||
|
||||
+24
-18
@@ -7,7 +7,8 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useProject
|
||||
useProject,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
|
||||
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
|
||||
@@ -18,7 +19,10 @@ import { PkiSubscribersTable } from "./PkiSubscribersTable";
|
||||
|
||||
export const PkiSubscriberSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const projectId = currentProject.id;
|
||||
|
||||
const canCreateLegacySubscribers = subscription.pkiLegacyTemplates;
|
||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
||||
|
||||
@@ -100,23 +104,25 @@ export const PkiSubscriberSection = () => {
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Create}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("pkiSubscriber")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Subscriber
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{canCreateLegacySubscribers && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Create}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("pkiSubscriber")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Subscriber
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PkiSubscribersTable handlePopUpOpen={handlePopUpOpen} />
|
||||
|
||||
@@ -68,8 +68,8 @@ const PageContent = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
|
||||
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
|
||||
import { PkiTemplateForm } from "./components/PkiTemplateForm";
|
||||
@@ -58,6 +59,7 @@ export const PkiTemplateListPage = () => {
|
||||
const { currentProject } = useProject();
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"certificateTemplate",
|
||||
"deleteTemplate",
|
||||
@@ -103,37 +105,39 @@ export const PkiTemplateListPage = () => {
|
||||
<title>{t("common.head-title", { title: "PKI Templates" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<div className="mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Templates"
|
||||
description="Manage certificate template to request and issue dynamic certificates following a strict format."
|
||||
/>
|
||||
</div>
|
||||
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Create}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div className="container mx-auto mb-6 max-w-8xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
{subscription?.pkiLegacyTemplates && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Create}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
@@ -267,7 +271,7 @@ export const PkiTemplateListPage = () => {
|
||||
onDeleteApproved={() => onRemovePkiSubscriberSubmit()}
|
||||
/>
|
||||
</div>
|
||||
<div className="container mx-auto max-w-7xl" />
|
||||
<div className="container mx-auto max-w-8xl" />
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp?.certificateTemplate?.isOpen}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
||||
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
||||
|
||||
enum TabSections {
|
||||
CertificateProfiles = "profiles",
|
||||
CertificateTemplatesV2 = "templates-v2"
|
||||
}
|
||||
|
||||
export const PoliciesPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentProject } = useProject();
|
||||
const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles);
|
||||
|
||||
if (!currentProject) {
|
||||
return <ContentLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
if (!isAllowed) {
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
|
||||
<p>You don't have permission to access certificate policies.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Policies"
|
||||
description="Manage certificate templates and profiles for unified certificate issuance"
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as TabSections)}
|
||||
>
|
||||
<TabList>
|
||||
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
||||
Certificate Profiles
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
||||
Certificate Templates
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
};
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import {
|
||||
TCertificateProfileWithDetails,
|
||||
useDeleteCertificateProfile
|
||||
} from "@app/hooks/api/certificateProfiles";
|
||||
|
||||
import { CreateProfileModal } from "./CreateProfileModal";
|
||||
import { ProfileList } from "./ProfileList";
|
||||
|
||||
export const CertificateProfilesTab = () => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedProfile, setSelectedProfile] = useState<TCertificateProfileWithDetails | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const deleteProfile = useDeleteCertificateProfile();
|
||||
|
||||
const canCreateProfile = permission.can(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const handleCreateProfile = () => {
|
||||
setIsCreateModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditProfile = (profile: TCertificateProfileWithDetails) => {
|
||||
setSelectedProfile(profile);
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteProfile = (profile: TCertificateProfileWithDetails) => {
|
||||
setSelectedProfile(profile);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!selectedProfile) return;
|
||||
|
||||
try {
|
||||
await deleteProfile.mutateAsync({
|
||||
profileId: selectedProfile.id
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelectedProfile(null);
|
||||
createNotification({
|
||||
text: `Certificate profile "${selectedProfile.slug}" deleted successfully`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to delete profile "${selectedProfile.slug}" (ID: ${selectedProfile.id}):`,
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<h2 className="text-xl font-semibold text-mineshaft-100">Certificate Profiles</h2>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Unified certificate issuance configurations combining CA, template, and enrollment
|
||||
method
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{canCreateProfile && (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="button"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={handleCreateProfile}
|
||||
>
|
||||
Create Profile
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ProfileList onEditProfile={handleEditProfile} onDeleteProfile={handleDeleteProfile} />
|
||||
|
||||
<CreateProfileModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
|
||||
|
||||
{selectedProfile && (
|
||||
<>
|
||||
<CreateProfileModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={() => {
|
||||
setIsEditModalOpen(false);
|
||||
setSelectedProfile(null);
|
||||
}}
|
||||
profile={selectedProfile}
|
||||
mode="edit"
|
||||
/>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title={`Delete Certificate Profile ${selectedProfile.slug}?`}
|
||||
onChange={(isOpen) => {
|
||||
setIsDeleteModalOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
setSelectedProfile(null);
|
||||
}
|
||||
}}
|
||||
deleteKey={selectedProfile.slug}
|
||||
onDeleteApproved={handleDeleteConfirm}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user