feat: completed migration of ssh product

This commit is contained in:
=
2024-12-29 22:29:41 +05:30
parent 7e5417a0eb
commit 19d32a1a3b
62 changed files with 3867 additions and 8 deletions

File diff suppressed because one or more lines are too long

View File

@ -79,6 +79,12 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
)
},
Ssh: {
SshCaByIDPage: setRoute(
"/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId"
)
},
Public: {
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId")
}

View File

@ -85,6 +85,9 @@ export enum ProjectPermissionSub {
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
CertificateTemplates = "certificate-templates",
SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificateTemplates = "ssh-certificate-templates",
SshCertificates = "ssh-certificates",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
@ -165,6 +168,9 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]

View File

@ -68,7 +68,8 @@ export const getProjectTitle = (type: ProjectType) => {
const titleConvert = {
[ProjectType.SecretManager]: "Secret Management",
[ProjectType.KMS]: "Key Management",
[ProjectType.CertificateManager]: "Cert Management"
[ProjectType.CertificateManager]: "Cert Management",
[ProjectType.SSH]: "SSH"
};
return titleConvert[type];
};

View File

@ -1,3 +1,5 @@
import { SshCaStatus } from "../sshCa";
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
import { CaStatus, CaType } from "./enums";
export const caTypeToNameMap: { [K in CaType]: string } = {
@ -11,7 +13,7 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
};
export const getCaStatusBadgeVariant = (status: CaStatus) => {
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => {
switch (status) {
case CaStatus.ACTIVE:
return "success";

View File

@ -38,6 +38,8 @@ export * from "./secretSharing";
export * from "./secretSnapshots";
export * from "./serverDetails";
export * from "./serviceTokens";
export * from "./sshCa";
export * from "./sshCertificateTemplates";
export * from "./ssoConfig";
export * from "./subscriptions";
export * from "./tags";

View File

@ -0,0 +1,14 @@
export enum SshCaStatus {
ACTIVE = "active",
DISABLED = "disabled"
}
export enum SshCertType {
USER = "user",
HOST = "host"
}
export const sshCertTypeToNameMap: { [K in SshCertType]: string } = {
[SshCertType.USER]: "User",
[SshCertType.HOST]: "Host"
};

View File

@ -0,0 +1,9 @@
export { SshCaStatus } from "./constants";
export {
useCreateSshCa,
useDeleteSshCa,
useIssueSshCreds,
useSignSshKey,
useUpdateSshCa
} from "./mutations";
export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries";

View File

@ -0,0 +1,101 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/query-keys";
import {
TCreateSshCaDTO,
TDeleteSshCaDTO,
TIssueSshCredsDTO,
TIssueSshCredsResponse,
TSignSshKeyDTO,
TSignSshKeyResponse,
TSshCertificateAuthority,
TUpdateSshCaDTO
} from "./types";
export const sshCaKeys = {
getSshCaById: (caId: string) => [{ caId }, "ssh-ca"]
};
export const useCreateSshCa = () => {
const queryClient = useQueryClient();
return useMutation<TSshCertificateAuthority, object, TCreateSshCaDTO>({
mutationFn: async (body) => {
const {
data: { ca }
} = await apiRequest.post<{ ca: TSshCertificateAuthority }>("/api/v1/ssh/ca/", body);
return ca;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) });
}
});
};
export const useUpdateSshCa = () => {
const queryClient = useQueryClient();
return useMutation<TSshCertificateAuthority, object, TUpdateSshCaDTO>({
mutationFn: async ({ caId, ...body }) => {
const {
data: { ca }
} = await apiRequest.patch<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`, body);
return ca;
},
onSuccess: ({ projectId }, { caId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) });
queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaById(caId) });
}
});
};
export const useDeleteSshCa = () => {
const queryClient = useQueryClient();
return useMutation<TSshCertificateAuthority, object, TDeleteSshCaDTO>({
mutationFn: async ({ caId }) => {
const {
data: { ca }
} = await apiRequest.delete<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`);
return ca;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) });
}
});
};
export const useSignSshKey = () => {
const queryClient = useQueryClient();
return useMutation<TSignSshKeyResponse, object, TSignSshKeyDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post<TSignSshKeyResponse>(
"/api/v1/ssh/certificates/sign",
body
);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.allWorkspaceSshCertificates(projectId)
});
}
});
};
export const useIssueSshCreds = () => {
const queryClient = useQueryClient();
return useMutation<TIssueSshCredsResponse, object, TIssueSshCredsDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post<TIssueSshCredsResponse>(
"/api/v1/ssh/certificates/issue",
body
);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.allWorkspaceSshCertificates(projectId)
});
}
});
};

View File

