mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
merge main
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { createNotification } from "../notifications";
|
||||
import { IconButton, Select, SelectItem, Tooltip } from "../v2";
|
||||
@@ -69,7 +70,11 @@ export default function NavHeader({
|
||||
<div className="mr-2 flex h-5 w-5 min-w-[1.25rem] items-center justify-center rounded-md bg-primary text-sm text-black">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<Link passHref legacyBehavior href={`/org/${currentOrg?.id}/overview`}>
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={`/org/${currentOrg?.id}/${ProjectType.SecretManager}/overview`}
|
||||
>
|
||||
<a className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{currentOrg?.name}
|
||||
</a>
|
||||
@@ -93,7 +98,10 @@ export default function NavHeader({
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: "/project/[id]/secrets/overview", query: { id: router.query.id } }}
|
||||
href={{
|
||||
pathname: `/${ProjectType.SecretManager}/[id]/secrets/overview`,
|
||||
query: { id: router.query.id }
|
||||
}}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||
</Link>
|
||||
@@ -130,7 +138,7 @@ export default function NavHeader({
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{
|
||||
pathname: "/project/[id]/secrets/[env]",
|
||||
pathname: `/${ProjectType.SecretManager}/[id]/secrets/[env]`,
|
||||
query: { id: router.query.id, env: router.query.env }
|
||||
}}
|
||||
>
|
||||
@@ -199,7 +207,10 @@ export default function NavHeader({
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
||||
href={{
|
||||
pathname: `/${ProjectType.SecretManager}/[id]/secrets/[env]`,
|
||||
query
|
||||
}}
|
||||
>
|
||||
<a
|
||||
className={twMerge(
|
||||
|
||||
@@ -72,7 +72,7 @@ ModalContent.displayName = "ModalContent";
|
||||
|
||||
export type ModalProps = Omit<DialogPrimitive.DialogProps, "open"> & { isOpen?: boolean };
|
||||
export const Modal = ({ isOpen, ...props }: ModalProps) => (
|
||||
<DialogPrimitive.Root open={isOpen} {...props} />
|
||||
<DialogPrimitive.Root open={isOpen} {...props} modal/>
|
||||
);
|
||||
|
||||
export const ModalTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -69,14 +69,19 @@ export const getProjectHomePage = (workspace: Workspace) => {
|
||||
return `/${workspace.type}/${workspace.id}/certificates`;
|
||||
}
|
||||
|
||||
return `/${workspace.type}/${workspace.id}/kms`;
|
||||
if (workspace.type === ProjectType.KMS) {
|
||||
return `/${workspace.type}/${workspace.id}/kms`;
|
||||
}
|
||||
|
||||
return `/${workspace.type}/${workspace.id}/ssh`;
|
||||
};
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
@@ -77,11 +77,16 @@ export const selectOrganization = async (data: {
|
||||
export const useSelectOrganization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
|
||||
mutationFn: async (details: {
|
||||
organizationId: string;
|
||||
userAgent?: UserAgentType;
|
||||
forceSetCredentials?: boolean;
|
||||
}) => {
|
||||
const data = await selectOrganization(details);
|
||||
|
||||
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
|
||||
if (!details.userAgent && !data.isMfaEnabled) {
|
||||
if ((!details.userAgent && !data.isMfaEnabled) || details.forceSetCredentials) {
|
||||
localStorage.setItem("orgData.id", details.organizationId);
|
||||
SecurityClient.setToken(data.token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { SshCaStatus } from "@app/hooks/api/ssh-ca";
|
||||
import { SshCertTemplateStatus } from "@app/hooks/api/sshCertificateTemplates";
|
||||
|
||||
import { CaStatus, CaType } from "./enums";
|
||||
|
||||
export const caTypeToNameMap: { [K in CaType]: string } = {
|
||||
@@ -11,7 +14,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";
|
||||
|
||||
@@ -38,6 +38,8 @@ export * from "./secretSharing";
|
||||
export * from "./secretSnapshots";
|
||||
export * from "./serverDetails";
|
||||
export * from "./serviceTokens";
|
||||
export * from "./ssh-ca";
|
||||
export * from "./sshCertificateTemplates";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./subscriptions";
|
||||
export * from "./tags";
|
||||
|
||||
@@ -80,6 +80,7 @@ export const useCreateIntegration = () => {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
azureLabel?: string;
|
||||
githubVisibility?: string;
|
||||
githubVisibilityRepoIds?: string[];
|
||||
kmsKeyId?: string;
|
||||
|
||||
@@ -41,6 +41,7 @@ export type TIntegration = {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
azureLabel?: string;
|
||||
|
||||
kmsKeyId?: string;
|
||||
secretSuffix?: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
@@ -67,7 +68,7 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr
|
||||
mutationFn: async ({ name }: { name: string }) => {
|
||||
const {
|
||||
data: { organization }
|
||||
} = await apiRequest.post("/api/v2/organizations", {
|
||||
} = await apiRequest.post<{ organization: { id: string } }>("/api/v2/organizations", {
|
||||
name
|
||||
});
|
||||
|
||||
@@ -437,10 +438,13 @@ export const useDeleteOrgById = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ organizationId }: { organizationId: string }) => {
|
||||
const {
|
||||
data: { organization }
|
||||
} = await apiRequest.delete<{ organization: Organization }>(
|
||||
data: { organization, accessToken }
|
||||
} = await apiRequest.delete<{ organization: Organization; accessToken: string }>(
|
||||
`/api/v2/organizations/${organizationId}`
|
||||
);
|
||||
SecurityClient.setToken(accessToken);
|
||||
localStorage.removeItem("orgData.id");
|
||||
|
||||
return organization;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export { SshCaStatus } from "./constants";
|
||||
export {
|
||||
useCreateSshCa,
|
||||
useDeleteSshCa,
|
||||
useIssueSshCreds,
|
||||
useSignSshKey,
|
||||
useUpdateSshCa
|
||||
} from "./mutations";
|
||||
export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries";
|
||||
@@ -0,0 +1,91 @@
|
||||
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, {}, TCreateSshCaDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.post<{ ca: TSshCertificateAuthority }>("/api/v1/ssh/ca/", body);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceSshCas(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSshCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshCertificateAuthority, {}, 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(workspaceKeys.getWorkspaceSshCas(projectId));
|
||||
queryClient.invalidateQueries(sshCaKeys.getSshCaById(caId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSshCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshCertificateAuthority, {}, TDeleteSshCaDTO>({
|
||||
mutationFn: async ({ caId }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.delete<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: ({ projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceSshCas(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignSshKey = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSignSshKeyResponse, {}, TSignSshKeyDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TSignSshKeyResponse>("/api/v1/ssh/sign", body);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.allWorkspaceSshCertificates(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useIssueSshCreds = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TIssueSshCredsResponse, {}, TIssueSshCredsDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TIssueSshCredsResponse>("/api/v1/ssh/issue", body);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.allWorkspaceSshCertificates(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
useCreateSshCertTemplate,
|
||||
useDeleteSshCertTemplate,
|
||||
useUpdateSshCertTemplate
|
||||
} from "./mutations";
|
||||
export { useGetSshCertTemplate } from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { sshCaKeys } from "../ssh-ca/queries";
|
||||
import {
|
||||
TCreateSshCertificateTemplateDTO,
|
||||
TDeleteSshCertificateTemplateDTO,
|
||||
TSshCertificateTemplate,
|
||||
TUpdateSshCertificateTemplateDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateSshCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshCertificateTemplate, {}, TCreateSshCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: certificateTemplate } = await apiRequest.post<TSshCertificateTemplate>(
|
||||
"/api/v1/ssh/certificate-templates",
|
||||
data
|
||||
);
|
||||
return certificateTemplate;
|
||||
},
|
||||
onSuccess: ({ sshCaId }) => {
|
||||
queryClient.invalidateQueries(sshCaKeys.getSshCaCertTemplates(sshCaId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSshCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshCertificateTemplate, {}, 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(sshCaKeys.getSshCaCertTemplates(sshCaId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSshCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TSshCertificateTemplate, {}, TDeleteSshCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: certificateTemplate } = await apiRequest.delete<TSshCertificateTemplate>(
|
||||
`/api/v1/ssh/certificate-templates/${data.id}`
|
||||
);
|
||||
return certificateTemplate;
|
||||
},
|
||||
onSuccess: ({ sshCaId }) => {
|
||||
queryClient.invalidateQueries(sshCaKeys.getSshCaCertTemplates(sshCaId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -32,6 +32,9 @@ export {
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspacePkiAlerts,
|
||||
useListWorkspacePkiCollections,
|
||||
useListWorkspaceSshCas,
|
||||
useListWorkspaceSshCertificates,
|
||||
useListWorkspaceSshCertificateTemplates,
|
||||
useNameWorkspaceSecrets,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateIdentityWorkspaceRole,
|
||||
|
||||
@@ -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 "../ssh-ca/types";
|
||||
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
||||
import { userKeys } from "../users/query-keys";
|
||||
import { TWorkspaceUser } from "../users/types";
|
||||
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
||||
@@ -731,6 +733,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),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -383,6 +383,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/ssh/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/org/${currentWorkspace?.id}/ssh/overview`
|
||||
}
|
||||
icon="system-regular-126-verified-hover-verified"
|
||||
>
|
||||
SSH
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -33,6 +33,7 @@ export const ProjectSidebarItem = () => {
|
||||
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 (
|
||||
<Menu>
|
||||
@@ -83,6 +84,18 @@ export const ProjectSidebarItem = () => {
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isSsh && (
|
||||
<Link href={`/${ProjectType.SSH}/${currentWorkspace?.id}/ssh`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/${ProjectType.SSH}/${currentWorkspace?.id}/ssh`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/${currentWorkspace.type}/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -10,6 +10,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import queryString from "query-string";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||
@@ -19,9 +20,11 @@ import {
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "../../../components/v2";
|
||||
import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
@@ -39,7 +42,9 @@ const schema = z.object({
|
||||
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
|
||||
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
|
||||
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior),
|
||||
secretPrefix: z.string().default("")
|
||||
secretPrefix: z.string().default(""),
|
||||
useLabels: z.boolean().default(false),
|
||||
azureLabel: z.string().min(1).optional()
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof schema>;
|
||||
@@ -60,6 +65,7 @@ export default function AzureAppConfigurationCreateIntegration() {
|
||||
const router = useRouter();
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
@@ -85,16 +91,28 @@ export default function AzureAppConfigurationCreateIntegration() {
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
const shouldUseLabels = watch("useLabels");
|
||||
|
||||
const handleIntegrationSubmit = async ({
|
||||
secretPath,
|
||||
useLabels,
|
||||
sourceEnvironment,
|
||||
baseUrl,
|
||||
initialSyncBehavior,
|
||||
secretPrefix
|
||||
secretPrefix,
|
||||
azureLabel
|
||||
}: TFormSchema) => {
|
||||
try {
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
if (useLabels && !azureLabel) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Label must be provided when 'Use Labels' is enabled"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
@@ -103,7 +121,8 @@ export default function AzureAppConfigurationCreateIntegration() {
|
||||
secretPath,
|
||||
metadata: {
|
||||
initialSyncBehavior,
|
||||
secretPrefix
|
||||
secretPrefix,
|
||||
...(useLabels && { azureLabel })
|
||||
}
|
||||
});
|
||||
|
||||
@@ -155,35 +174,70 @@ export default function AzureAppConfigurationCreateIntegration() {
|
||||
</div>
|
||||
</CardTitle>
|
||||
<div className="px-6">
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceEnvironment"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Environment"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-full"
|
||||
value={field.value}
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
<div className="">
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceEnvironment"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Environment"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
<Select
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-full"
|
||||
value={field.value}
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mb-2 flex w-full flex-col gap-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="useLabels"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="use-environment-labels"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
<FormLabel label="Use Labels" />
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
|
||||
{shouldUseLabels && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="azureLabel"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className=""
|
||||
// label="Label"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Input {...field} placeholder="pre-prod" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
|
||||
@@ -62,7 +62,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) => {
|
||||
@@ -70,7 +71,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 = {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { ProductOverview } from "../secret-manager/overview";
|
||||
|
||||
const SshManagerOverviewPage = () => <ProductOverview type={ProjectType.SSH} />;
|
||||
|
||||
Object.assign(SshManagerOverviewPage, { requireAuth: true });
|
||||
|
||||
export default SshManagerOverviewPage;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage";
|
||||
|
||||
const ProjectAllowlist = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<IPAllowlistPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectAllowlist;
|
||||
|
||||
ProjectAllowlist.requireAuth = true;
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import Head from "next/head";
|
||||
|
||||
import { SshCaPage } from "@app/views/Project/SshCaPage";
|
||||
|
||||
export default function SshCa() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>SSH Certificate Authority</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<SshCaPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SshCa.requireAuth = true;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage";
|
||||
|
||||
export default function ProjectIdentityDetailsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<IdentityDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectIdentityDetailsPage.requireAuth = true;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage";
|
||||
|
||||
export default function Page() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MemberDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Page.requireAuth = true;
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MembersPage } from "@app/views/Project/MembersPage";
|
||||
|
||||
export default function WorkspaceMemberSettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MembersPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
WorkspaceMemberSettings.requireAuth = true;
|
||||
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { RolePage } from "@app/views/Project/RolePage";
|
||||
|
||||
export default function Role() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: "Project Settings" })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<RolePage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Role.requireAuth = true;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage";
|
||||
|
||||
const ProjectSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<ProjectSettingsPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSettings;
|
||||
|
||||
ProjectSettings.requireAuth = true;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { SshPage } from "@app/views/Project/SshPage";
|
||||
|
||||
const Ssh = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<SshPage />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ssh;
|
||||
|
||||
Ssh.requireAuth = true;
|
||||
+2
-1
@@ -14,6 +14,7 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
|
||||
githubVisibilityRepoIds: "Github Visibility Repo Ids",
|
||||
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
|
||||
secretAWSTag: "Tags For Secrets Stored In AWS",
|
||||
azureLabel: "Azure Label",
|
||||
kmsKeyId: "AWS KMS Key ID",
|
||||
secretSuffix: "Secret Suffix",
|
||||
secretPrefix: "Secret Prefix",
|
||||
@@ -86,7 +87,7 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||
Object.entries(integration.metadata).map(([key, value]) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<p className="text-sm text-gray-400">
|
||||
{metadataMappings[key as keyof typeof metadataMappings]}
|
||||
{!!value && metadataMappings[key as keyof typeof metadataMappings]}
|
||||
</p>
|
||||
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { queryClient } from "@app/reactQuery";
|
||||
|
||||
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
const userOrgs = await fetchOrganizations().catch(() => []);
|
||||
|
||||
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
|
||||
|
||||
|
||||
@@ -71,12 +71,6 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
const onFormSubmit = async ({ name, description, slug }: FormData) => {
|
||||
try {
|
||||
console.log("onFormSubmit args: ", {
|
||||
name,
|
||||
description,
|
||||
slug
|
||||
});
|
||||
|
||||
if (!orgId) return;
|
||||
|
||||
if (role) {
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSIONS.length;
|
||||
const totalActions = getPermissionList(formName).length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
|
||||
@@ -6,7 +6,7 @@ import z from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
||||
import { useCreateOrg, useGetOrganizations, useSelectOrganization } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const schema = z
|
||||
@@ -23,9 +23,10 @@ interface CreateOrgModalProps {
|
||||
}
|
||||
|
||||
export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { refetch: refetchOrganizations } = useGetOrganizations();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -50,19 +51,21 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
|
||||
});
|
||||
|
||||
await selectOrg({
|
||||
organizationId: organization.id
|
||||
organizationId: organization.id,
|
||||
forceSetCredentials: true
|
||||
});
|
||||
|
||||
await refetchOrganizations();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created organization",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (router.isReady) router.push(`/org/${organization.id}/${ProjectType.SecretManager}/overview`);
|
||||
if (router.isReady)
|
||||
router.push(`/org/${organization.id}/${ProjectType.SecretManager}/overview`);
|
||||
else window.location.href = `/org/${organization.id}/${ProjectType.SecretManager}/overview`;
|
||||
|
||||
localStorage.setItem("orgData.id", organization.id);
|
||||
|
||||
reset();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
// DatePicker
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaType, useCreateCa, useGetCaById,useUpdateCa } from "@app/hooks/api/ca";
|
||||
import { CaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -72,7 +72,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
|
||||
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
||||
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateCa();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
||||
|
||||
@@ -151,7 +151,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
|
||||
if (ca) {
|
||||
// update
|
||||
await updateMutateAsync({
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faEllipsis,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* TODO (dangtony98): Reevaluate if this component should be in main
|
||||
* CertificateTab or under CA page in the future.
|
||||
*/
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -18,7 +14,7 @@ import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
|
||||
|
||||
type Props = {
|
||||
caId: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const CertificateTemplatesSection = ({ caId }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
|
||||
+35
@@ -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([]),
|
||||
@@ -210,6 +215,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
@@ -596,6 +604,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: [
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useRouter } from "next/router";
|
||||
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SshCaModal } from "../SshPage/components/SshCaModal";
|
||||
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
|
||||
|
||||
export const SshCaPage = withProjectPermission(
|
||||
() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
const router = useRouter();
|
||||
const caId = router.query.caId as string;
|
||||
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 });
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully deleted SSH CA",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteSshCa");
|
||||
router.push(`/project/${projectId}/ssh`);
|
||||
} 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 py-6 px-6">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
onClick={() => router.push(`/${ProjectType.SSH}/${projectId}/ssh`)}
|
||||
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>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.SshCertificateAuthorities }
|
||||
);
|
||||
@@ -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?: {}) => 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 />
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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/ssh-ca/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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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, isLoading } = useGetSshCaCertTemplates(sshCaId);
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={2} innerKey="project-cas" />}
|
||||
{!isLoading &&
|
||||
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>
|
||||
{!isLoading && !data?.certificateTemplates?.length && (
|
||||
<EmptyState
|
||||
title="No certificate templates have been created for this SSH CA"
|
||||
icon={faFileAlt}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { SshCaDetailsSection } from "./SshCaDetailsSection";
|
||||
export { SshCertificateModal } from "./SshCertificateModal";
|
||||
export { SshCertificateTemplatesSection } from "./SshCertificateTemplatesSection";
|
||||
@@ -0,0 +1 @@
|
||||
export { SshCaPage } from "./SshCaPage";
|
||||
@@ -0,0 +1,53 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { SshCaSection, SshCertificatesSection } from "./components";
|
||||
|
||||
enum TabSections {
|
||||
SshCa = "ssh-certificate-authorities",
|
||||
SshCertificates = "ssh-certificates"
|
||||
}
|
||||
|
||||
export const SshPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<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 py-6 px-6">
|
||||
<p className="mr-4 mb-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 }}
|
||||
>
|
||||
<SshCertificatesSection />
|
||||
</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 }}
|
||||
>
|
||||
<SshCaSection />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.SshCertificateAuthorities }
|
||||
);
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
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 router = useRouter();
|
||||
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
|
||||
});
|
||||
|
||||
router.push(`/${ProjectType.SSH}/${projectId}/ca/${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>
|
||||
);
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
await 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 });
|
||||
|
||||
await 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { faBan, faCertificate, faEllipsis, 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, 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?: {}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const SshCaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const router = useRouter();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceSshCas(currentWorkspace?.id || "");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="org-ssh-cas" />}
|
||||
{!isLoading &&
|
||||
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={() =>
|
||||
router.push(`/${ProjectType.SSH}/${currentWorkspace?.id}/ca/${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>
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<EmptyState
|
||||
title="No SSH certificate authorities have been created"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 "../../SshCaPage/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>
|
||||
);
|
||||
};
|
||||
@@ -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, isLoading } = 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>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="org-ssh-certificates" />}
|
||||
{!isLoading &&
|
||||
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>
|
||||
{!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !data?.certificates?.length && (
|
||||
<EmptyState title="No SSH certificates have been issued" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SshCaSection } from "./SshCaSection";
|
||||
export { SshCertificatesSection } from "./SshCertificatesSection";
|
||||
@@ -0,0 +1 @@
|
||||
export { SshPage } from "./SshPage";
|
||||
@@ -1125,6 +1125,7 @@ export const SecretOverviewPage = () => {
|
||||
bodyClassName="overflow-visible"
|
||||
title="Create Secrets"
|
||||
subTitle="Create a secret across multiple environments"
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<CreateSecretForm
|
||||
secretPath={secretPath}
|
||||
|
||||
Reference in New Issue
Block a user