Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll

This commit is contained in:
Sheen Capadngan
2024-08-27 02:13:59 +08:00
98 changed files with 2176 additions and 827 deletions
@@ -131,6 +131,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "rundeck":
link = `${window.location.origin}/integrations/rundeck/authorize`;
break;
case "azure-devops":
link = `${window.location.origin}/integrations/azure-devops/authorize`;
break;
default:
break;
}
@@ -1,65 +1,144 @@
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import {
faCheckCircle,
faChevronDown,
faExclamationCircle
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, Modal, ModalContent } from "@app/components/v2";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
Modal,
ModalContent,
Select,
SelectItem,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import { useAddUserToOrg, useFetchServerStatus } from "@app/hooks/api";
import {
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetUserWorkspaces
} from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim().lowercase()
import { OrgInviteLink } from "./OrgInviteLink";
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
const EmailSchema = z.string().email().min(1).trim().toLowerCase();
const addMemberFormSchema = z.object({
emails: z.string().min(1).trim().toLowerCase(),
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
});
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
type Props = {
popUp: UsePopUpState<["addMember"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
completeInviteLink: string;
setCompleteInviteLink: (link: string) => void;
completeInviteLinks: Array<{
email: string;
link: string;
}> | null;
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
};
export const AddOrgMemberModal = ({
popUp,
handlePopUpToggle,
completeInviteLink,
setCompleteInviteLink
completeInviteLinks,
setCompleteInviteLinks
}: Props) => {
const { currentOrg } = useOrganization();
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { data: projects } = useGetUserWorkspaces(true);
const {
control,
handleSubmit,
watch,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
const onAddMember = async ({ email }: TAddMemberForm) => {
const selectedProjectIds = watch("projectIds", []);
const onAddMembers = async ({
emails,
organizationRoleSlug,
projectIds,
projectRoleSlug
}: TAddMemberForm) => {
if (!currentOrg?.id) return;
const selectedProjects = projects?.filter((project) => projectIds.includes(String(project.id)));
if (selectedProjects?.length) {
// eslint-disable-next-line no-restricted-syntax
for (const project of selectedProjects) {
if (project.version !== ProjectVersion.V3) {
createNotification({
type: "error",
text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.`
});
return;
}
}
}
try {
const { data } = await addUserMutateAsync({
const parsedEmails = emails
.replace(/\s/g, "")
.split(",")
.map((email) => {
if (EmailSchema.safeParse(email).success) {
return email.trim();
}
return null;
});
if (parsedEmails.includes(null)) {
createNotification({
text: "Invalid email addresses provided.",
type: "error"
});
return;
}
const { data } = await addUsersMutateAsync({
organizationId: currentOrg?.id,
inviteeEmail: email
inviteeEmails: emails.split(",").map((email) => email.trim()),
organizationRoleSlug,
projectIds,
projectRoleSlug
});
setCompleteInviteLink(data?.completeInviteLink ?? "");
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
// only show this notification when email is configured.
// A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink) {
if (!data.completeInviteLinks) {
createNotification({
text: "Successfully invited user to the organization.",
type: "success"
@@ -80,47 +159,196 @@ export const AddOrgMemberModal = ({
reset();
};
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
setInviteLinkCopied.on();
};
return (
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink("");
setCompleteInviteLinks(null);
}}
>
<ModalContent
title={`Invite others to ${currentOrg?.name}`}
subTitle={
<div>
{!completeInviteLink && (
<div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>
{!completeInviteLinks && (
<div>An invite is specific to an email address and expires after 1 day.</div>
)}
{completeInviteLink &&
{completeInviteLinks &&
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
}
>
{!completeInviteLink && (
<form onSubmit={handleSubmit(onAddMember)}>
{!completeInviteLinks && (
<form onSubmit={handleSubmit(onAddMembers)}>
<Controller
control={control}
defaultValue=""
name="email"
name="emails"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
<FormControl label="Emails" isError={Boolean(error)} errorText={error?.message}>
<TextArea
{...field}
className="mt-1 h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 transition-all placeholder:text-bunker-400 focus:ring-2"
placeholder="[email protected], [email protected]..."
/>
</FormControl>
)}
/>
<Controller
control={control}
name="organizationRoleSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Select which organization role you want to assign to the user."
label="Assign organization role"
isError={Boolean(error)}
errorText={error?.message}
>
<div>
<Select
className="w-full"
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>
{organizationRoles?.map((role) => (
<SelectItem key={role.id} value={role.slug}>
{role.name}
</SelectItem>
))}
</Select>
</div>
</FormControl>
)}
/>
<div className="flex items-center justify-between gap-2">
<div className="w-full">
<Controller
control={control}
name="projectIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assign users to projects (optional)"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{projects && projects.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedProjectIds.length === 1
? projects.find((project) => project.id === selectedProjectIds[0])
?.name
: selectedProjectIds.length === 0
? "No projects selected"
: `${selectedProjectIds.length} projects selected`}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No projects found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{projects && projects.length > 0 ? (
projects.map((project) => {
const isSelected = selectedProjectIds.includes(String(project.id));
return (
<DropdownMenuItem
onSelect={(event) =>
projects.length > 1 && event.preventDefault()
}
onClick={() => {
if (selectedProjectIds.includes(String(project.id))) {
field.onChange(
selectedProjectIds.filter(
(projectId: string) => projectId !== String(project.id)
)
);
} else {
field.onChange([...selectedProjectIds, String(project.id)]);
}
}}
key={`project-id-${project.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
<div className="flex items-center gap-2 capitalize">
{project.name}
{project.version !== ProjectVersion.V3 && (
<Tooltip content="Project is not compatible with this action, please upgrade this project.">
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-xs opacity-50"
/>
</Tooltip>
)}
</div>
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<div className="flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Select which role to assign to the users in the selected projects."
label="Role"
isError={Boolean(error)}
errorText={error?.message}
>
<div>
<Select
isDisabled={selectedProjectIds.length === 0}
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>
{Object.entries(ProjectMembershipRole).map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, slug]) =>
slug !== "custom" && (
<SelectItem key={slug} value={slug}>
<span className="capitalize">{slug.replace("-", " ")}</span>
</SelectItem>
)
)}
</Select>
</div>
</FormControl>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -141,20 +369,11 @@ export const AddOrgMemberModal = ({
</div>
</form>
)}
{completeInviteLink && (
<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">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? 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>
{completeInviteLinks && (
<div className="space-y-3">
{completeInviteLinks.map((invite) => (
<OrgInviteLink key={`invite-${invite.email}`} invite={invite} />
))}
</div>
)}
</ModalContent>
@@ -0,0 +1,49 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { IconButton, Tooltip } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
invite: { email: string; link: string };
};
export const OrgInviteLink = ({ invite }: Props) => {
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const copyTokenToClipboard = () => {
if (isInviteLinkCopied) return;
navigator.clipboard.writeText(invite.link);
setInviteLinkCopied.timedToggle();
createNotification({
type: "info",
text: "Copied invitation link to clipboard"
});
};
return (
<div key={`invite-${invite.email}`}>
<p className="text-sm text-mineshaft-400">
Invite for <span className="font-medium">{invite.email}</span>
</p>
<div className="flex flex-col gap-1 rounded-md bg-white/[0.04] p-2 text-base text-gray-400">
<p className="line-clamp-1 mr-4 overflow-hidden text-ellipsis whitespace-nowrap ">
{invite.link}
</p>
<Tooltip content={`Copy invitation link for ${invite.email}`}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => copyTokenToClipboard()}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
);
};
@@ -27,7 +27,10 @@ export const OrgMembersSection = () => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id ?? "";
const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
const [completeInviteLinks, setCompleteInviteLinks] = useState<Array<{
email: string;
link: string;
}> | null>(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addMember",
@@ -132,13 +135,13 @@ export const OrgMembersSection = () => {
</div>
<OrgMembersTable
handlePopUpOpen={handlePopUpOpen}
setCompleteInviteLink={setCompleteInviteLink}
setCompleteInviteLinks={setCompleteInviteLinks}
/>
<AddOrgMemberModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
completeInviteLink={completeInviteLink}
setCompleteInviteLink={setCompleteInviteLink}
completeInviteLinks={completeInviteLinks}
setCompleteInviteLinks={setCompleteInviteLinks}
/>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
@@ -33,7 +33,7 @@ import {
useUser
} from "@app/context";
import {
useAddUserToOrg,
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetOrgUsers,
@@ -50,10 +50,10 @@ type Props = {
description?: string;
}
) => void;
setCompleteInviteLink: (link: string) => void;
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
const router = useRouter();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
@@ -68,7 +68,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const { data: serverDetails } = useFetchServerStatus();
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const onRoleChange = async (membershipId: string, role: string) => {
@@ -106,14 +106,15 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const onResendInvite = async (email: string) => {
try {
const { data } = await addUserMutateAsync({
const { data } = await addUsersMutateAsync({
organizationId: orgId,
inviteeEmail: email
inviteeEmails: [email],
organizationRoleSlug: "member"
});
setCompleteInviteLink(data?.completeInviteLink || "");
setCompleteInviteLinks(data?.completeInviteLinks || null);
if (!data.completeInviteLink) {
if (!data.completeInviteLinks) {
createNotification({
text: `Successfully resent invite to ${email}`,
type: "success"
@@ -18,7 +18,7 @@ import {
} from "@app/context";
import { useTimedReset } from "@app/hooks";
import {
useAddUserToOrg,
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgMembership,
useGetOrgRoles
@@ -44,18 +44,19 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
const { data: roles } = useGetOrgRoles(orgId);
const { data: serverDetails } = useFetchServerStatus();
const { data: membership } = useGetOrgMembership(orgId, membershipId);
const { mutateAsync: inviteUser, isLoading } = useAddUserToOrg();
const { mutateAsync: inviteUsers, isLoading } = useAddUsersToOrg();
const onResendInvite = async (email: string) => {
try {
const { data } = await inviteUser({
const { data } = await inviteUsers({
organizationId: orgId,
inviteeEmail: email
inviteeEmails: [email],
organizationRoleSlug: "member"
});
// setCompleteInviteLink(data?.completeInviteLink || "");
if (!data.completeInviteLink) {
if (!data.completeInviteLinks) {
createNotification({
text: `Successfully resent invite to ${email}`,
type: "success"
+10 -2
View File
@@ -22,7 +22,12 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components";
import {
CaCertificatesSection,
CaCrlsSection,
CaDetailsSection,
CaRenewalModal
} from "./components";
export const CaPage = withProjectPermission(
() => {
@@ -118,7 +123,10 @@ export const CaPage = withProjectPermission(
<div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
</div>
<CaCertificatesSection caId={caId} />
<div className="w-full">
<CaCertificatesSection caId={caId} />
<CaCrlsSection caId={caId} />
</div>
</div>
</div>
)}
@@ -0,0 +1,20 @@
import { CaCrlsTable } from "./CaCrlsTable";
type Props = {
caId: string;
};
export const CaCrlsSection = ({ caId }: Props) => {
return (
<div className="mt-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">
CA Certificate Revocation Lists (CRLs)
</h3>
</div>
<div className="py-4">
<CaCrlsTable caId={caId} />
</div>
</div>
);
};
@@ -0,0 +1,88 @@
import { faCertificate, faFileDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import * as x509 from "@peculiar/x509";
// import { format } from "date-fns";
import FileSaver from "file-saver";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useGetCaCrls } from "@app/hooks/api";
type Props = {
caId: string;
};
export const CaCrlsTable = ({ caId }: Props) => {
const { data: caCrls, isLoading } = useGetCaCrls(caId);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Distribution Point URL</Th>
{/* <Th>This Update</Th> */}
{/* <Th>Next Update</Th> */}
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="ca-certificates" />}
{!isLoading &&
caCrls?.map(({ id, crl }) => {
// const caCrlObj = new x509.X509Crl(crl);
return (
<Tr key={`ca-crl-${id}`}>
<Td>
<div className="flex items-center">
{`${window.origin}/api/v1/pki/crl/${id}`}
</div>
</Td>
{/* <Td>{format(new Date(caCrlObj.thisUpdate), "yyyy-MM-dd")}</Td> */}
{/* <Td>
{caCrlObj.nextUpdate
? format(new Date(caCrlObj.nextUpdate), "yyyy-MM-dd")
: "-"}
</Td> */}
<Td>
<Tooltip content="Download CRL">
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
downloadTxtFile("crl.pem", crl);
}}
>
<FontAwesomeIcon icon={faFileDownload} />
</IconButton>
</Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && !caCrls?.length && (
<EmptyState title="This CA does not have any CRLs" icon={faCertificate} />
)}
</TableContainer>
);
};
@@ -0,0 +1 @@
export { CaCrlsSection } from "./CaCrlsSection";
@@ -1,3 +1,4 @@
export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection";
export { CaCrlsSection } from "./CaCrlsSection";
export { CaDetailsSection } from "./CaDetailsSection";
export { CaRenewalModal } from "./CaRenewalModal";
@@ -1,106 +0,0 @@
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>
);
};
@@ -9,7 +9,6 @@ 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";
@@ -25,7 +24,6 @@ export const CaSection = () => {
"installCaCert",
"deleteCa",
"caStatus", // enable / disable
"caCrl", // enable / disable
"upgradePlan"
] as const);
@@ -95,7 +93,6 @@ export const CaSection = () => {
<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}
@@ -4,7 +4,6 @@ import {
faCertificate,
faEllipsis,
faEye,
faFile,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -29,12 +28,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
import {
caStatusToNameMap,
@@ -46,7 +40,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "caCrl", "upgradePlan"]
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "upgradePlan"]
>,
data?: {
caId?: string;
@@ -59,7 +53,6 @@ type Props = {
export const CaTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? ""
@@ -162,38 +155,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)}
</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={(e) => {
e.stopPropagation();
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}
@@ -32,6 +32,7 @@ import {
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
@@ -82,9 +83,11 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.friendlyName}</Td>
<Td>
<Badge className="" variant={variant}>
{label}
</Badge>
{certificate.status === CertStatus.REVOKED ? (
<Badge variant="danger">Revoked</Badge>
) : (
<Badge variant={variant}>{label}</Badge>
)}
</Td>
<Td>
{certificate.notBefore
@@ -45,11 +45,11 @@ const formSchema = z
name: z.string().optional(),
secretPath: z.string().optional(),
approvals: z.number().min(1),
approverUserIds: z.string().array().min(1),
approvers: z.string().array().min(1),
policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
});
@@ -75,8 +75,7 @@ export const AccessPolicyForm = ({
? {
...editValues,
environment: editValues.environment.slug,
approverUserIds:
editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
}
: undefined
});
@@ -264,7 +263,7 @@ export const AccessPolicyForm = ({
/>
<Controller
control={control}
name="approverUserIds"
name="approvers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Approvers"
@@ -1,4 +1,5 @@
import { Fragment, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faCheck,
faCheckCircle,
@@ -31,7 +32,7 @@ import {
useGetSecretApprovalRequests,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { ApprovalStatus, TSecretApprovalRequest } from "@app/hooks/api/types";
import { ApprovalStatus } from "@app/hooks/api/types";
import {
generateCommitText,
@@ -41,12 +42,13 @@ import {
export const SecretApprovalRequest = () => {
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const [selectedApproval, setSelectedApproval] = useState<TSecretApprovalRequest | null>(null);
const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null);
// filters
const [statusFilter, setStatusFilter] = useState<"open" | "close">("open");
const [envFilter, setEnvFilter] = useState<string>();
const [committerFilter, setCommitterFilter] = useState<string>();
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
const {
data: secretApprovalRequests,
@@ -64,12 +66,21 @@ export const SecretApprovalRequest = () => {
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
useGetSecretApprovalRequestCount({ workspaceId });
const { user: userSession } = useUser();
const router = useRouter();
const { permission } = useProjectPermission();
const { data: members } = useGetWorkspaceUsers(workspaceId);
const isSecretApprovalScreen = Boolean(selectedApproval);
const isSecretApprovalScreen = Boolean(selectedApprovalId);
const { requestId } = router.query;
useEffect(() => {
if (!requestId || usingUrlRequestId) return;
setSelectedApprovalId(requestId as string);
setUsingUrlRequestId(true);
}, [requestId]);
const handleGoBackSecretRequestDetail = () => {
setSelectedApproval(null);
setSelectedApprovalId(null);
refetch({ refetchPage: (_page, index) => index === 0 });
};
@@ -88,7 +99,7 @@ export const SecretApprovalRequest = () => {
>
<SecretApprovalRequestChanges
workspaceId={workspaceId}
approvalRequestId={selectedApproval?.id || ""}
approvalRequestId={selectedApprovalId || ""}
onGoBack={handleGoBackSecretRequestDetail}
/>
</motion.div>
@@ -219,9 +230,9 @@ export const SecretApprovalRequest = () => {
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setSelectedApproval(secretApproval)}
onClick={() => setSelectedApprovalId(secretApproval.id)}
onKeyDown={(evt) => {
if (evt.key === "Enter") setSelectedApproval(secretApproval);
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
}}
>
<div className="mb-1">