@ -0,0 +1,37 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
import { TSshCertificateAuthority } from "./types";
export const sshCaKeys = {
getSshCaById: (caId: string) => [{ caId }, "ssh-ca"],
getSshCaCertTemplates: (caId: string) => [{ caId }, "ssh-ca-cert-templates"]
};
export const useGetSshCaById = (caId: string) => {
return useQuery({
queryKey: sshCaKeys.getSshCaById(caId),
queryFn: async () => {
const {
data: { ca }
} = await apiRequest.get<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`);
return ca;
},
enabled: Boolean(caId)
});
};
export const useGetSshCaCertTemplates = (caId: string) => {
return useQuery({
queryKey: sshCaKeys.getSshCaCertTemplates(caId),
queryFn: async () => {
const { data } = await apiRequest.get<{
certificateTemplates: TSshCertificateTemplate[];
}>(`/api/v1/ssh/ca/${caId}/certificate-templates`);
return data;
},
enabled: Boolean(caId)
});
};

View File

@ -0,0 +1,74 @@
import { CertKeyAlgorithm } from "../certificates/enums";
import { SshCaStatus, SshCertType } from "./constants";
export type TSshCertificate = {
id: string;
sshCaId: string;
sshCertificateTemplateId: string;
serialNumber: string;
certType: SshCertType;
principals: string[];
keyId: string;
notBefore: string;
notAfter: string;
};
export type TSshCertificateAuthority = {
id: string;
projectId: string;
status: SshCaStatus;
friendlyName: string;
keyAlgorithm: CertKeyAlgorithm;
createdAt: string;
updatedAt: string;
publicKey: string;
};
export type TCreateSshCaDTO = {
projectId: string;
friendlyName?: string;
keyAlgorithm: CertKeyAlgorithm;
};
export type TUpdateSshCaDTO = {
caId: string;
friendlyName?: string;
status?: SshCaStatus;
};
export type TDeleteSshCaDTO = {
caId: string;
};
export type TSignSshKeyDTO = {
projectId: string;
certificateTemplateId: string;
publicKey?: string;
certType: SshCertType;
principals: string[];
ttl?: string;
keyId?: string;
};
export type TSignSshKeyResponse = {
serialNumber: string;
signedKey: string;
};
export type TIssueSshCredsDTO = {
projectId: string;
certificateTemplateId: string;
keyAlgorithm: CertKeyAlgorithm;
certType: SshCertType;
principals: string[];
ttl?: string;
keyId?: string;
};
export type TIssueSshCredsResponse = {
serialNumber: string;
signedKey: string;
privateKey: string;
publicKey: string;
keyAlgorithm: CertKeyAlgorithm;
};

View File

@ -0,0 +1,7 @@
export {
useCreateSshCertTemplate,
useDeleteSshCertTemplate,
useUpdateSshCertTemplate
} from "./mutations";
export { useGetSshCertTemplate } from "./queries";
export * from "./types";

View File

@ -0,0 +1,59 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { sshCaKeys } from "../sshCa/queries";
import {
TCreateSshCertificateTemplateDTO,
TDeleteSshCertificateTemplateDTO,
TSshCertificateTemplate,
TUpdateSshCertificateTemplateDTO
} from "./types";
export const useCreateSshCertTemplate = () => {
const queryClient = useQueryClient();
return useMutation<TSshCertificateTemplate, object, TCreateSshCertificateTemplateDTO>({
mutationFn: async (data) => {
const { data: certificateTemplate } = await apiRequest.post<TSshCertificateTemplate>(
"/api/v1/ssh/certificate-templates",
data
);
return certificateTemplate;
},
onSuccess: ({ sshCaId }) => {
queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaCertTemplates(sshCaId) });
}
});
};
export const useUpdateSshCertTemplate = () => {
const queryClient = useQueryClient();
return useMutation<TSshCertificateTemplate, object, TUpdateSshCertificateTemplateDTO>({
mutationFn: async (data) => {
const { data: certificateTemplate } = await apiRequest.patch<TSshCertificateTemplate>(
`/api/v1/ssh/certificate-templates/${data.id}`,
data
);
return certificateTemplate;
},
onSuccess: ({ sshCaId }) => {
queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaCertTemplates(sshCaId) });
}
});
};
export const useDeleteSshCertTemplate = () => {
const queryClient = useQueryClient();
return useMutation<TSshCertificateTemplate, object, TDeleteSshCertificateTemplateDTO>({
mutationFn: async (data) => {
const { data: certificateTemplate } = await apiRequest.delete<TSshCertificateTemplate>(
`/api/v1/ssh/certificate-templates/${data.id}`
);
return certificateTemplate;
},
onSuccess: ({ sshCaId }) => {
queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaCertTemplates(sshCaId) });
}
});
};

View File

@ -0,0 +1,22 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSshCertificateTemplate } from "./types";
export const certTemplateKeys = {
getSshCertTemplateById: (id: string) => [{ id }, "ssh-cert-template"]
};
export const useGetSshCertTemplate = (id: string) => {
return useQuery({
queryKey: certTemplateKeys.getSshCertTemplateById(id),
queryFn: async () => {
const { data: certificateTemplate } = await apiRequest.get<TSshCertificateTemplate>(
`/api/v1/ssh/certificate-templates/${id}`
);
return certificateTemplate;
},
enabled: Boolean(id)
});
};

View File

@ -0,0 +1,47 @@
export enum SshCertTemplateStatus {
ACTIVE = "active",
DISABLED = "disabled"
}
export type TSshCertificateTemplate = {
id: string;
sshCaId: string;
status: SshCertTemplateStatus;
name: string;
ttl: string;
maxTTL: string;
allowedUsers: string[];
allowedHosts: string[];
allowUserCertificates: boolean;
allowHostCertificates: boolean;
allowCustomKeyIds: boolean;
};
export type TCreateSshCertificateTemplateDTO = {
sshCaId: string;
name: string;
ttl: string;
maxTTL: string;
allowedUsers: string[];
allowedHosts: string[];
allowUserCertificates: boolean;
allowHostCertificates: boolean;
allowCustomKeyIds: boolean;
};
export type TUpdateSshCertificateTemplateDTO = {
id: string;
status?: SshCertTemplateStatus;
name?: string;
ttl?: string;
maxTTL?: string;
allowedUsers?: string[];
allowedHosts?: string[];
allowUserCertificates?: boolean;
allowHostCertificates?: boolean;
allowCustomKeyIds?: boolean;
};
export type TDeleteSshCertificateTemplateDTO = {
id: string;
};

View File

@ -32,6 +32,9 @@ export {
useListWorkspaceGroups,
useListWorkspacePkiAlerts,
useListWorkspacePkiCollections,
useListWorkspaceSshCas,
useListWorkspaceSshCertificates,
useListWorkspaceSshCertificateTemplates,
useNameWorkspaceSecrets,
useToggleAutoCapitalization,
useUpdateIdentityWorkspaceRole,

View File

@ -15,6 +15,8 @@ import { TIntegration } from "../integrations/types";
import { TPkiAlert } from "../pkiAlerts/types";
import { TPkiCollection } from "../pkiCollections/types";
import { EncryptedSecret } from "../secrets/types";
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
import { userKeys } from "../users/query-keys";
import { TWorkspaceUser } from "../users/types";
import { ProjectSlackConfig } from "../workflowIntegrations/types";
@ -758,6 +760,67 @@ export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspac
});
};
export const useListWorkspaceSshCertificates = ({
offset,
limit,
projectId
}: {
offset: number;
limit: number;
projectId: string;
}) => {
return useQuery({
queryKey: workspaceKeys.specificWorkspaceSshCertificates({
offset,
limit,
projectId
}),
queryFn: async () => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit)
});
const { data } = await apiRequest.get<{
certificates: TSshCertificate[];
totalCount: number;
}>(`/api/v2/workspace/${projectId}/ssh-certificates`, {
params
});
return data;
},
enabled: Boolean(projectId)
});
};
export const useListWorkspaceSshCas = (projectId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSshCas(projectId),
queryFn: async () => {
const {
data: { cas }
} = await apiRequest.get<{ cas: Omit<TSshCertificateAuthority, "publicKey">[] }>(
`/api/v2/workspace/${projectId}/ssh-cas`
);
return cas;
},
enabled: Boolean(projectId)
});
};
export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId),
queryFn: async () => {
const { data } = await apiRequest.get<{ certificateTemplates: TSshCertificateTemplate[] }>(
`/api/v2/workspace/${projectId}/ssh-certificate-templates`
);
return data;
},
enabled: Boolean(projectId)
});
};
export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId),

View File

@ -53,5 +53,19 @@ export const workspaceKeys = {
getWorkspaceCertificateTemplates: (workspaceId: string) =>
[{ workspaceId }, "workspace-certificate-templates"] as const,
getWorkspaceSlackConfig: (workspaceId: string) =>
[{ workspaceId }, "workspace-slack-config"] as const
[{ workspaceId }, "workspace-slack-config"] as const,
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
allWorkspaceSshCertificates: (projectId: string) =>
[{ projectId }, "workspace-ssh-certificates"] as const,
specificWorkspaceSshCertificates: ({
offset,
limit,
projectId
}: {
offset: number;
limit: number;
projectId: string;
}) => [...workspaceKeys.allWorkspaceSshCertificates(projectId), { offset, limit }] as const,
getWorkspaceSshCertificateTemplates: (projectId: string) =>
[{ projectId }, "workspace-ssh-certificate-templates"] as const
};

View File

@ -11,7 +11,8 @@ export enum ProjectVersion {
export enum ProjectType {
SecretManager = "secret-manager",
CertificateManager = "cert-manager",
KMS = "kms"
KMS = "kms",
SSH = "ssh"
}
export enum ProjectUserMembershipTemporaryMode {

View File

@ -92,6 +92,16 @@ export const OrganizationLayout = () => {
</MenuItem>
)}
</Link>
<Link to={`/organization/${ProjectType.SSH}/overview` as const}>
{({ isActive }) => (
<MenuItem
isSelected={isActive}
icon="system-regular-126-verified-hover-verified"
>
SSH
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="system-outline-96-groups">

View File

@ -78,6 +78,7 @@ export const ProjectLayout = () => {
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
const isCmek = currentWorkspace?.type === ProjectType.KMS;
const isSSH = currentWorkspace?.type === ProjectType.SSH;
return (
<>
@ -133,6 +134,20 @@ export const ProjectLayout = () => {
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="system-outline-90-lock-closed">
Overview
</MenuItem>
)}
</Link>
)}
<Link
to={`/${currentWorkspace.type}/$projectId/access-management` as const}
params={{

View File

@ -53,7 +53,8 @@ enum ProjectOrderBy {
const formatTitle = (type: ProjectType) => {
if (type === ProjectType.SecretManager) return "Secret Management";
if (type === ProjectType.CertificateManager) return "Cert Management";
return "Key Management";
if (type === ProjectType.KMS) return "Key Management";
return "SSH";
};
const formatDescription = (type: ProjectType) => {
@ -61,7 +62,9 @@ const formatDescription = (type: ProjectType) => {
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
if (type === ProjectType.CertificateManager)
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
return "Generate SSH credentials to provide secure and centralized SSH access control for your infrastructure.";
};
type Props = {

View File

@ -0,0 +1,5 @@
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage";
export const SshOverviewPage = () => <ProductOverviewPage type={ProjectType.SSH} />;

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import { SshOverviewPage } from './SshOverviewPage'
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/organization/_layout/ssh/overview',
)({
component: SshOverviewPage,
})

View File

@ -0,0 +1,18 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { ProjectAccessControlTabs } from "@app/types/project";
import { AccessControlPage } from "./AccessControlPage";
const AccessControlPageQuerySchema = z.object({
selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member)
});
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management"
)({
component: AccessControlPage,
validateSearch: zodValidator(AccessControlPageQuerySchema)
});

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId"
)({
component: IdentityDetailsByIDPage
});

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId"
)({
component: MemberDetailsByIDPage
});

View File

@ -126,6 +126,11 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
[]
),
[ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
@ -596,6 +601,33 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshCertificateAuthorities]: {
title: "SSH Certificate Authorities",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshCertificates]: {
title: "SSH Certificates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshCertificateTemplates]: {
title: "SSH Certificate Templates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.PkiCollections]: {
title: "PKI Collections",
actions: [

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug"
)({
component: RoleDetailsBySlugPage
});

View File

@ -0,0 +1,75 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SshCaSection, SshCertificatesSection } from "./components";
enum TabSections {
SshCa = "ssh-certificate-authorities",
SshCertificates = "ssh-certificates"
}
export const OverviewPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="h-full bg-bunker-800">
<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 px-6 py-6">
<p className="mb-4 mr-4 text-3xl font-semibold text-white">SSH</p>
<Tabs defaultValue={TabSections.SshCertificates}>
<TabList>
<Tab value={TabSections.SshCertificates}>SSH Certificates</Tab>
<Tab value={TabSections.SshCa}>Certificate Authorities</Tab>
</TabList>
<TabPanel value={TabSections.SshCertificates}>
<motion.div
key="panel-ssh-certificate-s"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificates}
renderGuardBanner
passThrough={false}
>
<SshCertificatesSection />
</ProjectPermissionCan>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.SshCa}>
<motion.div
key="panel-ssh-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
renderGuardBanner
passThrough={false}
>
<SshCaSection />
</ProjectPermissionCan>
</motion.div>
</TabPanel>
</Tabs>
</div>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,198 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["sshCa"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
};
const schema = z
.object({
friendlyName: z.string(),
keyAlgorithm: z.enum([
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
])
})
.required();
export type FormData = z.infer<typeof schema>;
export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || "");
const { mutateAsync: createMutateAsync } = useCreateSshCa();
const { mutateAsync: updateMutateAsync } = useUpdateSshCa();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
friendlyName: "",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
}
});
useEffect(() => {
if (ca) {
reset({
friendlyName: ca.friendlyName,
keyAlgorithm: ca.keyAlgorithm
});
} else {
reset({
friendlyName: "",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
});
}
}, [ca]);
const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => {
try {
if (!projectId) return;
if (ca) {
await updateMutateAsync({
caId: ca.id,
friendlyName
});
} else {
const { id: newCaId } = await createMutateAsync({
projectId,
friendlyName,
keyAlgorithm
});
navigate({
to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
params: {
projectId,
caId: newCaId
}
});
}
reset();
handlePopUpToggle("sshCa", false);
createNotification({
text: `Successfully ${ca ? "updated" : "created"} SSH CA`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create SSH CA",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.sshCa?.isOpen}
onOpenChange={(isOpen) => {
reset();
handlePopUpToggle("sshCa", isOpen);
}}
>
<ModalContent title={`${ca ? "View" : "Create"} SSH CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
{ca && (
<FormControl label="CA ID">
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
defaultValue=""
name="friendlyName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Friendly Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="My SSH CA" />
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.sshCa?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("sshCa", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,120 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SshCaStatus, useDeleteSshCa, useUpdateSshCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { SshCaModal } from "./SshCaModal";
import { SshCaTable } from "./SshCaTable";
export const SshCaSection = () => {
const { mutateAsync: deleteSshCa } = useDeleteSshCa();
const { mutateAsync: updateSshCa } = useUpdateSshCa();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"sshCa",
"deleteSshCa",
"sshCaStatus", // enable / disable
"upgradePlan"
] as const);
const onRemoveSshCaSubmit = async (caId: string) => {
try {
await deleteSshCa({ caId });
createNotification({
text: "Successfully deleted SSH CA",
type: "success"
});
handlePopUpClose("deleteSshCa");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete SSH CA",
type: "error"
});
}
};
const onUpdateSshCaStatus = async ({ caId, status }: { caId: string; status: SshCaStatus }) => {
try {
await updateSshCa({ caId, status });
createNotification({
text: `Successfully ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`,
type: "success"
});
handlePopUpClose("sshCaStatus");
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`,
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("sshCa")}
isDisabled={!isAllowed}
>
Create SSH CA
</Button>
)}
</ProjectPermissionCan>
</div>
<SshCaTable handlePopUpOpen={handlePopUpOpen} />
<SshCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSshCa.isOpen}
title="Are you sure want to remove the SSH CA?"
onChange={(isOpen) => handlePopUpToggle("deleteSshCa", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveSshCaSubmit((popUp?.deleteSshCa?.data as { caId: string })?.caId)
}
/>
<DeleteActionModal
isOpen={popUp.sshCaStatus.isOpen}
title={`Are you sure want to ${
(popUp?.sshCaStatus?.data as { status: string })?.status === SshCaStatus.ACTIVE
? "enable"
: "disable"
} the CA?`}
subTitle={
(popUp?.sshCaStatus?.data as { status: string })?.status === SshCaStatus.ACTIVE
? "This action will allow the SSH CA to start issuing certificates again."
: "This action will prevent the SSH CA from issuing new certificates."
}
onChange={(isOpen) => handlePopUpToggle("sshCaStatus", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onUpdateSshCaStatus(popUp?.sshCaStatus?.data as { caId: string; status: SshCaStatus })
}
/>
{/* <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/> */}
</div>
);
};

View File

@ -0,0 +1,160 @@
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { SshCaStatus, useListWorkspaceSshCas } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteSshCa", "sshCaStatus"]>,
data?: object
) => void;
};
export const SshCaTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListWorkspaceSshCas(currentWorkspace?.id || "");
return (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Friendly Name</Th>
<Th>Status</Th>
<Th />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="org-ssh-cas" />}
{!isPending &&
data &&
data.length > 0 &&
data.map((ca) => {
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`ca-${ca.id}`}
onClick={() =>
navigate({
to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
params: {
projectId: currentWorkspace.id,
caId: ca.id
}
})
}
>
<Td>{ca.friendlyName}</Td>
<Td>
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
{caStatusToNameMap[ca.status]}
</Badge>
</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{(ca.status === SshCaStatus.ACTIVE ||
ca.status === SshCaStatus.DISABLED) && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("sshCaStatus", {
caId: ca.id,
status:
ca.status === SshCaStatus.ACTIVE
? SshCaStatus.DISABLED
: SshCaStatus.ACTIVE
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />}
>
{`${
ca.status === SshCaStatus.ACTIVE ? "Disable" : "Enable"
} SSH CA`}
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteSshCa", {
caId: ca.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete SSH CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && data?.length === 0 && (
<EmptyState
title="No SSH certificate authorities have been created"
icon={faCertificate}
/>
)}
</TableContainer>
</div>
);
};

View File

@ -0,0 +1,39 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { SshCertificateModal } from "../../SshCaByIDPage/components/SshCertificateModal";
import { SshCertificatesTable } from "./SshCertificatesTable";
export const SshCertificatesSection = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["sshCertificate"] as const);
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshCertificates}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("sshCertificate")}
isDisabled={!isAllowed}
>
Request
</Button>
)}
</ProjectPermissionCan>
</div>
<SshCertificatesTable />
<SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};

View File

@ -0,0 +1,87 @@
import { useState } from "react";
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
import { format } from "date-fns";
import {
Badge,
EmptyState,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useListWorkspaceSshCertificates } from "@app/hooks/api";
import { getSshCertStatusBadgeDetails } from "./SshCertificatesTable.utils";
const PER_PAGE_INIT = 25;
export const SshCertificatesTable = () => {
const { currentWorkspace } = useWorkspace();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { data, isPending } = useListWorkspaceSshCertificates({
projectId: currentWorkspace?.id || "",
offset: (page - 1) * perPage,
limit: perPage
});
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Principals</Th>
<Th>Status</Th>
<Th>Not Before</Th>
<Th>Not After</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="org-ssh-certificates" />}
{!isPending &&
data?.certificates?.map((certificate) => {
const { variant, label } = getSshCertStatusBadgeDetails(certificate.notAfter);
return (
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.principals.join(", ")}</Td>
<Td>
<Badge variant={variant}>{label}</Badge>
</Td>
<Td>
{certificate.notBefore
? format(new Date(certificate.notBefore), "yyyy-MM-dd")
: "-"}
</Td>
<Td>
{certificate.notAfter
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
: "-"}
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
<Pagination
count={data.totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isPending && !data?.certificates?.length && (
<EmptyState title="No SSH certificates have been issued" icon={faCertificate} />
)}
</TableContainer>
);
};

View File

@ -0,0 +1,17 @@
export const getSshCertStatusBadgeDetails = (notAfter: string) => {
const currentDate = new Date().getTime();
const notAfterDate = new Date(notAfter).getTime();
let variant: "success" | "primary" | "danger" = "success";
let label = "Active";
if (notAfterDate > currentDate) {
variant = "success";
label = "Active";
} else {
variant = "danger";
label = "Expired";
}
return { variant, label };
};

View File

@ -0,0 +1,2 @@
export { SshCaSection } from "./SshCaSection";
export { SshCertificatesSection } from "./SshCertificatesSection";

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview"
)({
component: OverviewPage
});

View File

@ -0,0 +1,39 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
export const SettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet>
<div className="w-full max-w-7xl px-6">
<div className="my-6">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tabs defaultValue={tabs[0].key}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, Component }) => (
<TabPanel value={key} key={key}>
<Component />
</TabPanel>
))}
</Tabs>
</div>
</div>
);
};

View File

@ -0,0 +1,129 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useProjectPermission, useSubscription, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/workspace/queries";
const formSchema = z.object({
auditLogsRetentionDays: z.coerce.number().min(0)
});
type TForm = z.infer<typeof formSchema>;
export const AuditLogsRetentionSection = () => {
const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention();
const { currentWorkspace } = useWorkspace();
const { membership } = useProjectPermission();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const {
control,
formState: { isSubmitting, isDirty },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
auditLogsRetentionDays:
currentWorkspace?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0
}
});
if (!currentWorkspace) return null;
const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => {
try {
if (!subscription?.auditLogs) {
handlePopUpOpen("upgradePlan", {
description: "You can only configure audit logs retention if you upgrade your plan."
});
return;
}
if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) {
handlePopUpOpen("upgradePlan", {
description:
"To update your audit logs retention period to a higher value, upgrade your plan."
});
return;
}
await updateAuditLogsRetention({
auditLogsRetentionDays,
projectSlug: currentWorkspace.slug
});
createNotification({
text: "Successfully updated audit logs retention period",
type: "success"
});
} catch {
createNotification({
text: "Failed updating audit logs retention period",
type: "error"
});
}
};
// render only for dedicated/self-hosted instances of Infisical
if (
window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")
) {
return null;
}
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
return (
<>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Audit Logs Retention</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
Set the number of days to keep your project audit logs.
</p>
<form onSubmit={handleSubmit(handleAuditLogsRetentionSubmit)} autoComplete="off">
<div className="max-w-xs">
<Controller
control={control}
defaultValue={0}
name="auditLogsRetentionDays"
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Number of days"
>
<Input {...field} type="number" min={1} step={1} isDisabled={!isAdmin} />
</FormControl>
)}
/>
</div>
<Button
colorSchema="secondary"
type="submit"
isLoading={isSubmitting}
disabled={!isAdmin || !isDirty}
>
Save
</Button>
</form>
</div>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</>
);
};

View File

@ -0,0 +1 @@
export { AuditLogsRetentionSection } from "./AuditLogsRetentionSection";

View File

@ -0,0 +1,192 @@
import { useMemo } from "react";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteProjectSection = () => {
const navigate = useNavigate();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteWorkspace",
"leaveWorkspace"
] as const);
const { currentOrg } = useOrganization();
const { hasProjectRole, membership } = useProjectPermission();
const { currentWorkspace } = useWorkspace();
const [isDeleting, setIsDeleting] = useToggle();
const [isLeaving, setIsLeaving] = useToggle();
const deleteWorkspace = useDeleteWorkspace();
const leaveProject = useLeaveProject();
const { data: members, isPending: isMembersLoading } = useGetWorkspaceUsers(
currentWorkspace?.id || ""
);
// If isNoAccessMember is true, then the user can't read the workspace members. So we need to handle this case separately.
const isNoAccessMember = hasProjectRole("no-access");
const isOnlyAdminMember = useMemo(() => {
if (!members || !membership || !hasProjectRole("admin")) return false;
const adminMembers = members.filter(
(member) => member.roles.map((r) => r.role).includes("admin") && member.id !== membership.id // exclude the current user
);
return !adminMembers.length;
}, [members, membership]);
const handleDeleteWorkspaceSubmit = async () => {
setIsDeleting.on();
try {
if (!currentWorkspace?.id) return;
await deleteWorkspace.mutateAsync({
workspaceID: currentWorkspace?.id
});
createNotification({
text: "Successfully deleted project",
type: "success"
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
handlePopUpClose("deleteWorkspace");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete project",
type: "error"
});
} finally {
setIsDeleting.off();
}
};
const handleLeaveWorkspaceSubmit = async () => {
console.log({
currentWorkspace,
currentOrg,
members,
isNoAccessMember,
membership
});
try {
setIsLeaving.on();
if (!currentWorkspace?.id || !currentOrg?.id) return;
// If there's no members, and the user has access to read members, something went wrong.
if (!members && !isNoAccessMember) return;
// If the user has elevated permissions and can read members:
if (!isNoAccessMember) {
if (!members) return;
if (members.length < 2) {
createNotification({
text: "You can't leave the project as you are the only member",
type: "error"
});
return;
}
// If the user has access to read members, and there's less than 1 admin member excluding the current user, they can't leave the project.
if (isOnlyAdminMember) {
createNotification({
text: "You can't leave a project with no admin members left. Promote another member to admin first.",
type: "error"
});
return;
}
}
// If it's actually a no-access member, then we don't really care about the members.
await leaveProject.mutateAsync({
workspaceId: currentWorkspace.id
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to leave project",
type: "error"
});
} finally {
setIsLeaving.off();
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
<div className="space-x-4">
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
{!isOnlyAdminMember && (
<Button
disabled={isMembersLoading || (members && members?.length < 2)}
isLoading={isLeaving}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("leaveWorkspace")}
>
{`Leave ${currentWorkspace?.name}`}
</Button>
)}
</div>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Delete Project"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
<LeaveProjectModal
isOpen={popUp.leaveWorkspace.isOpen}
title="Are you sure want to leave this project?"
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Leave Project"
onLeaveApproved={handleLeaveWorkspaceSubmit}
/>
</div>
);
};

View File

@ -0,0 +1 @@
export { DeleteProjectSection } from "./DeleteProjectSection";

View File

@ -0,0 +1,13 @@
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
export const ProjectGeneralTab = () => {
return (
<div>
<ProjectOverviewChangeSection />
<AuditLogsRetentionSection />
<DeleteProjectSection />
</div>
);
};

View File

@ -0,0 +1 @@
export { ProjectGeneralTab } from "./ProjectGeneralTab";

View File

@ -0,0 +1,51 @@
import { useCallback } from "react";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
value: string;
hoverText: string;
notificationText: string;
children: React.ReactNode;
};
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
const copyToClipboard = useCallback(() => {
if (isProjectIdCopied) {
return;
}
setIsProjectIdCopied.on();
navigator.clipboard.writeText(value);
createNotification({
text: notificationText,
type: "success"
});
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
// eslint-disable-next-line consistent-return
return () => clearTimeout(timer);
}, [isProjectIdCopied]);
return (
<Button
colorSchema="secondary"
className="group relative"
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
onClick={copyToClipboard}
>
{children}
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{hoverText}
</span>
</Button>
);
};

View File

@ -0,0 +1,168 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api";
import { CopyButton } from "./CopyButton";
const formSchema = z.object({
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
description: z
.string()
.trim()
.max(256, "Description too long, max length is 256 characters")
.optional()
});
type FormData = z.infer<typeof formSchema>;
export const ProjectOverviewChangeSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isPending } = useUpdateProject();
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
useEffect(() => {
if (currentWorkspace) {
reset({
name: currentWorkspace.name,
description: currentWorkspace.description ?? ""
});
}
}, [currentWorkspace]);
const onFormSubmit = async ({ name, description }: FormData) => {
try {
if (!currentWorkspace?.id) return;
await mutateAsync({
projectID: currentWorkspace.id,
newProjectName: name,
newProjectDescription: description
});
createNotification({
text: "Successfully updated project overview",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update project overview",
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="justify-betweens flex">
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
<div className="space-x-2">
<CopyButton
value={currentWorkspace?.slug || ""}
hoverText="Click to project slug"
notificationText="Copied project slug to clipboard"
>
Copy Project Slug
</CopyButton>
<CopyButton
value={currentWorkspace?.id || ""}
hoverText="Click to project ID"
notificationText="Copied project ID to clipboard"
>
Copy Project ID
</CopyButton>
</div>
</div>
<div>
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
<div className="flex w-full flex-row items-end gap-4">
<div className="w-full max-w-md">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project name"
>
<Input
placeholder="Project name"
{...field}
className="bg-mineshaft-800"
isDisabled={!isAllowed}
/>
</FormControl>
)}
control={control}
name="name"
/>
)}
</ProjectPermissionCan>
</div>
</div>
<div className="flex w-full flex-row items-end gap-4">
<div className="w-full max-w-md">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project description"
>
<TextArea
placeholder="Project description"
{...field}
rows={3}
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
isDisabled={!isAllowed}
/>
</FormControl>
)}
control={control}
name="description"
/>
)}
</ProjectPermissionCan>
</div>
</div>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isLoading={isPending}
isDisabled={isPending || !isAllowed}
>
Save
</Button>
)}
</ProjectPermissionCan>
</div>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";

View File

@ -0,0 +1,2 @@
export { DeleteProjectSection } from "./DeleteProjectSection";
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { SettingsPage } from "./SettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings"
)({
component: SettingsPage
});

View File

@ -0,0 +1,167 @@
import { Helmet } from "react-helmet";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { SshCaModal } from "../OverviewPage/components/SshCaModal";
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
const Page = () => {
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const projectId = currentWorkspace?.id || "";
const caId = useParams({
from: ROUTE_PATHS.Ssh.SshCaByIDPage.id,
select: (el) => el.caId
});
const { data } = useGetSshCaById(caId);
const { mutateAsync: deleteSshCa } = useDeleteSshCa();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"sshCa",
"deleteSshCa"
] as const);
const onRemoveCaSubmit = async (caIdToDelete: string) => {
try {
if (!projectId) return;
await deleteSshCa({ caId: caIdToDelete });
createNotification({
text: "Successfully deleted SSH CA",
type: "success"
});
handlePopUpClose("deleteSshCa");
navigate({
to: `/${ProjectType.SSH}/$projectId/overview` as const,
params: {
projectId
}
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete SSH CA",
type: "error"
});
}
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() =>
navigate({
to: `/${ProjectType.SSH}/$projectId/overview`,
params: {
projectId
}
})
}
className="mb-4"
>
SSH Certificate Authorities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.friendlyName}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() =>
handlePopUpOpen("deleteSshCa", {
caId: data.id
})
}
disabled={!isAllowed}
>
Delete SSH CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex">
<div className="mr-4 w-96">
<SshCaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
</div>
<div className="w-full">
<SshCertificateTemplatesSection caId={caId} />
</div>
</div>
</div>
)}
<SshCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSshCa.isOpen}
title="Are you sure want to remove the SSH CA from the project?"
onChange={(isOpen) => handlePopUpToggle("deleteSshCa", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveCaSubmit((popUp?.deleteSshCa?.data as { caId: string })?.caId)
}
/>
</div>
);
};
export const SshCaByIDPage = () => {
return (
<>
<Helmet>
<title>SSH Certificate Authority</title>
</Helmet>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
passThrough={false}
renderGuardBanner
>
<Page />
</ProjectPermissionCan>
</>
);
};

View File

@ -0,0 +1,124 @@
import { faCheck, faCopy, faDownload, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetSshCaById } from "@app/hooks/api";
import { caStatusToNameMap } from "@app/hooks/api/ca/constants";
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
caId: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["sshCa"]>, data?: object) => void;
};
export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const [downloadText, isDownloading, setDownloadText] = useTimedReset<string>({
initialState: "Save public key"
});
const { data: ca } = useGetSshCaById(caId);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return ca ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">SSH CA Details</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) => {
return (
<Tooltip content="Edit SSH CA">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("sshCa", {
caId: ca.id
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">SSH CA ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Public Key</p>
<div className="group flex align-top">
<p className="flex-1 text-sm text-mineshaft-300">{ca.publicKey.substring(0, 20)}...</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={downloadText}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
setDownloadText("Saved");
downloadTxtFile("ssh_ca.pub", ca.publicKey);
}}
>
<FontAwesomeIcon icon={isDownloading ? faCheck : faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
) : (
<div />
);
};

View File

@ -0,0 +1,174 @@
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { IconButton, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
type Props = {
serialNumber: string;
signedKey: string;
privateKey?: string;
publicKey?: string;
};
export const SshCertificateContent = ({
serialNumber,
signedKey,
privateKey,
publicKey
}: Props) => {
const [copyTextSerialNumber, isCopyingSerialNumber, setCopyTextSerialNumber] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>(
{
initialState: "Copy to clipboard"
}
);
const [copyTextCertificateSk, isCopyingCertificateSk, setCopyTextCertificateSk] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
const [copyTextCertificatePk, isCopyingCertificatePk, setCopyTextCertificatePk] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<div>
<h2 className="mb-4">Serial Number</h2>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{serialNumber}</p>
<Tooltip content={copyTextSerialNumber}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(serialNumber);
setCopyTextSerialNumber("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingSerialNumber ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
<div className="mb-4 flex items-center justify-between">
<h2>SSH Certificate / Signed Key</h2>
<div className="flex">
<Tooltip content={copyTextCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(signedKey);
setCopyTextCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificate ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("user_key-cert.pub", signedKey);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{signedKey}</p>
</div>
{privateKey && (
<>
<div className="mb-4 flex items-center justify-between">
<h2>Private Key</h2>
<div className="flex">
<Tooltip content={copyTextCertificateSk}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setCopyTextCertificateSk("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificateSk ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content={copyTextCertificateSk}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("user_key.pem", privateKey);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{privateKey}</p>
</div>
</>
)}
{publicKey && (
<>
<div className="mb-4 flex items-center justify-between">
<h2>Public Key</h2>
<div className="flex">
<Tooltip content={copyTextCertificatePk}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(publicKey);
setCopyTextCertificatePk("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCertificatePk ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content={copyTextCertificatePk}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("user_key.pub", publicKey);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{publicKey}</p>
</div>
</>
)}
</div>
);
};

View File

@ -0,0 +1,373 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
SshCertTemplateStatus,
useGetSshCertTemplate,
useIssueSshCreds,
useListWorkspaceSshCertificateTemplates,
useSignSshKey
} from "@app/hooks/api";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { SshCertType } from "@app/hooks/api/sshCa/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { SshCertificateContent } from "./SshCertificateContent";
const schema = z.object({
templateId: z.string(),
publicKey: z.string().optional(),
keyAlgorithm: z.enum([
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
]),
certType: z.nativeEnum(SshCertType),
principals: z.string(),
ttl: z
.string()
.trim()
.refine((val) => ms(val) > 0, "TTL must be a valid time string such as 2 days, 1d, 2h 1y, ...")
.optional(),
keyId: z.string().optional()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["sshCertificate"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCertificate"]>, state?: boolean) => void;
};
type TSshCertificateDetails = {
serialNumber: string;
signedKey: string;
privateKey?: string;
publicKey?: string;
};
enum SshCertificateOperation {
SIGN_SSH_KEY = "sign-ssh-key",
ISSUE_SSH_CREDS = "issue-ssh-creds"
}
export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const [operation, setOperation] = useState<SshCertificateOperation>(
SshCertificateOperation.SIGN_SSH_KEY
);
const [certificateDetails, setCertificateDetails] = useState<TSshCertificateDetails | null>(null);
const { mutateAsync: signSshKey } = useSignSshKey();
const { mutateAsync: issueSshCreds } = useIssueSshCreds();
const popUpData = popUp?.sshCertificate?.data as {
sshCaId: string;
templateId: string;
};
const { data: templatesData } = useListWorkspaceSshCertificateTemplates(projectId);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
certType: SshCertType.USER
}
});
const templateId = watch("templateId");
const { data: templateData } = useGetSshCertTemplate(templateId);
useEffect(() => {
if (popUpData) {
setValue("templateId", popUpData.templateId);
} else if (templatesData && templatesData.certificateTemplates.length > 0) {
setValue("templateId", templatesData.certificateTemplates[0].id);
}
}, [popUpData]);
const onFormSubmit = async ({
keyAlgorithm,
certType,
publicKey: existingPublicKey,
principals,
ttl,
keyId
}: FormData) => {
try {
if (!templateData) return;
if (!projectId) return;
switch (operation) {
case SshCertificateOperation.SIGN_SSH_KEY: {
const { serialNumber, signedKey } = await signSshKey({
projectId,
certificateTemplateId: templateData.id,
publicKey: existingPublicKey,
certType,
principals: principals.split(",").map((user) => user.trim()),
ttl,
keyId
});
setCertificateDetails({
serialNumber,
signedKey
});
break;
}
case SshCertificateOperation.ISSUE_SSH_CREDS: {
const { serialNumber, publicKey, privateKey, signedKey } = await issueSshCreds({
projectId,
certificateTemplateId: templateData.id,
keyAlgorithm,
certType,
principals: principals.split(",").map((user) => user.trim()),
ttl,
keyId
});
setCertificateDetails({
serialNumber,
privateKey,
publicKey,
signedKey
});
break;
}
default: {
break;
}
}
reset();
createNotification({
text: "Successfully created SSH certificate",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create SSH certificate",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.sshCertificate?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("sshCertificate", isOpen);
reset();
setCertificateDetails(null);
}}
>
<ModalContent title="Issue SSH Certificate">
{!certificateDetails ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="templateId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Certificate Template"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(popUpData?.sshCaId)}
>
{(templatesData?.certificateTemplates || [])
.filter((template) => template.status === SshCertTemplateStatus.ACTIVE)
.map(({ id, name }) => (
<SelectItem value={id} key={`ssh-cert-template-${id}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormControl label="Operation">
<Select
defaultValue="issue-ssh-creds"
value={operation}
onValueChange={(e) => setOperation(e as SshCertificateOperation)}
className="w-full"
>
<SelectItem value="sign-ssh-key" key="sign-ssh-key">
Sign SSH Key
</SelectItem>
<SelectItem value="issue-ssh-creds" key="issue-ssh-creds">
Issue SSH Credentials
</SelectItem>
</Select>
</FormControl>
<Controller
control={control}
name="certType"
defaultValue={SshCertType.USER}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Certificate Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{templateData && templateData.allowUserCertificates && (
<SelectItem value={SshCertType.USER}>User</SelectItem>
)}
{templateData && templateData.allowHostCertificates && (
<SelectItem value={SshCertType.HOST}>Host</SelectItem>
)}
</Select>
</FormControl>
)}
/>
{operation === SshCertificateOperation.SIGN_SSH_KEY && (
<Controller
control={control}
name="publicKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="SSH Public Key"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="ssh-rsa AAA ..." />
</FormControl>
)}
/>
)}
{operation === SshCertificateOperation.ISSUE_SSH_CREDS && (
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="principals"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Principal(s)"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="ec2-user" />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl label="TTL" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
{templateData && templateData.allowCustomKeyIds && (
<Controller
control={control}
name="keyId"
render={({ field, fieldState: { error } }) => (
<FormControl label="Key ID" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="12345678" />
</FormControl>
)}
/>
)}
<div className="mt-4 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("sshCertificate", false)}
>
Cancel
</Button>
</div>
</form>
) : (
<SshCertificateContent
serialNumber={certificateDetails.serialNumber}
signedKey={certificateDetails.signedKey}
publicKey={certificateDetails.publicKey}
privateKey={certificateDetails.privateKey}
/>
)}
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,411 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Switch
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateSshCertTemplate,
useGetSshCaById,
useGetSshCertTemplate,
useListWorkspaceSshCas,
useUpdateSshCertTemplate
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
// Validates usernames or wildcard (*)
export const isValidUserPattern = (value: string): boolean => {
// Matches valid Linux usernames or a wildcard (*)
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
return userRegex.test(value);
};
// Validates hostnames, wildcard domains, or IP addresses
export const isValidHostPattern = (value: string): boolean => {
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
const hostRegex =
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
return hostRegex.test(value);
};
const schema = z
.object({
sshCaId: z.string(),
name: z
.string()
.trim()
.toLowerCase()
.min(1)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Invalid name. Name can only contain alphanumeric characters and hyphens."
}),
ttl: z
.string()
.trim()
.refine(
(val) => ms(val) > 0,
"TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
)
.default("1h"),
maxTTL: z
.string()
.trim()
.refine(
(val) => ms(val) > 0,
"Max TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
)
.default("30d"),
allowedUsers: z.string().refine(
(val) => {
const trimmed = val.trim();
if (trimmed === "") return true;
const users = trimmed.split(",").map((u) => u.trim());
return users.every(isValidUserPattern);
},
{
message: "Invalid user pattern in allowedUsers"
}
),
allowedHosts: z.string().refine(
(val) => {
const trimmed = val.trim();
if (trimmed === "") return true;
const users = trimmed.split(",").map((u) => u.trim());
return users.every(isValidHostPattern);
},
{
message: "Invalid host pattern in allowedHosts"
}
),
allowUserCertificates: z.boolean().optional().default(false),
allowHostCertificates: z.boolean().optional().default(false),
allowCustomKeyIds: z.boolean().optional().default(false)
})
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
message: "Max TLL must be greater than TTL",
path: ["maxTTL"]
});
export type FormData = z.infer<typeof schema>;
type Props = {
sshCaId: string;
popUp: UsePopUpState<["sshCertificateTemplate"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["sshCertificateTemplate"]>,
state?: boolean
) => void;
};
export const SshCertificateTemplateModal = ({ popUp, handlePopUpToggle, sshCaId }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: ca } = useGetSshCaById(sshCaId);
const { data: certTemplate } = useGetSshCertTemplate(
(popUp?.sshCertificateTemplate?.data as { id: string })?.id || ""
);
const { data: cas } = useListWorkspaceSshCas(currentWorkspace?.id || "");
const { mutateAsync: createSshCertTemplate } = useCreateSshCertTemplate();
const { mutateAsync: updateSshCertTemplate } = useUpdateSshCertTemplate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {}
});
useEffect(() => {
if (certTemplate) {
reset({
sshCaId: certTemplate.sshCaId,
name: certTemplate.name,
ttl: certTemplate.ttl,
maxTTL: certTemplate.maxTTL,
allowedUsers: certTemplate.allowedUsers.join(", "),
allowedHosts: certTemplate.allowedHosts.join(", "),
allowUserCertificates: certTemplate.allowUserCertificates,
allowHostCertificates: certTemplate.allowHostCertificates,
allowCustomKeyIds: certTemplate.allowCustomKeyIds
});
} else {
reset({
sshCaId,
name: "",
ttl: "1h",
maxTTL: "30d",
allowedUsers: "",
allowedHosts: "",
allowUserCertificates: false,
allowHostCertificates: false,
allowCustomKeyIds: false
});
}
}, [certTemplate, ca]);
const onFormSubmit = async ({
name,
ttl,
maxTTL,
allowUserCertificates,
allowHostCertificates,
allowedUsers,
allowedHosts,
allowCustomKeyIds
}: FormData) => {
try {
if (certTemplate) {
await updateSshCertTemplate({
id: certTemplate.id,
name,
ttl,
maxTTL,
allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [],
allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [],
allowUserCertificates,
allowHostCertificates,
allowCustomKeyIds
});
createNotification({
text: "Successfully updated SSH certificate template",
type: "success"
});
} else {
await createSshCertTemplate({
sshCaId,
name,
ttl,
maxTTL,
allowedUsers: allowedUsers ? allowedUsers.split(",").map((user) => user.trim()) : [],
allowedHosts: allowedHosts ? allowedHosts.split(",").map((host) => host.trim()) : [],
allowUserCertificates,
allowHostCertificates,
allowCustomKeyIds
});
createNotification({
text: "Successfully created SSH certificate template",
type: "success"
});
}
reset();
handlePopUpToggle("sshCertificateTemplate", false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to save changes",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.sshCertificateTemplate?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("sshCertificateTemplate", isOpen);
reset();
}}
>
<ModalContent
title={certTemplate ? "SSH Certificate Template" : "Create SSH Certificate Template"}
>
<form onSubmit={handleSubmit(onFormSubmit)}>
{certTemplate && (
<FormControl label="SSH Certificate Template ID">
<Input value={certTemplate.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="SSH Template Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="administrator" />
</FormControl>
)}
/>
<Controller
control={control}
name="sshCaId"
defaultValue={sshCaId}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing SSH CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled
>
{(cas || []).map(({ id, friendlyName }) => (
<SelectItem value={id} key={`ssh-ca-${id}`}>
{friendlyName}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="allowedUsers"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Users"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="ec2-user, developer, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="allowedHosts"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Hosts"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="*.compute.amazonaws.com, api.example.com, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Default TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="maxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Max TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="allowUserCertificates"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="allow-user-certificates"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="ml-1 w-full">Allow User Certificates</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="allowHostCertificates"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="allow-host-certificates"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="ml-1 w-full">Allow Host Certificates</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="allowCustomKeyIds"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="allow-custom-key-ids"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="ml-1 w-full">Allow Custom Key IDs</p>
</Switch>
</FormControl>
);
}}
/>
<div className="mt-4 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Save
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("sshCertificateTemplate", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,161 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
SshCertTemplateStatus,
useDeleteSshCertTemplate,
useUpdateSshCertTemplate
} from "@app/hooks/api";
import { SshCertificateModal } from "./SshCertificateModal";
import { SshCertificateTemplateModal } from "./SshCertificateTemplateModal";
import { SshCertificateTemplatesTable } from "./SshCertificateTemplatesTable";
type Props = {
caId: string;
};
export const SshCertificateTemplatesSection = ({ caId }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"sshCertificateTemplate",
"sshCertificateTemplateStatus",
"sshCertificate",
"deleteSshCertificateTemplate",
"upgradePlan"
] as const);
const { mutateAsync: deleteSshCertTemplate } = useDeleteSshCertTemplate();
const { mutateAsync: updateSshCertTemplate } = useUpdateSshCertTemplate();
const onRemoveSshCertificateTemplateSubmit = async (id: string) => {
try {
await deleteSshCertTemplate({
id
});
await createNotification({
text: "Successfully deleted SSH certificate template",
type: "success"
});
handlePopUpClose("deleteSshCertificateTemplate");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete SSH certificate template",
type: "error"
});
}
};
const onUpdateSshCaStatus = async ({
templateId,
status
}: {
templateId: string;
status: SshCertTemplateStatus;
}) => {
try {
await updateSshCertTemplate({ id: templateId, status });
await createNotification({
text: `Successfully ${
status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled"
} SSH certificate template`,
type: "success"
});
handlePopUpClose("sshCertificateTemplateStatus");
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${
status === SshCertTemplateStatus.ACTIVE ? "enabled" : "disabled"
} SSH certificate template`,
type: "error"
});
}
};
return (
<div className="h-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Certificate Templates</h3>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshCertificateTemplates}
>
{(isAllowed) => (
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => handlePopUpOpen("sshCertificateTemplate")}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div className="py-4">
<SshCertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} sshCaId={caId} />
</div>
<SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<SshCertificateTemplateModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
sshCaId={caId}
/>
<DeleteActionModal
isOpen={popUp.deleteSshCertificateTemplate.isOpen}
title={`Are you sure want to delete the SSH certificate template ${
(popUp?.deleteSshCertificateTemplate?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSshCertificateTemplate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveSshCertificateTemplateSubmit(
(popUp?.deleteSshCertificateTemplate?.data as { id: string })?.id
)
}
/>
<DeleteActionModal
isOpen={popUp.sshCertificateTemplateStatus.isOpen}
title={`Are you sure want to ${
(popUp?.sshCertificateTemplateStatus?.data as { status: string })?.status ===
SshCertTemplateStatus.ACTIVE
? "enable"
: "disable"
} this certificate template?`}
subTitle={
(popUp?.sshCertificateTemplateStatus?.data as { status: string })?.status ===
SshCertTemplateStatus.ACTIVE
? "This action will allow certificate issuance under this template again."
: "This action will prevent certificate issuance under this template."
}
onChange={(isOpen) => handlePopUpToggle("sshCertificateTemplateStatus", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onUpdateSshCaStatus(
popUp?.sshCertificateTemplateStatus?.data as {
templateId: string;
status: SshCertTemplateStatus;
}
)
}
buttonText={
(popUp?.sshCertificateTemplateStatus?.data as { status: string })?.status ===
SshCertTemplateStatus.ACTIVE
? "Enable"
: "Disable"
}
/>
</div>
);
};

View File

@ -0,0 +1,193 @@
import {
faBan,
faCertificate,
faEllipsis,
faFileAlt,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SshCertTemplateStatus, useGetSshCaCertTemplates } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
sshCaId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
[
"sshCertificateTemplate",
"sshCertificateTemplateStatus",
"sshCertificate",
"deleteSshCertificateTemplate",
"upgradePlan"
]
>,
data?: {
id?: string;
name?: string;
sshCaId?: string;
status?: SshCertTemplateStatus;
templateId?: string;
}
) => void;
};
export const SshCertificateTemplatesTable = ({ handlePopUpOpen, sshCaId }: Props) => {
const { data, isPending } = useGetSshCaCertTemplates(sshCaId);
return (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Status</Th>
<Th />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={2} innerKey="project-cas" />}
{!isPending &&
data?.certificateTemplates.map((certificateTemplate) => {
return (
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
<Td>{certificateTemplate.name}</Td>
<Td>
<Badge variant={getCaStatusBadgeVariant(certificateTemplate.status)}>
{caStatusToNameMap[certificateTemplate.status]}
</Badge>
</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshCertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("sshCertificateTemplateStatus", {
templateId: certificateTemplate.id,
status:
certificateTemplate.status === SshCertTemplateStatus.ACTIVE
? SshCertTemplateStatus.DISABLED
: SshCertTemplateStatus.ACTIVE
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faBan} />}
>
{`${
certificateTemplate.status === SshCertTemplateStatus.ACTIVE
? "Disable"
: "Enable"
} Template`}
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshCertificateTemplates}
>
<DropdownMenuItem
onClick={() => {
handlePopUpOpen("sshCertificate", {
sshCaId,
templateId: certificateTemplate.id
});
}}
icon={
<FontAwesomeIcon icon={faCertificate} size="sm" className="mr-1" />
}
>
Issue Certificate
</DropdownMenuItem>
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshCertificateTemplates}
>
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("sshCertificateTemplate", {
id: certificateTemplate.id
})
}
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
>
Edit Template
</DropdownMenuItem>
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SshCertificateTemplates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} size="sm" className="mr-1" />}
onClick={() =>
handlePopUpOpen("deleteSshCertificateTemplate", {
id: certificateTemplate.id,
name: certificateTemplate.name
})
}
>
Delete Template
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && !data?.certificateTemplates?.length && (
<EmptyState
title="No certificate templates have been created for this SSH CA"
icon={faFileAlt}
/>
)}
</TableContainer>
</div>
);
};

