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
+1 -1
View File
@@ -2,7 +2,7 @@ const path = require("path");
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' https://*.posthog.com https://*.*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;
+2 -1
View File
@@ -33,7 +33,8 @@ const integrationSlugNameMapping: Mapping = {
windmill: "Windmill",
"gcp-secret-manager": "GCP Secret Manager",
"hasura-cloud": "Hasura Cloud",
rundeck: "Rundeck"
rundeck: "Rundeck",
"azure-devops": "Azure DevOps"
};
const envMapping: Mapping = {
@@ -1,116 +0,0 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button";
import InputField from "../InputField";
type Props = {
isOpen: boolean;
closeModal: () => void;
submitModal: (email: string) => void;
email: string;
setEmail: (email: string) => void;
orgName: string;
};
const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => {
const submit = () => {
submitModal(email);
};
return (
<div className="z-50">
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
Invite others to {orgName}
</Dialog.Title>
<div className="mt-2 mb-4">
<p className="text-sm text-gray-500">
An invite is specific to an email address and expires after 1 day. For
security reasons, you will need to separately add members to projects.
</p>
</div>
<div className="max-h-28">
<InputField
label="Email"
onChangeHandler={setEmail}
type="varName"
value={email}
placeholder=""
isRequired
/>
</div>
<div className="mt-4 max-w-max">
<Button onButtonPressed={submit} color="mineshaft" text="Invite" size="md" />
</div>
</Dialog.Panel>
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-xl font-medium leading-6 text-gray-300 z-50"
>
Unleash Infisical's Full Power
</Dialog.Title>
<div className="mt-4 mb-4">
<p className="text-sm text-gray-400 mb-2">
You have exceeded the number of members in a free organization.
</p>
<p className="text-sm text-gray-400">
Upgrade now and get access to adding more members, as well as to other powerful enhancements.
</p>
</div>
<div className="mt-6">
<button
type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-sm font-medium text-black hover:opacity-80 hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() => router.push("/settings/billing/" + router.query.id)}
>
Upgrade Now
</button>
<button
type="button"
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-gray-500 hover:text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={closeModal}
>
Maybe Later
</button>
</div>
</Dialog.Panel> */}
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default AddUserDialog;
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useAddUserToOrg } from "@app/hooks/api";
import { useAddUsersToOrg } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -17,7 +17,7 @@ export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState("");
const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync } = useAddUserToOrg();
const { mutateAsync } = useAddUsersToOrg();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
// Redirect user to the getting started page
@@ -31,8 +31,9 @@ export default function TeamInviteStep(): JSX.Element {
.map((email) => email.trim())
.map(async (email) => {
mutateAsync({
inviteeEmail: email,
organizationId: String(localStorage.getItem("orgData.id"))
inviteeEmails: [email],
organizationId: String(localStorage.getItem("orgData.id")),
organizationRoleSlug: "member"
});
});
+1
View File
@@ -93,6 +93,7 @@ export type CompleteAccountDTO = {
salt: string;
verifier: string;
password: string;
tokenMetadata?: string;
};
export type CompleteAccountSignupDTO = CompleteAccountDTO & {
+1 -1
View File
@@ -8,4 +8,4 @@ export {
useSignIntermediate,
useUpdateCa
} from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries";
+10 -8
View File
@@ -7,6 +7,7 @@ import { TCertificateAuthority } from "./types";
export const caKeys = {
getCaById: (caId: string) => [{ caId }, "ca"],
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
@@ -74,16 +75,17 @@ export const useGetCaCsr = (caId: string) => {
});
};
export const useGetCaCrl = (caId: string) => {
export const useGetCaCrls = (caId: string) => {
return useQuery({
queryKey: caKeys.getCaCrl(caId),
queryKey: caKeys.getCaCrls(caId),
queryFn: async () => {
const {
data: { crl }
} = await apiRequest.get<{
crl: string;
}>(`/api/v1/pki/ca/${caId}/crl`);
return crl;
const { data } = await apiRequest.get<
{
id: string;
crl: string;
}[]
>(`/api/v1/pki/ca/${caId}/crls`);
return data;
},
enabled: Boolean(caId)
});
@@ -120,16 +120,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId,
azureDevOpsOrgName,
workspaceSlug
}: {
integrationAuthId: string;
teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string;
}) => {
const params: Record<string, string> = {};
if (teamId) {
params.teamId = teamId;
}
if (azureDevOpsOrgName) {
params.azureDevOpsOrgName = azureDevOpsOrgName;
}
if (workspaceSlug) {
params.workspaceSlug = workspaceSlug;
}
@@ -452,10 +458,12 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId,
azureDevOpsOrgName,
workspaceSlug
}: {
integrationAuthId: string;
teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string;
}) => {
return useQuery({
@@ -464,6 +472,7 @@ export const useGetIntegrationAuthApps = ({
fetchIntegrationAuthApps({
integrationAuthId,
teamId,
azureDevOpsOrgName,
workspaceSlug
}),
enabled: true
+1 -1
View File
@@ -47,7 +47,7 @@ export const roleQueryKeys = {
["user-project-permissions", { workspaceId }] as const
};
const getProjectRoles = async (projectId: string) => {
export const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v1/workspace/${projectId}/roles`
);
@@ -9,7 +9,15 @@ export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name, enforcementLevel }) => {
mutationFn: async ({
environment,
workspaceId,
approvals,
approvers,
secretPath,
name,
enforcementLevel
}) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
workspaceId,
+3 -2
View File
@@ -6,7 +6,7 @@ export {
} from "./mutation";
export {
fetchOrgUsers,
useAddUserToOrg,
useAddUsersToOrg,
useCreateAPIKey,
useDeleteAPIKey,
useDeleteMe,
@@ -26,4 +26,5 @@ export {
useRevokeMySessions,
useUpdateMfaEnabled,
useUpdateOrgMembership,
useUpdateUserAuthMethods} from "./queries";
useUpdateUserAuthMethods
} from "./queries";
+5 -2
View File
@@ -157,12 +157,15 @@ export const useGetOrgUsers = (orgId: string) =>
// mutation
// TODO(akhilmhdh): move all mutation to mutation file
export const useAddUserToOrg = () => {
export const useAddUsersToOrg = () => {
const queryClient = useQueryClient();
type Response = {
data: {
message: string;
completeInviteLink: string | undefined;
completeInviteLinks?: {
email: string;
link: string;
}[];
};
};
+4 -1
View File
@@ -149,7 +149,10 @@ export type DeletOrgMembershipDTO = {
};
export type AddUserToOrgDTO = {
inviteeEmail: string;
inviteeEmails: string[];
projectIds?: string[];
projectRoleSlug?: string;
organizationRoleSlug: string;
organizationId: string;
};
+8 -4
View File
@@ -138,8 +138,12 @@ export const useGetUpgradeProjectStatus = ({
});
};
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
params: {
includeRoles
}
});
return data.workspaces;
};
@@ -171,8 +175,8 @@ export const useGetWorkspaceById = (
});
};
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
@@ -1,3 +1,5 @@
import { TProjectRole } from "../roles/types";
export enum ProjectVersion {
V1 = 1,
V2 = 2,
@@ -22,6 +24,8 @@ export type Workspace = {
auditLogsRetentionDays: number;
slug: string;
createdAt: string;
roles?: TProjectRole[];
};
export type WorkspaceEnv = {
+10 -1
View File
@@ -8,6 +8,7 @@ type UseToggleReturn = [
on: VoidFn;
off: VoidFn;
toggle: VoidFn;
timedToggle: (timeout?: number) => void;
}
];
@@ -26,5 +27,13 @@ export const useToggle = (initialState = false): UseToggleReturn => {
setValue((prev) => (typeof isOpen === "boolean" ? isOpen : !prev));
}, []);
return [value, { on, off, toggle }];
const timedToggle = useCallback((timeout = 2000) => {
setValue((prev) => !prev);
setTimeout(() => {
setValue(false);
}, timeout);
}, []);
return [value, { on, off, toggle, timedToggle }];
};
@@ -0,0 +1,80 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
export default function AzureDevopsCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useSaveIntegrationAccessToken();
const [apiKey, setApiKey] = useState("");
const [devopsOrgName, setDevopsOrgName] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
localStorage.setItem("azure-devops-org-name", devopsOrgName);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "azure-devops",
accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username
});
setIsLoading(false);
router.push(`/integrations/azure-devops/create?integrationAuthId=${integrationAuth.id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
<FormControl
label="AzureDevops API Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<FormControl
label="Azure DevOps Organization Name"
tooltipText="This is the slug of the organization. An example would be 'my-acme-org'"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input
placeholder=""
value={devopsOrgName}
onChange={(e) => setDevopsOrgName(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to AzureDevOps
</Button>
</Card>
</div>
);
}
AzureDevopsCreateIntegrationPage.requireAuth = true;
@@ -0,0 +1,150 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
export default function AzureDevopsCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
azureDevOpsOrgName: localStorage.getItem("azure-devops-org-name") ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [targetApp, setTargetApp] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?.id) return;
setIsLoading(true);
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
app: localStorage.getItem("azure-devops-org-name") || "",
appId: targetApp,
sourceEnvironment: selectedSourceEnvironment,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AzureDevops Project" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-environment-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
AzureDevopsCreateIntegrationPage.requireAuth = true;
@@ -38,6 +38,7 @@ export default function LoginPage() {
const { user, isLoading: userLoading } = useUser();
const queryParams = new URLSearchParams(window.location.search);
const callbackPort = queryParams.get("callback_port");
const logout = useLogoutUser(true);
const handleLogout = useCallback(async () => {
@@ -52,8 +53,6 @@ export default function LoginPage() {
const handleSelectOrganization = useCallback(
async (organization: Organization) => {
const callbackPort = queryParams.get("callback_port");
if (organization.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
@@ -116,9 +115,8 @@ export default function LoginPage() {
[selectOrg]
);
useEffect(() => {
const handleCliRedirect = useCallback(() => {
const authToken = getAuthToken();
const callbackPort = queryParams.get("callback_port");
if (authToken && !callbackPort) {
const decodedJwt = jwt_decode(authToken) as any;
@@ -131,13 +129,27 @@ export default function LoginPage() {
if (!isLoggedIn()) {
router.push("/login");
}
}, []);
useEffect(() => {
if (callbackPort) {
handleCliRedirect();
}
}, [router]);
// Case: User has no organizations.
// This can happen if the user was previously a member, but the organization was deleted or the user was removed.
useEffect(() => {
if (!organizations.isLoading && organizations.data?.length === 0) {
if (organizations.isLoading || !organizations.data) return;
if (organizations.data.length === 0) {
router.push("/org/none");
} else if (organizations.data.length === 1) {
if (callbackPort) {
handleCliRedirect();
} else {
handleSelectOrganization(organizations.data[0]);
}
}
}, [organizations.isLoading, organizations.data]);
+6 -1
View File
@@ -64,6 +64,10 @@ export default function SignupInvite() {
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const metadata = queryParams.get("metadata") || undefined;
const { mutateAsync: selectOrganization } = useSelectOrganization();
useEffect(() => {
@@ -160,7 +164,8 @@ export default function SignupInvite() {
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
verifier: result.verifier,
tokenMetadata: metadata
});
// unset temporary signup JWT token and set JWT token
@@ -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">