authorizer/dashboard/src/pages/Users.tsx

585 lines
14 KiB
TypeScript
Raw Normal View History

2022-01-19 16:50:25 +00:00
import React from 'react';
2022-01-29 15:23:53 +00:00
import { useClient } from 'urql';
import dayjs from 'dayjs';
import {
Box,
Flex,
IconButton,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Table,
Tag,
Tbody,
Td,
Text,
TableCaption,
Th,
Thead,
Tooltip,
Tr,
Button,
Center,
Menu,
MenuButton,
MenuList,
MenuItem,
2022-01-30 05:09:35 +00:00
useToast,
2022-01-31 06:03:35 +00:00
Spinner,
2022-10-02 17:06:57 +00:00
TableContainer,
2022-01-29 15:23:53 +00:00
} from '@chakra-ui/react';
import {
FaAngleLeft,
FaAngleRight,
FaAngleDoubleLeft,
FaAngleDoubleRight,
FaExclamationCircle,
FaAngleDown,
} from 'react-icons/fa';
2022-03-16 08:38:22 +00:00
import { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries';
import { EnableAccess, RevokeAccess, UpdateUser } from '../graphql/mutation';
2022-01-30 05:09:35 +00:00
import EditUserModal from '../components/EditUserModal';
2022-01-31 07:53:38 +00:00
import DeleteUserModal from '../components/DeleteUserModal';
2022-03-15 18:37:58 +00:00
import InviteMembersModal from '../components/InviteMembersModal';
2022-01-29 15:23:53 +00:00
interface paginationPropTypes {
limit: number;
page: number;
offset: number;
total: number;
maxPages: number;
}
interface userDataTypes {
id: string;
email: string;
email_verified: boolean;
given_name: string;
family_name: string;
middle_name: string;
nickname: string;
gender: string;
birthdate: string;
phone_number: string;
picture: string;
2022-01-31 06:05:24 +00:00
signup_methods: string;
2022-01-29 15:23:53 +00:00
roles: [string];
created_at: number;
revoked_timestamp: number;
is_multi_factor_auth_enabled?: boolean;
}
const enum updateAccessActions {
REVOKE = 'REVOKE',
ENABLE = 'ENABLE',
2022-01-29 15:23:53 +00:00
}
const getMaxPages = (pagination: paginationPropTypes) => {
const { limit, total } = pagination;
if (total > 1) {
return total % limit === 0
? total / limit
: parseInt(`${total / limit}`) + 1;
} else return 1;
};
const getLimits = (pagination: paginationPropTypes) => {
const { total } = pagination;
const limits = [5];
if (total > 10) {
for (let i = 10; i <= total && limits.length <= 10; i += 5) {
limits.push(i);
}
}
return limits;
};
2022-01-19 16:50:25 +00:00
export default function Users() {
2022-01-29 15:23:53 +00:00
const client = useClient();
2022-01-30 05:09:35 +00:00
const toast = useToast();
2022-01-29 15:23:53 +00:00
const [paginationProps, setPaginationProps] =
React.useState<paginationPropTypes>({
limit: 5,
page: 1,
offset: 0,
total: 0,
maxPages: 1,
});
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
2022-01-31 06:03:35 +00:00
const [loading, setLoading] = React.useState<boolean>(false);
2022-03-16 08:38:22 +00:00
const [disableInviteMembers, setDisableInviteMembers] =
React.useState<boolean>(true);
2022-01-30 05:09:35 +00:00
const updateUserList = async () => {
2022-01-31 06:03:35 +00:00
setLoading(true);
2022-01-29 15:23:53 +00:00
const { data } = await client
.query(UserDetailsQuery, {
params: {
pagination: {
limit: paginationProps.limit,
page: paginationProps.page,
},
},
})
.toPromise();
if (data?._users) {
const { pagination, users } = data._users;
const maxPages = getMaxPages(pagination);
2022-01-31 08:56:19 +00:00
if (users && users.length > 0) {
setPaginationProps({ ...paginationProps, ...pagination, maxPages });
setUserList(users);
} else {
if (paginationProps.page !== 1) {
setPaginationProps({
...paginationProps,
...pagination,
maxPages,
page: 1,
});
}
}
2022-01-29 15:23:53 +00:00
}
2022-01-31 06:03:35 +00:00
setLoading(false);
2022-01-29 15:23:53 +00:00
};
2022-03-16 08:38:22 +00:00
const checkEmailVerification = async () => {
setLoading(true);
const { data } = await client.query(EmailVerificationQuery).toPromise();
if (data?._env) {
const { DISABLE_EMAIL_VERIFICATION } = data._env;
setDisableInviteMembers(DISABLE_EMAIL_VERIFICATION);
}
setLoading(false);
};
2022-01-29 15:23:53 +00:00
React.useEffect(() => {
2022-01-30 05:09:35 +00:00
updateUserList();
2022-03-16 08:38:22 +00:00
checkEmailVerification();
2022-01-29 15:23:53 +00:00
}, []);
React.useEffect(() => {
2022-01-30 05:09:35 +00:00
updateUserList();
2022-01-29 15:23:53 +00:00
}, [paginationProps.page, paginationProps.limit]);
const paginationHandler = (value: Record<string, number>) => {
setPaginationProps({ ...paginationProps, ...value });
};
const userVerificationHandler = async (user: userDataTypes) => {
const { id, email } = user;
2022-01-30 05:09:35 +00:00
const res = await client
2022-01-29 15:23:53 +00:00
.mutation(UpdateUser, {
params: {
id,
email,
email_verified: true,
},
})
.toPromise();
2022-01-30 05:09:35 +00:00
if (res.error) {
toast({
title: 'User verification failed',
isClosable: true,
status: 'error',
2022-12-22 18:49:32 +00:00
position: 'top-right',
2022-01-30 05:09:35 +00:00
});
} else if (res.data?._update_user?.id) {
toast({
title: 'User verification successful',
isClosable: true,
status: 'success',
2022-12-22 18:49:32 +00:00
position: 'top-right',
2022-01-30 05:09:35 +00:00
});
}
updateUserList();
2022-01-29 15:23:53 +00:00
};
2022-03-16 18:34:57 +00:00
const updateAccessHandler = async (
id: string,
2022-10-02 17:06:57 +00:00
action: updateAccessActions,
) => {
switch (action) {
case updateAccessActions.ENABLE:
const enableAccessRes = await client
.mutation(EnableAccess, {
param: {
user_id: id,
},
})
.toPromise();
if (enableAccessRes.error) {
toast({
title: 'User access enable failed',
isClosable: true,
status: 'error',
2022-12-22 18:49:32 +00:00
position: 'top-right',
});
} else {
toast({
title: 'User access enabled successfully',
isClosable: true,
status: 'success',
2022-12-22 18:49:32 +00:00
position: 'top-right',
});
}
updateUserList();
break;
case updateAccessActions.REVOKE:
const revokeAccessRes = await client
.mutation(RevokeAccess, {
param: {
user_id: id,
},
})
.toPromise();
if (revokeAccessRes.error) {
toast({
title: 'User access revoke failed',
isClosable: true,
status: 'error',
2022-12-22 18:49:32 +00:00
position: 'top-right',
});
} else {
toast({
title: 'User access revoked successfully',
isClosable: true,
status: 'success',
2022-12-22 18:49:32 +00:00
position: 'top-right',
});
}
updateUserList();
break;
default:
break;
}
};
const multiFactorAuthUpdateHandler = async (user: userDataTypes) => {
const res = await client
.mutation(UpdateUser, {
params: {
id: user.id,
is_multi_factor_auth_enabled: !user.is_multi_factor_auth_enabled,
},
})
.toPromise();
if (res.data?._update_user?.id) {
toast({
2022-10-02 17:06:57 +00:00
title: `Multi factor authentication ${
user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled'
} for user`,
isClosable: true,
status: 'success',
2022-12-22 18:49:32 +00:00
position: 'top-right',
});
updateUserList();
return;
}
toast({
title: 'Multi factor authentication update failed for user',
isClosable: true,
status: 'error',
2022-12-22 18:49:32 +00:00
position: 'top-right',
});
};
2022-01-29 15:23:53 +00:00
return (
<Box m="5" py="5" px="10" bg="white" rounded="md">
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
<Text fontSize="md" fontWeight="bold">
Users
</Text>
2022-03-16 14:43:18 +00:00
<InviteMembersModal
disabled={disableInviteMembers}
updateUserList={updateUserList}
/>
2022-01-29 15:23:53 +00:00
</Flex>
2022-01-31 06:03:35 +00:00
{!loading ? (
userList.length > 0 ? (
2022-08-30 16:26:28 +00:00
<TableContainer>
<Table variant="simple">
<Thead>
<Tr>
<Th>Email</Th>
<Th>Created At</Th>
<Th>Signup Methods</Th>
<Th>Roles</Th>
<Th>Verified</Th>
<Th>Access</Th>
<Th>
<Tooltip label="MultiFactor Authentication Enabled / Disabled">
MFA
2022-01-31 06:03:35 +00:00
</Tooltip>
2022-08-30 16:26:28 +00:00
</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{userList.map((user: userDataTypes) => {
const { email_verified, created_at, ...rest }: any = user;
return (
<Tr key={user.id} style={{ fontSize: 14 }}>
<Td maxW="300">{user.email}</Td>
<Td>
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
</Td>
<Td>{user.signup_methods}</Td>
<Td>{user.roles.join(', ')}</Td>
<Td>
<Tag
size="sm"
variant="outline"
colorScheme={user.email_verified ? 'green' : 'yellow'}
>
{user.email_verified.toString()}
</Tag>
</Td>
<Td>
<Tag
size="sm"
variant="outline"
colorScheme={user.revoked_timestamp ? 'red' : 'green'}
>
{user.revoked_timestamp ? 'Revoked' : 'Enabled'}
</Tag>
</Td>
<Td>
<Tag
size="sm"
variant="outline"
colorScheme={
user.is_multi_factor_auth_enabled ? 'green' : 'red'
}
>
{user.is_multi_factor_auth_enabled
? 'Enabled'
: 'Disabled'}
</Tag>
</Td>
<Td>
<Menu>
<MenuButton as={Button} variant="unstyled" size="sm">
<Flex
justifyContent="space-between"
alignItems="center"
>
<Text fontSize="sm" fontWeight="light">
Menu
</Text>
<FaAngleDown style={{ marginLeft: 10 }} />
</Flex>
</MenuButton>
<MenuList>
{!user.email_verified && (
<MenuItem
onClick={() => userVerificationHandler(user)}
>
Verify User
</MenuItem>
)}
<EditUserModal
user={rest}
updateUserList={updateUserList}
/>
<DeleteUserModal
user={rest}
updateUserList={updateUserList}
/>
{user.revoked_timestamp ? (
<MenuItem
onClick={() =>
updateAccessHandler(
user.id,
2022-10-02 17:06:57 +00:00
updateAccessActions.ENABLE,
2022-08-30 16:26:28 +00:00
)
}
>
Enable Access
</MenuItem>
) : (
<MenuItem
onClick={() =>
updateAccessHandler(
user.id,
2022-10-02 17:06:57 +00:00
updateAccessActions.REVOKE,
2022-08-30 16:26:28 +00:00
)
}
>
Revoke Access
</MenuItem>
)}
{user.is_multi_factor_auth_enabled ? (
<MenuItem
2022-10-02 17:06:57 +00:00
onClick={() =>
multiFactorAuthUpdateHandler(user)
}
2022-08-30 16:26:28 +00:00
>
Disable MultiFactor Authentication
</MenuItem>
) : (
<MenuItem
2022-10-02 17:06:57 +00:00
onClick={() =>
multiFactorAuthUpdateHandler(user)
}
2022-08-30 16:26:28 +00:00
>
Enable MultiFactor Authentication
</MenuItem>
)}
</MenuList>
</Menu>
</Td>
</Tr>
);
})}
</Tbody>
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
<TableCaption>
2022-01-31 06:03:35 +00:00
<Flex
2022-08-30 16:26:28 +00:00
justifyContent="space-between"
2022-01-31 06:03:35 +00:00
alignItems="center"
2022-08-30 16:26:28 +00:00
m="2% 0"
2022-01-31 06:03:35 +00:00
>
2022-08-30 16:26:28 +00:00
<Flex flex="1">
<Tooltip label="First Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: 1,
})
}
isDisabled={paginationProps.page <= 1}
mr={4}
icon={<FaAngleDoubleLeft />}
/>
</Tooltip>
<Tooltip label="Previous Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page - 1,
})
}
isDisabled={paginationProps.page <= 1}
icon={<FaAngleLeft />}
/>
</Tooltip>
</Flex>
<Flex
flex="8"
justifyContent="space-evenly"
alignItems="center"
>
<Text mr={8}>
Page{' '}
<Text fontWeight="bold" as="span">
{paginationProps.page}
</Text>{' '}
of{' '}
<Text fontWeight="bold" as="span">
{paginationProps.maxPages}
</Text>
2022-01-31 06:03:35 +00:00
</Text>
2022-08-30 16:26:28 +00:00
<Flex alignItems="center">
<Text flexShrink="0">Go to page:</Text>{' '}
<NumberInput
ml={2}
mr={8}
w={28}
min={1}
max={paginationProps.maxPages}
onChange={(value) =>
paginationHandler({
page: parseInt(value),
})
}
value={paginationProps.page}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Flex>
<Select
w={32}
value={paginationProps.limit}
onChange={(e) =>
2022-01-31 06:03:35 +00:00
paginationHandler({
2022-08-30 16:26:28 +00:00
page: 1,
limit: parseInt(e.target.value),
2022-01-31 06:03:35 +00:00
})
}
>
2022-08-30 16:26:28 +00:00
{getLimits(paginationProps).map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</Select>
</Flex>
<Flex flex="1">
<Tooltip label="Next Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page + 1,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
icon={<FaAngleRight />}
/>
</Tooltip>
<Tooltip label="Last Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.maxPages,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
ml={4}
icon={<FaAngleDoubleRight />}
/>
</Tooltip>
2022-01-31 06:03:35 +00:00
</Flex>
2022-01-29 15:23:53 +00:00
</Flex>
2022-08-30 16:26:28 +00:00
</TableCaption>
)}
</Table>
</TableContainer>
2022-01-31 06:03:35 +00:00
) : (
<Flex
flexDirection="column"
minH="25vh"
justifyContent="center"
alignItems="center"
2022-01-29 15:23:53 +00:00
>
2022-01-31 06:03:35 +00:00
<Center w="50px" marginRight="1.5%">
<FaExclamationCircle style={{ color: '#f0f0f0', fontSize: 70 }} />
</Center>
<Text
fontSize="2xl"
paddingRight="1%"
fontWeight="bold"
color="#d9d9d9"
>
No Data
</Text>
</Flex>
)
) : (
<Center minH="25vh">
<Spinner />
</Center>
2022-01-29 15:23:53 +00:00
)}
</Box>
);
}