mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
Merge remote-tracking branch 'origin/main' into feature/oidc
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { FormControl } from "../FormControl";
|
||||
import { Input } from "../Input";
|
||||
import { Modal, ModalClose, ModalContent } from "../Modal";
|
||||
|
||||
type Props = {
|
||||
deleteKey: string;
|
||||
title: string;
|
||||
onLeaveApproved: () => Promise<void>;
|
||||
onClose?: () => void;
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
isOpen?: boolean;
|
||||
subTitle?: string;
|
||||
buttonText?: string;
|
||||
};
|
||||
|
||||
export const LeaveProjectModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onChange,
|
||||
deleteKey,
|
||||
onLeaveApproved,
|
||||
title,
|
||||
subTitle,
|
||||
buttonText = "Leave Project"
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
|
||||
useEffect(() => {
|
||||
setInputData("");
|
||||
}, [isOpen]);
|
||||
|
||||
const onDelete = async () => {
|
||||
setIsLoading.on();
|
||||
try {
|
||||
await onLeaveApproved();
|
||||
} catch {
|
||||
setIsLoading.off();
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(isOpenState) => {
|
||||
setInputData("");
|
||||
if (onChange) onChange(isOpenState);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={title}
|
||||
subTitle={subTitle}
|
||||
footerContent={
|
||||
<div className="mx-2 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
colorSchema="danger"
|
||||
isDisabled={!(deleteKey === inputData) || isLoading}
|
||||
onClick={onDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>{" "}
|
||||
</div>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault();
|
||||
if (deleteKey === inputData) onDelete();
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
label={
|
||||
<div className="break-words pb-2 text-sm">
|
||||
Type <span className="font-bold">{deleteKey}</span> to leave the project
|
||||
</div>
|
||||
}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input
|
||||
value={inputData}
|
||||
onChange={(e) => setInputData(e.target.value)}
|
||||
placeholder="Type to confirm..."
|
||||
/>
|
||||
</FormControl>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { LeaveProjectModal } from "./LeaveProjectModal";
|
||||
@@ -24,7 +24,8 @@ export const publicPaths = [
|
||||
"/login/provider/error", // TODO: change
|
||||
"/login/sso",
|
||||
"/admin/signup",
|
||||
"/shared/secret/[id]"
|
||||
"/shared/secret/[id]",
|
||||
"/share-secret"
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
|
||||
@@ -24,7 +24,9 @@ export enum ProjectPermissionSub {
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity"
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -51,6 +53,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
|
||||
@@ -26,7 +26,6 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret",
|
||||
[EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
|
||||
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
|
||||
[EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth",
|
||||
[EventType.CREATE_ENVIRONMENT]: "Create environment",
|
||||
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
|
||||
[EventType.DELETE_ENVIRONMENT]: "Delete environment",
|
||||
@@ -43,7 +42,21 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.UPDATE_SECRET_IMPORT]: "Update secret import",
|
||||
[EventType.DELETE_SECRET_IMPORT]: "Delete secret import",
|
||||
[EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS]: "Update denied permissions",
|
||||
[EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role"
|
||||
[EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role",
|
||||
[EventType.CREATE_CA]: "Create CA",
|
||||
[EventType.GET_CA]: "Get CA",
|
||||
[EventType.UPDATE_CA]: "Update CA",
|
||||
[EventType.DELETE_CA]: "Delete CA",
|
||||
[EventType.GET_CA_CSR]: "Get CA CSR",
|
||||
[EventType.GET_CA_CERT]: "Get CA certificate",
|
||||
[EventType.SIGN_INTERMEDIATE]: "Sign intermediate",
|
||||
[EventType.IMPORT_CA_CERT]: "Import CA certificate",
|
||||
[EventType.GET_CA_CRL]: "Get CA CRL",
|
||||
[EventType.ISSUE_CERT]: "Issue certificate",
|
||||
[EventType.GET_CERT]: "Get certificate",
|
||||
[EventType.DELETE_CERT]: "Delete certificate",
|
||||
[EventType.REVOKE_CERT]: "Revoke certificate",
|
||||
[EventType.GET_CERT_BODY]: "Get certificate body"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
||||
@@ -56,5 +56,19 @@ export enum EventType {
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
||||
CREATE_CA = "create-certificate-authority",
|
||||
GET_CA = "get-certificate-authority",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
DELETE_CA = "delete-certificate-authority",
|
||||
GET_CA_CSR = "get-certificate-authority-csr",
|
||||
GET_CA_CERT = "get-certificate-authority-cert",
|
||||
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRL = "get-certificate-authority-crl",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
GET_CERT_BODY = "get-cert-body"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CaStatus } from "../ca";
|
||||
import { IdentityTrustedIp } from "../identities/types";
|
||||
import { ActorType, EventType, UserAgentType } from "./enums";
|
||||
|
||||
@@ -462,6 +463,125 @@ interface UpdateUserDeniedPermissions {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCa {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCa {
|
||||
type: EventType.GET_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCa {
|
||||
type: EventType.UPDATE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
status: CaStatus;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteCa {
|
||||
type: EventType.DELETE_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCsr {
|
||||
type: EventType.GET_CA_CSR;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCert {
|
||||
type: EventType.GET_CA_CERT;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignIntermediate {
|
||||
type: EventType.SIGN_INTERMEDIATE;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ImportCaCert {
|
||||
type: EventType.IMPORT_CA_CERT;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCrl {
|
||||
type: EventType.GET_CA_CRL;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IssueCert {
|
||||
type: EventType.ISSUE_CERT;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCert {
|
||||
type: EventType.GET_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteCert {
|
||||
type: EventType.DELETE_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RevokeCert {
|
||||
type: EventType.REVOKE_CERT;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertBody {
|
||||
type: EventType.GET_CERT_BODY;
|
||||
metadata: {
|
||||
certId: string;
|
||||
cn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -504,7 +624,21 @@ export type Event =
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions;
|
||||
| UpdateUserDeniedPermissions
|
||||
| CreateCa
|
||||
| GetCa
|
||||
| UpdateCa
|
||||
| DeleteCa
|
||||
| GetCaCsr
|
||||
| GetCaCert
|
||||
| SignIntermediate
|
||||
| ImportCaCert
|
||||
| GetCaCrl
|
||||
| IssueCert
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CaStatus,CaType } from "./enums";
|
||||
|
||||
export const caTypeToNameMap: { [K in CaType]: string } = {
|
||||
[CaType.ROOT]: "Root",
|
||||
[CaType.INTERMEDIATE]: "Intermediate"
|
||||
};
|
||||
|
||||
export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
[CaStatus.ACTIVE]: "Active",
|
||||
[CaStatus.DISABLED]: "Disabled",
|
||||
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export enum CaType {
|
||||
ROOT = "root",
|
||||
INTERMEDIATE = "intermediate"
|
||||
}
|
||||
|
||||
export enum CaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled",
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { CaStatus, CaType } from "./enums";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
useDeleteCa,
|
||||
useImportCaCertificate,
|
||||
useSignIntermediate,
|
||||
useUpdateCa
|
||||
} from "./mutations";
|
||||
export { useGetCaById, useGetCaCert, useGetCaCrl,useGetCaCsr } from "./queries";
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import {
|
||||
TCertificateAuthority,
|
||||
TCreateCaDTO,
|
||||
TCreateCertificateDTO,
|
||||
TCreateCertificateResponse,
|
||||
TDeleteCaDTO,
|
||||
TImportCaCertificateDTO,
|
||||
TImportCaCertificateResponse,
|
||||
TSignIntermediateDTO,
|
||||
TSignIntermediateResponse,
|
||||
TUpdateCaDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, {}, TCreateCaDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/pki/ca/", body);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, {}, TUpdateCaDTO>({
|
||||
mutationFn: async ({ caId, projectSlug, ...body }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificateAuthority, {}, TDeleteCaDTO>({
|
||||
mutationFn: async ({ caId }) => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
|
||||
return ca;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSignIntermediate = () => {
|
||||
// TODO: consider renaming
|
||||
return useMutation<TSignIntermediateResponse, {}, TSignIntermediateDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TSignIntermediateResponse>(
|
||||
`/api/v1/pki/ca/${body.caId}/sign-intermediate`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportCaCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TImportCaCertificateResponse, {}, TImportCaCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
const { data } = await apiRequest.post<TImportCaCertificateResponse>(
|
||||
`/api/v1/pki/ca/${caId}/import-certificate`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// consider rename to issue certificate
|
||||
export const useCreateCertificate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||
mutationFn: async ({ caId, ...body }) => {
|
||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||
`/api/v1/pki/ca/${caId}/issue-certificate`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateAuthority } from "./types";
|
||||
|
||||
export const caKeys = {
|
||||
getCaById: (caId: string) => [{ caId }, "ca"],
|
||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"]
|
||||
};
|
||||
|
||||
export const useGetCaById = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaById(caId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { ca }
|
||||
} = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
|
||||
return ca;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCaCert = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaCert(caId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
serialNumber: string;
|
||||
}>(`/api/v1/pki/ca/${caId}/certificate`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCaCsr = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaCsr(caId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { csr }
|
||||
} = await apiRequest.get<{
|
||||
csr: string;
|
||||
}>(`/api/v1/pki/ca/${caId}/csr`);
|
||||
return csr;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCaCrl = (caId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaCrl(caId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { crl }
|
||||
} = await apiRequest.get<{
|
||||
crl: string;
|
||||
}>(`/api/v1/pki/ca/${caId}/crl`);
|
||||
return crl;
|
||||
},
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||
import { CaStatus, CaType } from "./enums";
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
id: string;
|
||||
parentCaId?: string;
|
||||
projectId: string;
|
||||
type: CaType;
|
||||
status: CaStatus;
|
||||
friendlyName: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
commonName: string;
|
||||
dn: string;
|
||||
maxPathLength?: number;
|
||||
notAfter?: string;
|
||||
notBefore?: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateCaDTO = {
|
||||
projectSlug: string;
|
||||
type: string;
|
||||
friendlyName?: string;
|
||||
organization: string;
|
||||
ou: string;
|
||||
country: string;
|
||||
province: string;
|
||||
locality: string;
|
||||
commonName: string;
|
||||
notAfter?: string;
|
||||
maxPathLength: number;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
};
|
||||
|
||||
export type TUpdateCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
status?: CaStatus;
|
||||
};
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
};
|
||||
|
||||
export type TSignIntermediateDTO = {
|
||||
caId: string;
|
||||
csr: string;
|
||||
maxPathLength: number;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
};
|
||||
|
||||
export type TSignIntermediateResponse = {
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
issuingCaCertificate: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
export type TImportCaCertificateDTO = {
|
||||
caId: string;
|
||||
projectSlug: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
};
|
||||
|
||||
export type TImportCaCertificateResponse = {
|
||||
message: string;
|
||||
caId: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
ttl: string; // string compatible with ms
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateResponse = {
|
||||
certificate: string;
|
||||
issuingCertificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CertKeyAlgorithm, CertStatus,CrlReason } from "./enums";
|
||||
|
||||
export const certStatusToNameMap: { [K in CertStatus]: string } = {
|
||||
[CertStatus.ACTIVE]: "Active",
|
||||
[CertStatus.REVOKED]: "Revoked"
|
||||
};
|
||||
|
||||
export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
||||
[CertKeyAlgorithm.RSA_2048]: "RSA 2048",
|
||||
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
|
||||
[CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
|
||||
[CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384"
|
||||
};
|
||||
|
||||
export const certKeyAlgorithms = [
|
||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
|
||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_4096], value: CertKeyAlgorithm.RSA_4096 },
|
||||
{
|
||||
label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P256],
|
||||
value: CertKeyAlgorithm.ECDSA_P256
|
||||
},
|
||||
{
|
||||
label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P384],
|
||||
value: CertKeyAlgorithm.ECDSA_P384
|
||||
}
|
||||
];
|
||||
|
||||
export const crlReasonToNameMap: { [K in CrlReason]: string } = {
|
||||
[CrlReason.UNSPECIFIED]: "Unspecified",
|
||||
[CrlReason.KEY_COMPROMISE]: "Key Compromise",
|
||||
[CrlReason.CA_COMPROMISE]: "CA Compromise",
|
||||
[CrlReason.AFFILIATION_CHANGED]: "Affiliation Changed",
|
||||
[CrlReason.SUPERSEDED]: "Superseded",
|
||||
[CrlReason.CESSATION_OF_OPERATION]: "Cessation of Operation",
|
||||
[CrlReason.CERTIFICATE_HOLD]: "Certificate Hold",
|
||||
// [CrlReason.REMOVE_FROM_CRL]: "Remove from CRL",
|
||||
[CrlReason.PRIVILEGE_WITHDRAWN]: "Privilege Withdrawn",
|
||||
[CrlReason.A_A_COMPROMISE]: "A/A Compromise"
|
||||
};
|
||||
|
||||
export const crlReasons = [
|
||||
{ label: crlReasonToNameMap[CrlReason.UNSPECIFIED], value: CrlReason.UNSPECIFIED },
|
||||
{ label: crlReasonToNameMap[CrlReason.KEY_COMPROMISE], value: CrlReason.KEY_COMPROMISE },
|
||||
{ label: crlReasonToNameMap[CrlReason.CA_COMPROMISE], value: CrlReason.CA_COMPROMISE },
|
||||
{
|
||||
label: crlReasonToNameMap[CrlReason.AFFILIATION_CHANGED],
|
||||
value: CrlReason.AFFILIATION_CHANGED
|
||||
},
|
||||
{ label: crlReasonToNameMap[CrlReason.SUPERSEDED], value: CrlReason.SUPERSEDED },
|
||||
{
|
||||
label: crlReasonToNameMap[CrlReason.CESSATION_OF_OPERATION],
|
||||
value: CrlReason.CESSATION_OF_OPERATION
|
||||
},
|
||||
{ label: crlReasonToNameMap[CrlReason.CERTIFICATE_HOLD], value: CrlReason.CERTIFICATE_HOLD },
|
||||
{
|
||||
label: crlReasonToNameMap[CrlReason.PRIVILEGE_WITHDRAWN],
|
||||
value: CrlReason.PRIVILEGE_WITHDRAWN
|
||||
},
|
||||
{ label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE }
|
||||
];
|
||||
@@ -0,0 +1,24 @@
|
||||
export enum CertStatus {
|
||||
ACTIVE = "active",
|
||||
REVOKED = "revoked"
|
||||
}
|
||||
|
||||
export enum CertKeyAlgorithm {
|
||||
RSA_2048 = "RSA_2048",
|
||||
RSA_4096 = "RSA_4096",
|
||||
ECDSA_P256 = "EC_prime256v1",
|
||||
ECDSA_P384 = "EC_secp384r1"
|
||||
}
|
||||
|
||||
export enum CrlReason {
|
||||
UNSPECIFIED = "UNSPECIFIED",
|
||||
KEY_COMPROMISE = "KEY_COMPROMISE",
|
||||
CA_COMPROMISE = "CA_COMPROMISE",
|
||||
AFFILIATION_CHANGED = "AFFILIATION_CHANGED",
|
||||
SUPERSEDED = "SUPERSEDED",
|
||||
CESSATION_OF_OPERATION = "CESSATION_OF_OPERATION",
|
||||
CERTIFICATE_HOLD = "CERTIFICATE_HOLD",
|
||||
// REMOVE_FROM_CRL = "REMOVE_FROM_CRL",
|
||||
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
|
||||
A_A_COMPROMISE = "A_A_COMPROMISE"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useDeleteCert, useRevokeCert } from "./mutations";
|
||||
export { useGetCert, useGetCertBody } from "./queries";
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
||||
|
||||
export const useDeleteCert = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificate, {}, TDeleteCertDTO>({
|
||||
mutationFn: async ({ serialNumber }) => {
|
||||
const {
|
||||
data: { certificate }
|
||||
} = await apiRequest.delete<{ certificate: TCertificate }>(
|
||||
`/api/v1/pki/certificates/${serialNumber}`
|
||||
);
|
||||
return certificate;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRevokeCert = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TCertificate, {}, TRevokeCertDTO>({
|
||||
mutationFn: async ({ serialNumber, revocationReason }) => {
|
||||
const {
|
||||
data: { certificate }
|
||||
} = await apiRequest.post<{ certificate: TCertificate }>(
|
||||
`/api/v1/pki/certificates/${serialNumber}/revoke`,
|
||||
{
|
||||
revocationReason
|
||||
}
|
||||
);
|
||||
return certificate;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificate } from "./types";
|
||||
|
||||
export const certKeys = {
|
||||
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
|
||||
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"]
|
||||
};
|
||||
|
||||
export const useGetCert = (serialNumber: string) => {
|
||||
return useQuery({
|
||||
queryKey: certKeys.getCertById(serialNumber),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { certificate }
|
||||
} = await apiRequest.get<{ certificate: TCertificate }>(
|
||||
`/api/v1/pki/certificates/${serialNumber}`
|
||||
);
|
||||
return certificate;
|
||||
},
|
||||
enabled: Boolean(serialNumber)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCertBody = (serialNumber: string) => {
|
||||
return useQuery({
|
||||
queryKey: certKeys.getCertBody(serialNumber),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
serialNumber: string;
|
||||
}>(`/api/v1/pki/certificates/${serialNumber}/certificate`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(serialNumber)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CertStatus } from "./enums";
|
||||
|
||||
export type TCertificate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
status: CertStatus;
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
serialNumber: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
};
|
||||
|
||||
export type TDeleteCertDTO = {
|
||||
projectSlug: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
export type TRevokeCertDTO = {
|
||||
projectSlug: string;
|
||||
serialNumber: string;
|
||||
revocationReason: string;
|
||||
};
|
||||
@@ -5,6 +5,8 @@ export * from "./auditLogs";
|
||||
export * from "./auditLogStreams";
|
||||
export * from "./auth";
|
||||
export * from "./bots";
|
||||
export * from "./ca";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamicSecret";
|
||||
export * from "./dynamicSecretLease";
|
||||
export * from "./groups";
|
||||
|
||||
@@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreatePublicSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>(
|
||||
"/api/v1/secret-sharing/public",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
TSharedSecret,
|
||||
{ message: string },
|
||||
{ sharedSecretId: string }
|
||||
>({
|
||||
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||
`/api/v1/secret-sharing/${sharedSecretId}`
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useGetSharedSecrets = () => {
|
||||
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||
queryFn: async () => {
|
||||
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" });
|
||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||
);
|
||||
|
||||
@@ -36,4 +36,5 @@ export type SubscriptionPlan = {
|
||||
| null;
|
||||
trial_end: number | null;
|
||||
has_used_trial: boolean;
|
||||
caCrl: boolean;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
useAddGroupToWorkspace,
|
||||
useDeleteGroupFromWorkspace,
|
||||
useLeaveProject,
|
||||
useUpdateGroupWorkspaceRole
|
||||
} from "./mutations";
|
||||
export {
|
||||
@@ -21,6 +22,8 @@ export {
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates,
|
||||
useListWorkspaceGroups,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
|
||||
@@ -62,3 +62,15 @@ export const useDeleteGroupFromWorkspace = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useLeaveProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{}, {}, { workspaceId: string }>({
|
||||
mutationFn: ({ workspaceId }) => {
|
||||
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/leave`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { CaStatus } from "../ca/enums";
|
||||
import { TCertificateAuthority } from "../ca/types";
|
||||
import { TCertificate } from "../certificates/types";
|
||||
import { TGroupMembership } from "../groups/types";
|
||||
import { IdentityMembership } from "../identities/types";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
@@ -40,7 +43,23 @@ export const workspaceKeys = {
|
||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-groups"] as const
|
||||
[{ workspaceId }, "workspace-groups"] as const,
|
||||
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
||||
[{ projectSlug }, "workspace-cas"] as const,
|
||||
specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) =>
|
||||
[...workspaceKeys.getWorkspaceCas({ projectSlug }), { status }] as const,
|
||||
allWorkspaceCertificates: () => ["workspace-certificates"] as const,
|
||||
forWorkspaceCertificates: (slug: string) =>
|
||||
[...workspaceKeys.allWorkspaceCertificates(), slug] as const,
|
||||
specificWorkspaceCertificates: ({
|
||||
slug,
|
||||
offset,
|
||||
limit
|
||||
}: {
|
||||
slug: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -485,3 +504,70 @@ export const useListWorkspaceGroups = (projectSlug: string) => {
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceCas = ({
|
||||
projectSlug,
|
||||
status
|
||||
}: {
|
||||
projectSlug: string;
|
||||
status?: CaStatus;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.specificWorkspaceCas({
|
||||
projectSlug,
|
||||
status
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
...(status && { status })
|
||||
});
|
||||
|
||||
const {
|
||||
data: { cas }
|
||||
} = await apiRequest.get<{ cas: TCertificateAuthority[] }>(
|
||||
`/api/v2/workspace/${projectSlug}/cas`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
return cas;
|
||||
},
|
||||
enabled: Boolean(projectSlug)
|
||||
});
|
||||
};
|
||||
|
||||
export const useListWorkspaceCertificates = ({
|
||||
projectSlug,
|
||||
offset,
|
||||
limit
|
||||
}: {
|
||||
projectSlug: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.specificWorkspaceCertificates({
|
||||
slug: projectSlug,
|
||||
offset,
|
||||
limit
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
});
|
||||
|
||||
const {
|
||||
data: { certificates, totalCount }
|
||||
} = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>(
|
||||
`/api/v2/workspace/${projectSlug}/certificates`,
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
|
||||
return { certificates, totalCount };
|
||||
},
|
||||
enabled: Boolean(projectSlug)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -519,6 +519,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/certificates`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/certificates`
|
||||
}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Internal PKI
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
@@ -633,9 +645,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<Link href={`/org/${currentOrg?.id}/secret-sharing`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/org/${currentOrg?.id}/secret-sharing`
|
||||
}
|
||||
isSelected={router.asPath === `/org/${currentOrg?.id}/secret-sharing`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Secret Sharing
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { CertificatesPage } from "@app/views/Project/CertificatesPage";
|
||||
|
||||
const Certificates = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<CertificatesPage />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Certificates;
|
||||
|
||||
Certificates.requireAuth = true;
|
||||
@@ -0,0 +1,24 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||
|
||||
const ShareNewPublicSecretPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Securely Share Secrets | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="" />
|
||||
<meta name="og:description" content="" />
|
||||
</Head>
|
||||
<div className="dark h-full">
|
||||
<ShareSecretPublicPage isNewSession />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareNewPublicSecretPage;
|
||||
|
||||
ShareNewPublicSecretPage.requireAuth = false;
|
||||
@@ -2,7 +2,7 @@ import Head from "next/head";
|
||||
|
||||
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||
|
||||
const SecretApproval = () => {
|
||||
const SecretSharedPublicPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -12,13 +12,13 @@ const SecretApproval = () => {
|
||||
<meta property="og:title" content="" />
|
||||
<meta name="og:description" content="" />
|
||||
</Head>
|
||||
<div className="h-full">
|
||||
<ShareSecretPublicPage />
|
||||
<div className="dark h-full">
|
||||
<ShareSecretPublicPage isNewSession={false} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretApproval;
|
||||
export default SecretSharedPublicPage;
|
||||
|
||||
SecretApproval.requireAuth = false;
|
||||
SecretSharedPublicPage.requireAuth = false;
|
||||
|
||||
@@ -75,7 +75,12 @@ export default function SignupInvite() {
|
||||
// Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria.
|
||||
const signupErrorCheck = async () => {
|
||||
setIsLoading(true);
|
||||
let errorCheck = false;
|
||||
|
||||
let errorCheck = await checkPassword({
|
||||
password,
|
||||
setErrors
|
||||
});
|
||||
|
||||
if (!firstName) {
|
||||
setFirstNameError(true);
|
||||
errorCheck = true;
|
||||
@@ -89,11 +94,6 @@ export default function SignupInvite() {
|
||||
setLastNameError(false);
|
||||
}
|
||||
|
||||
errorCheck = await checkPassword({
|
||||
password,
|
||||
setErrors
|
||||
});
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
@@ -179,8 +179,9 @@ export const LogsFilter = ({ control, reset }: Props) => {
|
||||
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
onChange={(pickedDate) => {
|
||||
pickedDate?.setHours(23, 59, 59, 999); // we choose the end of today not the start of it (going off of aws cloud watch)
|
||||
onChange(pickedDate);
|
||||
setIsEndDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
|
||||
@@ -23,7 +23,8 @@ export const LogsSection = () => {
|
||||
defaultValues: {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1))
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
||||
endDate: new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -320,6 +320,30 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CA:
|
||||
case EventType.GET_CA:
|
||||
case EventType.UPDATE_CA:
|
||||
case EventType.DELETE_CA:
|
||||
case EventType.GET_CA_CSR:
|
||||
case EventType.GET_CA_CERT:
|
||||
case EventType.IMPORT_CA_CERT:
|
||||
case EventType.GET_CA_CRL:
|
||||
case EventType.SIGN_INTERMEDIATE:
|
||||
case EventType.ISSUE_CERT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`CA DN: ${event.metadata.dn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERT:
|
||||
case EventType.DELETE_CERT:
|
||||
case EventType.REVOKE_CERT:
|
||||
case EventType.GET_CERT_BODY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Cert CN: ${event.metadata.cn}`}</p>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { CaTab, CertificatesTab } from "./components";
|
||||
|
||||
enum TabSections {
|
||||
Ca = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
}
|
||||
|
||||
export const CertificatesPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Internal PKI</p>
|
||||
<Tabs defaultValue={TabSections.Certificates}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Certificates}>Certificates</Tab>
|
||||
<Tab value={TabSections.Ca}>Certificate Authorities</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<CertificatesTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Ca}>
|
||||
<CaTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { CaSection } from "./components";
|
||||
|
||||
export const CaTab = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-certificate-authorities"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<CaSection />
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useGetCaCert } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "../../CertificatesTab/components/CertificateContent";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["caCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCert"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CaCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data } = useGetCaCert((popUp?.caCert?.data as { caId: string })?.caId || "");
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.caCert?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("caCert", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="CA Certificate">
|
||||
{data ? (
|
||||
<CertificateContent
|
||||
serialNumber={data.serialNumber}
|
||||
certificate={data.certificate}
|
||||
certificateChain={data.certificateChain}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { useEffect } from "react";
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useGetCaCrl } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["caCrl"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCrl"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CaCrlModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [isCrlCopied, setIsCrlCopied] = useToggle(false);
|
||||
const { data: crl } = useGetCaCrl((popUp?.caCrl?.data as { caId: string })?.caId || "");
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isCrlCopied) {
|
||||
timer = setTimeout(() => setIsCrlCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isCrlCopied]);
|
||||
|
||||
const downloadTxtFile = (filename: string, content: string) => {
|
||||
const blob = new Blob([content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.caCrl?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("caCrl", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="CA Certificate Revocation List (CRL)">
|
||||
<div>
|
||||
{crl && (
|
||||
<>
|
||||
{/* <div className="mb-4 flex items-center justify-between">
|
||||
<h2>Manual CRL Rotation</h2>
|
||||
<Button
|
||||
// isLoading={isLoading}
|
||||
// isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
// onClick={() => handleAssignment(username, !isPartOfGroup)}
|
||||
onClick={() => {}}
|
||||
>
|
||||
Rotate
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Revocation List</h2>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(crl);
|
||||
setIsCrlCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCrlCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("crl.pem", crl);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Download
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{crl}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
// DatePicker,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useGetCaById,
|
||||
useGetCaCsr,
|
||||
useImportCaCertificate,
|
||||
useListWorkspaceCas,
|
||||
useSignIntermediate
|
||||
} from "@app/hooks/api";
|
||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
};
|
||||
|
||||
const getMiddleDate = (date1: Date, date2: Date) => {
|
||||
const timestamp1 = date1.getTime();
|
||||
const timestamp2 = date2.getTime();
|
||||
|
||||
const middleTimestamp = (timestamp1 + timestamp2) / 2;
|
||||
|
||||
return new Date(middleTimestamp);
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
parentCaId: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["installCaCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
enum ParentCaType {
|
||||
Internal = "internal",
|
||||
External = "external"
|
||||
}
|
||||
|
||||
export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [parentCaType] = useState<ParentCaType>(ParentCaType.Internal);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const caId = (popUp?.installCaCert?.data as { caId: string })?.caId || "";
|
||||
|
||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
const { data: ca } = useGetCaById(caId);
|
||||
const { data: csr } = useGetCaCsr(caId);
|
||||
|
||||
const { mutateAsync: signIntermediate } = useSignIntermediate();
|
||||
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
maxPathLength: "0"
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cas?.length) {
|
||||
setValue("parentCaId", cas[0].id);
|
||||
}
|
||||
}, [cas, setValue]);
|
||||
|
||||
const parentCaId = watch("parentCaId");
|
||||
const { data: parentCa } = useGetCaById(parentCaId);
|
||||
|
||||
useEffect(() => {
|
||||
if (parentCa?.maxPathLength) {
|
||||
setValue(
|
||||
"maxPathLength",
|
||||
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
|
||||
);
|
||||
}
|
||||
|
||||
if (parentCa?.notAfter) {
|
||||
const parentCaNotAfter = new Date(parentCa.notAfter);
|
||||
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
|
||||
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
|
||||
}
|
||||
}, [parentCa]);
|
||||
|
||||
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
|
||||
try {
|
||||
if (!csr || !caId || !currentWorkspace?.slug) return;
|
||||
|
||||
const { certificate, certificateChain } = await signIntermediate({
|
||||
caId: parentCaId,
|
||||
csr,
|
||||
maxPathLength: Number(maxPathLength),
|
||||
notAfter,
|
||||
notBefore: new Date().toISOString()
|
||||
});
|
||||
|
||||
await importCaCertificate({
|
||||
caId,
|
||||
projectSlug: currentWorkspace?.slug,
|
||||
certificate,
|
||||
certificateChain
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully installed certificate for CA",
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpToggle("installCaCert", false);
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to install certificate for CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
|
||||
if (parentCaMaxPathLength === -1) {
|
||||
return [-1, 0, 1, 2, 3];
|
||||
}
|
||||
|
||||
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
|
||||
}
|
||||
|
||||
const renderForm = (parentCaTypeInput: ParentCaType) => {
|
||||
switch (parentCaTypeInput) {
|
||||
case ParentCaType.Internal:
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parentCaId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Parent CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || [])
|
||||
.filter((c) => {
|
||||
const isParentCaNotSelf = c.id !== ca?.id;
|
||||
const isParentCaActive = c.status === CaStatus.ACTIVE;
|
||||
const isParentCaAllowedChildrenCas =
|
||||
c.maxPathLength && c.maxPathLength !== 0;
|
||||
|
||||
return (
|
||||
isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas
|
||||
);
|
||||
})
|
||||
.map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`parent-ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{/* <Controller
|
||||
name="notAfter"
|
||||
control={control}
|
||||
defaultValue={getDefaultNotAfterDate()}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Validity"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4"
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/> */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Valid Until"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxPathLength"
|
||||
// defaultValue="0"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Path Length"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
|
||||
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
|
||||
{`${value}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("installCaCert", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
default:
|
||||
return <div>External TODO</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.installCaCert?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("installCaCert", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Install Intermediate CA certificate">
|
||||
{/* <FormControl label="Parent CA Type" className="mt-4">
|
||||
<Select
|
||||
defaultValue={ParentCaType.Internal}
|
||||
value={parentCaType}
|
||||
onValueChange={(e) => setParentCaType(e as ParentCaType)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem
|
||||
value={ParentCaType.Internal}
|
||||
key={`parent-ca-type-${ParentCaType.Internal}`}
|
||||
>
|
||||
Infisical Private CA
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={ParentCaType.External}
|
||||
key={`parent-ca-type-${ParentCaType.External}`}
|
||||
>
|
||||
External Private CA
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl> */}
|
||||
{renderForm(parentCaType)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { CaInstallCertModal } from "./CaInstallCertModal";
|
||||
@@ -0,0 +1,433 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
// DatePicker
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaType, useCreateCa, useGetCaById } from "@app/hooks/api/ca";
|
||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
};
|
||||
|
||||
const getDateTenYearsFromToday = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(date.getFullYear() + 10);
|
||||
return format(date, "yyyy-MM-dd");
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
||||
friendlyName: z.string(),
|
||||
organization: z.string(),
|
||||
ou: z.string(),
|
||||
country: z.string(),
|
||||
province: z.string(),
|
||||
locality: z.string(),
|
||||
commonName: z.string(),
|
||||
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
|
||||
maxPathLength: z.string(),
|
||||
keyAlgorithm: z.enum([
|
||||
CertKeyAlgorithm.RSA_2048,
|
||||
CertKeyAlgorithm.RSA_4096,
|
||||
CertKeyAlgorithm.ECDSA_P256,
|
||||
CertKeyAlgorithm.ECDSA_P384
|
||||
])
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["ca"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const caTypes = [
|
||||
{ label: "Root", value: CaType.ROOT },
|
||||
{ label: "Intermediate", value: CaType.INTERMEDIATE }
|
||||
];
|
||||
|
||||
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
|
||||
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
||||
const { mutateAsync: createMutateAsync } = useCreateCa();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CaType.ROOT,
|
||||
friendlyName: "",
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
province: "",
|
||||
locality: "",
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1",
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
||||
}
|
||||
});
|
||||
|
||||
const caType = watch("type");
|
||||
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
reset({
|
||||
type: ca.type,
|
||||
friendlyName: ca.friendlyName,
|
||||
organization: ca.organization,
|
||||
ou: ca.ou,
|
||||
country: ca.country,
|
||||
province: ca.province,
|
||||
locality: ca.locality,
|
||||
commonName: ca.commonName,
|
||||
notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "",
|
||||
maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "",
|
||||
keyAlgorithm: ca.keyAlgorithm
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.ROOT,
|
||||
friendlyName: "",
|
||||
organization: "",
|
||||
ou: "",
|
||||
country: "",
|
||||
province: "",
|
||||
locality: "",
|
||||
commonName: "",
|
||||
notAfter: getDateTenYearsFromToday(),
|
||||
maxPathLength: "-1",
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048
|
||||
});
|
||||
}
|
||||
}, [ca]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
friendlyName,
|
||||
commonName,
|
||||
organization,
|
||||
ou,
|
||||
country,
|
||||
locality,
|
||||
province,
|
||||
notAfter,
|
||||
maxPathLength,
|
||||
keyAlgorithm
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await createMutateAsync({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
type,
|
||||
friendlyName,
|
||||
commonName,
|
||||
organization,
|
||||
ou,
|
||||
country,
|
||||
province,
|
||||
locality,
|
||||
notAfter,
|
||||
maxPathLength: Number(maxPathLength),
|
||||
keyAlgorithm
|
||||
});
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("ca", false);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created CA",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.ca?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
reset();
|
||||
handlePopUpToggle("ca", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${ca ? "View" : "Create"} Private CA`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue={CaType.ROOT}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{caTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{caType === CaType.ROOT && (
|
||||
<>
|
||||
{/* <Controller
|
||||
name="notAfter"
|
||||
control={control}
|
||||
defaultValue={getDefaultNotAfterDate()}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Validity"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mr-4"
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={(date) => {
|
||||
onChange(date);
|
||||
setIsStartDatePickerOpen(false);
|
||||
}}
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/> */}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="notAfter"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Valid Until"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="YYYY-MM-DD" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxPathLength"
|
||||
defaultValue="-1"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Path Length"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{[-1, 0, 1, 2, 3, 4].map((value) => (
|
||||
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
|
||||
{`${value}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyAlgorithm"
|
||||
defaultValue={CertKeyAlgorithm.RSA_2048}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Key Algorithm"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
isDisabled={Boolean(ca)}
|
||||
>
|
||||
{certKeyAlgorithms.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My CA" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="organization"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organization (O)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Acme Corp" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="ou"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organization Unit (OU)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Engineering" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="country"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Country Name (C)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="United States (US)" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="province"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="State or Province Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="California" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="locality"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Locality Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="San Francisco" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name (CN)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Example CA" isDisabled={Boolean(ca)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!ca && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.ca?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("ca", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
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, UpgradePlanModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { CaStatus, useDeleteCa, useUpdateCa } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CaCertModal } from "./CaCertModal";
|
||||
import { CaCrlModal } from "./CaCrlModal";
|
||||
import { CaInstallCertModal } from "./CaInstallCertModal";
|
||||
import { CaModal } from "./CaModal";
|
||||
import { CaTable } from "./CaTable";
|
||||
|
||||
export const CaSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCa } = useDeleteCa();
|
||||
const { mutateAsync: updateCa } = useUpdateCa();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"ca",
|
||||
"caCert",
|
||||
"installCaCert",
|
||||
"deleteCa",
|
||||
"caStatus", // enable / disable
|
||||
"caCrl", // enable / disable
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onRemoveCaSubmit = async (caId: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully deleted CA",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete CA",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
|
||||
|
||||
await createNotification({
|
||||
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("caStatus");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("ca")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create CA
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaCrlModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CaTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCa.isOpen}
|
||||
title={`Are you sure want to remove the CA ${
|
||||
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
|
||||
} from the project?`}
|
||||
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.caStatus.isOpen}
|
||||
title={`Are you sure want to ${
|
||||
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||
? "enable"
|
||||
: "disable"
|
||||
} the CA ${(popUp?.caStatus?.data as { dn: string })?.dn || ""} from the project?`}
|
||||
subTitle={
|
||||
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||
? "This action will allow the CA to start issuing certificates again."
|
||||
: "This action will prevent the CA from issuing new certificates."
|
||||
}
|
||||
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFile,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
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,
|
||||
useSubscription,
|
||||
useWorkspace} from "@app/context";
|
||||
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "caCrl", "upgradePlan"]
|
||||
>,
|
||||
data?: {
|
||||
caId?: string;
|
||||
dn?: string;
|
||||
status?: CaStatus;
|
||||
description?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? ""
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Type</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((ca) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`ca-${ca.id}`}>
|
||||
<Td>{ca.friendlyName}</Td>
|
||||
<Td>{caStatusToNameMap[ca.status]}</Td>
|
||||
<Td>{caTypeToNameMap[ca.type]}</Td>
|
||||
<Td>{ca.notAfter ? format(new Date(ca.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">
|
||||
{ca.status === CaStatus.PENDING_CERTIFICATE && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("installCaCert", {
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCertificate} />}
|
||||
>
|
||||
Install Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("caCert", {
|
||||
caId: ca.id
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faCertificate} />}
|
||||
>
|
||||
View Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
if (!subscription?.caCrl) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can use the certificate revocation list (CRL) feature if you upgrade your Infisical plan."
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("caCrl", {
|
||||
caId: ca.id
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faFile} />}
|
||||
>
|
||||
View CRL
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("ca", {
|
||||
caId: ca.id
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View CA
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("caStatus", {
|
||||
caId: ca.id,
|
||||
status:
|
||||
ca.status === CaStatus.ACTIVE
|
||||
? CaStatus.DISABLED
|
||||
: CaStatus.ACTIVE
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
{`${ca.status === CaStatus.ACTIVE ? "Disable" : "Enable"} CA`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("deleteCa", {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete CA
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<EmptyState title="No certificate authorities have been created" icon={faCertificate} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CaSection } from "./CaSection";
|
||||
@@ -0,0 +1 @@
|
||||
export { CaTab } from "./CaTab";
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { CertificatesSection } from "./components";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-certificates"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<CertificatesSection />
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useGetCertBody } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificateCert"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificateCert"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data } = useGetCertBody(
|
||||
(popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificateCert?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateCert", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Export Certificate">
|
||||
{data ? (
|
||||
<CertificateContent
|
||||
serialNumber={data.serialNumber}
|
||||
certificate={data.certificate}
|
||||
certificateChain={data.certificateChain}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey?: string;
|
||||
};
|
||||
|
||||
export const CertificateContent = ({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
}: Props) => {
|
||||
const [copyTextSerialNumber, isCopyingSerialNumber, setCopyTextSerialNumber] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset<string>(
|
||||
{
|
||||
initialState: "Copy to clipboard"
|
||||
}
|
||||
);
|
||||
const [copyTextCertificateChain, isCopyingCertificateChain, setCopyTextCertificateChain] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
const [copyTextCertificateSk, isCopyingCertificateSk, setCopyTextCertificateSk] =
|
||||
useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
|
||||
const downloadTxtFile = (filename: string, content: string) => {
|
||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||
FileSaver.saveAs(blob, filename);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-4">Serial Number</h2>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{serialNumber}</p>
|
||||
<Tooltip content={copyTextSerialNumber}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(serialNumber);
|
||||
setCopyTextSerialNumber("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingSerialNumber ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Body</h2>
|
||||
<div className="flex">
|
||||
<Tooltip content={copyTextCertificate}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(certificate);
|
||||
setCopyTextCertificate("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCertificate ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Download">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("cert.pem", certificate);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{certificate}</p>
|
||||
</div>
|
||||
{certificateChain && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Chain</h2>
|
||||
<div className="flex">
|
||||
<Tooltip content={copyTextCertificateChain}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(certificateChain);
|
||||
setCopyTextCertificateChain("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCertificateChain ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Download">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("chain.pem", certificateChain);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{certificateChain}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{privateKey && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2>Certificate Private Key</h2>
|
||||
<div className="flex">
|
||||
<Tooltip content={copyTextCertificateSk}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setCopyTextCertificateSk("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingCertificateSk ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={copyTextCertificateSk}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
downloadTxtFile("private_key.txt", privateKey);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 whitespace-pre-wrap break-all">{privateKey}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } 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({
|
||||
caId: z.string(),
|
||||
friendlyName: z.string(),
|
||||
commonName: z.string().trim().min(1),
|
||||
ttl: z.string().trim()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["certificate"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: cert } = useGetCert(
|
||||
(popUp?.certificate?.data as { serialNumber: string })?.serialNumber || ""
|
||||
);
|
||||
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
setValue
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
reset({
|
||||
caId: cert.caId,
|
||||
friendlyName: cert.friendlyName,
|
||||
commonName: cert.commonName,
|
||||
ttl: ""
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caId: "",
|
||||
friendlyName: "",
|
||||
commonName: "",
|
||||
ttl: ""
|
||||
});
|
||||
}
|
||||
}, [cert]);
|
||||
|
||||
const onFormSubmit = async ({ caId, friendlyName, commonName, ttl }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
caId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
ttl
|
||||
});
|
||||
|
||||
reset();
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cas?.length) {
|
||||
setValue("caId", cas[0].id);
|
||||
}
|
||||
}, [cas]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.certificate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificate", isOpen);
|
||||
reset();
|
||||
setCertificateDetails(null);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${cert ? "View" : "Issue"} Certificate`}>
|
||||
{!certificateDetails ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<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"
|
||||
isDisabled={Boolean(cert)}
|
||||
>
|
||||
{(cas || []).map(({ id, type, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name (CN)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="service.acme.com" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="ttl"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="TTL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="2 days, 1d, 2h, 1y, ..."
|
||||
isDisabled={Boolean(cert)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!cert && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
) : (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
certificate={certificateDetails.certificate}
|
||||
certificateChain={certificateDetails.certificateChain}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useRevokeCert } from "@app/hooks/api";
|
||||
import { crlReasons } from "@app/hooks/api/certificates/constants";
|
||||
import { CrlReason } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
revocationReason: z.enum([
|
||||
CrlReason.UNSPECIFIED,
|
||||
CrlReason.KEY_COMPROMISE,
|
||||
CrlReason.CA_COMPROMISE,
|
||||
CrlReason.AFFILIATION_CHANGED,
|
||||
CrlReason.SUPERSEDED,
|
||||
CrlReason.CESSATION_OF_OPERATION,
|
||||
CrlReason.CERTIFICATE_HOLD,
|
||||
CrlReason.PRIVILEGE_WITHDRAWN,
|
||||
CrlReason.A_A_COMPROMISE
|
||||
])
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["revokeCertificate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["revokeCertificate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: revokeCertificate } = useRevokeCert();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ revocationReason }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
const {serialNumber} = popUp.revokeCertificate.data as { serialNumber: string };
|
||||
|
||||
await revokeCertificate({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
serialNumber,
|
||||
revocationReason
|
||||
});
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("revokeCertificate", false);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully revoked certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to revoke certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.revokeCertificate?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("revokeCertificate", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Revoke Certificate">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="revocationReason"
|
||||
defaultValue={CrlReason.UNSPECIFIED}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Reason"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{crlReasons.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
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 { useDeleteCert } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||
import { CertificatesTable } from "./CertificatesTable";
|
||||
|
||||
export const CertificatesSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificate",
|
||||
"certificateCert",
|
||||
"deleteCertificate",
|
||||
"revokeCertificate"
|
||||
] as const);
|
||||
|
||||
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await deleteCert({ serialNumber, projectSlug: currentWorkspace.slug });
|
||||
|
||||
await createNotification({
|
||||
text: "Successfully deleted certificate",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteCertificate");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete certificate",
|
||||
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">Certificates</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Issue Certificate
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificate.isOpen}
|
||||
title={`Are you sure want to remove the certificate ${
|
||||
(popUp?.deleteCertificate?.data as { commonName: string })?.commonName || ""
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteCertificate", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveCertificateSubmit(
|
||||
(popUp?.deleteCertificate?.data as { serialNumber: string })?.serialNumber
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} 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";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["certificate", "deleteCertificate", "revokeCertificate", "certificateCert"]
|
||||
>,
|
||||
data?: {
|
||||
serialNumber?: string;
|
||||
commonName?: string;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
const PER_PAGE_INIT = 25;
|
||||
|
||||
export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useListWorkspaceCertificates({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { CertificatesSection } from "./CertificatesSection";
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CaTab } from "./CaTab";
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesPage } from "./CertificatesPage";
|
||||
+14
-2
@@ -4,6 +4,7 @@ import {
|
||||
faAnchorLock,
|
||||
faArrowLeft,
|
||||
faBook,
|
||||
faCertificate,
|
||||
faCog,
|
||||
faKey,
|
||||
faLock,
|
||||
@@ -13,8 +14,7 @@ import {
|
||||
faShield,
|
||||
faTags,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
@@ -117,6 +117,18 @@ const SINGLE_PERMISSION_LIST = [
|
||||
subtitle: "IP allowlist management control",
|
||||
icon: faNetworkWired,
|
||||
formName: "ip-allowlist"
|
||||
},
|
||||
{
|
||||
title: "Certificate Authorities",
|
||||
subtitle: "CA management control",
|
||||
icon: faCertificate,
|
||||
formName: "certificate-authorities"
|
||||
},
|
||||
{
|
||||
title: "Certificates",
|
||||
subtitle: "Certificate management control",
|
||||
icon: faCertificate,
|
||||
formName: "certificates"
|
||||
}
|
||||
] as const;
|
||||
|
||||
|
||||
+2
@@ -48,6 +48,8 @@ export const formSchema = z.object({
|
||||
tags: generalPermissionSchema,
|
||||
"audit-logs": generalPermissionSchema,
|
||||
"ip-allowlist": generalPermissionSchema,
|
||||
"certificate-authorities": generalPermissionSchema,
|
||||
certificates: generalPermissionSchema,
|
||||
// akhilmhdh: refactor all keys like below
|
||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
||||
workspace: z
|
||||
|
||||
+2
@@ -25,6 +25,8 @@ type Props = {
|
||||
| "audit-logs"
|
||||
| "ip-allowlist"
|
||||
| "identity"
|
||||
| "certificate-authorities"
|
||||
| "certificates"
|
||||
| ProjectPermissionSub.SecretApproval;
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
|
||||
+117
-10
@@ -1,29 +1,52 @@
|
||||
import { useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useDeleteWorkspace } from "@app/hooks/api";
|
||||
import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
export const DeleteProjectSection = () => {
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteWorkspace"
|
||||
"deleteWorkspace",
|
||||
"leaveWorkspace"
|
||||
] as const);
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { hasProjectRole, membership } = useProjectPermission();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [isLeaving, setIsLeaving] = useToggle();
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
const leaveProject = useLeaveProject();
|
||||
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(
|
||||
currentWorkspace?.id || ""
|
||||
);
|
||||
|
||||
// If isNoAccessMember is true, then the user can't read the workspace members. So we need to handle this case separately.
|
||||
const isNoAccessMember = hasProjectRole("no-access");
|
||||
|
||||
const isOnlyAdminMember = useMemo(() => {
|
||||
if (!members || !membership || !hasProjectRole("admin")) return false;
|
||||
|
||||
const adminMembers = members.filter(
|
||||
(member) => member.roles.map((r) => r.role).includes("admin") && member.id !== membership.id // exclude the current user
|
||||
);
|
||||
|
||||
return !adminMembers.length;
|
||||
}, [members, membership]);
|
||||
|
||||
const handleDeleteWorkspaceSubmit = async () => {
|
||||
setIsDeleting.on();
|
||||
@@ -53,23 +76,97 @@ export const DeleteProjectSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeaveWorkspaceSubmit = async () => {
|
||||
console.log({
|
||||
currentWorkspace,
|
||||
currentOrg,
|
||||
members,
|
||||
isNoAccessMember,
|
||||
membership
|
||||
});
|
||||
|
||||
try {
|
||||
setIsLeaving.on();
|
||||
|
||||
if (!currentWorkspace?.id || !currentOrg?.id) return;
|
||||
|
||||
// If there's no members, and the user has access to read members, something went wrong.
|
||||
if (!members && !isNoAccessMember) return;
|
||||
|
||||
// If the user has elevated permissions and can read members:
|
||||
if (!isNoAccessMember) {
|
||||
if (!members) return;
|
||||
|
||||
if (members.length < 2) {
|
||||
createNotification({
|
||||
text: "You can't leave the project as you are the only member",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// If the user has access to read members, and there's less than 1 admin member excluding the current user, they can't leave the project.
|
||||
if (isOnlyAdminMember) {
|
||||
createNotification({
|
||||
text: "You can't leave a project with no admin members left. Promote another member to admin first.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's actually a no-access member, then we don't really care about the members.
|
||||
|
||||
await leaveProject.mutateAsync({
|
||||
workspaceId: currentWorkspace.id
|
||||
});
|
||||
|
||||
router.push(`/org/${currentOrg.id}/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to leave project",
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsLeaving.off();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<div className="space-x-4">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Workspace}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isDeleting}
|
||||
isDisabled={!isAllowed || isDeleting}
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handlePopUpOpen("deleteWorkspace")}
|
||||
>
|
||||
{`Delete ${currentWorkspace?.name}`}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isOnlyAdminMember && (
|
||||
<Button
|
||||
isLoading={isDeleting}
|
||||
isDisabled={!isAllowed || isDeleting}
|
||||
disabled={isMembersLoading || (members && members?.length < 2)}
|
||||
isLoading={isLeaving}
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
onClick={() => handlePopUpOpen("deleteWorkspace")}
|
||||
onClick={() => handlePopUpOpen("leaveWorkspace")}
|
||||
>
|
||||
{`Delete ${currentWorkspace?.name}`}
|
||||
{`Leave ${currentWorkspace?.name}`}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteWorkspace.isOpen}
|
||||
title="Are you sure want to delete this project?"
|
||||
@@ -79,6 +176,16 @@ export const DeleteProjectSection = () => {
|
||||
buttonText="Delete Project"
|
||||
onDeleteApproved={handleDeleteWorkspaceSubmit}
|
||||
/>
|
||||
|
||||
<LeaveProjectModal
|
||||
isOpen={popUp.leaveWorkspace.isOpen}
|
||||
title="Are you sure want to leave this project?"
|
||||
subTitle={`If you leave ${currentWorkspace?.name} you will lose access to the project and it's contents.`}
|
||||
onChange={(isOpen) => handlePopUpToggle("leaveWorkspace", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Leave Project"
|
||||
onLeaveApproved={handleLeaveWorkspaceSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { Controller } from "react-hook-form";
|
||||
import { AxiosError } from "axios";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
|
||||
const schema = yup.object({
|
||||
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
||||
expiresAfterViews: yup.number().min(1).required().label("Expires After Views"),
|
||||
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
||||
expiresInUnit: yup.string().required().label("Expiration Unit")
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
export const AddShareSecretForm = ({
|
||||
isPublic,
|
||||
inModal,
|
||||
handleSubmit,
|
||||
control,
|
||||
isSubmitting,
|
||||
setNewSharedSecret
|
||||
}: {
|
||||
isPublic: boolean;
|
||||
inModal: boolean;
|
||||
handleSubmit: any;
|
||||
control: any;
|
||||
isSubmitting: boolean;
|
||||
setNewSharedSecret: (value: string) => void;
|
||||
}) => {
|
||||
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
||||
|
||||
const expirationUnitsAndActions = [
|
||||
{
|
||||
unit: "Minutes",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Hours",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Days",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Weeks",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
|
||||
}
|
||||
];
|
||||
const onFormSubmit = async ({
|
||||
value,
|
||||
expiresInValue,
|
||||
expiresInUnit,
|
||||
expiresAfterViews
|
||||
}: FormData) => {
|
||||
try {
|
||||
const key = crypto.randomBytes(16).toString("hex");
|
||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: value,
|
||||
key
|
||||
});
|
||||
|
||||
const expiresAt = new Date();
|
||||
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||
(item) => item.unit === expiresInUnit
|
||||
)?.action;
|
||||
if (updateExpiresAt && expiresInValue) {
|
||||
updateExpiresAt(expiresAt, expiresInValue);
|
||||
}
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
encryptedValue: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
});
|
||||
setNewSharedSecret(
|
||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||
hashedHex
|
||||
)}-${encodeURIComponent(key)}`
|
||||
);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created a shared secret",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const axiosError = err as AxiosError;
|
||||
if (axiosError?.response?.status === 401) {
|
||||
createNotification({
|
||||
text: "You do not have access to create shared secrets",
|
||||
type: "error"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Failed to create a shared secret",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form className="flex w-full flex-col items-center" onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className={`${!inModal && "border border-mineshaft-600 bg-mineshaft-800 p-4"}`}>
|
||||
<div className="mb-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Shared Secret"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
isVisible
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<div className="w-2/7 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresAfterViews"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-4 w-full"
|
||||
label="Expires After Views"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid number of views"
|
||||
>
|
||||
<Input {...field} type="number" min={1} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/7 flex items-center justify-center px-2">
|
||||
<p className="px-4 text-sm text-gray-400">OR</p>
|
||||
</div>
|
||||
<div className="w-4/7 flex">
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-2/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInValue"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expires after Time"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid time duration"
|
||||
>
|
||||
<Input {...field} type="number" min={0} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-3/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInUnit"
|
||||
defaultValue={expirationUnitsAndActions[0].unit}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Unit" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expirationUnitsAndActions.map(({ unit }) => (
|
||||
<SelectItem value={unit} key={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center ${!inModal && "justify-left pt-1"}`}>
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
{inModal ? "Create" : "Share Secret"}
|
||||
</Button>
|
||||
{inModal && (
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +1,14 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { AxiosError } from "axios";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const expirationUnitsAndActions = [
|
||||
{
|
||||
unit: "Minutes",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Hours",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Days",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
|
||||
},
|
||||
{
|
||||
unit: "Weeks",
|
||||
action: (expiresAt: Date, expiresInValue: number) =>
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
|
||||
}
|
||||
];
|
||||
import { AddShareSecretForm } from "./AddShareSecretForm";
|
||||
import { ViewAndCopySharedSecret } from "./ViewAndCopySharedSecret";
|
||||
|
||||
const schema = yup.object({
|
||||
value: yup.string().max(10000).required().label("Shared Secret Value"),
|
||||
@@ -65,9 +25,11 @@ type Props = {
|
||||
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
isPublic: boolean;
|
||||
inModal: boolean;
|
||||
};
|
||||
|
||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic, inModal }: Props) => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@@ -76,9 +38,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const createSharedSecret = useCreateSharedSecret();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||
|
||||
const [newSharedSecret, setNewSharedSecret] = useState("");
|
||||
const hasSharedSecret = Boolean(newSharedSecret);
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
initialState: false
|
||||
@@ -94,199 +55,54 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}, [isUrlCopied]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
value,
|
||||
expiresInValue,
|
||||
expiresInUnit,
|
||||
expiresAfterViews
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
const key = crypto.randomBytes(16).toString("hex");
|
||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: value,
|
||||
key
|
||||
});
|
||||
|
||||
const expiresAt = new Date();
|
||||
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||
(item) => item.unit === expiresInUnit
|
||||
)?.action;
|
||||
if (updateExpiresAt && expiresInValue) {
|
||||
updateExpiresAt(expiresAt, expiresInValue);
|
||||
}
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
encryptedValue: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
});
|
||||
setnewSharedSecret(
|
||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||
hashedHex
|
||||
)}-${encodeURIComponent(key)}`
|
||||
);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created a shared secret",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const axiosError = err as AxiosError;
|
||||
if (axiosError?.response?.status === 401) {
|
||||
createNotification({
|
||||
text: "You do not have access to create shared secrets",
|
||||
type: "error"
|
||||
});
|
||||
} else {
|
||||
createNotification({
|
||||
text: "Failed to create a shared secret",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return inModal ? (
|
||||
<Modal
|
||||
isOpen={popUp?.createSharedSecret?.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle("createSharedSecret", open);
|
||||
reset();
|
||||
setnewSharedSecret("");
|
||||
setNewSharedSecret("");
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Share a Secret"
|
||||
subTitle="This link is only accessible once. Please share this link with intended recipients. "
|
||||
subTitle="Once you share a secret, the share link is only accessible once."
|
||||
>
|
||||
{!hasSharedSecret ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="value"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Shared Secret"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
isVisible={false}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="w-2/7 flex">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresAfterViews"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-4 w-full"
|
||||
label="Expires After Views"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid number of views"
|
||||
>
|
||||
<Input {...field} type="number" min={1} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/7 flex items-center justify-center px-2">
|
||||
<p className="px-4 text-sm text-gray-400">OR</p>
|
||||
</div>
|
||||
<div className="w-4/7 flex">
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-2/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInValue"
|
||||
defaultValue={6}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Expires after Time"
|
||||
isError={Boolean(error)}
|
||||
errorText="Please enter a valid time duration"
|
||||
>
|
||||
<Input {...field} type="number" min={0} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-3/5 w-full justify-center">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresInUnit"
|
||||
defaultValue={expirationUnitsAndActions[0].unit}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Unit"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{expirationUnitsAndActions.map(({ unit }) => (
|
||||
<SelectItem value={unit} key={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
<AddShareSecretForm
|
||||
isPublic={isPublic}
|
||||
inModal={inModal}
|
||||
control={control}
|
||||
handleSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
setNewSharedSecret={setNewSharedSecret}
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{newSharedSecret}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={copyUrlToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<ViewAndCopySharedSecret
|
||||
inModal={inModal}
|
||||
newSharedSecret={newSharedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : !hasSharedSecret ? (
|
||||
<AddShareSecretForm
|
||||
isPublic={isPublic}
|
||||
inModal={inModal}
|
||||
control={control}
|
||||
handleSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
setNewSharedSecret={setNewSharedSecret}
|
||||
/>
|
||||
) : (
|
||||
<ViewAndCopySharedSecret
|
||||
inModal={inModal}
|
||||
newSharedSecret={newSharedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ShareSecretSection = () => {
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteSharedSecret.mutateAsync({
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id,
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted shared secret",
|
||||
@@ -40,7 +40,6 @@ export const ShareSecretSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<Head>
|
||||
<title>Secret Sharing</title>
|
||||
@@ -60,14 +59,18 @@ export const ShareSecretSection = () => {
|
||||
Share Secret
|
||||
</Button>
|
||||
</div>
|
||||
<ShareSecretsTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddShareSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
isPublic={false}
|
||||
inModal
|
||||
/>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} shared secret?`}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} shared secret?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
|
||||
@@ -75,4 +78,4 @@ export const ShareSecretSection = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton } from "@app/components/v2";
|
||||
|
||||
export const ViewAndCopySharedSecret = ({
|
||||
inModal,
|
||||
newSharedSecret,
|
||||
isUrlCopied,
|
||||
copyUrlToClipboard
|
||||
}: {
|
||||
inModal: boolean;
|
||||
newSharedSecret: string;
|
||||
isUrlCopied: boolean;
|
||||
copyUrlToClipboard: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex w-full justify-center ${!inModal ? "mx-auto max-w-[40rem]" : ""}`}>
|
||||
<div className={`${!inModal ? "border border-mineshaft-600 bg-mineshaft-800 p-4" : ""}`}>
|
||||
<div className="my-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{newSharedSecret}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={copyUrlToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Click to Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,27 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowRight, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { usePopUp, useTimedReset } from "@app/hooks";
|
||||
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal";
|
||||
import { SecretTable } from "./components";
|
||||
|
||||
export const ShareSecretPublicPage = () => {
|
||||
export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean }) => {
|
||||
const router = useRouter();
|
||||
const { id, key: urlEncodedPublicKey } = router.query;
|
||||
const [hashedHex, key] = urlEncodedPublicKey!.toString().split("-");
|
||||
const [hashedHex, key] = urlEncodedPublicKey
|
||||
? urlEncodedPublicKey.toString().split("-")
|
||||
: ["", ""];
|
||||
|
||||
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||
useEffect(() => {
|
||||
if (!id || !publicKey) {
|
||||
router.push("/404");
|
||||
}
|
||||
}, [id, publicKey]);
|
||||
|
||||
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
||||
id as string,
|
||||
hashedHex as string
|
||||
@@ -53,35 +54,107 @@ export const ShareSecretPublicPage = () => {
|
||||
navigator.clipboard.writeText(decryptedSecret);
|
||||
setIsUrlCopied(true);
|
||||
};
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["createSharedSecret"] as const);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
|
||||
<div className="h-screen bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200">
|
||||
<Head>
|
||||
<title>Secret Shared | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
|
||||
<div className="my-4 flex justify-center md:my-8">
|
||||
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
|
||||
</div>
|
||||
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
|
||||
A secret has been shared with you securely via Infisical
|
||||
</p>
|
||||
<div className="flex min-h-screen w-full flex-col md:flex-row">
|
||||
{/* <DragonMainImage /> */}
|
||||
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
|
||||
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
|
||||
Shared Secret
|
||||
</p>
|
||||
<div className="mb-4 rounded-lg md:p-2">
|
||||
<div className="h-screen w-full flex-col items-center justify-center dark:[color-scheme:dark]">
|
||||
<div className="mb-4 flex justify-center pt-8 md:pt-16">
|
||||
<Link href="https://infisical.com">
|
||||
<Image
|
||||
src="/images/gradientLogo.svg"
|
||||
height={90}
|
||||
width={120}
|
||||
alt="Infisical logo"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="mt-6 mb-4 bg-gradient-to-b from-white to-bunker-200 bg-clip-text px-4 text-center text-2xl font-medium text-transparent">
|
||||
{id ? "Someone shared a secret on Infisical with you." : "Share Secrets with Infisical"}
|
||||
</h1>
|
||||
<div className="m-auto mt-8 flex w-full max-w-xl justify-center px-4">
|
||||
{id && (
|
||||
<SecretTable
|
||||
isLoading={isLoading}
|
||||
decryptedSecret={decryptedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isNewSession && (
|
||||
<AddShareSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
inModal={false}
|
||||
isPublic
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-8 px-4 sm:my-8">
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
</div>
|
||||
|
||||
<div className="m-auto max-w-xl px-4">
|
||||
{!isNewSession && (
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-4 pb-4">
|
||||
<Button
|
||||
className="bg-mineshaft-700 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createSharedSecret");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Share your own Secret
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="m-auto mb-8 flex flex max-w-xl flex-col justify-center gap-2 rounded-md border border-primary-500/30 bg-primary/5 p-6">
|
||||
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||
Safe, Secure, & Open Source
|
||||
</p>
|
||||
<p className="md:text-md text-sm">
|
||||
Infisical is the #1 {" "}
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
open source
|
||||
</a>{" "}
|
||||
secrets management platform for developers. <br className="hidden md:inline" />
|
||||
<div className="pb-2" />
|
||||
Infisical Secret Sharing uses end-to-end encrypted architecture to ensure that your secrets are truly private, even from our servers.
|
||||
</p>
|
||||
<Link href="https://infisical.com">
|
||||
<span className="mt-4 cursor-pointer duration-200 hover:text-primary">
|
||||
Learn More <FontAwesomeIcon icon={faArrowRight} />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-0 flex w-full items-center justify-center bg-mineshaft-600 p-2 sm:absolute">
|
||||
<p className="text-center text-sm text-mineshaft-300">
|
||||
© 2024{" "}
|
||||
<a className="text-primary" href="https://infisical.com">
|
||||
Infisical
|
||||
</a>
|
||||
. All rights reserved.
|
||||
<br />
|
||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||
</p>
|
||||
</div>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic inModal />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export const DragonMainImage = () => {
|
||||
return (
|
||||
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
|
||||
<Image
|
||||
src="/images/dragon-book.svg"
|
||||
height={1000}
|
||||
width={1413}
|
||||
alt="Infisical Dragon - Came to send you a secret!"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { EmptyState, IconButton, SecretInput, Td, Tr } from "@app/components/v2";
|
||||
import { EmptyState, IconButton, Td, Tr } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
isLoading: boolean;
|
||||
@@ -16,7 +16,7 @@ export const SecretTable = ({
|
||||
isUrlCopied,
|
||||
copyUrlToClipboard
|
||||
}: Props) => (
|
||||
<div className="flex items-center rounded border border-solid border-mineshaft-700 bg-mineshaft-800 p-4">
|
||||
<div className="flex w-full items-center justify-center rounded border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
||||
{!isLoading && !decryptedSecret && (
|
||||
<Tr>
|
||||
@@ -26,19 +26,23 @@ export const SecretTable = ({
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoading && decryptedSecret && (
|
||||
<>
|
||||
<div className="min-w-[12rem] max-w-[20rem] flex-1 break-words pr-4">
|
||||
<SecretInput isVisible value={decryptedSecret} readOnly />
|
||||
<div className="dark relative flex h-full w-full items-center overflow-y-auto border border-mineshaft-700 bg-mineshaft-900 p-2">
|
||||
<div className="thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll break-words pr-4 dark:[color-scheme:dark]">
|
||||
<div className="align-center flex w-full min-w-full whitespace-pre-line">
|
||||
{decryptedSecret}
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
ariaLabel="copy to clipboard"
|
||||
onClick={copyUrlToClipboard}
|
||||
className="rounded p-2 hover:bg-gray-700"
|
||||
className="mx-1 flex max-h-8 items-center rounded"
|
||||
size="xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||
<FontAwesomeIcon className="pr-2" icon={isUrlCopied ? faCheck : faCopy} /> Copy
|
||||
</IconButton>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { DragonMainImage } from "./MainImage";
|
||||
export { SecretTable } from "./SecretTable";
|
||||
|
||||
@@ -3,7 +3,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
|
||||
import { Button, ContentLoader, FormControl, Input, UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetRateLimit, useUpdateRateLimit } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -21,7 +23,9 @@ type TRateLimitForm = z.infer<typeof formSchema>;
|
||||
|
||||
export const RateLimitPanel = () => {
|
||||
const { data: rateLimit, isLoading } = useGetRateLimit();
|
||||
const { subscription } = useSubscription();
|
||||
const { mutateAsync: updateRateLimit } = useUpdateRateLimit();
|
||||
const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -44,6 +48,11 @@ export const RateLimitPanel = () => {
|
||||
|
||||
const onRateLimitFormSubmit = async (formData: TRateLimitForm) => {
|
||||
try {
|
||||
if (subscription && !subscription.customRateLimits) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
readRateLimit,
|
||||
writeRateLimit,
|
||||
@@ -86,9 +95,7 @@ export const RateLimitPanel = () => {
|
||||
onSubmit={handleSubmit(onRateLimitFormSubmit)}
|
||||
>
|
||||
<div className="mb-8 flex flex-col justify-start">
|
||||
<div className="mb-4 text-xl font-semibold text-mineshaft-100">
|
||||
Configure rate limits
|
||||
</div>
|
||||
<div className="mb-4 text-xl font-semibold text-mineshaft-100">Configure rate limits</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="readRateLimit"
|
||||
@@ -245,6 +252,11 @@ export const RateLimitPanel = () => {
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting || !isDirty}>
|
||||
Save
|
||||
</Button>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can configure custom rate limits if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user