diff --git a/src/App.tsx b/src/App.tsx index 0f7b653c9bfa1dd77c4637fd1934e9576b0524dc..b9797b0c763a2b654774ee5b4930004bfa9cfb48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Helmet } from 'react-helmet'; -import { Routes, Route, Navigate } from 'react-router-dom'; +import { Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { Toaster } from 'react-hot-toast'; import { useAuth } from 'src/services/auth'; @@ -10,7 +10,13 @@ import { LoginCallback } from './modules/login/LoginCallback'; // eslint-disable-next-line @typescript-eslint/no-unused-vars function App() { - const { authToken } = useAuth(); + const { authToken, currentUser, isAdmin } = useAuth(); + + const redirectToLogin = !authToken || !currentUser?.app_roles; + + const ProtectedRoute = () => { + return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />; + }; return ( <> @@ -26,7 +32,7 @@ function App() { </Helmet> <div className="app bg-gray-50 min-h-screen flex flex-col"> - {!authToken ? ( + {redirectToLogin ? ( <Routes> <Route path="/login" element={<Login />} /> <Route path="/login-callback" element={<LoginCallback />} /> @@ -36,7 +42,9 @@ function App() { <Layout> <Routes> <Route path="/dashboard" element={<Dashboard />} /> - <Route path="/users" element={<Users />} /> + <Route path="/users" element={<ProtectedRoute />}> + <Route path="/users" element={<Users />} /> + </Route> <Route path="*" element={<Navigate to="/dashboard" />} /> </Routes> </Layout> diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 141da56678ebb00defb443fd77fba26379a45b1f..a58e2d82adfcb801fa24535dcc59e0da38e0d92e 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -8,20 +8,28 @@ import clsx from 'clsx'; import { CurrentUserModal } from './components/CurrentUserModal'; const navigation = [ - { name: 'Dashboard', to: '/dashboard' }, - { name: 'Users', to: '/users' }, + { name: 'Dashboard', to: '/dashboard', requiresAdmin: false }, + { name: 'Users', to: '/users', requiresAdmin: true }, ]; function classNames(...classes: any[]) { return classes.filter(Boolean).join(' '); } +function filterNavigationByDashboardRole(isAdmin: boolean) { + if (isAdmin) { + return navigation; + } + + return navigation.filter((item) => !item.requiresAdmin); +} + // eslint-disable-next-line @typescript-eslint/no-empty-interface interface HeaderProps {} const Header: React.FC<HeaderProps> = () => { const [currentUserModal, setCurrentUserModal] = useState(false); - const { logOut, currentUser } = useAuth(); + const { logOut, currentUser, isAdmin } = useAuth(); const { pathname } = useLocation(); @@ -30,6 +38,8 @@ const Header: React.FC<HeaderProps> = () => { }; const currentUserModalClose = () => setCurrentUserModal(false); + const navigationItems = filterNavigationByDashboardRole(isAdmin); + return ( <> <Disclosure as="nav" className="bg-white shadow relative z-10"> @@ -55,7 +65,7 @@ const Header: React.FC<HeaderProps> = () => { </Link> <div className="hidden sm:ml-6 sm:flex sm:space-x-8"> {/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */} - {navigation.map((item) => ( + {navigationItems.map((item) => ( <Link key={item.name} to={item.to} diff --git a/src/components/Header/components/CurrentUserModal/CurrentUserModal.tsx b/src/components/Header/components/CurrentUserModal/CurrentUserModal.tsx index cf578cb060057cb8d18aa552e77d3aa410ec8cd0..29a3c166923ea2b8275105ed86f7ea37c1d56f0a 100644 --- a/src/components/Header/components/CurrentUserModal/CurrentUserModal.tsx +++ b/src/components/Header/components/CurrentUserModal/CurrentUserModal.tsx @@ -1,31 +1,33 @@ +import _ from 'lodash'; import React, { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { Modal, Banner } from 'src/components'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { Modal } from 'src/components'; import { Input, Select } from 'src/components/Form'; +import { useAuth } from 'src/services/auth'; import { User, UserRole, useUsers } from 'src/services/users'; -import { appAccessList } from './consts'; +import { appAccessList, initialUserForm } from './consts'; import { UserModalProps } from './types'; export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => { const { editUserById, userModalLoading } = useUsers(); + const { isAdmin } = useAuth(); const { control, reset, handleSubmit } = useForm<User>({ - defaultValues: { - name: null, - email: null, - id: null, - role_id: null, - status: null, - }, + defaultValues: initialUserForm, + }); + + const { fields } = useFieldArray({ + control, + name: 'app_roles', }); useEffect(() => { - if (user) { + if (user && !_.isEmpty(user)) { reset(user); } return () => { - reset({ name: null, email: null, id: null }); + reset(initialUserForm); }; }, [user, reset]); @@ -54,7 +56,7 @@ export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => { return ( <Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={userModalLoading} useCancelButton> <div className="bg-white px-4"> - <div className="space-y-4 divide-y divide-gray-200"> + <div className="space-y-10 divide-y divide-gray-200"> <div> <div> <h3 className="text-lg leading-6 font-medium text-gray-900">Profile</h3> @@ -69,77 +71,90 @@ export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => { <Input control={control} name="email" label="Email" type="email" onKeyPress={handleKeyPress} /> </div> - <div className="sm:col-span-6"> - <Select - control={control} - name="role_id" - label="Role" - options={[ - { value: UserRole.Admin, name: 'Admin' }, - { value: UserRole.User, name: 'User' }, - ]} - /> - </div> - - <div className="sm:col-span-6"> - <Banner title="Editing user status and app access coming soon." titleSm="Comming soon!" /> - </div> - - <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none"> - {/* <Select control={control} name="status" label="Status" options={['Admin', 'Inactive']} /> */} - <label htmlFor="status" className="block text-sm font-medium text-gray-700"> - Status - </label> - <div className="mt-1"> - <select - id="status" - name="status" - className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" - > - <option>Active</option> - <option>Inactive</option> - <option>Banned</option> - </select> - </div> - </div> + {isAdmin && ( + <> + <div className="sm:col-span-3"> + {fields + .filter((field) => field.name === 'dashboard') + .map((item, index) => ( + <Select + key={item.id} + control={control} + name={`app_roles.${index}.role`} + label="Role" + options={[ + { value: UserRole.Admin, name: 'Admin' }, + { value: UserRole.User, name: 'User' }, + ]} + /> + ))} + </div> + + <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none"> + {/* <Select control={control} name="status" label="Status" options={['Admin', 'Inactive']} /> */} + <label htmlFor="status" className="block text-sm font-medium text-gray-700"> + Status + </label> + <div className="mt-1"> + <select + id="status" + name="status" + className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" + > + <option>Active</option> + <option>Inactive</option> + <option>Banned</option> + </select> + </div> + </div> + </> + )} </div> </div> - <div className="opacity-40 cursor-default pointer-events-none select-none"> - <div className="mt-4"> - <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3> - </div> - + {isAdmin && ( <div> - <div className="flow-root mt-6"> - <ul className="-my-5 divide-y divide-gray-200 "> - {appAccessList.map((app: any) => { - return ( - <li className="py-4" key={app.name}> - <div className="flex items-center space-x-4"> - <div className="flex-shrink-0 flex-1 flex items-center"> - <img className="h-10 w-10 rounded-md overflow-hidden" src={app.image} alt={app.name} /> - <h3 className="ml-4 text-md leading-6 font-medium text-gray-900">{app.name}</h3> - </div> - <div> - <select - id={app.name} - name={app.name} - className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" - > - <option>User</option> - <option>Admin</option> - <option>Super Admin</option> - </select> + <div className="mt-8"> + <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3> + </div> + + <div> + <div className="flow-root mt-6"> + <ul className="-my-5 divide-y divide-gray-200 "> + {fields + .filter((field) => field.name !== 'dashboard') + .map((item, index) => ( + <li className="py-4" key={item.name}> + <div className="flex items-center space-x-4"> + <div className="flex-shrink-0 flex-1 flex items-center"> + <img + className="h-10 w-10 rounded-md overflow-hidden" + src={_.find(appAccessList, ['name', item.name!])?.image} + alt={item.name ?? 'Image'} + /> + <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> + {_.find(appAccessList, ['name', item.name!])?.label} + </h3> + </div> + <div> + <Select + key={item.id} + control={control} + name={`app_roles.${index}.role`} + options={[ + { value: UserRole.Admin, name: 'Admin' }, + { value: UserRole.User, name: 'User' }, + ]} + /> + </div> </div> - </div> - </li> - ); - })} - </ul> + </li> + ))} + </ul> + </div> </div> </div> - </div> + )} </div> </div> </Modal> diff --git a/src/components/Header/components/CurrentUserModal/consts.ts b/src/components/Header/components/CurrentUserModal/consts.ts index 080c939e33132e6c1741c135d24ab9e3c1f1f100..abda7084a71020249dc970cbaaf53fbc986e8b87 100644 --- a/src/components/Header/components/CurrentUserModal/consts.ts +++ b/src/components/Header/components/CurrentUserModal/consts.ts @@ -1,18 +1,55 @@ +import { UserRole } from 'src/services/users'; + export const appAccessList = [ { + name: 'wekan', image: '/assets/wekan.svg', - name: 'Wekan', + label: 'Wekan', }, { + name: 'wordpress', image: '/assets/wordpress.svg', - name: 'Wordpress', + label: 'Wordpress', }, { + name: 'next-cloud', image: '/assets/nextcloud.svg', - name: 'NextCloud', + label: 'NextCloud', }, { + name: 'zulip', image: '/assets/zulip.svg', - name: 'Zulip', + label: 'Zulip', }, ]; + +const initialAppRoles = [ + { + name: 'dashboard', + role: UserRole.User, + }, + { + name: 'wekan', + role: UserRole.User, + }, + { + name: 'wordpress', + role: UserRole.User, + }, + { + name: 'next-cloud', + role: UserRole.User, + }, + { + name: 'zulip', + role: UserRole.User, + }, +]; + +export const initialUserForm = { + id: '', + name: '', + email: '', + app_roles: initialAppRoles, + status: '', +}; diff --git a/src/components/Header/components/CurrentUserModal/types.ts b/src/components/Header/components/CurrentUserModal/types.ts index 00bc7d6e15c8dace161085e8d8ba6dc44637c519..aa11c0eba5a76bed2a9e63a03cf7e4ccf08ceec0 100644 --- a/src/components/Header/components/CurrentUserModal/types.ts +++ b/src/components/Header/components/CurrentUserModal/types.ts @@ -3,5 +3,5 @@ import { User } from 'src/services/users'; export type UserModalProps = { open: boolean; onClose: () => void; - user: User; + user: User | null; }; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 09eb629c747017780e955d2e55181c3d0ed8fe6f..157b72896aea57e98df4683667a6346fa7dcbe1c 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -148,26 +148,28 @@ export const Table = <T extends Record<string, unknown>>({ ); }) ) : ( - <td colSpan={4} className="py-24"> - <div className="flex flex-col justify-center items-center"> - <div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150"> - <svg - className="animate-spin h-6 w-6 text-primary" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - > - <circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> - <path - className="opacity-100" - fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" - /> - </svg> + <tr> + <td colSpan={4} className="py-24"> + <div className="flex flex-col justify-center items-center"> + <div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150"> + <svg + className="animate-spin h-6 w-6 text-primary" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + > + <circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> + <path + className="opacity-100" + fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + /> + </svg> + </div> + <p className="text-sm text-primary-600 mt-2">Loading users</p> </div> - <p className="text-sm text-primary-600 mt-2">Loading users</p> - </div> - </td> + </td> + </tr> )} </tbody> </table> diff --git a/src/modules/users/Users.tsx b/src/modules/users/Users.tsx index 899065ae6ce1e4db92f28a7fe81fdae84fd810fb..6ba32657e1483aaa0b7d486441fb3551ce7615e7 100644 --- a/src/modules/users/Users.tsx +++ b/src/modules/users/Users.tsx @@ -5,6 +5,7 @@ import { CogIcon, TrashIcon } from '@heroicons/react/outline'; import { useUsers } from 'src/services/users'; import { Table } from 'src/components'; import { debounce } from 'lodash'; +import { useAuth } from 'src/services/auth'; import { UserModal } from './components/UserModal'; export const Users: React.FC = () => { @@ -13,6 +14,7 @@ export const Users: React.FC = () => { const [userId, setUserId] = useState(null); const [search, setSearch] = useState(''); const { users, loadUsers, userTableLoading } = useUsers(); + const { isAdmin } = useAuth(); const handleSearch = (event: any) => { setSearch(event.target.value); @@ -60,23 +62,27 @@ export const Users: React.FC = () => { Cell: (props: any) => { const { row } = props; - return ( - <div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity"> - <button - onClick={() => configureModalOpen(row.original.id)} - type="button" - className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" - > - <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> - Configure - </button> - </div> - ); + if (isAdmin) { + return ( + <div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity"> + <button + onClick={() => configureModalOpen(row.original.id)} + type="button" + className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" + > + <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> + Configure + </button> + </div> + ); + } + + return null; }, width: 'auto', }, ], - [], + [isAdmin], ); const selectedRows = useCallback((rows: Record<string, boolean>) => { @@ -88,16 +94,19 @@ export const Users: React.FC = () => { <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow"> <div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between"> <h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1> - <div className="mt-3 sm:mt-0 sm:ml-4"> - <button - onClick={() => configureModalOpen(null)} - type="button" - className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800" - > - <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> - Add new user - </button> - </div> + + {isAdmin && ( + <div className="mt-3 sm:mt-0 sm:ml-4"> + <button + onClick={() => configureModalOpen(null)} + type="button" + className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800" + > + <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" /> + Add new user + </button> + </div> + )} </div> <div className="flex justify-between w-100 my-3 items-center mb-5 "> @@ -153,7 +162,7 @@ export const Users: React.FC = () => { </div> </div> - <UserModal open={configureModal} onClose={configureModalClose} userId={userId} /> + <UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} /> </div> </div> ); diff --git a/src/modules/users/components/UserModal/UserModal.tsx b/src/modules/users/components/UserModal/UserModal.tsx index 5b3814bfc4ef7c3843caeb3f92facd332e2a1f5a..b31d2182e56b691b49f0c829bdf43bce16de8501 100644 --- a/src/modules/users/components/UserModal/UserModal.tsx +++ b/src/modules/users/components/UserModal/UserModal.tsx @@ -1,26 +1,27 @@ import React, { useEffect, useState } from 'react'; +import _ from 'lodash'; import { TrashIcon } from '@heroicons/react/outline'; -import { useForm } from 'react-hook-form'; -import { Modal, Banner, ConfirmationModal } from 'src/components'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { Modal, ConfirmationModal } from 'src/components'; import { Input, Select } from 'src/components/Form'; import { User, UserRole, useUsers } from 'src/services/users'; import { useAuth } from 'src/services/auth'; -import { appAccessList } from './consts'; +import { appAccessList, initialUserForm } from './consts'; import { UserModalProps } from './types'; -export const UserModal = ({ open, onClose, userId }: UserModalProps) => { +export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) => { const [deleteModal, setDeleteModal] = useState(false); - const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById } = useUsers(); - const { currentUser } = useAuth(); + const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById, clearSelectedUser } = + useUsers(); + const { currentUser, isAdmin } = useAuth(); const { control, reset, handleSubmit } = useForm<User>({ - defaultValues: { - name: null, - email: null, - id: null, - role_id: null, - status: null, - }, + defaultValues: initialUserForm, + }); + + const { fields } = useFieldArray({ + control, + name: 'app_roles', }); useEffect(() => { @@ -28,19 +29,19 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { loadUser(userId); } - reset({ name: null, email: null, id: null, status: null }); + reset(initialUserForm); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userId]); + }, [userId, open]); useEffect(() => { - if (user) { + if (!_.isEmpty(user)) { reset(user); } return () => { - reset({ name: null, email: null, id: null, status: null }); + reset(initialUserForm); }; - }, [user, reset]); + }, [user, reset, open]); const handleSave = async () => { try { @@ -48,13 +49,14 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { await handleSubmit((data) => editUserById(data))(); } else { await handleSubmit((data) => createNewUser(data))(); - reset({ name: null, email: null, id: null, status: null }); } - - onClose(); } catch (e: any) { // Continue } + + onClose(); + clearSelectedUser(); + setUserId(null); }; const handleKeyPress = (e: any) => { @@ -65,6 +67,8 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { const handleClose = () => { onClose(); + clearSelectedUser(); + setUserId(null); }; const deleteModalOpen = () => setDeleteModal(true); @@ -74,6 +78,9 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { if (userId) { deleteUserById(userId); } + + clearSelectedUser(); + setUserId(null); handleClose(); deleteModalClose(); }; @@ -87,7 +94,7 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { isLoading={userModalLoading} leftActions={ userId && - user.email !== currentUser.email && ( + user.email !== currentUser?.email && ( <button onClick={deleteModalOpen} type="button" @@ -101,7 +108,7 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { useCancelButton > <div className="bg-white px-4"> - <div className="space-y-4 divide-y divide-gray-200"> + <div className="space-y-10 divide-y divide-gray-200"> <div> <div> <h3 className="text-lg leading-6 font-medium text-gray-900">{userId ? 'Edit user' : 'Add new user'}</h3> @@ -116,77 +123,91 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => { <Input control={control} name="email" label="Email" type="email" onKeyPress={handleKeyPress} /> </div> - <div className="sm:col-span-6"> - <Select - control={control} - name="role_id" - label="Role" - options={[ - { value: UserRole.Admin, name: 'Admin' }, - { value: UserRole.User, name: 'User' }, - ]} - /> - </div> - - <div className="sm:col-span-6"> - <Banner title="Editing user status and app access coming soon." titleSm="Comming soon!" /> + <div className="sm:col-span-3"> + {fields + .filter((field) => field.name === 'dashboard') + .map((item, index) => ( + <Select + key={item.name} + control={control} + name={`app_roles.${index}.role`} + label="Role" + options={[ + { value: UserRole.User, name: 'User' }, + { value: UserRole.Admin, name: 'Admin' }, + ]} + /> + ))} </div> - <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none"> - {/* <Select control={control} name="status" label="Status" options={['Active', 'Inactive']} /> */} - <label htmlFor="status" className="block text-sm font-medium text-gray-700"> - Status - </label> - <div className="mt-1"> - <select - id="status" - name="status" - className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" - > - <option>Active</option> - <option>Inactive</option> - <option>Banned</option> - </select> + {isAdmin && ( + <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none"> + <label htmlFor="status" className="block text-sm font-medium text-gray-700"> + Status + </label> + <div className="mt-1"> + <select + id="status" + name="status" + className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" + > + <option>Active</option> + <option>Inactive</option> + <option>Banned</option> + </select> + </div> </div> - </div> + )} </div> </div> - <div className="opacity-40 cursor-default pointer-events-none select-none"> - <div className="mt-4"> - <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3> - </div> - + {isAdmin && ( <div> - <div className="flow-root mt-6"> - <ul className="-my-5 divide-y divide-gray-200 "> - {appAccessList.map((app: any) => { - return ( - <li className="py-4" key={app.name}> - <div className="flex items-center space-x-4"> - <div className="flex-shrink-0 flex-1 flex items-center"> - <img className="h-10 w-10 rounded-md overflow-hidden" src={app.image} alt={app.name} /> - <h3 className="ml-4 text-md leading-6 font-medium text-gray-900">{app.name}</h3> - </div> - <div> - <select - id={app.name} - name={app.name} - className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md" - > - <option>User</option> - <option>Admin</option> - <option>Super Admin</option> - </select> + <div className="mt-8"> + <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3> + </div> + + <div> + <div className="flow-root mt-6"> + <ul className="-my-5 divide-y divide-gray-200 "> + {fields.map((item, index) => { + if (item.name === 'dashboard') { + return null; + } + + return ( + <li className="py-4" key={item.name}> + <div className="flex items-center space-x-4"> + <div className="flex-shrink-0 flex-1 flex items-center"> + <img + className="h-10 w-10 rounded-md overflow-hidden" + src={_.find(appAccessList, ['name', item.name!])?.image} + alt={item.name ?? 'Image'} + /> + <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> + {_.find(appAccessList, ['name', item.name!])?.label} + </h3> + </div> + <div> + <Select + key={item.id} + control={control} + name={`app_roles.${index}.role`} + options={[ + { value: UserRole.User, name: 'User' }, + { value: UserRole.Admin, name: 'Admin' }, + ]} + /> + </div> </div> - </div> - </li> - ); - })} - </ul> + </li> + ); + })} + </ul> + </div> </div> </div> - </div> + )} </div> </div> </Modal> diff --git a/src/modules/users/components/UserModal/consts.ts b/src/modules/users/components/UserModal/consts.ts index 080c939e33132e6c1741c135d24ab9e3c1f1f100..a47d6de433a455886514141886474bc9a8d8b51a 100644 --- a/src/modules/users/components/UserModal/consts.ts +++ b/src/modules/users/components/UserModal/consts.ts @@ -1,18 +1,55 @@ +import { UserRole } from 'src/services/users'; + export const appAccessList = [ { + name: 'wekan', image: '/assets/wekan.svg', - name: 'Wekan', + label: 'Wekan', }, { + name: 'wordpress', image: '/assets/wordpress.svg', - name: 'Wordpress', + label: 'Wordpress', }, { + name: 'nextcloud', image: '/assets/nextcloud.svg', - name: 'NextCloud', + label: 'NextCloud', }, { + name: 'zulip', image: '/assets/zulip.svg', - name: 'Zulip', + label: 'Zulip', }, ]; + +const initialAppRoles = [ + { + name: 'dashboard', + role: UserRole.User, + }, + { + name: 'wekan', + role: UserRole.User, + }, + { + name: 'wordpress', + role: UserRole.User, + }, + { + name: 'nextcloud', + role: UserRole.User, + }, + { + name: 'zulip', + role: UserRole.User, + }, +]; + +export const initialUserForm = { + id: '', + name: '', + email: '', + app_roles: initialAppRoles, + status: '', +}; diff --git a/src/modules/users/components/UserModal/types.ts b/src/modules/users/components/UserModal/types.ts index 710d29ea85e1c1d2724dd14c756018f086ab1f2a..f7804ae58e543185431fe57a25be0fe379fb1a94 100644 --- a/src/modules/users/components/UserModal/types.ts +++ b/src/modules/users/components/UserModal/types.ts @@ -2,4 +2,5 @@ export type UserModalProps = { open: boolean; onClose: () => void; userId: string | null; + setUserId: any; }; diff --git a/src/services/auth/hooks/use-auth.ts b/src/services/auth/hooks/use-auth.ts index 89fc5ffe5e43121bf77be1a6391f5063a138c030..cd9356b8401992f109bff6078740d46974bddbcb 100644 --- a/src/services/auth/hooks/use-auth.ts +++ b/src/services/auth/hooks/use-auth.ts @@ -1,11 +1,12 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { getAuthToken, getCurrentUser, signIn, signOut } from '../redux'; +import { getAuthToken, getCurrentUser, getIsAdmin, signIn, signOut } from '../redux'; export function useAuth() { const dispatch = useDispatch(); const currentUser = useSelector(getCurrentUser); const authToken = useSelector(getAuthToken); + const isAdmin = useSelector(getIsAdmin); const logIn = useCallback( (params) => { @@ -21,6 +22,7 @@ export function useAuth() { return { authToken, currentUser, + isAdmin, logIn, logOut, }; diff --git a/src/services/auth/redux/index.ts b/src/services/auth/redux/index.ts index d65fe6caa488cd439b1e6f25eb0eac6a38fcbe2e..c1d00839ab777ac7e5e9907a9bd2f93cdd048a7e 100644 --- a/src/services/auth/redux/index.ts +++ b/src/services/auth/redux/index.ts @@ -1,4 +1,4 @@ export { signIn, signOut, AuthActionTypes } from './actions'; export { default as reducer } from './reducers'; -export { getAuth, getIsAuthLoading, getAuthToken, getCurrentUser } from './selectors'; +export { getAuth, getIsAuthLoading, getAuthToken, getCurrentUser, getIsAdmin } from './selectors'; export * from './types'; diff --git a/src/services/auth/redux/reducers.ts b/src/services/auth/redux/reducers.ts index 8deb265ea48363c59f359c970165180f525cf711..43fb2ddd8fbdc50353bb57a5955bd478deb3cfaa 100644 --- a/src/services/auth/redux/reducers.ts +++ b/src/services/auth/redux/reducers.ts @@ -6,12 +6,12 @@ import { AuthActionTypes } from './actions'; import { transformAuthUser } from '../transformations'; const initialCurrentUserState: User = { - email: null, - name: null, - id: null, - role_id: null, - status: null, - preferredUsername: null, + email: '', + name: '', + id: '', + app_roles: [], + status: '', + preferredUsername: '', }; const initialState: AuthState = { diff --git a/src/services/auth/redux/selectors.ts b/src/services/auth/redux/selectors.ts index 4982e5eaa27a4f706eb6a0673a553a61c1b82bb3..0feaae547d0ed851a9a8b1b5d3308a2ea6633f87 100644 --- a/src/services/auth/redux/selectors.ts +++ b/src/services/auth/redux/selectors.ts @@ -1,6 +1,7 @@ import { State } from 'src/redux'; import { isLoading } from 'src/services/api'; +import { UserRole } from 'src/services/users'; export const getAuth = (state: State) => state.auth; @@ -8,6 +9,20 @@ export const getAuthToken = (state: State) => state.auth.token; export const getCurrentUser = (state: State) => state.auth.userInfo; +export const getIsAdmin = (state: State) => { + // check since old users wont have this + if (state.auth.userInfo) { + if (!state.auth.userInfo.app_roles) { + return false; + } + const isAdmin = state.auth.userInfo.app_roles.find( + (role) => role.name === 'dashboard' && role.role === UserRole.Admin, + ); + return !!isAdmin; + } + return false; +}; + export const getIsAuthLoading = (state: State) => isLoading(getAuth(state)); export const getToken = (state: State) => state.auth.token; diff --git a/src/services/auth/transformations.ts b/src/services/auth/transformations.ts index 2cfebaf69aba03e26c37951f188b3b835450f0aa..1f39058d270f54011fe9d9f75c6315c0d10c5cff 100644 --- a/src/services/auth/transformations.ts +++ b/src/services/auth/transformations.ts @@ -1,18 +1,21 @@ -import { UserRole } from '../users'; +import { User } from '../users'; import { Auth } from './types'; +import { transformAppRoles } from '../users/transformations'; -export const transformAuthUser = (response: any): Auth => { - const resolvedUserRole = !response.userInfo.role_id ? UserRole.User : response.userInfo.role_id; +const transformUser = (response: any): User => { + return { + id: response.id ?? '', + app_roles: response.app_roles ? response.app_roles.map(transformAppRoles) : [], + email: response.email ?? '', + name: response.name ?? '', + preferredUsername: response.preferredUsername ?? '', + status: response.state ?? '', + }; +}; +export const transformAuthUser = (response: any): Auth => { return { token: response.accessToken, - userInfo: { - id: response.userInfo.id, - role_id: resolvedUserRole, - email: response.userInfo.email ?? null, - name: response.userInfo.name ?? null, - preferredUsername: response.userInfo.preferredUsername, - status: response.userInfo.state ?? null, - }, + userInfo: response.userInfo ? transformUser(response.userInfo) : null, }; }; diff --git a/src/services/auth/types.ts b/src/services/auth/types.ts index f1963855afe10a1b0d261b8b5ba3e88e501d620d..a8ba8c597eddc03b12a8b36baa697a2fd4dd455d 100644 --- a/src/services/auth/types.ts +++ b/src/services/auth/types.ts @@ -2,5 +2,5 @@ import { User } from '../users'; export interface Auth { token: string | null; - userInfo: User; + userInfo: User | null; } diff --git a/src/services/users/hooks/use-users.ts b/src/services/users/hooks/use-users.ts index 8595252809756fdc6642485512ad3bc9e333a38b..0e5dd35c1f1233fa06e9702f08da866a83109c29 100644 --- a/src/services/users/hooks/use-users.ts +++ b/src/services/users/hooks/use-users.ts @@ -1,5 +1,13 @@ import { useDispatch, useSelector } from 'react-redux'; -import { getUsers, fetchUsers, fetchUserById, updateUserById, createUser, deleteUser } from '../redux'; +import { + getUsers, + fetchUsers, + fetchUserById, + updateUserById, + createUser, + deleteUser, + clearCurrentUser, +} from '../redux'; import { getUserById, getUserModalLoading, getUserslLoading } from '../redux/selectors'; export function useUsers() { @@ -17,6 +25,10 @@ export function useUsers() { return dispatch(fetchUserById(id)); } + function clearSelectedUser() { + return dispatch(clearCurrentUser()); + } + function editUserById(data: any) { return dispatch(updateUserById(data)); } @@ -39,5 +51,6 @@ export function useUsers() { userTableLoading, createNewUser, deleteUserById, + clearSelectedUser, }; } diff --git a/src/services/users/redux/actions.ts b/src/services/users/redux/actions.ts index 67be7eee4097a6b3287cd966c36a058d3153696e..7c350342383da1f03645b464b97da178a9ef5998 100644 --- a/src/services/users/redux/actions.ts +++ b/src/services/users/redux/actions.ts @@ -1,5 +1,6 @@ import { Dispatch } from 'redux'; import { showToast, ToastType } from 'src/common/util/show-toast'; +import { State } from 'src/redux/types'; import { performApiCall } from 'src/services/api'; import { AuthActionTypes } from 'src/services/auth'; import { transformRequestUser, transformUser } from '../transformations'; @@ -68,9 +69,11 @@ export const fetchUserById = (id: string) => async (dispatch: Dispatch<any>) => dispatch(setUserModalLoading(false)); }; -export const updateUserById = (user: any) => async (dispatch: Dispatch<any>) => { +export const updateUserById = (user: any) => async (dispatch: Dispatch<any>, getState: any) => { dispatch(setUserModalLoading(true)); + const state: State = getState(); + try { const { data } = await performApiCall({ path: `/users/${user.id}`, @@ -83,10 +86,12 @@ export const updateUserById = (user: any) => async (dispatch: Dispatch<any>) => payload: transformUser(data), }); - dispatch({ - type: AuthActionTypes.UPDATE_AUTH_USER, - payload: transformUser(data), - }); + if (state.auth.userInfo?.id === user.id) { + dispatch({ + type: AuthActionTypes.UPDATE_AUTH_USER, + payload: transformUser(data), + }); + } showToast('User updated successfully.', ToastType.Success); @@ -148,3 +153,10 @@ export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => { dispatch(setUserModalLoading(false)); }; + +export const clearCurrentUser = () => (dispatch: Dispatch<any>) => { + dispatch({ + type: UserActionTypes.DELETE_USER, + payload: {}, + }); +}; diff --git a/src/services/users/transformations.ts b/src/services/users/transformations.ts index 5bef470c658c278ee917040bf3a9a90fc2a94b50..5f1c2f75c19c37151c4abf375dd102375a8c363a 100644 --- a/src/services/users/transformations.ts +++ b/src/services/users/transformations.ts @@ -1,33 +1,38 @@ -import _ from 'lodash'; +import { AppRoles, User, UserRole } from './types'; -import { User, UserRole } from './types'; +export const transformAppRoles = (data: any): AppRoles => { + const resolvedAdminRole = data.role_id === 1 ? UserRole.Admin : UserRole.User; -export const transformUser = (response: any): User => { - const userResponse = _.get(response, 'user', response); + return { + name: data.name ?? '', + role: resolvedAdminRole ?? UserRole.User, + }; +}; - const resolvedUserRole = !userResponse.traits.role_id ? UserRole.User : userResponse.traits.role_id; +export const transformRequestAppRoles = (data: AppRoles): any => { + const resolvedRequestRole = data.role === UserRole.Admin ? 1 : null; return { - id: userResponse.id, - role_id: resolvedUserRole, - email: userResponse.traits.email, - name: userResponse.traits.name ?? null, - preferredUsername: userResponse.preferredUsername, - status: userResponse.state, + name: data.name ?? '', + role_id: resolvedRequestRole, }; }; -export const transformRequestUser = (data: Pick<User, 'role_id' | 'name' | 'email'>) => { - if (data.role_id === UserRole.User) { - return { - email: data.email, - name: data.name, - }; - } +export const transformUser = (response: any): User => { + return { + id: response.id ?? '', + app_roles: response.traits.app_roles ? response.traits.app_roles.map(transformAppRoles) : [], + email: response.traits.email ?? '', + name: response.traits.name ?? '', + preferredUsername: response.preferredUsername ?? '', + status: response.state ?? '', + }; +}; +export const transformRequestUser = (data: Pick<User, 'app_roles' | 'name' | 'email'>) => { return { - role_id: Number(data.role_id), - email: data.email, - name: data.name, + app_roles: data.app_roles.map(transformRequestAppRoles), + email: data.email ?? '', + name: data.name ?? '', }; }; diff --git a/src/services/users/types.ts b/src/services/users/types.ts index 2ab4e1b703cef7e0408b2abdec8fb33eb8c12477..924524784986ec17d61a9d2bec137003e002fd8e 100644 --- a/src/services/users/types.ts +++ b/src/services/users/types.ts @@ -1,10 +1,10 @@ export interface User { - id: number | null; - role_id: UserRole | null; - email: string | null; - name: string | null; - preferredUsername: string | null; - status: string | null; + id: string; + app_roles: AppRoles[]; + email: string; + name: string; + preferredUsername: string; + status: string; } export interface FormUser extends User { @@ -13,8 +13,13 @@ export interface FormUser extends User { } export enum UserRole { - Admin = '1', - User = '2', + Admin = 'admin', + User = 'user', +} + +export interface AppRoles { + name: string | null; + role: UserRole | null; } export interface UserApiRequest {