Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/1808 consortia join request #1818

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/coinstac-api-server/seed/populate.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ async function populateConsortia() {
[USER_IDS[5]]: 'author',
},
isPrivate: false,
isJoinByRequest: false,
joinRequests: {},
createDate: 1551333489519,
},
{
Expand All @@ -133,6 +135,8 @@ async function populateConsortia() {
[USER_IDS[5]]: 'author',
},
isPrivate: false,
isJoinByRequest: false,
joinRequests: {},
createDate: 1551666489519,
},
]);
Expand Down
96 changes: 96 additions & 0 deletions packages/coinstac-api-server/src/data/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,98 @@ const resolvers = {

return helperFunctions.getUserDetails(credentials.username);
},
/**
* Approve or reject consortium join request
* @param {object} auth User object from JWT middleware validateFunc
* @param {object} args
* @param {string} args.consortiumId Consortium id that has join request
* @param {string} args.userId User id to join consortium
* @return {object} Updated consortium
*/
approveOrRejectConsortiumJoinRequest: async (parent, args, { credentials }) => {
const db = database.getDbInstance();
const { id: currentUserId } = credentials;

const { consortiumId, userId, isApprove } = args;

const consortium = await db.collection('consortia').findOne({ _id: ObjectID(consortiumId) });
if (!consortium) {
return Boom.forbidden('Invalid consortium');
}

const ownerIds = Object.keys(consortium.owners);

if (!ownerIds.includes(currentUserId)) {
return Boom.forbidden('Action not permitted');
}

const userToAdd = await helperFunctions.getUserDetailsByID(userId);
if (!userToAdd) {
return Boom.forbidden('Invalid user');
}

if (isApprove) {
await addUserPermissions({
userId: ObjectID(userId),
userName: userToAdd.username,
role: 'member',
doc: ObjectID(consortiumId),
table: 'consortia',
});
}

const updateObj = {
$unset: {
[`joinRequests.${userId}`]: '',
},
};

const consortiaUpdateResult = await db.collection('consortia').findOneAndUpdate(
{ _id: ObjectID(consortiumId) },
updateObj,
{ returnDocument: 'after' }
);

eventEmitter.emit(CONSORTIUM_CHANGED, consortiaUpdateResult.value);

return transformToClient(consortiaUpdateResult.value);
},
/**
* Send consortium join request
* @param {object} auth User object from JWT middleware validateFunc
* @param {object} args
* @param {string} args.consortiumId Consortium id that has join request
* @return {object} Updated consortium
*/
sendConsortiumJoinRequest: async (parent, args, { credentials }) => {
const db = database.getDbInstance();
const { id: currentUserId } = credentials;

const { consortiumId } = args;

const consortium = await db.collection('consortia').findOne({ _id: ObjectID(consortiumId) });
if (!consortium) {
return Boom.forbidden('Invalid consortium');
}

const user = await helperFunctions.getUserDetailsByID(currentUserId);

const updateObj = {
$set: {
[`joinRequests.${currentUserId}`]: user.username,
},
};

const consortiaUpdateResult = await db.collection('consortia').findOneAndUpdate(
{ _id: ObjectID(consortiumId) },
updateObj,
{ returnDocument: 'after' }
);

eventEmitter.emit(CONSORTIUM_CHANGED, consortiaUpdateResult.value);

return transformToClient(consortiaUpdateResult.value);
},
/**
* Remove logged user from consortium members list
* @param {object} auth User object from JWT middleware validateFunc
Expand Down Expand Up @@ -1131,6 +1223,10 @@ const resolvers = {
consortiumData.activePipelineId = ObjectID(consortiumData.activePipelineId);
}

if (!consortiumData.isJoinByRequest) {
consortiumData.joinRequests = {}
}

await db.collection('consortia').replaceOne({
_id: consortiumData.id
}, consortiumData, {
Expand Down
2 changes: 2 additions & 0 deletions packages/coinstac-api-server/src/data/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ const typeDefs = `
deleteConsortiumById(consortiumId: ID): Consortium
deletePipeline(pipelineId: ID): Pipeline
joinConsortium(consortiumId: ID!): Consortium
approveOrRejectConsortiumJoinRequest(consortiumId: ID!, userId: ID!, isApprove: Boolean!): Consortium
sendConsortiumJoinRequest(consortiumId: ID!): Consortium
leaveConsortium(consortiumId: ID!): Consortium
removeComputation(computationId: ID): Computation
removeUserRole(userId: ID!, table: String!, doc: String!, role: String!, userName: String, roleType: String!): User
Expand Down
2 changes: 2 additions & 0 deletions packages/coinstac-api-server/src/data/shared-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const schemaFields = {
pipelines: [ID]
results: [ID]
isPrivate: Boolean
isJoinByRequest: Boolean
joinRequests: JSON
mappedForRun: [ID]
createDate: String
`,
Expand Down
20 changes: 10 additions & 10 deletions packages/coinstac-api-server/tests/resolvers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,18 @@ test('createPasswordResetToken, savePasswordResetToken, validateResetToken and r

// should return error if cannot find user with token
const invalidTokenReq = { payload: { token: 'token' } };
let error = await helperFunctions.validateResetToken(invalidTokenReq, { response: () => {} });
let error = await helperFunctions.validateResetToken(invalidTokenReq, { response: () => { } });
t.is(error.message, INVALID_TOKEN);

// should return error if token cannot be verified
sinon.stub(jwt, 'verify').throws(new Error('error'));
error = await helperFunctions.validateResetToken(req, { response: () => {} });
error = await helperFunctions.validateResetToken(req, { response: () => { } });
t.is(error.message, 'error');
jwt.verify.restore();

// should return error due to invalid email
sinon.stub(jwt, 'verify').resolves({ email: 'email@mrn.org' });
error = await helperFunctions.validateResetToken(req, { response: () => {} });
error = await helperFunctions.validateResetToken(req, { response: () => { } });
t.is(error.message, INVALID_EMAIL);
jwt.verify.restore();

Expand Down Expand Up @@ -232,12 +232,12 @@ test('validateToken and validateUser', async (t) => {

// should return error due to invalid username
const req2 = { payload: { username: 'invalid-user', password: 'password' } };
let error = await helperFunctions.validateUser(req2, { response: () => {} });
let error = await helperFunctions.validateUser(req2, { response: () => { } });
t.is(error.message, INCORRECT_CREDENTIAL);

// should return error due to invalid password
const req3 = { payload: { username: 'test1', password: 'password1' } };
error = await helperFunctions.validateUser(req3, { response: () => {} });
error = await helperFunctions.validateUser(req3, { response: () => { } });
t.is(error.message, INCORRECT_CREDENTIAL);
});

Expand Down Expand Up @@ -291,7 +291,7 @@ test('validateEmail', async (t) => {

// should return error if provided email is not valid
req.payload.email = 'email@mrn.org';
const error = await helperFunctions.validateEmail(req, { response: () => {} });
const error = await helperFunctions.validateEmail(req, { response: () => { } });
t.is(error.message, INVALID_EMAIL);
});

Expand All @@ -304,12 +304,12 @@ test('validateUniqueUsername', async (t) => {
test('validateUniqueUser', async (t) => {
// should return error if username is already taken
let req = { payload: { username: 'test1', email: 'test@mrn.org' } };
let error = await helperFunctions.validateUniqueUser(req, { response: () => {} });
let error = await helperFunctions.validateUniqueUser(req, { response: () => { } });
t.is(error.message, USERNAME_TAKEN);

// should return error if email is already taken
req = { payload: { username: 'newuser', email: 'test@mrn.org' } };
error = await helperFunctions.validateUniqueUser(req, { response: () => {} });
error = await helperFunctions.validateUniqueUser(req, { response: () => { } });
t.is(error.message, EMAIL_TAKEN);

// should return true if provided username and email are unique
Expand Down Expand Up @@ -501,7 +501,7 @@ test('searchDatasets and deleteDataset', async (t) => {
});

test('fetchDataset', async (t) => {
const datasets = await Query.searchDatasets('', { });
const datasets = await Query.searchDatasets('', {});
const datasetId = datasets[0].id;
const dataset = await Query.fetchDataset('', { id: datasetId });
t.is(dataset.id, datasetId);
Expand Down Expand Up @@ -748,7 +748,7 @@ test('saveActivePipeline', async (t) => {
test('saveConsortium', async (t) => {
/* saveConsortium */
let auth = {
credentials: { id: USER_IDS[0], username: 'test1', permissions: { } },
credentials: { id: USER_IDS[0], username: 'test1', permissions: {} },
};

