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: Remove Collaboration #488

Merged
merged 10 commits into from
Aug 28, 2020
11 changes: 6 additions & 5 deletions src/client/components/pages/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,15 @@ class CollectionPage extends React.Component {
onToggleRow: this.toggleRow,
selectedEntities: this.state.selectedEntities,
showAdd: false,
showCheckboxes: this.props.isOwner || this.props.isCollaborator
showCheckboxes: Boolean(this.props.isOwner) || Boolean(this.props.isCollaborator)
};
return (
<div>
<DeleteOrRemoveCollaborationModal
collaboratorId={this.props.userId}
collection={this.props.collection}
delete={this.props.isOwner}
isDelete={this.props.isOwner}
show={this.state.showDeleteModal}
userId={this.props.userId}
onCloseModal={this.handleCloseDeleteModal}
/>
<AddEntityToCollectionModal
Expand Down Expand Up @@ -350,15 +350,16 @@ CollectionPage.propTypes = {
isOwner: PropTypes.bool,
nextEnabled: PropTypes.bool.isRequired,
size: PropTypes.number,
userId: PropTypes.number.isRequired
userId: PropTypes.node
MonkeyDo marked this conversation as resolved.
Show resolved Hide resolved
};
CollectionPage.defaultProps = {
entities: [],
from: 0,
isCollaborator: false,
isOwner: false,
showCheckboxes: false,
size: 20
size: 20,
userId: null
};

export default CollectionPage;
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ class DeleteOrRemoveCollaborationModal extends React.Component {

handleSubmit() {
request.post(this.postUrl)
.send()
.send(this.postData)
.then((res) => {
if (this.props.delete) {
if (this.props.isDelete) {
window.location.href = `/editor/${this.props.collection.ownerId}/collections`;
}
else {
window.location.href = `/editor/${this.props.collaboratorId}/collections`;
window.location.href = `/editor/${this.props.userId}/collections`;
}
}, (error) => {
this.setState({
Expand All @@ -38,8 +38,9 @@ class DeleteOrRemoveCollaborationModal extends React.Component {
const {collection} = this.props;
// eslint-disable-next-line one-var
let modalBody, modalTitle, submitButton;
if (this.props.delete) {
if (this.props.isDelete) {
this.postUrl = `/collection/${collection.id}/delete/handler`;
this.postData = {};
modalTitle = 'Confirm deletion';
modalBody = (
<Alert bsStyle="warning">
MonkeyDo marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -60,7 +61,9 @@ class DeleteOrRemoveCollaborationModal extends React.Component {
);
}
else {
this.postUrl = `/collection/${collection.id}/collaborator/remove/${this.props.collaboratorId}`;
// loggedInUser must be collaborator here
this.postUrl = `/collection/${collection.id}/collaborator/remove`;
this.postData = {collaboratorIds: [this.props.userId]};
modalTitle = 'Remove yourself as a collaborator';
modalBody = (
<Alert bsStyle="warning">
Expand All @@ -69,7 +72,7 @@ class DeleteOrRemoveCollaborationModal extends React.Component {
You’re about to remove yourself as a collaborator of Collection: {collection.name}.
</h4>
<p>
Are you sure you want to process ? You won’t be able to undo this.
Are you sure you want to proceed ? You won’t be able to undo this.
MonkeyDo marked this conversation as resolved.
Show resolved Hide resolved
</p>
</Alert>
);
Expand Down Expand Up @@ -112,15 +115,14 @@ class DeleteOrRemoveCollaborationModal extends React.Component {

DeleteOrRemoveCollaborationModal.displayName = 'DeleteOrRemoveCollaborationModal';
DeleteOrRemoveCollaborationModal.propTypes = {
collaboratorId: PropTypes.number,
collection: PropTypes.object.isRequired,
delete: PropTypes.bool,
isDelete: PropTypes.bool,
onCloseModal: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired
show: PropTypes.bool.isRequired,
userId: PropTypes.node.isRequired
MonkeyDo marked this conversation as resolved.
Show resolved Hide resolved
};
DeleteOrRemoveCollaborationModal.defaultProps = {
collaboratorId: -1,
delete: true
isDelete: true
};

export default DeleteOrRemoveCollaborationModal;
17 changes: 17 additions & 0 deletions src/server/helpers/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,20 @@ export function validateBBIDsForCollectionRemove(req, res, next) {

return next();
}

export function validateCollaboratorIdsForCollectionRemove(req, res, next) {
const {collaboratorIds = []} = req.body;
if (!collaboratorIds.length) {
return next(new error.BadRequestError('CollaboratorIds array is empty'));
}
const {collection} = res.locals;
for (let i = 0; i < collaboratorIds.length; i++) {
const collaboratorId = collaboratorIds[i];
const isCollaborator = collection.collaborators.find(collaborator => collaborator.id === collaboratorId);
if (!isCollaborator) {
return next(new error.BadRequestError(`User ${collaboratorId} is not a collaborator of collection ${collection.id}`, req));
}
}

return next();
}
32 changes: 19 additions & 13 deletions src/server/routes/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,9 @@ router.get('/:collectionId', auth.isAuthenticatedForCollectionView, async (req,
const entitiesPromise = newResultsArray.map(bbid => orm.func.entity.getEntity(orm, collection.entityType, bbid, relations));
const entities = await Promise.all(entitiesPromise);
const isOwner = req.user && parseInt(collection.ownerId, 10) === parseInt(req.user?.id, 10);
let isCollaborator = false;
if (req.user && collection.collaborators.filter(collaborator => collaborator.id === req.user.id).length) {
isCollaborator = true;
}
const userId = parseInt(req.user?.id, 10);
const isCollaborator = req.user && collection.collaborators.filter(collaborator => collaborator.id === req.user.id).length;
const userId = req.user ? parseInt(req.user.id, 10) : null;

const props = generateProps(req, res, {
collection,
entities,
Expand Down Expand Up @@ -285,21 +283,29 @@ router.post('/:collectionId/add', auth.isAuthenticated, auth.isCollectionOwnerOr
}
});

router.post('/:collectionId/collaborator/remove/:collaboratorId', auth.isAuthenticated, async (req, res, next) => {
router.post('/:collectionId/collaborator/remove', auth.isAuthenticated, middleware.validateCollaboratorIdsForCollectionRemove, async (req, res, next) => {
try {
const {collection} = res.locals;
const collaboratorId = parseInt(req.params.collaboratorId, 10);
if (parseInt(req.user.id, 10) !== collaboratorId ||
!collection.collaborators.find(collaborator => collaborator.id === collaboratorId)) {
throw new error.PermissionDeniedError(
'You do not have permission to edit this collection’s collaborators', req
);
const collaboratorIdsToRemove = req.body.collaboratorIds;
const userId = parseInt(req.user.id, 10);
// user is allowed to make this change if they are owner of the collection OR they are collaborator and they want to remove themselves
let isAllowedToEdit = userId === collection.ownerId;
if (!isAllowedToEdit) {
if (collaboratorIdsToRemove.length === 1 && parseInt(collaboratorIdsToRemove[0], 10) === userId) {
isAllowedToEdit = true;
}
}
if (!isAllowedToEdit) {
return next(new error.PermissionDeniedError(
'You do not have permission to remove collaborators from this collection', req
));
}

const {UserCollection, UserCollectionCollaborator} = req.app.locals.orm;
await new UserCollectionCollaborator()
.query((qb) => {
qb.where('collection_id', collection.id);
qb.andWhere('collaborator_id', collaboratorId);
qb.whereIn('collaborator_id', collaboratorIdsToRemove);
}).destroy();
await new UserCollection({id: collection.id}).save({
lastModified: new Date()
Expand Down
5 changes: 5 additions & 0 deletions src/server/routes/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ router.get('/reindex', auth.isAuthenticated, (req, res) => {
// TODO: This is hacky, and we should replace it once we switch to SOLR.
const trustedUsers = ['Leftmost Cat', 'LordSputnik', 'Monkey', 'iliekcomputers'];

const NO_MATCH = -1;
if (trustedUsers.indexOf(req.user.name) === NO_MATCH) {
throw new error.PermissionDeniedError(null, req);
}

resolve();
})
.then(() => search.generateIndex(orm))
Expand Down
Loading