mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Wired access controls for environemnts to frontend
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Project Members",
|
||||
"description": "This page shows the members of the selected project."
|
||||
"description": "This page shows the members of the selected project, and allows you to modify their permissions."
|
||||
}
|
||||
|
||||
@@ -42,15 +42,15 @@ const UpgradePlanModal = ({
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-bunker-400 p-6 pt-5 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-500 p-6 pt-5 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-bunker-200'
|
||||
className='text-xl font-medium leading-6 text-primary'
|
||||
>
|
||||
Unleash Infisical's Full Power
|
||||
</Dialog.Title>
|
||||
<div className='mt-2'>
|
||||
<p className='text-sm text-bunker-300 mb-0.5'>
|
||||
<p className='text-sm text-bunker-300 mb-1'>
|
||||
{text}
|
||||
</p>
|
||||
<p className='text-sm text-bunker-300'>
|
||||
@@ -60,7 +60,7 @@ const UpgradePlanModal = ({
|
||||
<div className='mt-4'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-primary opacity-90 hover:opacity-100 px-4 py-2 text-sm font-medium text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-primary opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium 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={() => router.push(`/settings/billing/${localStorage.getItem("projectData.id")}`)}
|
||||
>
|
||||
Upgrade Now
|
||||
|
||||
299
frontend/src/components/basic/table/ProjectUsersTable.tsx
Normal file
299
frontend/src/components/basic/table/ProjectUsersTable.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { plans } from 'public/data/frequentConstants';
|
||||
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
import { Select, SelectItem } from '@app/components/v2';
|
||||
import updateUserProjectPermission from '@app/ee/api/memberships/UpdateUserProjectPermission';
|
||||
import getOrganizationSubscriptions from '@app/pages/api/organization/GetOrgSubscription';
|
||||
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
|
||||
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
|
||||
import { decryptAssymmetric, encryptAssymmetric } from '../../utilities/cryptography/crypto';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
import UpgradePlanModal from '../dialog/UpgradePlan';
|
||||
|
||||
// const roles = ['admin', 'user'];
|
||||
// TODO: Set type for this
|
||||
type Props = {
|
||||
userData: any[];
|
||||
changeData: (users: any[]) => void;
|
||||
myUser: string;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
type EnvironmentProps = {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that shows the users of a certin project
|
||||
* #TODO: add the possibility of choosing and doing operations on multiple users.
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
const [roleSelected, setRoleSelected] = useState(
|
||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
);
|
||||
const host = window.location.origin;
|
||||
const router = useRouter();
|
||||
const [myRole, setMyRole] = useState('member');
|
||||
const [currentPlan, setCurrentPlan] = useState('');
|
||||
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
|
||||
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const workspaceId = router.query.id as string;
|
||||
// Delete the row in the table (e.g. a user)
|
||||
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
|
||||
const handleDelete = (membershipId: string, index: number) => {
|
||||
// setUserIdToBeDeleted(userId);
|
||||
// onClick();
|
||||
deleteUserFromWorkspace(membershipId);
|
||||
changeData(userData.filter((v, i) => i !== index));
|
||||
setRoleSelected([
|
||||
...roleSelected.slice(0, index),
|
||||
...roleSelected.slice(index + 1, userData?.length)
|
||||
]);
|
||||
};
|
||||
|
||||
// Update the rold of a certain user
|
||||
const handleRoleUpdate = (index: number, e: string) => {
|
||||
changeUserRoleInWorkspace(userData[index].membershipId, e.toLowerCase());
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
{
|
||||
key: userData[index].key,
|
||||
firstName: userData[index].firstName,
|
||||
lastName: userData[index].lastName,
|
||||
email: userData[index].email,
|
||||
role: e.toLocaleLowerCase(),
|
||||
status: userData[index].status,
|
||||
userId: userData[index].userId,
|
||||
membershipId: userData[index].membershipId,
|
||||
publicKey: userData[index].publicKey,
|
||||
deniedPermissions: userData[index].deniedPermissions
|
||||
}
|
||||
],
|
||||
...userData.slice(index + 1, userData?.length)
|
||||
]);
|
||||
createNotification({
|
||||
text: `Successfully changed user role.`,
|
||||
type: 'success'
|
||||
});
|
||||
};
|
||||
|
||||
const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
|
||||
let denials: { ability: string; environmentSlug: string; }[];
|
||||
if (val === "Read Only") {
|
||||
denials = [{
|
||||
ability: "write",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
} else if (val === "No Access") {
|
||||
denials = [{
|
||||
ability: "write",
|
||||
environmentSlug: slug
|
||||
}, {
|
||||
ability: "read",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
} else {
|
||||
denials = [];
|
||||
}
|
||||
|
||||
if (currentPlan !== plans.professional && host !== 'https://app.infisical.com') {
|
||||
setIsUpgradeModalOpen(true);
|
||||
} else {
|
||||
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
|
||||
updateUserProjectPermission({ membershipId, denials: allDenials});
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
{
|
||||
key: userData[index].key,
|
||||
firstName: userData[index].firstName,
|
||||
lastName: userData[index].lastName,
|
||||
email: userData[index].email,
|
||||
role: userData[index].role,
|
||||
status: userData[index].status,
|
||||
userId: userData[index].userId,
|
||||
membershipId: userData[index].membershipId,
|
||||
publicKey: userData[index].publicKey,
|
||||
deniedPermissions: allDenials
|
||||
}
|
||||
],
|
||||
...userData.slice(index + 1, userData?.length)
|
||||
]);
|
||||
createNotification({
|
||||
text: `Successfully changed user permissions.`,
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
||||
(async () => {
|
||||
const result = await getProjectInfo({ projectId: workspaceId });
|
||||
setWorkspaceEnvs(result.environments);
|
||||
|
||||
const orgId = localStorage.getItem('orgData.id') as string;
|
||||
const subscriptions = await getOrganizationSubscriptions({
|
||||
orgId
|
||||
});
|
||||
if (subscriptions) {
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
}
|
||||
})();
|
||||
}, [userData, myUser]);
|
||||
|
||||
const grantAccess = async (id: string, publicKey: string) => {
|
||||
const result = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(workspaceId, id, ciphertext, nonce);
|
||||
router.reload();
|
||||
};
|
||||
|
||||
const closeUpgradeModal = () => {
|
||||
setIsUpgradeModalOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
||||
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
||||
<UpgradePlanModal
|
||||
isOpen={isUpgradeModalOpen}
|
||||
onClose={closeUpgradeModal}
|
||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||
/>
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
||||
{workspaceEnvs.map(env => (
|
||||
<th key={guidGenerator()} className="text-left pl-8 py-1 max-w-min break-normal">
|
||||
<span>{env.name.toUpperCase()}<br/></span>
|
||||
{/* <span>PERMISSION</span> */}
|
||||
</th>
|
||||
))}
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userData?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
).length > 0 &&
|
||||
userData
|
||||
?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName} {row.lastName}
|
||||
</td>
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.email}
|
||||
</td>
|
||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<div className="justify-start h-full flex flex-row items-center">
|
||||
<Select
|
||||
className="w-36"
|
||||
// open={isOpen}
|
||||
onValueChange={(e) => handleRoleUpdate(index, e)}
|
||||
value={row.role}
|
||||
disabled={myRole !== 'admin' || myUser === row.email}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
</Select>
|
||||
{row.status === 'completed' && myUser !== row.email && (
|
||||
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||
color="mineshaft"
|
||||
text="Grant Access"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-8 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<Select
|
||||
className="w-36"
|
||||
// open={isOpen}
|
||||
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
|
||||
value={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
||||
? "No Access"
|
||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") ? "Read Only" : "Read & Write")
|
||||
}
|
||||
disabled={myUser === row.email || myRole !== 'admin'}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
>
|
||||
<SelectItem value="No Access">No Access</SelectItem>
|
||||
<SelectItem value="Read Only">Read Only</SelectItem>
|
||||
<SelectItem value="Read & Write">Read & Write</SelectItem>
|
||||
</Select>
|
||||
</td>)}
|
||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
{myUser !== row.email &&
|
||||
// row.role !== "admin" &&
|
||||
myRole !== 'member' ? (
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-9 h-9" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectUsersTable;
|
||||
@@ -110,14 +110,14 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
||||
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">EMAIL</th>
|
||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -136,18 +136,15 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5">
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName}
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName} {row.lastName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.lastName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.email}
|
||||
</td>
|
||||
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
<div className="justify-end mr-6 mx-2 w-full h-full flex flex-row items-center">
|
||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<div className="justify-start h-full flex flex-row items-center">
|
||||
{row.status === 'granted' &&
|
||||
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
||||
myUser !== row.email ? (
|
||||
@@ -157,14 +154,12 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
data={
|
||||
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
|
||||
}
|
||||
text="Role: "
|
||||
/>
|
||||
) : (
|
||||
row.status !== 'invited' &&
|
||||
row.status !== 'verified' && (
|
||||
<Listbox
|
||||
isSelected={row.role}
|
||||
text="Role: "
|
||||
onChange={() => {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
@@ -173,7 +168,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
)
|
||||
)}
|
||||
{(row.status === 'invited' || row.status === 'verified') && (
|
||||
<div className="w-full pl-9">
|
||||
<div className="w-full pr-20">
|
||||
<Button
|
||||
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
|
||||
color="mineshaft"
|
||||
@@ -193,10 +188,12 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
{myUser !== row.email &&
|
||||
// row.role !== "admin" &&
|
||||
myRole !== 'member' ? (
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center">
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
color="red"
|
||||
|
||||
@@ -23,26 +23,26 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`inline-flex items-center justify-between data-[placeholder]:text-gray-500
|
||||
px-4 py-2.5 font-inter text-sm text-white rounded-md bg-mineshaft-800`,
|
||||
px-3 py-2 font-inter text-sm text-bunker-200 font-normal rounded-md bg-mineshaft-800 outline-none`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
{!props.disabled && <SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Icon>}
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
className="overflow-hidden text-white rounded-md shadow-md font-inter bg-mineshaft-800"
|
||||
style={{ width: 'var(--radix-select-trigger-width)' }}
|
||||
// position="popper"
|
||||
sideOffset={4}
|
||||
className="overflow-hidden text-bunker-100 rounded-md shadow-md font-inter bg-mineshaft-800"
|
||||
style={{ width: 'var(--radix-select-trigger-width) + 6' }}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton>
|
||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport className="p-2">
|
||||
<SelectPrimitive.Viewport className="p-1.5">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner size="xs" />
|
||||
@@ -75,16 +75,16 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
<SelectPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`text-sm rounded-sm transition-all hover:text-primary
|
||||
hover:bg-mineshaft-700 flex items-center pl-10 pr-4 py-2 cursor-pointer
|
||||
`text-sm rounded-sm transition-all hover:bg-mineshaft-500
|
||||
flex items-center pl-10 pr-4 py-2 cursor-pointer rounded-md
|
||||
select-none outline-none relative`,
|
||||
isSelected && 'text-primary',
|
||||
isSelected && 'bg-primary',
|
||||
isDisabled && 'text-gray-600 hover:bg-transparent cursor-not-allowed hover:text-gray-600',
|
||||
className
|
||||
)}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-2">
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
|
||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function updates user permissions for a certain environment in a project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.membershipId - membershipId of a certain user in a project
|
||||
* @param {*[]} obj.denials - permissions that we are prohibitting users to do
|
||||
* @returns
|
||||
*/
|
||||
const updateUserProjectPermission = async ({
|
||||
membershipId,
|
||||
denials
|
||||
}: {
|
||||
membershipId: string;
|
||||
denials: {
|
||||
ability: string;
|
||||
environmentSlug: string;
|
||||
}[]
|
||||
}) =>
|
||||
SecurityClient.fetchCall(`/api/v1/membership/${membershipId}/deny-permissions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permissions: denials
|
||||
})
|
||||
}).then(async (res) => {
|
||||
console.log({
|
||||
permissions: denials
|
||||
}, res)
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log('Failed to update user permissions for a certain environment in a project');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default updateUserProjectPermission;
|
||||
@@ -13,6 +13,7 @@ export interface IMembershipOrg {
|
||||
organization: string;
|
||||
role: 'owner' | 'admin' | 'member';
|
||||
status: 'invited' | 'accepted';
|
||||
deniedPermissions: any[];
|
||||
}
|
||||
/**
|
||||
* This route lets us get all the users in an org.
|
||||
|
||||
22
frontend/src/pages/api/workspace/getWorkspaceEnvironments.ts
Normal file
22
frontend/src/pages/api/workspace/getWorkspaceEnvironments.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This route lets us get the environments that a certain user has acess to in a certain project
|
||||
* @param {string} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceEnvironments = ({ workspaceId }: { workspaceId: string }) =>
|
||||
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res?.status === 200) {
|
||||
return (await res.json()).accessibleEnvironments;
|
||||
}
|
||||
console.log('Failed to get accessible environments');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getWorkspaceEnvironments;
|
||||
@@ -41,11 +41,13 @@ import updateSecrets from '../api/files/UpdateSecrets';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
type WorkspaceEnv = {
|
||||
name: string;
|
||||
slug: string;
|
||||
isWriteDenied: boolean;
|
||||
};
|
||||
|
||||
interface SecretDataProps {
|
||||
@@ -133,7 +135,8 @@ export default function Dashboard() {
|
||||
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState<WorkspaceEnv>();
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
|
||||
name: '',
|
||||
slug: ''
|
||||
slug: '',
|
||||
isWriteDenied: false
|
||||
});
|
||||
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
|
||||
const secretsTop = useRef<HTMLDivElement>(null);
|
||||
@@ -214,9 +217,10 @@ export default function Dashboard() {
|
||||
router.push(`/dashboard/${userWorkspaces?.[0]?._id}`);
|
||||
}
|
||||
|
||||
setWorkspaceEnvs(workspace?.environments || []);
|
||||
const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId });
|
||||
setWorkspaceEnvs(accessibleEnvironments || []);
|
||||
// set env
|
||||
const env = workspace?.environments?.[0] || {
|
||||
const env = accessibleEnvironments?.[0] || {
|
||||
name: 'unknown',
|
||||
slug: 'unkown'
|
||||
};
|
||||
@@ -351,6 +355,7 @@ export default function Dashboard() {
|
||||
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
|
||||
|
||||
if (nameErrors) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Solve all name errors before saving secrets.',
|
||||
type: 'error'
|
||||
@@ -358,12 +363,21 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
if (duplicatesExist) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Remove duplicated secret names before saving.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedEnv.isWriteDenied) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'You are not allowed to edit this environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
// Once "Save changes" is clicked, disable that button
|
||||
setButtonReady(false);
|
||||
|
||||
@@ -580,7 +594,8 @@ export default function Dashboard() {
|
||||
setSelectedEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -660,7 +675,8 @@ export default function Dashboard() {
|
||||
setSelectedEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -673,7 +689,8 @@ export default function Dashboard() {
|
||||
setSelectedSnapshotEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import AddProjectMemberDialog from '@app/components/basic/dialog/AddProjectMemberDialog';
|
||||
import UserTable from '@app/components/basic/table/UserTable';
|
||||
import ProjectUsersTable from '@app/components/basic/table/ProjectUsersTable';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
@@ -33,6 +33,7 @@ interface UserProps {
|
||||
}
|
||||
|
||||
interface MembershipProps {
|
||||
deniedPermissions: any[];
|
||||
user: UserProps;
|
||||
inviteEmail: string;
|
||||
role: string;
|
||||
@@ -76,6 +77,7 @@ export default function Users() {
|
||||
status: membership?.status,
|
||||
userId: membership.user?._id,
|
||||
membershipId: membership._id,
|
||||
deniedPermissions: membership.deniedPermissions,
|
||||
publicKey: membership.user?.publicKey
|
||||
}));
|
||||
setUserList(tempUserList);
|
||||
@@ -144,7 +146,7 @@ export default function Users() {
|
||||
};
|
||||
|
||||
return userList ? (
|
||||
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start">
|
||||
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start max-w-[calc(100vw-240px)]">
|
||||
<Head>
|
||||
<title>{t('common:head-title', { title: t('settings-members:title') })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
@@ -168,7 +170,7 @@ export default function Users() {
|
||||
setEmail={setEmail}
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="px-6 pb-1 w-full flex flex-row items-start min-w-6xl max-w-6xl">
|
||||
<div className="px-6 pb-1 w-full flex flex-row items-start">
|
||||
<div className="h-10 w-full bg-white/5 mt-2 rounded-md flex flex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
||||
@@ -191,14 +193,12 @@ export default function Users() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="block overflow-y-auto min-w-6xl max-w-6xl px-6">
|
||||
<UserTable
|
||||
<div className="block overflow-x-scroll px-6 pb-6 no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
<ProjectUsersTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
myUser={personalEmail}
|
||||
filter={searchUsers}
|
||||
resendInvite={submitAddModal}
|
||||
isOrg={false}
|
||||
// onClick={openDeleteModal}
|
||||
// deleteUser={deleteMembership}
|
||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||
|
||||
Reference in New Issue
Block a user