mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 18:28:12 +00:00
Wired access controls for environemnts to frontend
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"title": "Project Members",
|
"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'
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo='opacity-0 scale-95'
|
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
|
<Dialog.Title
|
||||||
as='h3'
|
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
|
Unleash Infisical's Full Power
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
<p className='text-sm text-bunker-300 mb-0.5'>
|
<p className='text-sm text-bunker-300 mb-1'>
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-sm text-bunker-300'>
|
<p className='text-sm text-bunker-300'>
|
||||||
@@ -60,7 +60,7 @@ const UpgradePlanModal = ({
|
|||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
<button
|
<button
|
||||||
type='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")}`)}
|
onClick={() => router.push(`/settings/billing/${localStorage.getItem("projectData.id")}`)}
|
||||||
>
|
>
|
||||||
Upgrade Now
|
Upgrade Now
|
||||||
|
|||||||
@@ -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 (
|
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" />
|
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
||||||
<table className="w-full my-0.5">
|
<table className="w-full my-0.5">
|
||||||
<thead className="text-gray-400 text-sm font-light">
|
<thead className="text-gray-400 text-sm font-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
<th className="text-left pl-4 py-3.5">NAME</th>
|
||||||
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
||||||
<th className="text-left pl-6 py-3.5">EMAIL</th>
|
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
||||||
<th aria-label="buttons" />
|
<th aria-label="buttons" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -136,18 +136,15 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
user.email?.toLowerCase().includes(filter)
|
user.email?.toLowerCase().includes(filter)
|
||||||
)
|
)
|
||||||
.map((row, index) => (
|
.map((row, index) => (
|
||||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5">
|
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
|
||||||
<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.firstName}
|
{row.firstName} {row.lastName}
|
||||||
</td>
|
</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.lastName}
|
|
||||||
</td>
|
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
|
||||||
{row.email}
|
{row.email}
|
||||||
</td>
|
</td>
|
||||||
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<div className="justify-end mr-6 mx-2 w-full h-full flex flex-row items-center">
|
<div className="justify-start h-full flex flex-row items-center">
|
||||||
{row.status === 'granted' &&
|
{row.status === 'granted' &&
|
||||||
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
||||||
myUser !== row.email ? (
|
myUser !== row.email ? (
|
||||||
@@ -157,14 +154,12 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
data={
|
data={
|
||||||
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
|
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
|
||||||
}
|
}
|
||||||
text="Role: "
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
row.status !== 'invited' &&
|
row.status !== 'invited' &&
|
||||||
row.status !== 'verified' && (
|
row.status !== 'verified' && (
|
||||||
<Listbox
|
<Listbox
|
||||||
isSelected={row.role}
|
isSelected={row.role}
|
||||||
text="Role: "
|
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
}}
|
}}
|
||||||
@@ -173,7 +168,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{(row.status === 'invited' || row.status === 'verified') && (
|
{(row.status === 'invited' || row.status === 'verified') && (
|
||||||
<div className="w-full pl-9">
|
<div className="w-full pr-20">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
|
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
@@ -193,10 +188,12 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 &&
|
{myUser !== row.email &&
|
||||||
// row.role !== "admin" &&
|
// row.role !== "admin" &&
|
||||||
myRole !== 'member' ? (
|
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
|
<Button
|
||||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||||
color="red"
|
color="red"
|
||||||
|
|||||||
@@ -23,26 +23,26 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`inline-flex items-center justify-between data-[placeholder]:text-gray-500
|
`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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.Value placeholder={placeholder} />
|
<SelectPrimitive.Value placeholder={placeholder} />
|
||||||
<SelectPrimitive.Icon className="ml-3">
|
{!props.disabled && <SelectPrimitive.Icon className="ml-3">
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>}
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
position="popper"
|
// position="popper"
|
||||||
sideOffset={5}
|
sideOffset={4}
|
||||||
className="overflow-hidden text-white rounded-md shadow-md font-inter bg-mineshaft-800"
|
className="overflow-hidden text-bunker-100 rounded-md shadow-md font-inter bg-mineshaft-800"
|
||||||
style={{ width: 'var(--radix-select-trigger-width)' }}
|
style={{ width: 'var(--radix-select-trigger-width) + 6' }}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ScrollUpButton>
|
<SelectPrimitive.ScrollUpButton>
|
||||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
<SelectPrimitive.Viewport className="p-2">
|
<SelectPrimitive.Viewport className="p-1.5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<Spinner size="xs" />
|
<Spinner size="xs" />
|
||||||
@@ -75,16 +75,16 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`text-sm rounded-sm transition-all hover:text-primary
|
`text-sm rounded-sm transition-all hover:bg-mineshaft-500
|
||||||
hover:bg-mineshaft-700 flex items-center pl-10 pr-4 py-2 cursor-pointer
|
flex items-center pl-10 pr-4 py-2 cursor-pointer rounded-md
|
||||||
select-none outline-none relative`,
|
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',
|
isDisabled && 'text-gray-600 hover:bg-transparent cursor-not-allowed hover:text-gray-600',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.ItemIndicator className="absolute left-2">
|
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
|
||||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
<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;
|
organization: string;
|
||||||
role: 'owner' | 'admin' | 'member';
|
role: 'owner' | 'admin' | 'member';
|
||||||
status: 'invited' | 'accepted';
|
status: 'invited' | 'accepted';
|
||||||
|
deniedPermissions: any[];
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This route lets us get all the users in an org.
|
* This route lets us get all the users in an org.
|
||||||
|
|||||||
@@ -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 getUser from '../api/user/getUser';
|
||||||
import checkUserAction from '../api/userActions/checkUserAction';
|
import checkUserAction from '../api/userActions/checkUserAction';
|
||||||
import registerUserAction from '../api/userActions/registerUserAction';
|
import registerUserAction from '../api/userActions/registerUserAction';
|
||||||
|
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
|
||||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||||
|
|
||||||
type WorkspaceEnv = {
|
type WorkspaceEnv = {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
isWriteDenied: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SecretDataProps {
|
interface SecretDataProps {
|
||||||
@@ -133,7 +135,8 @@ export default function Dashboard() {
|
|||||||
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState<WorkspaceEnv>();
|
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState<WorkspaceEnv>();
|
||||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
|
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
|
||||||
name: '',
|
name: '',
|
||||||
slug: ''
|
slug: '',
|
||||||
|
isWriteDenied: false
|
||||||
});
|
});
|
||||||
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
|
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
|
||||||
const secretsTop = useRef<HTMLDivElement>(null);
|
const secretsTop = useRef<HTMLDivElement>(null);
|
||||||
@@ -214,9 +217,10 @@ export default function Dashboard() {
|
|||||||
router.push(`/dashboard/${userWorkspaces?.[0]?._id}`);
|
router.push(`/dashboard/${userWorkspaces?.[0]?._id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkspaceEnvs(workspace?.environments || []);
|
const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId });
|
||||||
|
setWorkspaceEnvs(accessibleEnvironments || []);
|
||||||
// set env
|
// set env
|
||||||
const env = workspace?.environments?.[0] || {
|
const env = accessibleEnvironments?.[0] || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unkown'
|
slug: 'unkown'
|
||||||
};
|
};
|
||||||
@@ -351,6 +355,7 @@ export default function Dashboard() {
|
|||||||
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
|
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
|
||||||
|
|
||||||
if (nameErrors) {
|
if (nameErrors) {
|
||||||
|
setSaveLoading(false);
|
||||||
return createNotification({
|
return createNotification({
|
||||||
text: 'Solve all name errors before saving secrets.',
|
text: 'Solve all name errors before saving secrets.',
|
||||||
type: 'error'
|
type: 'error'
|
||||||
@@ -358,12 +363,21 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (duplicatesExist) {
|
if (duplicatesExist) {
|
||||||
|
setSaveLoading(false);
|
||||||
return createNotification({
|
return createNotification({
|
||||||
text: 'Remove duplicated secret names before saving.',
|
text: 'Remove duplicated secret names before saving.',
|
||||||
type: 'error'
|
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
|
// Once "Save changes" is clicked, disable that button
|
||||||
setButtonReady(false);
|
setButtonReady(false);
|
||||||
|
|
||||||
@@ -580,7 +594,8 @@ export default function Dashboard() {
|
|||||||
setSelectedEnv(
|
setSelectedEnv(
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unknown'
|
slug: 'unknown',
|
||||||
|
isWriteDenied: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -660,7 +675,8 @@ export default function Dashboard() {
|
|||||||
setSelectedEnv(
|
setSelectedEnv(
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
name: 'unknown',
|
name: 'unknown',
|
||||||
slug: 'unknown'
|
slug: 'unknown',
|
||||||
|
isWriteDenied: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -673,7 +689,8 @@ export default function Dashboard() {
|
|||||||
setSelectedSnapshotEnv(
|
setSelectedSnapshotEnv(
|
||||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
name: 'unknown',
|
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 Button from '@app/components/basic/buttons/Button';
|
||||||
import AddProjectMemberDialog from '@app/components/basic/dialog/AddProjectMemberDialog';
|
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 NavHeader from '@app/components/navigation/NavHeader';
|
||||||
import guidGenerator from '@app/components/utilities/randomId';
|
import guidGenerator from '@app/components/utilities/randomId';
|
||||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
@@ -33,6 +33,7 @@ interface UserProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MembershipProps {
|
interface MembershipProps {
|
||||||
|
deniedPermissions: any[];
|
||||||
user: UserProps;
|
user: UserProps;
|
||||||
inviteEmail: string;
|
inviteEmail: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -76,6 +77,7 @@ export default function Users() {
|
|||||||
status: membership?.status,
|
status: membership?.status,
|
||||||
userId: membership.user?._id,
|
userId: membership.user?._id,
|
||||||
membershipId: membership._id,
|
membershipId: membership._id,
|
||||||
|
deniedPermissions: membership.deniedPermissions,
|
||||||
publicKey: membership.user?.publicKey
|
publicKey: membership.user?.publicKey
|
||||||
}));
|
}));
|
||||||
setUserList(tempUserList);
|
setUserList(tempUserList);
|
||||||
@@ -144,7 +146,7 @@ export default function Users() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return userList ? (
|
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>
|
<Head>
|
||||||
<title>{t('common:head-title', { title: t('settings-members:title') })}</title>
|
<title>{t('common:head-title', { title: t('settings-members:title') })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
@@ -168,7 +170,7 @@ export default function Users() {
|
|||||||
setEmail={setEmail}
|
setEmail={setEmail}
|
||||||
/>
|
/>
|
||||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
{/* <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">
|
<div className="h-10 w-full bg-white/5 mt-2 rounded-md flex flex-row items-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
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>
|
</div>
|
||||||
<div className="block overflow-y-auto min-w-6xl max-w-6xl px-6">
|
<div className="block overflow-x-scroll px-6 pb-6 no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||||
<UserTable
|
<ProjectUsersTable
|
||||||
userData={userList}
|
userData={userList}
|
||||||
changeData={setUserList}
|
changeData={setUserList}
|
||||||
myUser={personalEmail}
|
myUser={personalEmail}
|
||||||
filter={searchUsers}
|
filter={searchUsers}
|
||||||
resendInvite={submitAddModal}
|
|
||||||
isOrg={false}
|
|
||||||
// onClick={openDeleteModal}
|
// onClick={openDeleteModal}
|
||||||
// deleteUser={deleteMembership}
|
// deleteUser={deleteMembership}
|
||||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||||
|
|||||||
Reference in New Issue
Block a user