diff --git a/api/src/controllers/communityUserController.js b/api/src/controllers/communityUserController.js index 457a3c2a5..86f04513e 100644 --- a/api/src/controllers/communityUserController.js +++ b/api/src/controllers/communityUserController.js @@ -1,21 +1,6 @@ const { Op } = require('sequelize') const db = require('../models') -// @desc Get the community-users -// @route GET /api/community-users -// @access Public - -const getCommunityUsers = async (req, res) => { - try { - const data = await db.CommunityUser.findAll() - res.json({ - data - }) - } catch (error) { - res.status(400).json({ error }) - } -} - // @desc follow community // @route POST /api/community-users/follow // @access Public @@ -82,10 +67,38 @@ const followCommunity = async (req, res) => { const getAllMembers = async (req, res) => { try { - const data = await db.CommunityUser.findAll( + const communityId = req.params.id + const member = await db.CommunityUser.findOne({ where: { userId: req.user.id, communityId }, attributes: ['role'] }) + + if (member.dataValues.role === 'manager') { + const data = await db.CommunityUser.findAll( + { + where: { communityId: req.params.id, active: true }, + attributes: ['id', 'userId', 'role'], + include: [{ + model: db.User, + attributes: ['firstName', 'lastName', 'email', 'phone', 'dateOfBirth'] + }], + required: true + } + ) + + // flattening the array to show only one object + const newArray = data.map(item => { + const { userId, role, id } = item.dataValues + return { id, userId, role, ...item.user.dataValues } + }) + + res.json({ + results: newArray + }) + return + } + + const newData = await db.CommunityUser.findAll( { - where: { communityId: req.params.id, active: true }, - attributes: ['userId'], + where: { communityId, active: true }, + attributes: ['userId', 'role'], include: [{ model: db.User, attributes: ['firstName'] @@ -93,20 +106,33 @@ const getAllMembers = async (req, res) => { required: true } ) - res.json({ - data + return res.json({ + results: newData }) } catch (error) { res.status(400).json({ error }) } } +// @desc Update the community users +// @route PUT /api/community-users/:memberId/community/:id/ +// @access Public + +const updateMemberRole = async (req, res) => { + try { + const { role } = req.body + await db.CommunityUser.update({ role }, { where: { id: parseInt(req.params.memberId) } }) + res.json({ message: 'Successfully role updated' }) + } catch (error) { + res.status(400).json({ error }) + } +} + // @desc Search Name // @route POST /api/news/community/:id/search // @access Private const searchMemberName = (req, res) => { const { name } = req.query - const order = req.query.order || 'ASC' db.CommunityUser.findAll( { @@ -129,4 +155,4 @@ const searchMemberName = (req, res) => { .catch(err => res.json({ error: err }).status(400)) } -module.exports = { getCommunityUsers, followCommunity, getAllMembers, searchMemberName } +module.exports = { followCommunity, getAllMembers, searchMemberName, updateMemberRole } diff --git a/api/src/middleware/permission.js b/api/src/middleware/permission.js index 5bdd4be4e..d693b3c6b 100644 --- a/api/src/middleware/permission.js +++ b/api/src/middleware/permission.js @@ -1,15 +1,48 @@ -const permit = (role) => { - return (req, res, next) => { - if (checkRole(req, role)) { - next() - } else { - res.json({ error: 'Sorry, You don\'t have permission' }) +const db = require('../models') + +const permit = (category, roles) => { + return async (req, res, next) => { + + switch(category) { + case 'community-member': + checkMemberRoles(roles, next, res, req) + break; + case 'user': + checkUserRoles(roles, next, res, req) + break; + default: + res.json({ error: 'Sorry, You don\'t have permission' }) } + // // getting member role + // const member = await db.CommunityUser.findOne({ where: { userId: req.user.id, communityId: req.params.id }, attributes: ['role'] }) + + // if (member.dataValues.role) { + // checkMemberRoles(roles, member, next, res) + // } else { + // checkUserRoles(roles, next, res, req) + // } } } -function checkRole (req, role) { - return role.some(el => el === req.user.role) +const checkRole = (roles, dbRole) => { + return roles.includes(dbRole) +} + +const checkMemberRoles = async (roles, next, res, req) => { + const member = await db.CommunityUser.findOne({ where: { userId: req.user.id, communityId: req.params.id }, attributes: ['role'] }) + if (checkRole(roles, member.dataValues.role)) { + next() + } else { + res.json({ error: 'Sorry, You don\'t have permission' }) + } +} + +const checkUserRoles = (roles, next, res, req) => { + if (checkRole(roles, req.user.role)) { + next() + } else { + res.json({ error: 'Sorry, You don\'t have permission' }) + } } module.exports = permit diff --git a/api/src/migrations/20210728085949-alter_community_user.js b/api/src/migrations/20210728085949-alter_community_user.js new file mode 100644 index 000000000..e05727f59 --- /dev/null +++ b/api/src/migrations/20210728085949-alter_community_user.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + queryInterface.addColumn('communities_users', 'role', { + type: Sequelize.STRING, + defaultValue: 'member' + }) + }, + + down: async (queryInterface, Sequelize) => { + queryInterface.removeColumn('communities_users', 'role') + } +} diff --git a/api/src/models/communityUserModel.js b/api/src/models/communityUserModel.js index 84b332fe5..e5c0a30f5 100644 --- a/api/src/models/communityUserModel.js +++ b/api/src/models/communityUserModel.js @@ -15,6 +15,10 @@ module.exports = (sequelize, DataTypes) => { active: { type: DataTypes.INTEGER, defaultValue: true + }, + role: { + type: DataTypes.STRING, + defaultValue: 'member' } }, { timestamps: true } diff --git a/api/src/routes/categoriesRouter.js b/api/src/routes/categoriesRouter.js index 22482b388..83a91ba44 100644 --- a/api/src/routes/categoriesRouter.js +++ b/api/src/routes/categoriesRouter.js @@ -10,7 +10,7 @@ const { } = require('../controllers/categoryController') const permit = require('../middleware/permission') -router.route('/').get(protect, getCategories).post(protect, permit(['sysadmin']), addCategory) -router.route('/:id').get(getSingleCategory).put(protect, permit(['sysadmin']), updateCategory).delete(protect, permit(['sysadmin']), deleteCategory) +router.route('/').get(protect, getCategories).post(protect, permit('user', ['sysadmin']), addCategory) +router.route('/:id').get(getSingleCategory).put(protect, permit('user', ['sysadmin']), updateCategory).delete(protect, permit(['sysadmin']), deleteCategory) module.exports = router diff --git a/api/src/routes/communityUserRouter.js b/api/src/routes/communityUserRouter.js index 0c448b74d..67638bb0d 100644 --- a/api/src/routes/communityUserRouter.js +++ b/api/src/routes/communityUserRouter.js @@ -1,11 +1,11 @@ -const { getCommunityUsers, followCommunity, getAllMembers, searchMemberName } = require('../controllers/communityUserController') +const { followCommunity, getAllMembers, searchMemberName, updateMemberRole } = require('../controllers/communityUserController') const protect = require('../middleware/authMiddleware') +const permit = require('../middleware/permission') const router = require('express').Router() -router.get('/', getCommunityUsers) router.post('/follow', protect, followCommunity) -router.get('/community/:id', getAllMembers) +router.get('/community/:id', protect, permit('community-member', ['manager','member']), getAllMembers) router.get('/community/:id/search', searchMemberName) -// router.put('/:id', updateCommunityUsers); +router.put('/:memberId/community/:id/', protect, permit('community-member', ['manager']), updateMemberRole) module.exports = router diff --git a/src/App.jsx b/src/App.jsx index 37cc2e1aa..b8ad1f88d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -52,6 +52,7 @@ import AddTest from './screens/addTest/AddTest' import LogoutUser from './screens/logoutUser/LogoutUser' import Category from './screens/category/Category' import PageNotFound from './screens/pageNotFound/PageNotFound' +import CommunityMemberAdmin from './screens/admin/CommunityMemberAdmin' function App () { return ( @@ -78,6 +79,7 @@ function App () { + diff --git a/src/actions/memberActions.js b/src/actions/memberActions.js index 3e9b02af5..778d14164 100644 --- a/src/actions/memberActions.js +++ b/src/actions/memberActions.js @@ -1,5 +1,8 @@ -import { getApi } from '../utils/apiFunc' +import { getApi, putApi } from '../utils/apiFunc' import { + MEMBER_ACCESS_FAIL, + MEMBER_ACCESS_REQUEST, + MEMBER_ACCESS_SUCCESS, MEMBER_LIST_FAIL, MEMBER_LIST_REQUEST, MEMBER_LIST_SUCCESS, MEMBER_SEARCH_FAIL, MEMBER_SEARCH_REQUEST, MEMBER_SEARCH_SUCCESS @@ -39,13 +42,12 @@ export const searchMembers = (search) => async ( ) => { try { dispatch({ type: MEMBER_SEARCH_REQUEST }) - const { data } = await getApi( + await getApi( dispatch, `${process.env.REACT_APP_API_BASE_URL}/api/communities-users/community/${currentCommunity.id}/search?name=${search}` ) dispatch({ - type: MEMBER_SEARCH_SUCCESS, - payload: data.member + type: MEMBER_SEARCH_SUCCESS }) } catch (error) { dispatch({ @@ -57,3 +59,28 @@ export const searchMembers = (search) => async ( }) } } + +export const allowAccess = (id, role) => async ( + dispatch +) => { + try { + dispatch({ type: MEMBER_ACCESS_REQUEST }) + const { data } = await putApi( + dispatch, + `${process.env.REACT_APP_API_BASE_URL}/api/communities-users/${id}/community/${currentCommunity.id}`, + { role } + ) + dispatch({ + type: MEMBER_ACCESS_SUCCESS, + payload: data + }) + } catch (error) { + dispatch({ + type: MEMBER_ACCESS_FAIL, + payload: + error.response && error.response.data.message + ? error.response.data.message + : error.message + }) + } +} diff --git a/src/components/cardImage/CardImage.jsx b/src/components/cardImage/CardImage.jsx index b60283d83..ff5880c45 100644 --- a/src/components/cardImage/CardImage.jsx +++ b/src/components/cardImage/CardImage.jsx @@ -43,7 +43,7 @@ function CardImage ({ data = [], className }) { group-profile
-
{profile?.user.firstName || 'anonymous'}
+
{profile.user !== undefined ? (profile.user.firstName || 'anonymous') : (profile.firstName || 'anonymous')}
{Follow()} diff --git a/src/components/dropDown/DropDown.jsx b/src/components/dropDown/DropDown.jsx new file mode 100644 index 000000000..b2687cab2 --- /dev/null +++ b/src/components/dropDown/DropDown.jsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react' +import { useEffect } from 'react' +import { useRef } from 'react' +import useHideOnClick from '../../utils/useHideOnClick' +import './DropDown.scss' + +const DropDown = ({ data, title, getRole, id, role }) => { + const [active, setActive] = useState() + const domNode = useHideOnClick(() => { + setActive(false) + }) + + function clickHandler (item) { + setActive(false) + getRole(item, id) + } + + function showDropdown () { + setActive(!active) + } + + return ( +
+
+ {title} + +
+ {active && ( +
    + {data.length > 0 && + data.filter(el => el !== role).map((item) => ( +
  • clickHandler(item)}>{item}
  • + ))} +
+ )} +
+ ) +} + +export default DropDown diff --git a/src/components/dropDown/DropDown.scss b/src/components/dropDown/DropDown.scss new file mode 100644 index 000000000..05b955cec --- /dev/null +++ b/src/components/dropDown/DropDown.scss @@ -0,0 +1,45 @@ +.dropdown-wrapper { + width: 200px; + position: relative; + + .dropdown-title { + color: var(--primary-white); + display: flex; + justify-content: space-between; + align-items: center; + top: 10px; + z-index: 999; + border: 1px solid var(--primary-white); + cursor: pointer; + padding: 11px; + width: 100%; + text-transform: capitalize; + border-radius: 4px; + } + + .dropdown-lists { + color: var(--primary-white); + position: absolute; + top: 2.3em; + background: var(--background-color-light); + margin-top: 0.5em; + border: 1px solid var(--dropdowns); + border-radius: 4px; + z-index: 2000; + width: 100%; + list-style: none; + + li { + padding: 1em 2em; + width: 100%; + cursor: pointer; + background: var(--background-color); + color: #ffff; + text-transform: capitalize; + + &:hover { + background: var(--primary-color); + } + } + } + } diff --git a/src/components/table/Table.jsx b/src/components/table/Table.jsx index 033ecae4e..6db5c1901 100644 --- a/src/components/table/Table.jsx +++ b/src/components/table/Table.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import DropDown from '../dropDown/DropDown' import './Table.scss' // description: @@ -18,7 +19,7 @@ import './Table.scss' // img: the path of the image you want to show as button // action: this holds function that is executed when the button is clicked (fn: returns id of the item) -const Table = ({ addSymbolNumber, data = { tblData: [] }, options = [] }) => { +const Table = ({ addSymbolNumber, data = { tblData: [] }, options = [], dropdown = { dropdownList: [] } }) => { const [property, setProperty] = useState([]) const [header, setHeader] = useState([]) const [tblData, setTblData] = useState([]) @@ -29,6 +30,10 @@ const Table = ({ addSymbolNumber, data = { tblData: [] }, options = [] }) => { setProperty(tblProperty || Object.keys(tblData[0])) }, []) + const getRole = (role, id) => { + dropdown.action(id, role) + } + return (
@@ -40,7 +45,8 @@ const Table = ({ addSymbolNumber, data = { tblData: [] }, options = [] }) => { return }) } - {options.length && } + {options.length > 0 && } + {dropdown.dropdownList.length > 0 && } @@ -54,7 +60,7 @@ const Table = ({ addSymbolNumber, data = { tblData: [] }, options = [] }) => { return }) } - {options.length && } + { + dropdown.dropdownList.length > 0 && + } ) }) diff --git a/src/constants/memberConstants.js b/src/constants/memberConstants.js index 23cd06a25..1ab519271 100644 --- a/src/constants/memberConstants.js +++ b/src/constants/memberConstants.js @@ -5,3 +5,7 @@ export const MEMBER_LIST_FAIL = 'MEMBER_LIST_FAIL' export const MEMBER_SEARCH_REQUEST = 'MEMBER_SEARCH_REQUEST' export const MEMBER_SEARCH_SUCCESS = 'MEMBER_SEARCH_SUCCESS' export const MEMBER_SEARCH_FAIL = 'MEMBER_SEARCH_FAIL' + +export const MEMBER_ACCESS_REQUEST = 'MEMBER_ACCESS_REQUEST' +export const MEMBER_ACCESS_SUCCESS = 'MEMBER_ACCESS_SUCCESS' +export const MEMBER_ACCESS_FAIL = 'MEMBER_ACCESS_FAIL' diff --git a/src/reducers/memberReducers.js b/src/reducers/memberReducers.js index 0d98695f2..24d8b9617 100644 --- a/src/reducers/memberReducers.js +++ b/src/reducers/memberReducers.js @@ -1,4 +1,7 @@ import { + MEMBER_ACCESS_FAIL, + MEMBER_ACCESS_REQUEST, + MEMBER_ACCESS_SUCCESS, MEMBER_LIST_FAIL, MEMBER_LIST_REQUEST, MEMBER_LIST_SUCCESS, MEMBER_SEARCH_FAIL, MEMBER_SEARCH_REQUEST, MEMBER_SEARCH_SUCCESS @@ -11,7 +14,7 @@ export const memberListReducer = (state = { members: [] }, action) => { case MEMBER_LIST_SUCCESS: return { loading: false, - members: action.payload, + members: action.payload.results, pages: action.payload.pages, page: action.payload.page } @@ -32,3 +35,19 @@ export const memberListReducer = (state = { members: [] }, action) => { return state } } + +export const memberAccessReducer = (state = { success: false}, action) => { + switch (action.type) { + case MEMBER_ACCESS_REQUEST: + return { loading: true} + case MEMBER_ACCESS_SUCCESS: + return { + loading: false, + success: true + } + case MEMBER_ACCESS_FAIL: + return { loading: false, error: action.payload } + default: + return state + } +} diff --git a/src/screens/admin/CommunityMemberAdmin.jsx b/src/screens/admin/CommunityMemberAdmin.jsx new file mode 100644 index 000000000..aa5174655 --- /dev/null +++ b/src/screens/admin/CommunityMemberAdmin.jsx @@ -0,0 +1,57 @@ +import React, { useEffect, useLayoutEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { allowAccess, listMembers } from '../../actions/memberActions' +import Table from '../../components/table/Table' +import DashboardLayout from '../../layout/dashboardLayout/DashboardLayout' +import { getApi} from '../../utils/apiFunc' +import { getCommunity } from '../../utils/getCommunity' +import './CommunityMemberAdmin.scss' + +const ComMemberAdmin = () => { + const [data, setData] = useState([]); + const memberData = useSelector(state => state.accessMember) + const { members } = useSelector(state => state.listMember) + const {success} = memberData + const dispatch = useDispatch() + + // fetching current community + const currentCommunity = getCommunity() + + useEffect(() => { + dispatch(listMembers()) + if(members) { + setData(members) + } + }, [dispatch, success]) + + const allowAccessFunc = async (id, role) => { + dispatch(allowAccess(id, role)); + } + + // const getMemberDetails = async () => { + // const { data } = await getApi( + // dispatch, + // `${process.env.REACT_APP_API_BASE_URL}/api/communities-users/community/${currentCommunity.id}` + // ) + // setData(data.results) + // } + + return ( + + {members.length &&
{propKey}OptionsOptionsOptions
{item[propkey]} + {options.length > 0 &&
{ options.map(el => { @@ -63,6 +69,9 @@ const Table = ({ addSymbolNumber, data = { tblData: [] }, options = [] }) => { }
} + + ) +} + +export default ComMemberAdmin diff --git a/src/screens/admin/CommunityMemberAdmin.scss b/src/screens/admin/CommunityMemberAdmin.scss new file mode 100644 index 000000000..a4e8391fb --- /dev/null +++ b/src/screens/admin/CommunityMemberAdmin.scss @@ -0,0 +1,38 @@ +.com-member-container { + width: 100%; + display: flex; + flex-direction: column; + color: #fff; +} + +.com-member-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + background: var(--primary-color); + padding: 12px; + text-align: center; + + p { + flex: 1; + } +} + +.com-member-item { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px; + text-align: center; + background: var(--dropdowns); + + p { + flex: 1; + } +} + +.access-btn { + cursor: pointer; +} diff --git a/src/screens/communityMembers/CommunityMembers.jsx b/src/screens/communityMembers/CommunityMembers.jsx index 8aff3dc18..26f266ccb 100644 --- a/src/screens/communityMembers/CommunityMembers.jsx +++ b/src/screens/communityMembers/CommunityMembers.jsx @@ -35,7 +35,7 @@ function CommunityMembers ({ history }) {
- {members && } + {members.length && }
diff --git a/src/store.js b/src/store.js index a30506543..036b2958f 100644 --- a/src/store.js +++ b/src/store.js @@ -61,7 +61,7 @@ import { communityDeleteReducer, communityUpdateReducer } from './reducers/communityReducers' -import { memberListReducer } from './reducers/memberReducers' +import { memberAccessReducer, memberListReducer } from './reducers/memberReducers' import { addEnrollReducer } from './reducers/enrollReducer' import { questionDeleteReducer, questionListReducer, questionUpdateReducer } from './reducers/questionReducers' import { @@ -110,6 +110,7 @@ const reducer = combineReducers({ newsDelete: newsDeleteReducer, newsUpdate: newsUpdateReducer, listMember: memberListReducer, + accessMember: memberAccessReducer, userLogin: userLoginReducer, userRegister: userRegisterReducer, userConfirmCode: userConfirmCodeReducer, diff --git a/src/utils/apiFunc.jsx b/src/utils/apiFunc.jsx index 190c3b760..3febf2c4b 100644 --- a/src/utils/apiFunc.jsx +++ b/src/utils/apiFunc.jsx @@ -15,3 +15,7 @@ export const getApi = async (dispatch, url, config) => { export const postApi = async (dispatch, url, data, config) => { return await axios.post(url, data, configFunc(config)) } + +export const putApi = async (dispatch, url, data, config) => { + return await axios.put(url, data, configFunc(config)) +} diff --git a/src/utils/getCommunity.js b/src/utils/getCommunity.js new file mode 100644 index 000000000..dc7b67074 --- /dev/null +++ b/src/utils/getCommunity.js @@ -0,0 +1,5 @@ +export const getCommunity = () => { + return localStorage.getItem('currentCommunity') + ? JSON.parse(localStorage.getItem('currentCommunity')) + : null +}