mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-05 07:30:33 +00:00
Compare commits
18 Commits
secret-sha
...
secret-sha
Author | SHA1 | Date | |
---|---|---|---|
|
7e9389cb26 | ||
|
eda57881ec | ||
|
553d51e5b3 | ||
|
16e0a441ae | ||
|
d6c0941fa9 | ||
|
4b83b92725 | ||
|
fe72f034c1 | ||
|
6803553b21 | ||
|
1c8299054a | ||
|
98b6373d6a | ||
|
1d97921c7c | ||
|
61ebec25b3 | ||
|
de886f8dd0 | ||
|
b3db29ac37 | ||
|
ce1db38afd | ||
|
9dd675ff98 | ||
|
8fd3e50d04 | ||
|
391ed0723e |
@@ -336,31 +336,36 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: this part can be optimized
|
||||
for await (const userId of userIds) {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (const userId of userIds) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
);
|
||||
})()
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if (membersToRemoveFromGroupPending.length) {
|
||||
|
23
docs/documentation/guides/migrating-from-envkey.mdx
Normal file
23
docs/documentation/guides/migrating-from-envkey.mdx
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "Migrating from EnvKey to Infisical"
|
||||
sidebarTitle: "Migration"
|
||||
description: "Learn how to migrate from EnvKey to Infisical in the easiest way possible."
|
||||
---
|
||||
|
||||
## What is Infisical?
|
||||
|
||||
[Infisical](https://infisical.com) is an open-source all-in-one secret management platform that helps developers manage secrets (e.g., API-keys, DB access tokens, [certificates](https://infisical.com/docs/documentation/platform/pki/overview)) across their infrastructure. In addition, Infisical provides [secret sharing](https://infisical.com/docs/documentation/platform/secret-sharing) functionality, ability to [prevent secret leaks](https://infisical.com/docs/cli/scanning-overview), and more.
|
||||
|
||||
Infisical is used by 10,000+ organizations across all indsutries including First American Financial Corporation, Deivery Hero, and [Hugging Face](https://infisical.com/customers/hugging-face).
|
||||
|
||||
## Migrating from EnvKey
|
||||
|
||||
To facilitate customer transition from EnvKey to Infisical, we have been working closely with the EnvKey team to provide a simple migration path for all EnvKey customers.
|
||||
|
||||
## Automated migration
|
||||
|
||||
Our team is currently working on creating an automated migration process that would include secrets, policies, and other important resources. If you are interested in that, please [reach out to our team](mailto:support@infisical.com) with any questions.
|
||||
|
||||
## Talk to our team
|
||||
|
||||
To make the migration process even more seamless, you can [schedule a meeting with our team](https://infisical.cal.com/vlad/migration-from-envkey-to-infisical) to learn more about how Infisical compares to EnvKey and discuss unique needs of your organization. You are also welcome to email us at [support@infisical.com](mailto:support@infisical.com) to ask any questions or get any technical help.
|
@@ -25,7 +25,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
return data ? (
|
||||
<div className="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">Details</h3>
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
|
@@ -48,7 +48,7 @@ export const IdentityProjectRow = ({
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`identity-project-membership-${id}`}
|
||||
onClick={() => {
|
||||
if (isAccessible) {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { isTabSection, TabSections } from "@app/views/Org/Types";;
|
||||
import { isTabSection, TabSections } from "@app/views/Org/Types";
|
||||
|
||||
import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
|
||||
|
||||
|
@@ -86,7 +86,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
data?.map(({ identity: { id, name }, role, customRole }) => {
|
||||
return (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`identity-${id}`}
|
||||
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
|
||||
>
|
||||
|
@@ -184,7 +184,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
|
||||
return (
|
||||
<Tr
|
||||
key={`org-membership-${orgMembershipId}`}
|
||||
className="h-10 w-full cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => router.push(`/org/${orgId}/memberships/${orgMembershipId}`)}
|
||||
>
|
||||
<Td className={isActive ? "" : "text-mineshaft-400"}>{name}</Td>
|
||||
|
@@ -93,7 +93,7 @@ export const OrgRoleTable = () => {
|
||||
return (
|
||||
<Tr
|
||||
key={`role-list-${id}`}
|
||||
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => router.push(`/org/${orgId}/roles/${id}`)}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
|
@@ -26,7 +26,7 @@ export const RoleDetailsSection = ({ roleId, handlePopUpOpen }: Props) => {
|
||||
return data ? (
|
||||
<div className="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">Details</h3>
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Org Role Details</h3>
|
||||
{isCustomRole && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
|
||||
{(isAllowed) => {
|
||||
|
@@ -150,7 +150,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td>
|
||||
|
@@ -16,23 +16,23 @@ import { RolePermissionRow } from "./RolePermissionRow";
|
||||
|
||||
const SIMPLE_PERMISSION_OPTIONS = [
|
||||
{
|
||||
title: "User management",
|
||||
title: "User Management",
|
||||
formName: "member"
|
||||
},
|
||||
{
|
||||
title: "Group management",
|
||||
title: "Group Management",
|
||||
formName: "groups"
|
||||
},
|
||||
{
|
||||
title: "Machine identity management",
|
||||
title: "Machine Identity Management",
|
||||
formName: "identity"
|
||||
},
|
||||
{
|
||||
title: "Billing & usage",
|
||||
title: "Usage & Billing",
|
||||
formName: "billing"
|
||||
},
|
||||
{
|
||||
title: "Role management",
|
||||
title: "Role Management",
|
||||
formName: "role"
|
||||
},
|
||||
{
|
||||
@@ -40,7 +40,7 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
||||
formName: "incident-contact"
|
||||
},
|
||||
{
|
||||
title: "Organization profile",
|
||||
title: "Organization Profile",
|
||||
formName: "settings"
|
||||
},
|
||||
{
|
||||
|
@@ -3,7 +3,8 @@ import {
|
||||
faCheckCircle,
|
||||
faCircleXmark,
|
||||
faCopy,
|
||||
faPencil} from "@fortawesome/free-solid-svg-icons";
|
||||
faPencil
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -82,7 +83,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
return membership ? (
|
||||
<div className="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">Details</h3>
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">User Details</h3>
|
||||
{userId !== membership.user.id && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||
{(isAllowed) => {
|
||||
|
@@ -9,7 +9,7 @@ import { useWorkspace } from "@app/context";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { TabSections } from "@app/views/Org/Types";;
|
||||
import { TabSections } from "@app/views/Org/Types";
|
||||
|
||||
type Props = {
|
||||
membership: TWorkspaceUser;
|
||||
@@ -44,7 +44,7 @@ export const UserProjectRow = ({
|
||||
|
||||
return (
|
||||
<Tr
|
||||
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`user-project-membership-${id}`}
|
||||
onClick={() => {
|
||||
if (isAccessible) {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { isTabSection,TabSections } from "../Types";
|
||||
import { IdentityTab, MembersTab,ProjectRoleListTab, ServiceTokenTab } from "./components";
|
||||
import { TabSections, isTabSection } from '../Types';
|
||||
|
||||
|
||||
export const MembersPage = withProjectPermission(
|
||||
|
@@ -92,7 +92,7 @@ export const ProjectRoleList = () => {
|
||||
return (
|
||||
<Tr
|
||||
key={`role-list-${id}`}
|
||||
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => router.push(`/project/${projectId}/roles/${slug}`)}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
|
@@ -1,258 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
|
||||
import {
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
formName: "secrets";
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
getValue: UseFormGetValues<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: IconProp;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
export const MultiEnvProjectPermission = ({
|
||||
isNonEditable,
|
||||
setValue,
|
||||
getValue,
|
||||
control,
|
||||
formName,
|
||||
title,
|
||||
subtitle,
|
||||
icon
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
const customRule = useWatch({
|
||||
control,
|
||||
name: `permissions.${formName}.custom`
|
||||
});
|
||||
const isCustom = Boolean(customRule);
|
||||
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const { read, delete: del, edit, create } = allRule || {};
|
||||
if (read && del && edit && create) return Permission.FullAccess;
|
||||
if (read) return Permission.ReadOnly;
|
||||
return Permission.NoAccess;
|
||||
}, [allRule]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if(!val) return
|
||||
switch (val) {
|
||||
case Permission.NoAccess: {
|
||||
const permissions = getValue("permissions");
|
||||
if (permissions) delete permissions[formName];
|
||||
setValue("permissions", permissions, { shouldDirty: true });
|
||||
break;
|
||||
}
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ all: { read: true, edit: true, create: true, delete: true } },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ all: { read: true, edit: false, create: false, delete: false } },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ custom: { read: false, edit: false, create: false, delete: false } },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
||||
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
|
||||
"border-l-2 border-primary-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={icon} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg font-medium">{title}</div>
|
||||
<div className="text-xs font-light">{subtitle}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={Permission.NoAccess}
|
||||
isDisabled={isNonEditable}
|
||||
value={isCustom ? Permission.Custom : selectedPermissionCategory}
|
||||
onValueChange={handlePermissionChange}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: isCustom ? "auto" : 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<TableContainer className="mt-6 border-mineshaft-500">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th className="min-w-[8rem]">
|
||||
<div className="flex items-center gap-2">
|
||||
Secret Path
|
||||
<span className="text-xs normal-case">
|
||||
<GlobPatternExamples />
|
||||
</span>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="text-center">View</Th>
|
||||
<Th className="text-center">Create</Th>
|
||||
<Th className="text-center">Modify</Th>
|
||||
<Th className="text-center">Delete</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isCustom &&
|
||||
environments.map(({ name, slug }) => (
|
||||
<Tr key={`custom-role-project-secret-${slug}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
name={`permissions.${formName}.${slug}.secretPath`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
/* eslint-disable-next-line no-template-curly-in-string */
|
||||
<FormControl helperText="Supports glob path pattern string">
|
||||
<Input
|
||||
{...field}
|
||||
className="w-full overflow-ellipsis"
|
||||
placeholder="Glob patterns are supported"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
name={`permissions.${formName}.${slug}.read`}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${formName}.${slug}.read`}
|
||||
isDisabled={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
name={`permissions.${formName}.${slug}.create`}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
id={`permissions.${formName}.${slug}.modify`}
|
||||
isDisabled={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
name={`permissions.${formName}.${slug}.edit`}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
id={`permissions.${formName}.${slug}.modify`}
|
||||
isDisabled={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
defaultValue={false}
|
||||
name={`permissions.${formName}.${slug}.delete`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${formName}.${slug}.delete`}
|
||||
isDisabled={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,319 +0,0 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { faElementor } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faAnchorLock,
|
||||
faArrowLeft,
|
||||
faBook,
|
||||
faCertificate,
|
||||
faCog,
|
||||
faKey,
|
||||
faLock,
|
||||
faNetworkWired,
|
||||
faPuzzlePiece,
|
||||
faServer,
|
||||
faShield,
|
||||
faTags,
|
||||
faUser,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Spinner } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useCreateProjectRole,
|
||||
useGetProjectRoleBySlug,
|
||||
useUpdateProjectRole
|
||||
} from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
formSchema,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
import { SecretRollbackPermission } from "./SecretRollbackPermission";
|
||||
import { SingleProjectPermission } from "./SingleProjectPermission";
|
||||
import { WsProjectPermission } from "./WsProjectPermission";
|
||||
|
||||
const SINGLE_PERMISSION_LIST = [
|
||||
{
|
||||
title: "Integrations",
|
||||
subtitle: "Integration management control",
|
||||
icon: faPuzzlePiece,
|
||||
formName: "integrations"
|
||||
},
|
||||
{
|
||||
title: "Secret Protect policy",
|
||||
subtitle: "Manage policies for secret protection for unauthorized secret changes",
|
||||
icon: faShield,
|
||||
formName: ProjectPermissionSub.SecretApproval
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
subtitle: "Role management control",
|
||||
icon: faUsers,
|
||||
formName: "role"
|
||||
},
|
||||
{
|
||||
title: "User management",
|
||||
subtitle: "Add, view and remove users from the project",
|
||||
icon: faUser,
|
||||
formName: "member"
|
||||
},
|
||||
{
|
||||
title: "Group management",
|
||||
subtitle: "Add, view and remove user groups from the project",
|
||||
icon: faUsers,
|
||||
formName: "groups"
|
||||
},
|
||||
{
|
||||
title: "Machine identity management",
|
||||
subtitle: "Add, view, update and remove (machine) identities from the project",
|
||||
icon: faServer,
|
||||
formName: "identity"
|
||||
},
|
||||
{
|
||||
title: "Webhooks",
|
||||
subtitle: "Webhook management control",
|
||||
icon: faAnchorLock,
|
||||
formName: "webhooks"
|
||||
},
|
||||
{
|
||||
title: "Service Tokens",
|
||||
subtitle: "Token management control",
|
||||
icon: faKey,
|
||||
formName: "service-tokens"
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
subtitle: "Settings control",
|
||||
icon: faCog,
|
||||
formName: "settings"
|
||||
},
|
||||
{
|
||||
title: "Environments",
|
||||
subtitle: "Environment management control",
|
||||
icon: faElementor,
|
||||
formName: "environments"
|
||||
},
|
||||
{
|
||||
title: "Tags",
|
||||
subtitle: "Tag management control",
|
||||
icon: faTags,
|
||||
formName: "tags"
|
||||
},
|
||||
{
|
||||
title: "Audit Logs",
|
||||
subtitle: "Audit log management control",
|
||||
icon: faBook,
|
||||
formName: "audit-logs"
|
||||
},
|
||||
{
|
||||
title: "IP Allowlist",
|
||||
subtitle: "IP allowlist management control",
|
||||
icon: faNetworkWired,
|
||||
formName: "ip-allowlist"
|
||||
},
|
||||
{
|
||||
title: "Certificate Authorities",
|
||||
subtitle: "CA management control",
|
||||
icon: faCertificate,
|
||||
formName: "certificate-authorities"
|
||||
},
|
||||
{
|
||||
title: "Certificates",
|
||||
subtitle: "Certificate management control",
|
||||
icon: faCertificate,
|
||||
formName: "certificates"
|
||||
}
|
||||
] as const;
|
||||
|
||||
type Props = {
|
||||
roleSlug?: string;
|
||||
onGoBack: VoidFunction;
|
||||
};
|
||||
|
||||
export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
|
||||
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
|
||||
const isNewRole = !roleSlug;
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
|
||||
currentWorkspace?.slug || "",
|
||||
roleSlug as string
|
||||
);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isSubmitting, isDirty, errors },
|
||||
setValue,
|
||||
getValues,
|
||||
control
|
||||
} = useForm<TFormSchema>({
|
||||
values: roleDetails
|
||||
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
|
||||
: ({} as TProjectRole),
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync: createRole } = useCreateProjectRole();
|
||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||
|
||||
const handleRoleUpdate = async (el: TFormSchema) => {
|
||||
if (!roleDetails?.id) return;
|
||||
|
||||
try {
|
||||
await updateRole({
|
||||
id: roleDetails?.id as string,
|
||||
projectSlug,
|
||||
...el,
|
||||
permissions: formRolePermission2API(el.permissions)
|
||||
});
|
||||
createNotification({ type: "success", text: "Successfully updated role" });
|
||||
onGoBack();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({ type: "error", text: "Failed to update role" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (el: TFormSchema) => {
|
||||
if (!isNewRole) {
|
||||
await handleRoleUpdate(el);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createRole({
|
||||
projectSlug,
|
||||
...el,
|
||||
permissions: formRolePermission2API(el.permissions)
|
||||
});
|
||||
createNotification({ type: "success", text: "Created new role" });
|
||||
onGoBack();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({ type: "error", text: "Failed to create role" });
|
||||
}
|
||||
};
|
||||
|
||||
if (!isNewRole && isRoleDetailsLoading) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center p-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-mineshaft-100">
|
||||
{isNewRole ? "New" : "Edit"} Role
|
||||
</h1>
|
||||
<Button
|
||||
onClick={onGoBack}
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">
|
||||
Project-level roles allow you to define permissions for resources within projects at a
|
||||
granular level
|
||||
</p>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormControl
|
||||
label="Name"
|
||||
isRequired
|
||||
className="mb-0"
|
||||
isError={Boolean(errors?.name)}
|
||||
errorText={errors?.name?.message}
|
||||
>
|
||||
<Input {...register("name")} placeholder="Billing Team" isReadOnly={isNonEditable} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Slug"
|
||||
isRequired
|
||||
isError={Boolean(errors?.slug)}
|
||||
errorText={errors?.slug?.message}
|
||||
>
|
||||
<Input {...register("slug")} placeholder="biller" isReadOnly={isNonEditable} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Description"
|
||||
helperText="A short description about this role"
|
||||
isError={Boolean(errors?.description)}
|
||||
errorText={errors?.description?.message}
|
||||
>
|
||||
<Input {...register("description")} isReadOnly={isNonEditable} />
|
||||
</FormControl>
|
||||
<div className="flex items-center justify-between border-t border-t-mineshaft-800 pt-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">Add Permission</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<MultiEnvProjectPermission
|
||||
getValue={getValues}
|
||||
isNonEditable={isNonEditable}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
icon={faLock}
|
||||
title="Secrets"
|
||||
subtitle="Create, modify and remove secrets, folders and secret imports"
|
||||
formName="secrets"
|
||||
/>
|
||||
</div>
|
||||
<div key="permission-ws">
|
||||
<WsProjectPermission
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isNonEditable={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
|
||||
<div key={`permission-${title}`}>
|
||||
<SingleProjectPermission
|
||||
isNonEditable={isNonEditable}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
icon={icon}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
formName={formName}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div key="permission-secret-rollback">
|
||||
<SecretRollbackPermission
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isNonEditable={isNonEditable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 flex items-center space-x-4">
|
||||
<Button
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || isNonEditable || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
{isNewRole ? "Create Role" : "Save Role"}
|
||||
</Button>
|
||||
<Button onClick={onGoBack} variant="outline_bg">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,147 +0,0 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "create", label: "Perform Rollback" },
|
||||
{ action: "read", label: "View" }
|
||||
] as const;
|
||||
|
||||
export const SecretRollbackPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.secret-rollback"
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if(!val) return;
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue(
|
||||
"permissions.secret-rollback",
|
||||
{ read: false, create: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.secret-rollback",
|
||||
{ read: true, create: true },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
"permissions.secret-rollback",
|
||||
{ read: true, create: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setValue(
|
||||
"permissions.secret-rollback",
|
||||
{ read: false, create: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg font-medium">Secret Rollback</div>
|
||||
<div className="text-xs font-light">Secret rollback control actions</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={Permission.NoAccess}
|
||||
isDisabled={isNonEditable}
|
||||
value={selectedPermissionCategory}
|
||||
onValueChange={handlePermissionChange}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
|
||||
>
|
||||
{isCustom &&
|
||||
PERMISSIONS.map(({ action, label }) => (
|
||||
<Controller
|
||||
name={`permissions.secret-rollback.${action}`}
|
||||
key={`permissions.secret-rollback.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.secret-rollback.${action}`}
|
||||
isDisabled={isNonEditable}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,194 +0,0 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
formName:
|
||||
| "role"
|
||||
| "member"
|
||||
| "groups"
|
||||
| "integrations"
|
||||
| "webhooks"
|
||||
| "service-tokens"
|
||||
| "settings"
|
||||
| "environments"
|
||||
| "tags"
|
||||
| "audit-logs"
|
||||
| "ip-allowlist"
|
||||
| "identity"
|
||||
| "certificate-authorities"
|
||||
| "certificates"
|
||||
| ProjectPermissionSub.SecretApproval;
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: IconProp;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "read", label: "View" },
|
||||
{ action: "create", label: "Create" },
|
||||
{ action: "edit", label: "Modify" },
|
||||
{ action: "delete", label: "Remove" }
|
||||
] as const;
|
||||
|
||||
const MEMBERS_PERMISSIONS = [
|
||||
{ action: "read", label: "View all members" },
|
||||
{ action: "create", label: "Invite members" },
|
||||
{ action: "edit", label: "Edit members" },
|
||||
{ action: "delete", label: "Remove members" }
|
||||
] as const;
|
||||
|
||||
const getPermissionList = (option: Props["formName"]) => {
|
||||
switch (option) {
|
||||
case "member":
|
||||
return MEMBERS_PERMISSIONS;
|
||||
default:
|
||||
return PERMISSIONS;
|
||||
}
|
||||
};
|
||||
|
||||
export const SingleProjectPermission = ({
|
||||
isNonEditable,
|
||||
setValue,
|
||||
control,
|
||||
formName,
|
||||
subtitle,
|
||||
title,
|
||||
icon
|
||||
}: Props) => {
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: `permissions.${formName}`
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if(!val) return;
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={icon} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg font-medium">{title}</div>
|
||||
<div className="text-xs font-light">{subtitle}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={Permission.NoAccess}
|
||||
isDisabled={isNonEditable}
|
||||
value={selectedPermissionCategory}
|
||||
onValueChange={handlePermissionChange}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
|
||||
>
|
||||
{isCustom &&
|
||||
getPermissionList(formName).map(({ action, label }) => (
|
||||
<Controller
|
||||
name={`permissions.${formName}.${action}`}
|
||||
key={`permissions.${formName}.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${formName}.${action}`}
|
||||
isDisabled={isNonEditable}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,126 +0,0 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
FullAccess = "full-acess",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "edit", label: "Update project details" },
|
||||
{ action: "delete", label: "Delete projects" }
|
||||
] as const;
|
||||
|
||||
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.workspace"
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if(!val) return;
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue("permissions.workspace", { edit: true, delete: true }, { shouldDirty: true });
|
||||
break;
|
||||
default:
|
||||
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 px-10 py-6",
|
||||
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-1 text-lg font-medium">Project</div>
|
||||
<div className="text-xs font-light">Project control actions</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
defaultValue={Permission.NoAccess}
|
||||
isDisabled={isNonEditable}
|
||||
value={selectedPermissionCategory}
|
||||
onValueChange={handlePermissionChange}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
|
||||
>
|
||||
{isCustom &&
|
||||
PERMISSIONS.map(({ action, label }) => (
|
||||
<Controller
|
||||
name={`permissions.workspace.${action}`}
|
||||
key={`permissions.workspace.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.workspace.${action}`}
|
||||
isDisabled={isNonEditable}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1 +0,0 @@
|
||||
export { ProjectRoleModifySection } from "./ProjectRoleModifySection";
|
@@ -20,8 +20,8 @@ import { withProjectPermission } from "@app/hoc";
|
||||
import { useDeleteProjectRole,useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { TabSections } from "../Types";
|
||||
import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./components";
|
||||
import { TabSections } from '../Types';
|
||||
|
||||
export const RolePage = withProjectPermission(
|
||||
() => {
|
||||
|
@@ -26,7 +26,7 @@ export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
|
||||
return data ? (
|
||||
<div className="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">Details</h3>
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project Role Details</h3>
|
||||
{isCustomRole && (
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => {
|
||||
|
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
|
||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||
|
||||
const GENERAL_PERMISSIONS = [
|
||||
{ action: "read", label: "View" },
|
||||
@@ -153,7 +153,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td>
|
||||
|
@@ -19,7 +19,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
|
||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
formSchema,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
|
||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
||||
@@ -33,15 +33,15 @@ const SINGLE_PERMISSION_LIST = [
|
||||
formName: "role"
|
||||
},
|
||||
{
|
||||
title: "User management",
|
||||
title: "User Management",
|
||||
formName: "member"
|
||||
},
|
||||
{
|
||||
title: "Group management",
|
||||
title: "Group Management",
|
||||
formName: "groups"
|
||||
},
|
||||
{
|
||||
title: "Machine identity management",
|
||||
title: "Machine Identity Management",
|
||||
formName: "identity"
|
||||
},
|
||||
{
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -6,7 +7,7 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { IconButton, Tooltip, DeleteActionModal } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
@@ -59,6 +60,11 @@ export const SecretEditRow = ({
|
||||
}
|
||||
});
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
const toggleModal = useCallback(() => {
|
||||
setIsModalOpen((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const handleFormReset = () => {
|
||||
reset();
|
||||
@@ -94,18 +100,29 @@ export const SecretEditRow = ({
|
||||
reset({ value });
|
||||
};
|
||||
|
||||
const handleDeleteSecret = async () => {
|
||||
const handleDeleteSecret = useCallback(async () => {
|
||||
setIsDeleting.on();
|
||||
setIsModalOpen(false);
|
||||
|
||||
try {
|
||||
await onSecretDelete(environment, secretName, secretId);
|
||||
reset({ value: null });
|
||||
} finally {
|
||||
setIsDeleting.off();
|
||||
}
|
||||
};
|
||||
}, [onSecretDelete, environment, secretName, secretId, reset, setIsDeleting]);
|
||||
|
||||
return (
|
||||
<div className="group flex w-full cursor-text items-center space-x-2">
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={toggleModal}
|
||||
title="Do you want to delete the selected secret?"
|
||||
deleteKey="delete"
|
||||
onDeleteApproved={handleDeleteSecret}
|
||||
/>
|
||||
|
||||
<div className="flex-grow border-r border-r-mineshaft-600 pr-2 pl-1">
|
||||
<Controller
|
||||
disabled={isImportedSecret && !defaultValue}
|
||||
@@ -193,7 +210,7 @@ export const SecretEditRow = ({
|
||||
variant="plain"
|
||||
ariaLabel="delete-value"
|
||||
className="h-full"
|
||||
onClick={handleDeleteSecret}
|
||||
onClick={toggleModal}
|
||||
isDisabled={isDeleting || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
|
@@ -49,8 +49,9 @@ export const SelectionPanel = ({
|
||||
"bulkDeleteEntries"
|
||||
] as const);
|
||||
|
||||
const selectedCount =
|
||||
Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length;
|
||||
const selectedFolderCount = Object.keys(selectedEntries.folder).length
|
||||
const selectedKeysCount = Object.keys(selectedEntries.secret).length
|
||||
const selectedCount = selectedFolderCount + selectedKeysCount
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
@@ -68,6 +69,16 @@ export const SelectionPanel = ({
|
||||
)
|
||||
);
|
||||
|
||||
const getDeleteModalTitle = () => {
|
||||
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
|
||||
return "Do you want to delete the selected secrets and folders across environments?";
|
||||
}
|
||||
if (selectedKeysCount > 0) {
|
||||
return "Do you want to delete the selected secrets across environments?";
|
||||
}
|
||||
return "Do you want to delete the selected folders across environments?";
|
||||
}
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
let processedEntries = 0;
|
||||
|
||||
@@ -180,7 +191,7 @@ export const SelectionPanel = ({
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.bulkDeleteEntries.isOpen}
|
||||
deleteKey="delete"
|
||||
title="Do you want to delete the selected secrets and folders across envs?"
|
||||
title={getDeleteModalTitle()}
|
||||
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
|
||||
onDeleteApproved={handleBulkDelete}
|
||||
/>
|
||||
|
@@ -7,9 +7,9 @@ import { ShareSecretForm } from "./components";
|
||||
|
||||
export const ShareSecretPublicPage = () => {
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<div />
|
||||
<div className="mx-auto w-full max-w-xl px-4">
|
||||
<div className="mx-auto w-full max-w-xl p-4">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mb-4 flex justify-center pt-8">
|
||||
<Link href="https://infisical.com">
|
||||
@@ -43,36 +43,34 @@ export const ShareSecretPublicPage = () => {
|
||||
<div className="m-auto my-8 flex w-full">
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
</div>
|
||||
<div className="m-auto flex max-w-2xl flex-col items-center justify-center">
|
||||
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||
Open source{" "}
|
||||
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||
secret management
|
||||
</span>{" "}
|
||||
for developers
|
||||
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||
Open source{" "}
|
||||
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||
secret management
|
||||
</span>{" "}
|
||||
for developers
|
||||
</p>
|
||||
<div className="flex flex-col items-start sm:flex-row sm:items-center">
|
||||
<p className="md:text-md text-md mr-4">
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||
>
|
||||
Infisical
|
||||
</a>{" "}
|
||||
is the all-in-one secret management platform to securely manage secrets, configs, and
|
||||
certificates across your team and infrastructure.
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<p className="md:text-md text-md mr-4">
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||
>
|
||||
Infisical
|
||||
</a>{" "}
|
||||
is the all-in-one secret management platform to securely manage secrets, configs,
|
||||
and certificates across your team and infrastructure.
|
||||
</p>
|
||||
<div className="cursor-pointer">
|
||||
<Link href="https://infisical.com">
|
||||
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-4 cursor-pointer sm:mt-0">
|
||||
<Link href="https://infisical.com">
|
||||
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -22,9 +22,9 @@ export const ViewSecretPublicPage = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<div />
|
||||
<div className="mx-auto w-full max-w-xl px-4 ">
|
||||
<div className="mx-auto w-full max-w-xl p-4 ">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mb-4 flex justify-center pt-8">
|
||||
<Link href="https://infisical.com">
|
||||
@@ -57,47 +57,44 @@ export const ViewSecretPublicPage = () => {
|
||||
<div className="m-auto my-8 flex w-full">
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
</div>
|
||||
<div className="m-auto flex max-w-2xl flex-col items-center justify-center">
|
||||
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||
Open source{" "}
|
||||
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||
secret management
|
||||
</span>{" "}
|
||||
for developers
|
||||
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||
Open source{" "}
|
||||
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||
secret management
|
||||
</span>{" "}
|
||||
for developers
|
||||
</p>
|
||||
<div className="flex flex-col items-start sm:flex-row sm:items-center">
|
||||
<p className="md:text-md text-md mr-4">
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||
>
|
||||
Infisical
|
||||
</a>{" "}
|
||||
is the all-in-one secret management platform to securely manage secrets, configs, and
|
||||
certificates across your team and infrastructure.
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<p className="md:text-md text-md mr-4">
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||
>
|
||||
Infisical
|
||||
</a>{" "}
|
||||
is the all-in-one secret management platform to securely manage secrets, configs,
|
||||
and certificates across your team and infrastructure.
|
||||
</p>
|
||||
<div className="cursor-pointer">
|
||||
<Link href="https://infisical.com">
|
||||
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-4 cursor-pointer sm:mt-0">
|
||||
<Link href="https://infisical.com">
|
||||
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-mineshaft-600 p-2">
|
||||
<p className="text-center text-sm text-mineshaft-300">
|
||||
© 2024{" "}
|
||||
Made with ❤️ by{" "}
|
||||
<a className="text-primary" href="https://infisical.com">
|
||||
Infisical
|
||||
</a>
|
||||
. All rights reserved.
|
||||
<br />
|
||||
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||
</p>
|
||||
|
Reference in New Issue
Block a user