View File

@ -0,0 +1,3 @@
export { SshCaDetailsSection } from "./SshCaDetailsSection";
export { SshCertificateModal } from "./SshCertificateModal";
export { SshCertificateTemplatesSection } from "./SshCertificateTemplatesSection";

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { SshCaByIDPage } from "./SshCaByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId"
)({
component: SshCaByIDPage
});

View File

@ -0,0 +1,25 @@
import { createFileRoute } from "@tanstack/react-router";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
}
});

View File

@ -39,6 +39,7 @@ import { Route as authProviderSuccessPageRouteImport } from './pages/auth/Provid
import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route'
import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route'
import { Route as adminOverviewPageRouteImport } from './pages/admin/OverviewPage/route'
import { Route as sshLayoutImport } from './pages/ssh/layout'
import { Route as secretManagerLayoutImport } from './pages/secret-manager/layout'
import { Route as kmsLayoutImport } from './pages/kms/layout'
import { Route as certManagerLayoutImport } from './pages/cert-manager/layout'
@ -50,14 +51,18 @@ import { Route as organizationBillingPageRouteImport } from './pages/organizatio
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
import { Route as organizationAdminPageRouteImport } from './pages/organization/AdminPage/route'
import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
import { Route as projectAccessControlPageRouteKmsImport } from './pages/project/AccessControlPage/route-kms'
import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager'
import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route'
import { Route as sshOverviewPageRouteImport } from './pages/ssh/OverviewPage/route'
import { Route as secretManagerSettingsPageRouteImport } from './pages/secret-manager/SettingsPage/route'
import { Route as secretManagerSecretRotationPageRouteImport } from './pages/secret-manager/SecretRotationPage/route'
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
import { Route as secretManagerSecretApprovalsPageRouteImport } from './pages/secret-manager/SecretApprovalsPage/route'
import { Route as secretManagerIPAllowlistPageRouteImport } from './pages/secret-manager/IPAllowlistPage/route'
import { Route as organizationSshOverviewPageRouteImport } from './pages/organization/SshOverviewPage/route'
import { Route as organizationSecretManagerOverviewPageRouteImport } from './pages/organization/SecretManagerOverviewPage/route'
import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route'
import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route'
@ -69,6 +74,9 @@ import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/ro
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
import { Route as certManagerSettingsPageRouteImport } from './pages/cert-manager/SettingsPage/route'
import { Route as certManagerCertificatesPageRouteImport } from './pages/cert-manager/CertificatesPage/route'
import { Route as projectRoleDetailsBySlugPageRouteSshImport } from './pages/project/RoleDetailsBySlugPage/route-ssh'
import { Route as projectMemberDetailsByIDPageRouteSshImport } from './pages/project/MemberDetailsByIDPage/route-ssh'
import { Route as projectIdentityDetailsByIDPageRouteSshImport } from './pages/project/IdentityDetailsByIDPage/route-ssh'
import { Route as projectRoleDetailsBySlugPageRouteSecretManagerImport } from './pages/project/RoleDetailsBySlugPage/route-secret-manager'
import { Route as projectMemberDetailsByIDPageRouteSecretManagerImport } from './pages/project/MemberDetailsByIDPage/route-secret-manager'
import { Route as projectIdentityDetailsByIDPageRouteSecretManagerImport } from './pages/project/IdentityDetailsByIDPage/route-secret-manager'
@ -79,6 +87,7 @@ import { Route as projectRoleDetailsBySlugPageRouteCertManagerImport } from './p
import { Route as certManagerPkiCollectionDetailsByIDPageRoutesImport } from './pages/cert-manager/PkiCollectionDetailsByIDPage/routes'
import { Route as projectMemberDetailsByIDPageRouteCertManagerImport } from './pages/project/MemberDetailsByIDPage/route-cert-manager'
import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager'
import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/route'
import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route'
import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
import { Route as organizationAppConnectionsGithubOauthCallbackPageRouteImport } from './pages/organization/AppConnections/GithubOauthCallbackPage/route'
@ -98,6 +107,9 @@ const AuthenticateAdminImport = createFileRoute('/_authenticate/admin')()
const AuthenticateInjectOrgDetailsOrganizationImport = createFileRoute(
'/_authenticate/_inject-org-details/organization',
)()
const AuthenticateInjectOrgDetailsSshProjectIdImport = createFileRoute(
'/_authenticate/_inject-org-details/ssh/$projectId',
)()
const AuthenticateInjectOrgDetailsSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/secret-manager/$projectId',
@ -273,6 +285,13 @@ const authLoginPageRouteRoute = authLoginPageRouteImport.update({
getParentRoute: () => RestrictLoginSignupLoginRoute,
} as any)
const AuthenticateInjectOrgDetailsSshProjectIdRoute =
AuthenticateInjectOrgDetailsSshProjectIdImport.update({
id: '/ssh/$projectId',
path: '/ssh/$projectId',
getParentRoute: () => middlewaresInjectOrgDetailsRoute,
} as any)
const AuthenticateInjectOrgDetailsSecretManagerProjectIdRoute =
AuthenticateInjectOrgDetailsSecretManagerProjectIdImport.update({
id: '/secret-manager/$projectId',
@ -327,6 +346,11 @@ const adminOverviewPageRouteRoute = adminOverviewPageRouteImport.update({
getParentRoute: () => adminLayoutRoute,
} as any)
const sshLayoutRoute = sshLayoutImport.update({
id: '/_ssh-layout',
getParentRoute: () => AuthenticateInjectOrgDetailsSshProjectIdRoute,
} as any)
const secretManagerLayoutRoute = secretManagerLayoutImport.update({
id: '/_secret-manager-layout',
getParentRoute: () => AuthenticateInjectOrgDetailsSecretManagerProjectIdRoute,
@ -400,6 +424,13 @@ const organizationAccessManagementPageRouteRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const projectAccessControlPageRouteSshRoute =
projectAccessControlPageRouteSshImport.update({
id: '/access-management',
path: '/access-management',
getParentRoute: () => sshLayoutRoute,
} as any)
const projectAccessControlPageRouteSecretManagerRoute =
projectAccessControlPageRouteSecretManagerImport.update({
id: '/access-management',
@ -421,6 +452,18 @@ const projectAccessControlPageRouteCertManagerRoute =
getParentRoute: () => certManagerLayoutRoute,
} as any)
const sshSettingsPageRouteRoute = sshSettingsPageRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => sshLayoutRoute,
} as any)
const sshOverviewPageRouteRoute = sshOverviewPageRouteImport.update({
id: '/overview',
path: '/overview',
getParentRoute: () => sshLayoutRoute,
} as any)
const secretManagerSettingsPageRouteRoute =
secretManagerSettingsPageRouteImport.update({
id: '/settings',
@ -456,6 +499,13 @@ const secretManagerIPAllowlistPageRouteRoute =
getParentRoute: () => secretManagerLayoutRoute,
} as any)
const organizationSshOverviewPageRouteRoute =
organizationSshOverviewPageRouteImport.update({
id: '/ssh/overview',
path: '/ssh/overview',
getParentRoute: () => organizationLayoutRoute,
} as any)
const organizationSecretManagerOverviewPageRouteRoute =
organizationSecretManagerOverviewPageRouteImport.update({
id: '/secret-manager/overview',
@ -531,6 +581,27 @@ const certManagerCertificatesPageRouteRoute =
getParentRoute: () => certManagerLayoutRoute,
} as any)
const projectRoleDetailsBySlugPageRouteSshRoute =
projectRoleDetailsBySlugPageRouteSshImport.update({
id: '/roles/$roleSlug',
path: '/roles/$roleSlug',
getParentRoute: () => sshLayoutRoute,
} as any)
const projectMemberDetailsByIDPageRouteSshRoute =
projectMemberDetailsByIDPageRouteSshImport.update({
id: '/members/$membershipId',
path: '/members/$membershipId',
getParentRoute: () => sshLayoutRoute,
} as any)
const projectIdentityDetailsByIDPageRouteSshRoute =
projectIdentityDetailsByIDPageRouteSshImport.update({
id: '/identities/$identityId',
path: '/identities/$identityId',
getParentRoute: () => sshLayoutRoute,
} as any)
const projectRoleDetailsBySlugPageRouteSecretManagerRoute =
projectRoleDetailsBySlugPageRouteSecretManagerImport.update({
id: '/roles/$roleSlug',
@ -601,6 +672,12 @@ const projectIdentityDetailsByIDPageRouteCertManagerRoute =
getParentRoute: () => certManagerLayoutRoute,
} as any)
const sshSshCaByIDPageRouteRoute = sshSshCaByIDPageRouteImport.update({
id: '/ca/$caId',
path: '/ca/$caId',
getParentRoute: () => sshLayoutRoute,
} as any)
const secretManagerSecretDashboardPageRouteRoute =
secretManagerSecretDashboardPageRouteImport.update({
id: '/secrets/$envSlug',
@ -864,6 +941,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdImport
parentRoute: typeof middlewaresInjectOrgDetailsImport
}
'/_authenticate/_inject-org-details/ssh/$projectId': {
id: '/_authenticate/_inject-org-details/ssh/$projectId'
path: '/ssh/$projectId'
fullPath: '/ssh/$projectId'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsSshProjectIdImport
parentRoute: typeof middlewaresInjectOrgDetailsImport
}
'/_authenticate/_inject-org-details/organization/_layout/access-management': {
id: '/_authenticate/_inject-org-details/organization/_layout/access-management'
path: '/access-management'
@ -941,6 +1025,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerLayoutImport
parentRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout'
path: ''
fullPath: '/ssh/$projectId'
preLoaderRoute: typeof sshLayoutImport
parentRoute: typeof AuthenticateInjectOrgDetailsSshProjectIdImport
}
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview': {
id: '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview'
path: '/overview'
@ -1018,6 +1109,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSecretManagerOverviewPageRouteImport
parentRoute: typeof organizationLayoutImport
}
'/_authenticate/_inject-org-details/organization/_layout/ssh/overview': {
id: '/_authenticate/_inject-org-details/organization/_layout/ssh/overview'
path: '/ssh/overview'
fullPath: '/organization/ssh/overview'
preLoaderRoute: typeof organizationSshOverviewPageRouteImport
parentRoute: typeof organizationLayoutImport
}
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/allowlist': {
id: '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/allowlist'
path: '/allowlist'
@ -1053,6 +1151,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerSettingsPageRouteImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview'
path: '/overview'
fullPath: '/ssh/$projectId/overview'
preLoaderRoute: typeof sshOverviewPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings'
path: '/settings'
fullPath: '/ssh/$projectId/settings'
preLoaderRoute: typeof sshSettingsPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management': {
id: '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management'
path: '/access-management'
@ -1074,6 +1186,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectAccessControlPageRouteSecretManagerImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management'
path: '/access-management'
fullPath: '/ssh/$projectId/access-management'
preLoaderRoute: typeof projectAccessControlPageRouteSshImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId': {
id: '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId'
path: '/ca/$caId'
@ -1088,6 +1207,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerSecretDashboardPageRouteImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId'
path: '/ca/$caId'
fullPath: '/ssh/$projectId/ca/$caId'
preLoaderRoute: typeof sshSshCaByIDPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': {
id: '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId'
path: '/identities/$identityId'
@ -1158,6 +1284,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId'
path: '/identities/$identityId'
fullPath: '/ssh/$projectId/identities/$identityId'
preLoaderRoute: typeof projectIdentityDetailsByIDPageRouteSshImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId'
path: '/members/$membershipId'
fullPath: '/ssh/$projectId/members/$membershipId'
preLoaderRoute: typeof projectMemberDetailsByIDPageRouteSshImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug': {
id: '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug'
path: '/roles/$roleSlug'
fullPath: '/ssh/$projectId/roles/$roleSlug'
preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSshImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback': {
id: '/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback'
path: '/app-connections/github/oauth/callback'
@ -1186,6 +1333,7 @@ interface organizationLayoutRouteChildren {
organizationUserDetailsByIDPageRouteRoute: typeof organizationUserDetailsByIDPageRouteRoute
organizationRoleByIDPageRouteRoute: typeof organizationRoleByIDPageRouteRoute
organizationSecretManagerOverviewPageRouteRoute: typeof organizationSecretManagerOverviewPageRouteRoute
organizationSshOverviewPageRouteRoute: typeof organizationSshOverviewPageRouteRoute
organizationAppConnectionsGithubOauthCallbackPageRouteRoute: typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
}
@ -1213,6 +1361,7 @@ const organizationLayoutRouteChildren: organizationLayoutRouteChildren = {
organizationRoleByIDPageRouteRoute: organizationRoleByIDPageRouteRoute,
organizationSecretManagerOverviewPageRouteRoute:
organizationSecretManagerOverviewPageRouteRoute,
organizationSshOverviewPageRouteRoute: organizationSshOverviewPageRouteRoute,
organizationAppConnectionsGithubOauthCallbackPageRouteRoute:
organizationAppConnectionsGithubOauthCallbackPageRouteRoute,
}
@ -1369,11 +1518,53 @@ const AuthenticateInjectOrgDetailsSecretManagerProjectIdRouteWithChildren =
AuthenticateInjectOrgDetailsSecretManagerProjectIdRouteChildren,
)
interface sshLayoutRouteChildren {
sshOverviewPageRouteRoute: typeof sshOverviewPageRouteRoute
sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute
projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute
sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute
projectIdentityDetailsByIDPageRouteSshRoute: typeof projectIdentityDetailsByIDPageRouteSshRoute
projectMemberDetailsByIDPageRouteSshRoute: typeof projectMemberDetailsByIDPageRouteSshRoute
projectRoleDetailsBySlugPageRouteSshRoute: typeof projectRoleDetailsBySlugPageRouteSshRoute
}
const sshLayoutRouteChildren: sshLayoutRouteChildren = {
sshOverviewPageRouteRoute: sshOverviewPageRouteRoute,
sshSettingsPageRouteRoute: sshSettingsPageRouteRoute,
projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute,
sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute,
projectIdentityDetailsByIDPageRouteSshRoute:
projectIdentityDetailsByIDPageRouteSshRoute,
projectMemberDetailsByIDPageRouteSshRoute:
projectMemberDetailsByIDPageRouteSshRoute,
projectRoleDetailsBySlugPageRouteSshRoute:
projectRoleDetailsBySlugPageRouteSshRoute,
}
const sshLayoutRouteWithChildren = sshLayoutRoute._addFileChildren(
sshLayoutRouteChildren,
)
interface AuthenticateInjectOrgDetailsSshProjectIdRouteChildren {
sshLayoutRoute: typeof sshLayoutRouteWithChildren
}
const AuthenticateInjectOrgDetailsSshProjectIdRouteChildren: AuthenticateInjectOrgDetailsSshProjectIdRouteChildren =
{
sshLayoutRoute: sshLayoutRouteWithChildren,
}
const AuthenticateInjectOrgDetailsSshProjectIdRouteWithChildren =
AuthenticateInjectOrgDetailsSshProjectIdRoute._addFileChildren(
AuthenticateInjectOrgDetailsSshProjectIdRouteChildren,
)
interface middlewaresInjectOrgDetailsRouteChildren {
AuthenticateInjectOrgDetailsOrganizationRoute: typeof AuthenticateInjectOrgDetailsOrganizationRouteWithChildren
AuthenticateInjectOrgDetailsCertManagerProjectIdRoute: typeof AuthenticateInjectOrgDetailsCertManagerProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsKmsProjectIdRoute: typeof AuthenticateInjectOrgDetailsKmsProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsSecretManagerProjectIdRoute: typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdRouteWithChildren
AuthenticateInjectOrgDetailsSshProjectIdRoute: typeof AuthenticateInjectOrgDetailsSshProjectIdRouteWithChildren
}
const middlewaresInjectOrgDetailsRouteChildren: middlewaresInjectOrgDetailsRouteChildren =
@ -1386,6 +1577,8 @@ const middlewaresInjectOrgDetailsRouteChildren: middlewaresInjectOrgDetailsRoute
AuthenticateInjectOrgDetailsKmsProjectIdRouteWithChildren,
AuthenticateInjectOrgDetailsSecretManagerProjectIdRoute:
AuthenticateInjectOrgDetailsSecretManagerProjectIdRouteWithChildren,
AuthenticateInjectOrgDetailsSshProjectIdRoute:
AuthenticateInjectOrgDetailsSshProjectIdRouteWithChildren,
}
const middlewaresInjectOrgDetailsRouteWithChildren =
@ -1562,6 +1755,7 @@ export interface FileRoutesByFullPath {
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/admin': typeof organizationAdminPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
@ -1581,16 +1775,21 @@ export interface FileRoutesByFullPath {
'/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute
'/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
'/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute
'/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
'/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute
'/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute
@ -1601,6 +1800,9 @@ export interface FileRoutesByFullPath {
'/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
'/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
'/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
'/ssh/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute
'/ssh/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute
'/ssh/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute
'/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
}
@ -1630,6 +1832,7 @@ export interface FileRoutesByTo {
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
'/organization/admin': typeof organizationAdminPageRouteRoute
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
@ -1649,16 +1852,21 @@ export interface FileRoutesByTo {
'/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute
'/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/secret-manager/$projectId/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
'/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute
'/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
'/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute
'/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute
@ -1669,6 +1877,9 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
'/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
'/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
'/ssh/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute
'/ssh/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute
'/ssh/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute
'/organization/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
}
@ -1708,6 +1919,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/kms/$projectId': typeof AuthenticateInjectOrgDetailsKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/organization/_layout': typeof organizationLayoutRouteWithChildren
'/_authenticate/_inject-org-details/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/ssh/$projectId': typeof AuthenticateInjectOrgDetailsSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/organization/_layout/access-management': typeof organizationAccessManagementPageRouteRoute
'/_authenticate/_inject-org-details/organization/_layout/admin': typeof organizationAdminPageRouteRoute
'/_authenticate/_inject-org-details/organization/_layout/audit-logs': typeof organizationAuditLogsPageRouteRoute
@ -1719,6 +1931,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout': typeof certManagerLayoutRouteWithChildren
'/_authenticate/_inject-org-details/kms/$projectId/_kms-layout': typeof kmsLayoutRouteWithChildren
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout': typeof secretManagerLayoutRouteWithChildren
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute
'/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
@ -1730,16 +1943,21 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/organization/_layout/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/organization/_layout/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/organization/_layout/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/allowlist': typeof secretManagerIPAllowlistPageRouteRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval': typeof secretManagerSecretApprovalsPageRouteRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/settings': typeof secretManagerSettingsPageRouteRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview': typeof sshOverviewPageRouteRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/access-management': typeof projectAccessControlPageRouteKmsRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId': typeof sshSshCaByIDPageRouteRoute
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute
@ -1750,6 +1968,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute
'/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute
'/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback': typeof organizationAppConnectionsGithubOauthCallbackPageRouteRoute
}
@ -1785,6 +2006,7 @@ export interface FileRouteTypes {
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/access-management'
| '/organization/admin'
| '/organization/audit-logs'
@ -1804,16 +2026,21 @@ export interface FileRouteTypes {
| '/organization/members/$membershipId'
| '/organization/roles/$roleId'
| '/organization/secret-manager/overview'
| '/organization/ssh/overview'
| '/secret-manager/$projectId/allowlist'
| '/secret-manager/$projectId/approval'
| '/secret-manager/$projectId/overview'
| '/secret-manager/$projectId/secret-rotation'
| '/secret-manager/$projectId/settings'
| '/ssh/$projectId/overview'
| '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management'
| '/kms/$projectId/access-management'
| '/secret-manager/$projectId/access-management'
| '/ssh/$projectId/access-management'
| '/cert-manager/$projectId/ca/$caId'
| '/secret-manager/$projectId/secrets/$envSlug'
| '/ssh/$projectId/ca/$caId'
| '/cert-manager/$projectId/identities/$identityId'
| '/cert-manager/$projectId/members/$membershipId'
| '/cert-manager/$projectId/pki-collections/$collectionId'
@ -1824,6 +2051,9 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/identities/$identityId'
| '/secret-manager/$projectId/members/$membershipId'
| '/secret-manager/$projectId/roles/$roleSlug'
| '/ssh/$projectId/identities/$identityId'
| '/ssh/$projectId/members/$membershipId'
| '/ssh/$projectId/roles/$roleSlug'
| '/organization/app-connections/github/oauth/callback'
fileRoutesByTo: FileRoutesByTo
to:
@ -1852,6 +2082,7 @@ export interface FileRouteTypes {
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/access-management'
| '/organization/admin'
| '/organization/audit-logs'
@ -1871,16 +2102,21 @@ export interface FileRouteTypes {
| '/organization/members/$membershipId'
| '/organization/roles/$roleId'
| '/organization/secret-manager/overview'
| '/organization/ssh/overview'
| '/secret-manager/$projectId/allowlist'
| '/secret-manager/$projectId/approval'
| '/secret-manager/$projectId/overview'
| '/secret-manager/$projectId/secret-rotation'
| '/secret-manager/$projectId/settings'
| '/ssh/$projectId/overview'
| '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management'
| '/kms/$projectId/access-management'
| '/secret-manager/$projectId/access-management'
| '/ssh/$projectId/access-management'
| '/cert-manager/$projectId/ca/$caId'
| '/secret-manager/$projectId/secrets/$envSlug'
| '/ssh/$projectId/ca/$caId'
| '/cert-manager/$projectId/identities/$identityId'
| '/cert-manager/$projectId/members/$membershipId'
| '/cert-manager/$projectId/pki-collections/$collectionId'
@ -1891,6 +2127,9 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/identities/$identityId'
| '/secret-manager/$projectId/members/$membershipId'
| '/secret-manager/$projectId/roles/$roleSlug'
| '/ssh/$projectId/identities/$identityId'
| '/ssh/$projectId/members/$membershipId'
| '/ssh/$projectId/roles/$roleSlug'
| '/organization/app-connections/github/oauth/callback'
id:
| '__root__'
@ -1928,6 +2167,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/kms/$projectId'
| '/_authenticate/_inject-org-details/organization/_layout'
| '/_authenticate/_inject-org-details/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/ssh/$projectId'
| '/_authenticate/_inject-org-details/organization/_layout/access-management'
| '/_authenticate/_inject-org-details/organization/_layout/admin'
| '/_authenticate/_inject-org-details/organization/_layout/audit-logs'
@ -1939,6 +2179,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout'
| '/_authenticate/_inject-org-details/kms/$projectId/_kms-layout'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/settings'
| '/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/overview'
@ -1950,16 +2191,21 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/organization/_layout/members/$membershipId'
| '/_authenticate/_inject-org-details/organization/_layout/roles/$roleId'
| '/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview'
| '/_authenticate/_inject-org-details/organization/_layout/ssh/overview'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/allowlist'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/approval'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/overview'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secret-rotation'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/settings'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management'
| '/_authenticate/_inject-org-details/kms/$projectId/_kms-layout/access-management'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/access-management'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/members/$membershipId'
| '/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId'
@ -1970,6 +2216,9 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/identities/$identityId'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/members/$membershipId'
| '/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId'
| '/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug'
| '/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback'
fileRoutesById: FileRoutesById
}
@ -2068,7 +2317,8 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/organization",
"/_authenticate/_inject-org-details/cert-manager/$projectId",
"/_authenticate/_inject-org-details/kms/$projectId",
"/_authenticate/_inject-org-details/secret-manager/$projectId"
"/_authenticate/_inject-org-details/secret-manager/$projectId",
"/_authenticate/_inject-org-details/ssh/$projectId"
]
},
"/_authenticate/admin": {
@ -2206,6 +2456,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/organization/_layout/members/$membershipId",
"/_authenticate/_inject-org-details/organization/_layout/roles/$roleId",
"/_authenticate/_inject-org-details/organization/_layout/secret-manager/overview",
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview",
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback"
]
},
@ -2216,6 +2467,13 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
]
},
"/_authenticate/_inject-org-details/ssh/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details",
"children": [
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
]
},
"/_authenticate/_inject-org-details/organization/_layout/access-management": {
"filePath": "organization/AccessManagementPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/organization/_layout"
@ -2290,6 +2548,19 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug"
]
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout": {
"filePath": "ssh/layout.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId",
"children": [
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId",
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug"
]
},
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/overview": {
"filePath": "cert-manager/CertificatesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout"
@ -2334,6 +2605,10 @@ export const routeTree = rootRoute
"filePath": "organization/SecretManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/organization/_layout"
},
"/_authenticate/_inject-org-details/organization/_layout/ssh/overview": {
"filePath": "organization/SshOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/organization/_layout"
},
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/allowlist": {
"filePath": "secret-manager/IPAllowlistPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
@ -2354,6 +2629,14 @@ export const routeTree = rootRoute
"filePath": "secret-manager/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview": {
"filePath": "ssh/OverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/settings": {
"filePath": "ssh/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/access-management": {
"filePath": "project/AccessControlPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout"
@ -2366,6 +2649,10 @@ export const routeTree = rootRoute
"filePath": "project/AccessControlPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management": {
"filePath": "project/AccessControlPage/route-ssh.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/ca/$caId": {
"filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout"
@ -2374,6 +2661,10 @@ export const routeTree = rootRoute
"filePath": "secret-manager/SecretDashboardPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId": {
"filePath": "ssh/SshCaByIDPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/identities/$identityId": {
"filePath": "project/IdentityDetailsByIDPage/route-cert-manager.tsx",
"parent": "/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout"
@ -2414,6 +2705,18 @@ export const routeTree = rootRoute
"filePath": "project/RoleDetailsBySlugPage/route-secret-manager.tsx",
"parent": "/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId": {
"filePath": "project/IdentityDetailsByIDPage/route-ssh.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId": {
"filePath": "project/MemberDetailsByIDPage/route-ssh.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug": {
"filePath": "project/RoleDetailsBySlugPage/route-ssh.tsx",
"parent": "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback": {
"filePath": "organization/AppConnections/GithubOauthCallbackPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/organization/_layout"

View File

@ -11,6 +11,7 @@ const organizationRoutes = route("/organization", [
layout("organization/layout.tsx", [
route("/secret-manager/overview", "organization/SecretManagerOverviewPage/route.tsx"),
route("/cert-manager/overview", "organization/CertManagerOverviewPage/route.tsx"),
route("/ssh/overview", "organization/SshOverviewPage/route.tsx"),
route("/kms/overview", "organization/KmsOverviewPage/route.tsx"),
route("/access-management", "organization/AccessManagementPage/route.tsx"),
route("/admin", "organization/AdminPage/route.tsx"),
@ -70,6 +71,18 @@ const kmsRoutes = route("/kms/$projectId", [
])
]);
const sshRoutes = route("/ssh/$projectId", [
layout("ssh-layout", "ssh/layout.tsx", [
route("/overview", "ssh/OverviewPage/route.tsx"),
route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"),
route("/settings", "ssh/SettingsPage/route.tsx"),
route("/access-management", "project/AccessControlPage/route-ssh.tsx"),
route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-ssh.tsx"),
route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-ssh.tsx"),
route("/members/$membershipId", "project/MemberDetailsByIDPage/route-ssh.tsx")
])
]);
export const routes = rootRoute("root.tsx", [
index("index.tsx"),
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
@ -104,7 +117,8 @@ export const routes = rootRoute("root.tsx", [
organizationRoutes,
secretManagerRoutes,
certManagerRoutes,
kmsRoutes
kmsRoutes,
sshRoutes
])
])
]);