const consortium1 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import IconButton from '@material-ui/core/IconButton';
import Fab from '@material-ui/core/Fab';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
import ThumbDownIcon from '@material-ui/icons/ThumbDown';
import ThumbUpIcon from '@material-ui/icons/ThumbUp';
import PropTypes from 'prop-types';

const useStyles = makeStyles(theme => ({
root: {
margin: 0,
padding: theme.spacing(2),
},
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500],
},
content: {
padding: theme.spacing(2),
},
requestRow: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
rowActions: {
flexShrink: 0,
display: 'flex',
alignItems: 'center',
gap: 4,
},
}));

const ConsortiaJoinDialog = ({
joinRequests,
onApproveRequest,
onRejectRequest,
onClose,
}) => {
const classes = useStyles();

return (
<Dialog open maxWidth="xs" fullWidth onClose={onClose}>
<DialogTitle disableTypography className={classes.root}>
<Typography variant="h6">Consortium Join Request(s)</Typography>
{onClose && (
<IconButton className={classes.closeButton} onClick={onClose}>
<CloseIcon />
</IconButton>
)}
</DialogTitle>
<DialogContent dividers className={classes.content}>
{joinRequests.map((joinRequest, idx) => (
<Box
key={`${joinRequest.consortium.id}-${joinRequest.user.id}`}
className={classes.requestRow}
paddingTop={idx === 0 ? 0 : 1}
paddingBottom={idx === joinRequests.length - 1 ? 0 : 1}
borderBottom={idx < joinRequests.length - 1 ? '1px solid #d9d9d9' : 'none'}
>
<Box fontSize={16}>
<b>{joinRequest.user.username}</b>
&nbsp;to join&nbsp;
<b>{joinRequest.consortium.name}</b>
</Box>
<Box className={classes.rowActions}>
<Fab
color="primary"
size="small"
onClick={() => onApproveRequest(joinRequest)}
>
<ThumbUpIcon />
</Fab>
<Fab
color="secondary"
size="small"
onClick={() => onRejectRequest(joinRequest)}
>
<ThumbDownIcon />
</Fab>
</Box>
</Box>
))}
</DialogContent>
</Dialog>
);
};

ConsortiaJoinDialog.propTypes = {
joinRequests: PropTypes.arrayOf(PropTypes.shape({
consortium: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string }),
user: PropTypes.shape({ id: PropTypes.string, username: PropTypes.string }),
})).isRequired,
onApproveRequest: PropTypes.func.isRequired,
onRejectRequest: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

export default ConsortiaJoinDialog;
Loading