mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 11:38:55 +00:00
Merge branch 'feature/certificate-template' into feature/est-simpleenroll
This commit is contained in:
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -87,7 +88,13 @@ type Props = {
|
||||
};
|
||||
|
||||
const createTagSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
color: z.string().trim()
|
||||
});
|
||||
|
||||
@@ -110,7 +117,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(createTagSchema)
|
||||
});
|
||||
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
|
||||
@@ -123,13 +130,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
if (!isOpen) reset();
|
||||
}, [isOpen]);
|
||||
|
||||
const onFormSubmit = async ({ name, color }: FormData) => {
|
||||
const onFormSubmit = async ({ slug, color }: FormData) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
workspaceID: workspaceId,
|
||||
tagName: name,
|
||||
tagColor: color,
|
||||
tagSlug: name.replace(" ", "_")
|
||||
tagSlug: slug
|
||||
});
|
||||
onToggle(false);
|
||||
reset();
|
||||
@@ -155,11 +161,11 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
name="slug"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Type your tag name" />
|
||||
<FormControl label="Tag Slug" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Type your tag slug" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
subTitle?: string;
|
||||
onDeleteApproved: () => Promise<void>;
|
||||
buttonText?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
@@ -26,7 +27,8 @@ export const DeleteActionModal = ({
|
||||
onDeleteApproved,
|
||||
title,
|
||||
subTitle = "This action is irreversible.",
|
||||
buttonText = "Delete"
|
||||
buttonText = "Delete",
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
@@ -97,6 +99,7 @@ export const DeleteActionModal = ({
|
||||
placeholder={`Type ${deleteKey} here`}
|
||||
/>
|
||||
</FormControl>
|
||||
{children}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum ProjectPermissionSub {
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates",
|
||||
CertificateTemplates = "certificate-templates",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms"
|
||||
@@ -59,6 +60,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
|
||||
@@ -68,7 +68,11 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_PKI_COLLECTION_ITEMS]: "Get PKI collection items",
|
||||
[EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item",
|
||||
[EventType.DELETE_PKI_COLLECTION_ITEM]: "Delete PKI collection item",
|
||||
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
|
||||
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project",
|
||||
[EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template",
|
||||
[EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template",
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -82,5 +82,9 @@ export enum EventType {
|
||||
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
}
|
||||
|
||||
@@ -679,6 +679,46 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplate {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplate {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplate {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteCertificateTemplate {
|
||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -747,7 +787,11 @@ export type Event =
|
||||
| GetPkiCollectionItems
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| OrgAdminAccessProjectEvent;
|
||||
| OrgAdminAccessProjectEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
||||
};
|
||||
|
||||
export const getStatusBadgeVariant = (status: CaStatus) => {
|
||||
export const getCaStatusBadgeVariant = (status: CaStatus) => {
|
||||
switch (status) {
|
||||
case CaStatus.ACTIVE:
|
||||
return "success";
|
||||
|
||||
@@ -101,9 +101,9 @@ export const useImportCaCertificate = () => {
|
||||
export const useCreateCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||
`/api/v1/pki/ca/${caId}/issue-certificate`,
|
||||
"/api/v1/pki/certificates/issue-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
@@ -126,6 +126,7 @@ export const useRenewCa = () => {
|
||||
},
|
||||
onSuccess: (_, { caId, projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
queryClient.invalidateQueries(caKeys.getCaById(caId));
|
||||
queryClient.invalidateQueries(caKeys.getCaCert(caId));
|
||||
queryClient.invalidateQueries(caKeys.getCaCerts(caId));
|
||||
queryClient.invalidateQueries(caKeys.getCaCsr(caId));
|
||||
|
||||
@@ -19,6 +19,7 @@ export type TCertificateAuthority = {
|
||||
notAfter?: string;
|
||||
notBefore?: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
activeCaCertId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -78,7 +79,9 @@ export type TImportCaCertificateResponse = {
|
||||
|
||||
export type TCreateCertificateDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string; // sans
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations";
|
||||
export { useGetCertTemplate } from "./queries";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, {}, TCreateCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: certificateTemplate } = await apiRequest.post<TCertificateTemplate>(
|
||||
"/api/v1/pki/certificate-templates",
|
||||
data
|
||||
);
|
||||
return certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateTemplate, {}, TUpdateCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
const { data: certificateTemplate } = await apiRequest.patch<TCertificateTemplate>(
|
||||
`/api/v1/pki/certificate-templates/${data.id}`,
|
||||
data
|
||||
);
|
||||
|
||||
return certificateTemplate;
|
||||
},
|
||||
onSuccess: (_, { projectId, id }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCertTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, {}, TDeleteCertificateTemplateDTO>({
|
||||
mutationFn: async (data) => {
|
||||
return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`);
|
||||
},
|
||||
onSuccess: (_, { projectId, id }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate } from "./types";
|
||||
|
||||
export const certTemplateKeys = {
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"]
|
||||
};
|
||||
|
||||
export const useGetCertTemplate = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.getCertTemplateById(id),
|
||||
queryFn: async () => {
|
||||
const { data: certificateTemplate } = await apiRequest.get<TCertificateTemplate>(
|
||||
`/api/v1/pki/certificate-templates/${id}`
|
||||
);
|
||||
return certificateTemplate;
|
||||
},
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export type TCertificateTemplate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
caName: string;
|
||||
projectId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateDTO = {
|
||||
caId: string;
|
||||
pkiCollectionId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateDTO = {
|
||||
id: string;
|
||||
caId?: string;
|
||||
pkiCollectionId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateDTO = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
};
|
||||
@@ -1,10 +1,21 @@
|
||||
import { CertKeyAlgorithm, CertStatus,CrlReason } from "./enums";
|
||||
import { CertKeyAlgorithm, CertStatus, CrlReason } from "./enums";
|
||||
|
||||
export const certStatusToNameMap: { [K in CertStatus]: string } = {
|
||||
[CertStatus.ACTIVE]: "Active",
|
||||
[CertStatus.REVOKED]: "Revoked"
|
||||
};
|
||||
|
||||
export const getCertStatusBadgeVariant = (status: CertStatus) => {
|
||||
switch (status) {
|
||||
case CertStatus.ACTIVE:
|
||||
return "success";
|
||||
case CertStatus.REVOKED:
|
||||
return "danger";
|
||||
default:
|
||||
return "primary";
|
||||
}
|
||||
};
|
||||
|
||||
export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
||||
[CertKeyAlgorithm.RSA_2048]: "RSA 2048",
|
||||
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CertStatus } from "./enums";
|
||||
export type TCertificate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
certificateTemplateId?: string;
|
||||
status: CertStatus;
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./auth";
|
||||
export * from "./bots";
|
||||
export * from "./ca";
|
||||
export * from "./certificates";
|
||||
export * from "./certificateTemplates";
|
||||
export * from "./dynamicSecret";
|
||||
export * from "./dynamicSecretLease";
|
||||
export * from "./groups";
|
||||
|
||||
@@ -110,8 +110,15 @@ export const useCreateIntegration = () => {
|
||||
export const useDeleteIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, { id: string; workspaceId: string }>({
|
||||
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
|
||||
return useMutation<
|
||||
{},
|
||||
{},
|
||||
{ id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean }
|
||||
>({
|
||||
mutationFn: ({ id, shouldDeleteIntegrationSecrets }) =>
|
||||
apiRequest.delete(
|
||||
`/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}`
|
||||
),
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type TPkiCollection = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
projectId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -9,12 +10,14 @@ export type TPkiCollection = {
|
||||
export type TCreatePkiCollectionDTO = {
|
||||
projectId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type TUpdatePkiCollectionTO = {
|
||||
collectionId: string;
|
||||
projectId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TDeletePkiCollectionDTO = {
|
||||
|
||||
@@ -5,6 +5,5 @@ export type TRateLimit = {
|
||||
authRateLimit: number;
|
||||
inviteUserRateLimit: number;
|
||||
mfaRateLimit: number;
|
||||
creationLimit: number;
|
||||
publicEndpointLimit: number;
|
||||
};
|
||||
|
||||
@@ -28,11 +28,10 @@ export const useCreateWsTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<WsTag, {}, CreateTagDTO>({
|
||||
mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => {
|
||||
mutationFn: async ({ workspaceID, tagColor, tagSlug }) => {
|
||||
const { data } = await apiRequest.post<{ workspaceTag: WsTag }>(
|
||||
`/api/v1/workspace/${workspaceID}/tags`,
|
||||
{
|
||||
name: tagName,
|
||||
color: tagColor || "",
|
||||
slug: tagSlug
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ export type UserWsTags = WsTag[];
|
||||
|
||||
export type WsTag = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
color?: string;
|
||||
projectId: string;
|
||||
@@ -16,7 +15,6 @@ export type WorkspaceTag = { id: string; name: string; slug: string };
|
||||
export type CreateTagDTO = {
|
||||
workspaceID: string;
|
||||
tagSlug: string;
|
||||
tagName: string;
|
||||
tagColor: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
useListWorkspaceCertificateTemplates,
|
||||
useListWorkspaceGroups,
|
||||
useListWorkspacePkiAlerts,
|
||||
useListWorkspacePkiCollections,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { CaStatus } from "../ca/enums";
|
||||
import { TCertificateAuthority } from "../ca/types";
|
||||
import { TCertificate } from "../certificates/types";
|
||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||
import { TGroupMembership } from "../groups/types";
|
||||
import { identitiesKeys } from "../identities/queries";
|
||||
import { IdentityMembership } from "../identities/types";
|
||||
@@ -68,7 +69,9 @@ export const workspaceKeys = {
|
||||
getWorkspacePkiAlerts: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-pki-collections"] as const
|
||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-certificate-templates"] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -639,3 +642,19 @@ export const useListWorkspacePkiCollections = ({ workspaceId }: { workspaceId: s
|
||||
enabled: Boolean(workspaceId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspaceId: string }) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceCertificateTemplates(workspaceId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { certificateTemplates }
|
||||
} = await apiRequest.get<{ certificateTemplates: TCertificateTemplate[] }>(
|
||||
`/api/v2/workspace/${workspaceId}/certificate-templates`
|
||||
);
|
||||
|
||||
return { certificateTemplates };
|
||||
},
|
||||
enabled: Boolean(workspaceId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -273,8 +273,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
await addUsersToProject.mutateAsync({
|
||||
usernames: orgUsers
|
||||
.map((member) => member.user.username)
|
||||
.filter((username) => username !== user.username),
|
||||
.filter(
|
||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||
)
|
||||
.map((member) => member.user.username),
|
||||
projectId: newProjectId,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
@@ -482,7 +484,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
)}
|
||||
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Organization Admin Console
|
||||
Organization Admin Console
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
|
||||
@@ -277,18 +277,14 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
<div className="mt-2 ml-1">
|
||||
<Switch
|
||||
id="delete-aws"
|
||||
onCheckedChange={() => setShouldDisableDelete(!shouldDisableDelete)}
|
||||
onCheckedChange={setShouldDisableDelete}
|
||||
isChecked={shouldDisableDelete}
|
||||
>
|
||||
Disable deleting secrets in AWS Parameter Store
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="mt-4 ml-1">
|
||||
<Switch
|
||||
id="tag-aws"
|
||||
onCheckedChange={() => setShouldTag(!shouldTag)}
|
||||
isChecked={shouldTag}
|
||||
>
|
||||
<Switch id="tag-aws" onCheckedChange={setShouldTag} isChecked={shouldTag}>
|
||||
Tag in AWS Parameter Store
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -552,8 +552,10 @@ const OrganizationPage = () => {
|
||||
|
||||
await addUsersToProject.mutateAsync({
|
||||
usernames: orgUsers
|
||||
.map((member) => member.user.username)
|
||||
.filter((username) => username !== user.username),
|
||||
.filter(
|
||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||
)
|
||||
.map((member) => member.user.username),
|
||||
projectId: newProjectId,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
|
||||
@@ -106,9 +106,13 @@ export const IntegrationsPage = withProjectPermission(
|
||||
handleProviderIntegration(provider);
|
||||
};
|
||||
|
||||
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
|
||||
const handleIntegrationDelete = async (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => {
|
||||
try {
|
||||
await deleteIntegration({ id: integrationId, workspaceId });
|
||||
await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
|
||||
if (cb) cb();
|
||||
createNotification({
|
||||
type: "success",
|
||||
@@ -152,7 +156,7 @@ export const IntegrationsPage = withProjectPermission(
|
||||
isLoading={isIntegrationLoading}
|
||||
integrations={integrations}
|
||||
environments={environments}
|
||||
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)}
|
||||
onIntegrationDelete={handleIntegrationDelete}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
<CloudIntegrationSection
|
||||
|
||||
+57
-9
@@ -7,6 +7,7 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormLabel,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
@@ -25,7 +26,11 @@ type Props = {
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
integrations?: TIntegration[];
|
||||
isLoading?: boolean;
|
||||
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
|
||||
onIntegrationDelete: (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => Promise<void>;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@@ -37,10 +42,12 @@ export const IntegrationsSection = ({
|
||||
workspaceId
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteConfirmation"
|
||||
"deleteConfirmation",
|
||||
"deleteSecretsConfirmation"
|
||||
] as const);
|
||||
|
||||
const { mutate: syncIntegration } = useSyncIntegration();
|
||||
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
@@ -249,7 +256,10 @@ export const IntegrationsSection = ({
|
||||
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
|
||||
onClick={() => {
|
||||
setShouldDeleteSecrets.off();
|
||||
handlePopUpOpen("deleteConfirmation", integration);
|
||||
}}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
@@ -281,11 +291,49 @@ export const IntegrationsSection = ({
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
|
||||
""
|
||||
}
|
||||
onDeleteApproved={async () =>
|
||||
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
|
||||
handlePopUpClose("deleteConfirmation")
|
||||
)
|
||||
}
|
||||
onDeleteApproved={async () => {
|
||||
if (shouldDeleteSecrets) {
|
||||
handlePopUpOpen("deleteSecretsConfirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
false,
|
||||
() => handlePopUpClose("deleteConfirmation")
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
id="delete-integration-secrets"
|
||||
checkIndicatorBg="text-white"
|
||||
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
|
||||
>
|
||||
Delete previously synced secrets from the destination
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</DeleteActionModal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretsConfirmation.isOpen}
|
||||
title={`Are you sure you also want to delete secrets on ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration
|
||||
}?`}
|
||||
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
true,
|
||||
() => {
|
||||
handlePopUpClose("deleteSecretsConfirmation");
|
||||
handlePopUpClose("deleteConfirmation");
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -406,6 +406,28 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
||||
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Certificate Authority ID: ${event.metadata.caId}`}</p>
|
||||
<p>{`Name: ${event.metadata.name}`}</p>
|
||||
<p>{`Common Name: ${event.metadata.commonName}`}</p>
|
||||
<p>{`Subject Alternative Name: ${event.metadata.subjectAlternativeName}`}</p>
|
||||
<p>{`TTL: ${event.metadata.ttl}`}</p>
|
||||
{event.metadata.pkiCollectionId && (
|
||||
<p>{`Collection ID: ${event.metadata.pkiCollectionId}`}</p>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE:
|
||||
case EventType.DELETE_CERTIFICATE_TEMPLATE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import {
|
||||
caStatusToNameMap,
|
||||
caTypeToNameMap,
|
||||
getStatusBadgeVariant
|
||||
getCaStatusBadgeVariant
|
||||
} from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -92,7 +92,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
>
|
||||
<Td>{ca.friendlyName}</Td>
|
||||
<Td>
|
||||
<Badge variant={getStatusBadgeVariant(ca.status)}>
|
||||
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
|
||||
{caStatusToNameMap[ca.status]}
|
||||
</Badge>
|
||||
</Td>
|
||||
@@ -100,9 +100,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Td>
|
||||
<div className="flex items-center ">
|
||||
<p>{ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}</p>
|
||||
{/* <Badge variant="danger" className="ml-4">
|
||||
Expires Soon
|
||||
</Badge> */}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
|
||||
+4
@@ -1,5 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { PkiCollectionSection } from "../PkiAlertsTab/components";
|
||||
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
|
||||
import { CertificatesSection } from "./components";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
@@ -11,6 +13,8 @@ export const CertificatesTab = () => {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<PkiCollectionSection />
|
||||
<CertificateTemplatesSection />
|
||||
<CertificatesSection />
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
+143
-11
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -7,21 +9,33 @@ import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreateCertificate,
|
||||
useGetCert,
|
||||
useGetCertTemplate,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificateTemplates,
|
||||
useListWorkspacePkiCollections
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
const schema = z.object({
|
||||
certificateTemplateId: z.string().optional(),
|
||||
caId: z.string(),
|
||||
collectionId: z.string().optional(),
|
||||
friendlyName: z.string(),
|
||||
commonName: z.string().trim().min(1),
|
||||
altNames: z.string(),
|
||||
@@ -42,6 +56,8 @@ type TCertificateDetails = {
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
const CERT_TEMPLATE_NONE_VALUE = "none";
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@@ -54,6 +70,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { data } = useListWorkspacePkiCollections({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||
|
||||
const {
|
||||
@@ -61,11 +85,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const selectedCertTemplateId = watch("certificateTemplateId");
|
||||
const hasCertTemplateSelected =
|
||||
selectedCertTemplateId !== "" && selectedCertTemplateId !== CERT_TEMPLATE_NONE_VALUE;
|
||||
|
||||
const { data: selectedCertTemplate } = useGetCertTemplate(
|
||||
hasCertTemplateSelected ? (selectedCertTemplateId as string) : ""
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
reset({
|
||||
@@ -73,6 +106,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
friendlyName: cert.friendlyName,
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames,
|
||||
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||
ttl: ""
|
||||
});
|
||||
} else {
|
||||
@@ -81,18 +115,34 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
friendlyName: "",
|
||||
commonName: "",
|
||||
altNames: "",
|
||||
ttl: ""
|
||||
ttl: "",
|
||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE
|
||||
});
|
||||
}
|
||||
}, [cert]);
|
||||
|
||||
const onFormSubmit = async ({ caId, friendlyName, commonName, altNames, ttl }: FormData) => {
|
||||
useEffect(() => {
|
||||
if (!cert && selectedCertTemplate) {
|
||||
setValue("ttl", selectedCertTemplate.ttl);
|
||||
}
|
||||
}, [selectedCertTemplate, cert]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
friendlyName,
|
||||
collectionId,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||
caId: !selectedCertTemplate ? caId : undefined,
|
||||
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId,
|
||||
pkiCollectionId: collectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
@@ -141,11 +191,31 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
name="certificateTemplateId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Certificate Template"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
When a template is selected, the details provided are validated
|
||||
against the template policies.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
@@ -158,15 +228,77 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
|
||||
None
|
||||
</SelectItem>
|
||||
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`cert-template-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{(!selectedCertTemplateId ||
|
||||
selectedCertTemplateId === CERT_TEMPLATE_NONE_VALUE ||
|
||||
cert) && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(data?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreateCertTemplate,
|
||||
useGetCertTemplate,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspacePkiCollections,
|
||||
useUpdateCertTemplate
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const validateTemplateRegexField = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message:
|
||||
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
caId: z.string(),
|
||||
collectionId: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
commonName: validateTemplateRegexField,
|
||||
subjectAlternativeName: validateTemplateRegexField,
|
||||
ttl: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateTemplate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["certificateTemplate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: certTemplate } = useGetCertTemplate(
|
||||
(popUp?.certificateTemplate?.data as { id: string })?.id || ""
|
||||
);
|
||||
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { data: collectionsData } = useListWorkspacePkiCollections({
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertTemplate } = useCreateCertTemplate();
|
||||
const { mutateAsync: updateCertTemplate } = useUpdateCertTemplate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (certTemplate) {
|
||||
reset({
|
||||
caId: certTemplate.caId,
|
||||
name: certTemplate.name,
|
||||
commonName: certTemplate.commonName,
|
||||
subjectAlternativeName: certTemplate.subjectAlternativeName,
|
||||
collectionId: certTemplate.pkiCollectionId ?? undefined,
|
||||
ttl: certTemplate.ttl
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caId: "",
|
||||
name: "",
|
||||
commonName: "",
|
||||
ttl: ""
|
||||
});
|
||||
}
|
||||
}, [certTemplate]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
collectionId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
}: FormData) => {
|
||||
if (!currentWorkspace?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (certTemplate) {
|
||||
await updateCertTemplate({
|
||||
id: certTemplate.id,
|
||||
projectId: currentWorkspace.id,
|
||||
pkiCollectionId: collectionId,
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated certificate template",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await createCertTemplate({
|
||||
projectId: currentWorkspace.id,
|
||||
pkiCollectionId: collectionId,
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate template",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("certificateTemplate", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to save changes",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateTemplate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateTemplate", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title={certTemplate ? "Certificate Template" : "Create Certificate Template"}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate Template" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="collectionId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Collection (Optional)"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(collectionsData?.collections || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`pki-collection-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Common Name (CN)"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
This field accepts limited regular expressions: spaces, *, ., @, -, \
|
||||
(for escaping), and alphanumeric characters only
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder=".*\.acme.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="subjectAlternativeName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={
|
||||
<div>
|
||||
<FormLabel
|
||||
isRequired
|
||||
label="Alternative Names (SAN)"
|
||||
icon={
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
This field accepts limited regular expressions: spaces, *, ., @, -, \
|
||||
(for escaping), and alphanumeric characters only
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="service\.acme.\..*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Max TTL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("certificateTemplate", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
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, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteCertTemplate } from "@app/hooks/api";
|
||||
|
||||
import { CertificateTemplateModal } from "./CertificateTemplateModal";
|
||||
import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
|
||||
|
||||
export const CertificateTemplatesSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificateTemplate",
|
||||
"deleteCertificateTemplate"
|
||||
] as const);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCertTemplate } = useDeleteCertTemplate();
|
||||
|
||||
const onRemoveCertificateTemplateSubmit = async (id: string) => {
|
||||
if (!currentWorkspace?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteCertTemplate({
|
||||
id,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully deleted certificate template",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCertificateTemplate");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete certificate template",
|
||||
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 Templates</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificateTemplate.isOpen}
|
||||
title={`Are you sure want to delete the certificate template ${
|
||||
(popUp?.deleteCertificateTemplate?.data as { name: string })?.name || ""
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteCertificateTemplate", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveCertificateTemplateSubmit(
|
||||
(popUp?.deleteCertificateTemplate?.data as { id: string })?.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { 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 {
|
||||
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 { useListWorkspaceCertificateTemplates } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["certificateTemplate", "deleteCertificateTemplate"]>,
|
||||
data?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceCertificateTemplates({
|
||||
workspaceId: currentWorkspace?.id ?? ""
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Certificate Authority</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>{certificateTemplate.caName}</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">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handlePopUpOpen("certificateTemplate", {
|
||||
id: certificateTemplate.id
|
||||
})
|
||||
}
|
||||
icon={<FontAwesomeIcon icon={faFileAlt} />}
|
||||
>
|
||||
Manage Policies
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteCertificateTemplate", {
|
||||
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" icon={faFileAlt} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -61,7 +61,7 @@ export const CertificatesSection = () => {
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue Certificate
|
||||
Issue
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
+153
-142
@@ -13,6 +13,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -31,9 +32,10 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api";
|
||||
import { certStatusToNameMap } from "@app/hooks/api/certificates/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
@@ -60,146 +62,155 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{!isLoading &&
|
||||
data?.certificates.map((certificate) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.friendlyName}</Td>
|
||||
<Td>{certStatusToNameMap[certificate.status]}</Td>
|
||||
<Td>
|
||||
{certificate.notAfter
|
||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</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">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificateCert", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faFileExport} />}
|
||||
>
|
||||
Export Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
Revoke Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("deleteCertificate", {
|
||||
serialNumber: certificate.serialNumber,
|
||||
commonName: certificate.commonName
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</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 certificates have been created" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Not Before</Th>
|
||||
<Th>Not After</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{!isLoading &&
|
||||
data?.certificates.map((certificate) => {
|
||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.friendlyName}</Td>
|
||||
<Td>
|
||||
<Badge className="" 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>
|
||||
<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">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificateCert", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faFileExport} />}
|
||||
>
|
||||
Export Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("certificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
Revoke Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("deleteCertificate", {
|
||||
serialNumber: certificate.serialNumber,
|
||||
commonName: certificate.commonName
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</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 certificates have been issued" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import ms from "ms";
|
||||
|
||||
export const getCertValidUntilBadgeDetails = (notAfter: string) => {
|
||||
const currentDate = new Date().getTime();
|
||||
const notAfterDate = new Date(notAfter).getTime();
|
||||
const diffInMs = notAfterDate - currentDate;
|
||||
|
||||
let variant: "success" | "primary" | "danger" = "success";
|
||||
let label = "Healthy";
|
||||
|
||||
if (diffInMs > ms("60d")) {
|
||||
variant = "success";
|
||||
} else if (diffInMs > ms("30d")) {
|
||||
variant = "primary";
|
||||
} else {
|
||||
variant = "danger";
|
||||
}
|
||||
|
||||
if (diffInMs > ms("60d")) {
|
||||
label = "Healthy";
|
||||
} else if (diffInMs > ms("0d")) {
|
||||
label = `Expires in ${ms(diffInMs)}`;
|
||||
} else {
|
||||
label = "Expired";
|
||||
}
|
||||
|
||||
return { variant, label };
|
||||
};
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { PkiAlertsSection, PkiCollectionSection } from "./components";
|
||||
import { PkiAlertsSection } from "./components";
|
||||
|
||||
export const PkiAlertsTab = () => {
|
||||
return (
|
||||
@@ -11,7 +11,6 @@ export const PkiAlertsTab = () => {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<PkiCollectionSection />
|
||||
<PkiAlertsSection />
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
+22
-4
@@ -15,7 +15,8 @@ import {
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().trim().min(1)
|
||||
name: z.string().trim().min(1),
|
||||
description: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
@@ -49,16 +50,18 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
useEffect(() => {
|
||||
if (pkiCollection) {
|
||||
reset({
|
||||
name: pkiCollection.name
|
||||
name: pkiCollection.name,
|
||||
description: pkiCollection.description
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: ""
|
||||
name: "",
|
||||
description: ""
|
||||
});
|
||||
}
|
||||
}, [pkiCollection]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
@@ -67,12 +70,14 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
await updatePkiCollection({
|
||||
collectionId: pkiCollection.id,
|
||||
name,
|
||||
description,
|
||||
projectId
|
||||
});
|
||||
} else {
|
||||
// create
|
||||
const { id: createdId } = await createPkiCollection({
|
||||
name,
|
||||
description,
|
||||
projectId
|
||||
});
|
||||
|
||||
@@ -121,6 +126,19 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="A collection to house certificates for ACME device"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export const PkiCollectionSection = () => {
|
||||
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 Collection</p>
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Collections</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.PkiCollections}
|
||||
|
||||
+3
-1
@@ -46,11 +46,12 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Description</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={2} innerKey="pki-collections" />}
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="pki-collections" />}
|
||||
{!isLoading &&
|
||||
data?.collections.map((pkiCollection) => {
|
||||
return (
|
||||
@@ -62,6 +63,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
|
||||
}
|
||||
>
|
||||
<Td>{pkiCollection.name}</Td>
|
||||
<Td>{pkiCollection.description ? pkiCollection.description : "-"}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
|
||||
+5
-1
@@ -71,10 +71,14 @@ export const PkiCollectionDetailsSection = ({ collectionId, handlePopUpOpen }: P
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{pkiCollection.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
|
||||
<p className="text-sm text-mineshaft-300">{pkiCollection.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { faBoxesStacked, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBoxesStacked, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
@@ -85,7 +85,7 @@ export const PkiCollectionItemsTable = ({ collectionId, type, handlePopUpOpen }:
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ export const formSchema = z.object({
|
||||
certificates: generalPermissionSchema,
|
||||
"pki-alerts": generalPermissionSchema,
|
||||
"pki-collections": generalPermissionSchema,
|
||||
"certificate-templates": generalPermissionSchema,
|
||||
// akhilmhdh: refactor all keys like below
|
||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
||||
workspace: z
|
||||
|
||||
+4
@@ -81,6 +81,10 @@ const SINGLE_PERMISSION_LIST = [
|
||||
title: "Certificates",
|
||||
formName: "certificates"
|
||||
},
|
||||
{
|
||||
title: "Certificate Templates",
|
||||
formName: "certificate-templates"
|
||||
},
|
||||
{
|
||||
title: "PKI Collections",
|
||||
formName: "pki-collections"
|
||||
|
||||
+6
-6
@@ -97,7 +97,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</Td>
|
||||
<Td>{secretVersion?.secretComment}</Td>
|
||||
<Td>
|
||||
{secretVersion?.tags?.map(({ name, id: tagId, color }) => (
|
||||
{secretVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={`${secretVersion.id}-${tagId}`}
|
||||
@@ -106,7 +106,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
@@ -119,7 +119,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</Td>
|
||||
<Td>{newVersion?.secretComment}</Td>
|
||||
<Td>
|
||||
{newVersion?.tags?.map(({ name, id: tagId, color }) => (
|
||||
{newVersion?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={`${newVersion.id}-${tagId}`}
|
||||
@@ -128,7 +128,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
@@ -157,7 +157,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</Td>
|
||||
<Td>
|
||||
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
|
||||
({ name, id: tagId, color }) => (
|
||||
({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={`${
|
||||
@@ -168,7 +168,7 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -327,7 +327,7 @@ export const ActionBar = ({
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="rounded-l-none">
|
||||
<DropdownMenuLabel>Apply tags to filter secrets</DropdownMenuLabel>
|
||||
{tags.map(({ id, name, color }) => (
|
||||
{tags.map(({ id, slug, color }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
@@ -342,7 +342,7 @@ export const ActionBar = ({
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
@@ -254,7 +254,7 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
<FormControl label="Tags" className="">
|
||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
||||
{fields.map(({ tagColor, id: formId, name, id }) => (
|
||||
{fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
@@ -271,7 +271,7 @@ export const SecretDetailSidebar = ({
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
@@ -296,7 +296,7 @@ export const SecretDetailSidebar = ({
|
||||
<DropdownMenuContent align="end" className="z-[100]">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, name, color } = tag;
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
@@ -311,7 +311,7 @@ export const SecretDetailSidebar = ({
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
@@ -336,7 +336,7 @@ export const SecretItem = memo(
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, name, color } = tag;
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
@@ -358,7 +358,7 @@ export const SecretItem = memo(
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,6 @@ export const formSchema = z.object({
|
||||
tags: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
tagColor: z.string().optional()
|
||||
})
|
||||
|
||||
@@ -151,7 +151,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
|
||||
<Td className="border-r border-mineshaft-600">Tags</Td>
|
||||
{isModified && (
|
||||
<Td className="border-r border-mineshaft-600">
|
||||
{preSecret?.tags?.map(({ name, id: tagId, color }) => (
|
||||
{preSecret?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={`${preSecret.id}-${tagId}`}
|
||||
@@ -160,13 +160,13 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
)}
|
||||
<Td>
|
||||
{postSecret?.tags?.map(({ name, id: tagId, color }) => (
|
||||
{postSecret?.tags?.map(({ slug, id: tagId, color }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={`${postSecret.id}-${tagId}`}
|
||||
@@ -175,7 +175,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: color || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet
|
||||
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle();
|
||||
const { data } = useGetSSOConfig(currentOrg?.id ?? "");
|
||||
|
||||
const { control, handleSubmit, reset, watch } = useForm<AddSSOFormData>({
|
||||
const { control, handleSubmit, reset, watch, setValue, getValues } = useForm<AddSSOFormData>({
|
||||
defaultValues: {
|
||||
authProvider: AuthProvider.OKTA_SAML
|
||||
},
|
||||
@@ -188,8 +188,8 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet
|
||||
entityId: "SP Entity ID",
|
||||
entryPoint: "SSO URL",
|
||||
entryPointPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx",
|
||||
issuer: "IdP Entity ID",
|
||||
issuerPlaceholder: "https://accounts.google.com/o/saml2/idp?idpid=xxx"
|
||||
issuer: "Issuer",
|
||||
issuerPlaceholder: window.origin
|
||||
};
|
||||
default:
|
||||
return {
|
||||
@@ -204,6 +204,11 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDelet
|
||||
};
|
||||
|
||||
const authProvider = watch("authProvider");
|
||||
useEffect(() => {
|
||||
if (authProvider === AuthProvider.GOOGLE_SAML && getValues("issuer") === "") {
|
||||
setValue("issuer", window.origin);
|
||||
}
|
||||
}, [authProvider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+14
-13
@@ -1,6 +1,7 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
@@ -8,11 +9,13 @@ import { useWorkspace } from "@app/context";
|
||||
import { useCreateWsTag } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required().label("Tag Name")
|
||||
const schema = z.object({
|
||||
slug: z.string().refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
})
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>;
|
||||
@@ -26,7 +29,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const createWsTag = useCreateWsTag();
|
||||
const {
|
||||
@@ -35,17 +37,16 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
const onFormSubmit = async ({ slug }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await createWsTag.mutateAsync({
|
||||
workspaceID: currentWorkspace?.id,
|
||||
tagName: name,
|
||||
tagSlug: name.replace(/\s+/g, " ").replace(" ", "_"),
|
||||
tagSlug: slug,
|
||||
tagColor: ""
|
||||
});
|
||||
|
||||
@@ -80,11 +81,11 @@ export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
name="slug"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Type your tag name" />
|
||||
<FormControl label="Tag Slug" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Type your tag slug" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
+3
-5
@@ -40,7 +40,6 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Tag</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
@@ -49,9 +48,8 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.map(({ id, name, slug }) => (
|
||||
<Tr key={name}>
|
||||
<Td>{name}</Td>
|
||||
data.map(({ id, slug }) => (
|
||||
<Tr key={id}>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<ProjectPermissionCan
|
||||
@@ -62,7 +60,7 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteTagConfirmation", {
|
||||
name,
|
||||
name: slug,
|
||||
id
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Table>
|
||||
{!isLoading &&
|
||||
data?.secrets &&
|
||||
data.secrets.length >= perPage &&
|
||||
data?.totalCount >= perPage &&
|
||||
data?.totalCount !== undefined && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
|
||||
@@ -15,7 +15,6 @@ const formSchema = z.object({
|
||||
authRateLimit: z.number(),
|
||||
inviteUserRateLimit: z.number(),
|
||||
mfaRateLimit: z.number(),
|
||||
creationLimit: z.number(),
|
||||
publicEndpointLimit: z.number()
|
||||
});
|
||||
|
||||
@@ -41,7 +40,6 @@ export const RateLimitPanel = () => {
|
||||
authRateLimit: rateLimit?.authRateLimit ?? 60,
|
||||
inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30,
|
||||
mfaRateLimit: rateLimit?.mfaRateLimit ?? 20,
|
||||
creationLimit: rateLimit?.creationLimit ?? 30,
|
||||
publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30
|
||||
}
|
||||
});
|
||||
@@ -60,7 +58,6 @@ export const RateLimitPanel = () => {
|
||||
authRateLimit,
|
||||
inviteUserRateLimit,
|
||||
mfaRateLimit,
|
||||
creationLimit,
|
||||
publicEndpointLimit
|
||||
} = formData;
|
||||
|
||||
@@ -71,7 +68,6 @@ export const RateLimitPanel = () => {
|
||||
authRateLimit,
|
||||
inviteUserRateLimit,
|
||||
mfaRateLimit,
|
||||
creationLimit,
|
||||
publicEndpointLimit
|
||||
});
|
||||
createNotification({
|
||||
@@ -210,25 +206,6 @@ export const RateLimitPanel = () => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={300}
|
||||
name="creationLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="New resource creation requests per minute"
|
||||
className="w-72"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={300}
|
||||
|
||||
Reference in New Issue
Block a user