Wired access controls for environemnts to frontend

This commit is contained in:
Vladyslav Matsiiako
2023-01-30 20:36:04 -08:00
parent 61c17ccc5e
commit 4ef8c273f7
10 changed files with 422 additions and 47 deletions
@@ -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&apos;s Full Power Unleash Infisical&apos;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"
+12 -12
View File
@@ -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;
+23 -6
View File
@@ -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
} }
) )
} }
+7 -7
View File
@@ -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}