mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 20:26:17 +00:00
Fix: Move standalone components to individual files
This commit is contained in:
+266
@@ -0,0 +1,266 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateAccessApprovalPolicy,
|
||||||
|
useUpdateAccessApprovalPolicy
|
||||||
|
} from "@app/hooks/api/accessApproval";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
members?: TWorkspaceUser[];
|
||||||
|
projectSlug: string;
|
||||||
|
editValues?: TAccessApprovalPolicy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
environment: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
approvals: z.number().min(1),
|
||||||
|
approvers: z.string().array().min(1)
|
||||||
|
})
|
||||||
|
.refine((data) => data.approvals <= data.approvers.length, {
|
||||||
|
path: ["approvals"],
|
||||||
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const AccessPolicyForm = ({
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
members = [],
|
||||||
|
projectSlug,
|
||||||
|
editValues
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) reset({});
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const isEditMode = Boolean(editValues);
|
||||||
|
|
||||||
|
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||||
|
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
||||||
|
|
||||||
|
const handleCreatePolicy = async (data: TFormSchema) => {
|
||||||
|
if (!projectSlug) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createAccessApprovalPolicy({
|
||||||
|
...data,
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created policy"
|
||||||
|
});
|
||||||
|
onToggle(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePolicy = async (data: TFormSchema) => {
|
||||||
|
if (!projectSlug) return;
|
||||||
|
if (!editValues?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateAccessApprovalPolicy({
|
||||||
|
id: editValues?.id,
|
||||||
|
...data,
|
||||||
|
projectSlug
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated policy"
|
||||||
|
});
|
||||||
|
onToggle(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "failed to update policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: TFormSchema) => {
|
||||||
|
if (isEditMode) {
|
||||||
|
await handleUpdatePolicy(data);
|
||||||
|
} else {
|
||||||
|
await handleCreatePolicy(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
|
<ModalContent title={isEditMode ? "Edit Access Policy" : "Create Access Policy"}>
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Policy Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Environment"
|
||||||
|
isRequired
|
||||||
|
className="mt-4"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={isEditMode}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} value={field.value || ""} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="approvers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Approvers Required"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={value?.length ? `${value.length} selected` : "None"}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
Select members that are allowed to approve changes
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{members.map(({ id, user }) => {
|
||||||
|
const isChecked = value?.includes(id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`create-policy-members-${id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="approvals"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Approvals Required"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => onToggle(false)} variant="outline_bg">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection";
|
||||||
|
|
||||||
|
export const RequestAccessModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
policies
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
policies: TAccessApprovalPolicy[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-4xl"
|
||||||
|
title="Request Access"
|
||||||
|
subTitle="Your role has limited permissions, please contact your administrator to gain access"
|
||||||
|
>
|
||||||
|
<SpecificPrivilegeSecretForm onClose={() => onOpenChange(false)} policies={policies} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
|
import { ProjectPermissionActions } from "@app/context";
|
||||||
|
import { useReviewAccessRequest } from "@app/hooks/api";
|
||||||
|
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
|
||||||
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
export const ReviewAccessRequestModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
request,
|
||||||
|
projectSlug,
|
||||||
|
selectedRequester,
|
||||||
|
selectedEnvSlug
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null };
|
||||||
|
projectSlug: string;
|
||||||
|
selectedRequester: string | undefined;
|
||||||
|
selectedEnvSlug: string | undefined;
|
||||||
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
|
||||||
|
|
||||||
|
const accessDetails = {
|
||||||
|
env: request.environmentName,
|
||||||
|
// secret path will be inside $glob operator
|
||||||
|
secretPath: request.policy.secretPath,
|
||||||
|
read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)),
|
||||||
|
edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)),
|
||||||
|
create: request.permissions?.some(({ action }) =>
|
||||||
|
action.includes(ProjectPermissionActions.Create)
|
||||||
|
),
|
||||||
|
delete: request.permissions?.some(({ action }) =>
|
||||||
|
action.includes(ProjectPermissionActions.Delete)
|
||||||
|
),
|
||||||
|
|
||||||
|
temporaryAccess: {
|
||||||
|
isTemporary: request.isTemporary,
|
||||||
|
temporaryRange: request.temporaryRange
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestedAccess = useMemo(() => {
|
||||||
|
const access: string[] = [];
|
||||||
|
if (accessDetails.read) access.push("Read");
|
||||||
|
if (accessDetails.edit) access.push("Edit");
|
||||||
|
if (accessDetails.create) access.push("Create");
|
||||||
|
if (accessDetails.delete) access.push("Delete");
|
||||||
|
|
||||||
|
return access.join(", ");
|
||||||
|
}, [accessDetails]);
|
||||||
|
|
||||||
|
const getAccessLabel = () => {
|
||||||
|
if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange)
|
||||||
|
return "Permanent";
|
||||||
|
|
||||||
|
// convert the range to human readable format
|
||||||
|
ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge>
|
||||||
|
{`Valid for ${ms(ms(accessDetails.temporaryAccess.temporaryRange), {
|
||||||
|
long: true
|
||||||
|
})} after approval`}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reviewAccessRequest = useReviewAccessRequest();
|
||||||
|
|
||||||
|
const handleReview = useCallback(async (status: "approved" | "rejected") => {
|
||||||
|
setIsLoading(status);
|
||||||
|
try {
|
||||||
|
await reviewAccessRequest.mutateAsync({
|
||||||
|
requestId: request.id,
|
||||||
|
status,
|
||||||
|
projectSlug,
|
||||||
|
envSlug: selectedEnvSlug,
|
||||||
|
requestedBy: selectedRequester
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setIsLoading(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
title: `Request ${status}`,
|
||||||
|
text: `The request has been ${status}`,
|
||||||
|
type: status === "approved" ? "success" : "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(null);
|
||||||
|
onOpenChange(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-4xl"
|
||||||
|
title="Review Request"
|
||||||
|
subTitle="Review the request and approve or deny access."
|
||||||
|
>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span>
|
||||||
|
<span className="font-bold">
|
||||||
|
{request.user?.firstName} {request.user?.lastName} ({request.user?.email})
|
||||||
|
</span>{" "}
|
||||||
|
is requesting access to the following resource:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="mt-4 mb-2 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
||||||
|
<div className="mb-1 lowercase">
|
||||||
|
<span className="font-bold capitalize">Requested path: </span>
|
||||||
|
<Badge>{accessDetails.env + accessDetails.secretPath || ""}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1">
|
||||||
|
<span className="font-bold">Permissions: </span>
|
||||||
|
<Badge>{requestedAccess}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-bold">Access Type: </span>
|
||||||
|
<span>{getAccessLabel()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "approved"}
|
||||||
|
isDisabled={!!isLoading}
|
||||||
|
onClick={() => handleReview("approved")}
|
||||||
|
className="mt-4"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Approve Request
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading === "rejected"}
|
||||||
|
isDisabled={!!isLoading}
|
||||||
|
onClick={() => handleReview("rejected")}
|
||||||
|
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Reject Request
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user