Merge branch 'feature/certificate-template' into feature/est-simpleenroll

This commit is contained in:
Sheen Capadngan
2024-08-19 17:04:22 +08:00
199 changed files with 4510 additions and 989 deletions
@@ -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 } = {
+5 -1
View File
@@ -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"
}
+45 -1
View File
@@ -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;
+1 -1
View File
@@ -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";
+3 -2
View File
@@ -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));
+4 -1
View File
@@ -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;
+1
View File
@@ -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;
};
+1 -2
View File
@@ -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
View File
@@ -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,
+20 -1
View File
@@ -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)
});
};
+5 -3
View File
@@ -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
@@ -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">
@@ -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>
);
@@ -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=""
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -61,7 +61,7 @@ export const CertificatesSection = () => {
onClick={() => handlePopUpOpen("certificate")}
isDisabled={!isAllowed}
>
Issue Certificate
Issue
</Button>
)}
</ProjectPermissionCan>
@@ -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>
);
};
@@ -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,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>
);
@@ -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"
@@ -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}
@@ -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">
@@ -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>
)}
@@ -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
@@ -81,6 +81,10 @@ const SINGLE_PERMISSION_LIST = [
title: "Certificates",
formName: "certificates"
},
{
title: "Certificate Templates",
formName: "certificate-templates"
},
{
title: "PKI Collections",
formName: "pki-collections"
@@ -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 (
<>
@@ -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>
)}
/>
@@ -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}