From ee322e9f635083de68f62449a48f7742f0e3ebdf Mon Sep 17 00:00:00 2001 From: Jure Triglav <juretriglav@gmail.com> Date: Thu, 5 Dec 2019 01:12:31 +0100 Subject: [PATCH] fix: several bugfixes --- app/components/App.js | 2 + .../src/components/Reviews.js | 2 +- .../src/components/sections/EditorItem.js | 1 + .../src/components/sections/ReviewerItem.js | 1 + .../src/components/DecisionPage.js | 2 +- .../src/components/ReviewPage.js | 39 +- .../src/components/ReviewersPage.js | 24 +- .../components/assignEditors/AssignEditor.js | 7 +- .../src/components/review/ReviewForm.js | 39 +- .../src/components/review/ReviewLayout.js | 7 +- .../src/components/SubmitPage.js | 16 +- app/config/journal/metadata.js | 4 +- app/routes.js | 7 +- config/authsome.js | 494 +++++------ config/authsomeGraphql.js | 789 ------------------ config/default.js | 2 +- cypress/integration/upload_spec.js | 149 +++- scripts/clearAndSeed.js | 30 + server/manuscript/src/resolvers.js | 3 +- server/manuscript/src/typeDefs.js | 2 +- 20 files changed, 461 insertions(+), 1159 deletions(-) delete mode 100644 config/authsomeGraphql.js diff --git a/app/components/App.js b/app/components/App.js index 0126f13780..549bd167a9 100644 --- a/app/components/App.js +++ b/app/components/App.js @@ -102,6 +102,8 @@ const App = ({ authorized, children, history, match }) => { <Root converting={conversion.converting}> <AppBar brand={journal.metadata.name} + brandLink="/dashboard" + loginLink="/login?next=/dashboard" navLinkComponents={links} onLogoutClick={() => logoutUser(client)} user={currentUser} diff --git a/app/components/component-xpub-dashboard/src/components/Reviews.js b/app/components/component-xpub-dashboard/src/components/Reviews.js index b2bb5ab11b..1ba09e86e1 100644 --- a/app/components/component-xpub-dashboard/src/components/Reviews.js +++ b/app/components/component-xpub-dashboard/src/components/Reviews.js @@ -52,7 +52,7 @@ const Reviews = ({ version, journal }) => ( <JournalContext.Consumer> {journal => journal.reviewStatus.map(status => ( - <BadgeContainer key={status}> + <BadgeContainer key={status} data-testid={status}> <Badge count={countStatus(version, status)} label={status} /> </BadgeContainer> )) diff --git a/app/components/component-xpub-dashboard/src/components/sections/EditorItem.js b/app/components/component-xpub-dashboard/src/components/sections/EditorItem.js index 4cb8d02403..381df9490a 100644 --- a/app/components/component-xpub-dashboard/src/components/sections/EditorItem.js +++ b/app/components/component-xpub-dashboard/src/components/sections/EditorItem.js @@ -34,6 +34,7 @@ const EditorItemLinks = ({ version, journals }) => ( Summary Info </Action> <Action + data-testid="control-panel" to={`/journals/${journals.id}/versions/${version.id}/decisions/${version.id}`} > {version.decision && version.decision.status === 'submitted' diff --git a/app/components/component-xpub-dashboard/src/components/sections/ReviewerItem.js b/app/components/component-xpub-dashboard/src/components/sections/ReviewerItem.js index 9b017adf1e..3fb7e1b159 100644 --- a/app/components/component-xpub-dashboard/src/components/sections/ReviewerItem.js +++ b/app/components/component-xpub-dashboard/src/components/sections/ReviewerItem.js @@ -65,6 +65,7 @@ const ReviewerItem = ({ version, journals, currentUser, reviewerRespond }) => { <Actions> <ActionContainer> <Button + data-testid="accept-review" onClick={() => { reviewerRespond({ variables: { diff --git a/app/components/component-xpub-review/src/components/DecisionPage.js b/app/components/component-xpub-review/src/components/DecisionPage.js index 7310071acb..0a93823b09 100644 --- a/app/components/component-xpub-review/src/components/DecisionPage.js +++ b/app/components/component-xpub-review/src/components/DecisionPage.js @@ -216,7 +216,7 @@ export default compose( }), }, }).then(() => { - history.push('/') + history.push('/dashboard') }) }, }), diff --git a/app/components/component-xpub-review/src/components/ReviewPage.js b/app/components/component-xpub-review/src/components/ReviewPage.js index 65493408ca..35fe9955d8 100644 --- a/app/components/component-xpub-review/src/components/ReviewPage.js +++ b/app/components/component-xpub-review/src/components/ReviewPage.js @@ -3,7 +3,7 @@ import { graphql } from '@apollo/react-hoc' import { gql } from 'apollo-client-preset' import { withFormik } from 'formik' import { withLoader } from 'pubsweet-client' -import { cloneDeep } from 'lodash' +import { cloneDeep, debounce } from 'lodash' import { getCommentContent } from './review/util' import ReviewLayout from '../components/review/ReviewLayout' @@ -264,12 +264,16 @@ export default compose( input: reviewData, }, update: (proxy, { data: { updateReview } }) => { - const data = proxy.readQuery({ - query, - variables: { - id: manuscript.id, - }, - }) + const data = JSON.parse( + JSON.stringify( + proxy.readQuery({ + query, + variables: { + id: manuscript.id, + }, + }), + ), + ) let reviewIndex = data.manuscript.reviews.findIndex( review => review.id === updateReview.id, ) @@ -300,14 +304,12 @@ export default compose( const team = cloneDeep(manuscript.teams).find( team => team.role === 'reviewerEditor', ) - team.members = team.members - .map(m => { - if (m.user.id === currentUser.id) { - return { user: { id: m.user.id }, status: 'completed' } - } - return undefined - }) - .filter(m => m) + team.members = team.members.map(m => { + if (m.user.id === currentUser.id) { + return { user: { id: m.user.id }, status: 'completed' } + } + return { user: { id: m.user.id }, status: m.status } + }) updateTeam({ variables: { @@ -317,15 +319,16 @@ export default compose( }, }, }).then(() => { - history.push('/') + history.push('/dashboard') }) }, }), ), withFormik({ - enableReinitialize: true, mapPropsToValues: props => - props.manuscript.reviews.find(review => !review.isDecision) || { + props.manuscript.reviews.find( + review => review.user.id === props.currentUser.id && !review.isDecision, + ) || { id: null, comments: [], recommendation: null, diff --git a/app/components/component-xpub-review/src/components/ReviewersPage.js b/app/components/component-xpub-review/src/components/ReviewersPage.js index 42b05b69f0..54f5b799dd 100644 --- a/app/components/component-xpub-review/src/components/ReviewersPage.js +++ b/app/components/component-xpub-review/src/components/ReviewersPage.js @@ -107,12 +107,16 @@ const query = gql` ` const update = match => (proxy, { data: { updateTeam, createTeam } }) => { - const data = proxy.readQuery({ - query, - variables: { - id: match.params.version, - }, - }) + const data = JSON.parse( + JSON.stringify( + proxy.readQuery({ + query, + variables: { + id: match.params.version, + }, + }), + ), + ) if (updateTeam) { const teamIndex = data.teams.findIndex(team => team.id === updateTeam.id) @@ -148,7 +152,13 @@ const handleSubmit = ( if (team.id) { const newTeam = { ...omit(team, ['object', 'id', '__typename']), - members: cloneDeep(team.members), + // TODO: Find a cleaner way of updating members + members: team.members.map(member => ({ + user: { + id: member.user.id, + }, + status: member.status, + })), } newTeam.members.push({ user: { id: user.id }, status: 'invited' }) diff --git a/app/components/component-xpub-review/src/components/assignEditors/AssignEditor.js b/app/components/component-xpub-review/src/components/assignEditors/AssignEditor.js index 11811a22ec..127f5d9ffc 100644 --- a/app/components/component-xpub-review/src/components/assignEditors/AssignEditor.js +++ b/app/components/component-xpub-review/src/components/assignEditors/AssignEditor.js @@ -65,6 +65,7 @@ const AssignEditor = ({ options, }) => ( <Menu + data-testid={`assign${teamRole}`} label={teamName} onChange={user => { if (value) { @@ -91,7 +92,7 @@ export default compose( variables: { id: team.id, input: { - members: [{ id: userId }], + members: [{ user: { id: userId } }], }, }, }) @@ -111,7 +112,7 @@ export default compose( name: teamRole === 'seniorEditor' ? 'Senior Editor' : 'Handling Editor', role: teamRole, - members: [{ id: userId }], + members: [{ user: { id: userId } }], } mutate({ @@ -137,7 +138,7 @@ export default compose( return { teamName, options: optionUsers, - value: members.length > 0 ? members[0].id : undefined, + value: members.length > 0 ? members[0].user.id : undefined, } }), withLoader(), diff --git a/app/components/component-xpub-review/src/components/review/ReviewForm.js b/app/components/component-xpub-review/src/components/review/ReviewForm.js index 1eefa22857..8d5ce88681 100644 --- a/app/components/component-xpub-review/src/components/review/ReviewForm.js +++ b/app/components/component-xpub-review/src/components/review/ReviewForm.js @@ -57,15 +57,16 @@ const AttachmentsInput = ({ const NoteInput = ({ field, - form: { values, setFieldValue }, + form: { values, setFieldValue, setTouched }, updateReview, + ...rest }) => ( <NoteEditor key="note-comment" placeholder="Enter your review…" title="Comments to the Author" {...field} - onChange={value => { + onBlur={value => { const { comment } = createComments( values, { @@ -94,7 +95,7 @@ const ConfidentialInput = ({ placeholder="Enter a confidential note to the editor (optional)…" title="Confidential Comments to Editor (Optional)" {...field} - onChange={value => { + onBlur={value => { const { comment } = createComments( values, { @@ -132,11 +133,9 @@ const ReviewComment = props => ( <> <AdminSection> <div name="note"> - <Field - component={extraProps => <NoteInput {...props} {...extraProps} />} - key="noteField" - name="comments.0.content" - /> + <Field key="noteField" name="comments.0.content"> + {extraProps => <NoteInput {...props} {...extraProps} />} + </Field> <Field component={extraProps => ( <AttachmentsInput type="note" {...props} {...extraProps} /> @@ -146,13 +145,9 @@ const ReviewComment = props => ( </AdminSection> <AdminSection> <div name="confidential"> - <Field - component={extraProps => ( - <ConfidentialInput {...props} {...extraProps} /> - )} - key="confidentialField" - name="comments.1.content" - /> + <Field key="confidentialField" name="comments.1.content"> + {extraProps => <ConfidentialInput {...props} {...extraProps} />} + </Field> <Field component={extraProps => ( <AttachmentsInput type="confidential" {...props} {...extraProps} /> @@ -174,21 +169,23 @@ const ReviewForm = ({ review, }) => ( <form onSubmit={handleSubmit}> - <ReviewComment updateReview={updateReview} uploadFile={uploadFile} /> + <ReviewComment + review={review} + updateReview={updateReview} + uploadFile={uploadFile} + /> <AdminSection> <div name="Recommendation"> <Title>Recommendation</Title> - <Field - component={props => ( + <Field name="recommendation" updateReview={updateReview}> + {props => ( <RecommendationInput journal={journal} updateReview={updateReview} {...props} /> )} - name="recommendation" - updateReview={updateReview} - /> + </Field> </div> </AdminSection> diff --git a/app/components/component-xpub-review/src/components/review/ReviewLayout.js b/app/components/component-xpub-review/src/components/review/ReviewLayout.js index 3872593897..0153dc0168 100644 --- a/app/components/component-xpub-review/src/components/review/ReviewLayout.js +++ b/app/components/component-xpub-review/src/components/review/ReviewLayout.js @@ -29,6 +29,7 @@ const ReviewLayout = ({ const reviewSections = [] const editorSections = [] const manuscriptVersions = manuscript.manuscriptVersions || [] + manuscriptVersions.forEach(manuscript => { const label = moment().format('YYYY-MM-DD') reviewSections.push({ @@ -36,7 +37,11 @@ const ReviewLayout = ({ <div> <ReviewMetadata manuscript={manuscript} /> <Review - review={manuscript.reviews.find(review => !review.isDecision) || {}} + review={manuscript.reviews.find(review => { + return ( + (review.user.id === currentUser.id && !review.isDecision) || {} + ) + })} /> </div> ), diff --git a/app/components/component-xpub-submit/src/components/SubmitPage.js b/app/components/component-xpub-submit/src/components/SubmitPage.js index 1e63f237a8..1bc4c31b54 100644 --- a/app/components/component-xpub-submit/src/components/SubmitPage.js +++ b/app/components/component-xpub-submit/src/components/SubmitPage.js @@ -1,4 +1,4 @@ -import { throttle, cloneDeep, isEmpty, set } from 'lodash' +import { debounce, cloneDeep, isEmpty, set } from 'lodash' import { compose, withProps, withState, withHandlers } from 'recompose' import { graphql } from '@apollo/react-hoc' import { gql } from 'apollo-client-preset' @@ -186,20 +186,24 @@ export default compose( }), graphql(updateMutation, { props: ({ mutate, ownProps }) => { - const updateManuscript = (value, path) => { + const debouncers = {} + const onChange = (value, path) => { const input = {} set(input, path, value) + debouncers[path] = debouncers[path] || debounce(updateManuscript, 300) + return debouncers[path](input) + } + + const updateManuscript = input => mutate({ variables: { id: ownProps.match.params.version, input: JSON.stringify(emptyToUndefined(input)), }, }) - } return { - // TODO: do this on blur, rather than on every keystroke? - onChange: throttle(updateManuscript, 1000, { trailing: false }), + onChange, } }, }), @@ -216,7 +220,7 @@ export default compose( input: JSON.stringify(updateManuscript), }, }).then(() => { - history.push('/') + history.push('/dashboard') }) }, }), diff --git a/app/config/journal/metadata.js b/app/config/journal/metadata.js index eee1ed1efe..bdb2ef3289 100644 --- a/app/config/journal/metadata.js +++ b/app/config/journal/metadata.js @@ -1,4 +1,4 @@ export default { - issn: '2474-7394', - name: 'Collabra: Psychology', + issn: '0000-0001', + name: 'SimpleJ', } diff --git a/app/routes.js b/app/routes.js index ba04b64b51..86e18426d1 100644 --- a/app/routes.js +++ b/app/routes.js @@ -19,9 +19,8 @@ import FormBuilderPage from './components/component-xpub-formbuilder/src/compone import App from './components/App' -const createReturnUrl = ({ pathname, search = '' }) => pathname + search - -const loginUrl = location => `/login?next=${createReturnUrl(location)}` +// const createReturnUrl = ({ pathname, search = '' }) => pathname + search +// const loginUrl = location => `/login?next=${createReturnUrl(location)}` const PrivateRoute = ({ component: Component, ...rest }) => ( <Route @@ -30,7 +29,7 @@ const PrivateRoute = ({ component: Component, ...rest }) => ( localStorage.getItem('token') ? ( <Component {...props} /> ) : ( - <Redirect to={loginUrl(props.location)} /> + <Redirect to="/login?next=/dashboard" /> ) } /> diff --git a/config/authsome.js b/config/authsome.js index 08646dbc3a..5bd2973bae 100644 --- a/config/authsome.js +++ b/config/authsome.js @@ -1,8 +1,8 @@ const { pickBy } = require('lodash') -class XpubCollabraMode { +class SimpleJMode { /** - * Creates a new instance of XpubCollabraMode + * Creates a new instance of SimpleJMode * * @param {string} userId A user's UUID * @param {string} operation The operation you're authorizing for @@ -12,7 +12,7 @@ class XpubCollabraMode { */ constructor(userId, operation, object, context) { this.userId = userId - this.operation = XpubCollabraMode.mapOperation(operation) + this.operation = SimpleJMode.mapOperation(operation) this.object = object this.context = context } @@ -42,6 +42,7 @@ class XpubCollabraMode { * @returns {boolean} */ async isTeamMember(role, object) { + console.log(this.user, this.user.teams) if (!this.user || !Array.isArray(this.user.teams)) { return false } @@ -49,17 +50,23 @@ class XpubCollabraMode { let membershipCondition if (object) { // We're asking if a user is a member of a team for a specific object - membershipCondition = team => - team.role === role && team.object && team.object.id === object.id + membershipCondition = team => { + // TODO: This needs to be fixed... + const objectId = team.objectId || (team.object && team.object.objectId) + return team.role === role && objectId === object.id + } } else { // We're asking if a user is a member of a global team - membershipCondition = team => team.role === role && !team.object + membershipCondition = team => team.role === role && team.global } const memberships = await Promise.all( this.user.teams.map(async teamId => { - const team = await this.context.models.Team.find(teamId) + // TODO: This needs to be fixed... + const id = teamId.id ? teamId.id : teamId + const team = await this.context.models.Team.find(id) if (!team) return [false] + return membershipCondition(team) }), ) @@ -79,33 +86,22 @@ class XpubCollabraMode { } /** - * Checks if the user is an author, as represented with the owners + * Checks if the user is an admin, as represented with the owners * relationship * - * @param {any} object * @returns {boolean} */ - isAuthor(object) { - if (!object || !object.owners || !this.user) { - return false - } - - const authorCheck = object.owners.includes(this.user.id) - if (authorCheck) { - return true - } - - return object.owners.some(user => user.id === this.user.id) + isAdmin() { + return this.user && this.user.admin } /** - * Checks if the user is an admin, as represented with the owners - * relationship + * Checks if user is a author editor (member of a team of type author) for an object * * @returns {boolean} */ - isAdmin() { - return this.user && this.user.admin + isAuthor(object) { + return this.isTeamMember('author', object) } /** @@ -141,7 +137,7 @@ class XpubCollabraMode { * @returns {boolean} */ isAssignedReviewerEditor(object) { - return this.isTeamMember('reviewer', object) + return this.isTeamMember('reviewerEditor', object) } /** @@ -169,43 +165,34 @@ class XpubCollabraMode { * @returns {boolean} * */ - async canReadCollection() { + async canReadManuscript() { if (!this.isAuthenticated()) { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) - const collection = this.object + const manuscript = this.object - if (await this.isManagingEditor(collection)) { - return true - } + // TODO: Enable more team types + // if (await this.isManagingEditor(manuscript)) { + // return true + // } - let permission = this.checkTeamMembers( - ['isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - collection, + let permission = await this.checkTeamMembers( + [ + 'isAssignedSeniorEditor', + 'isAssignedHandlingEditor', + 'isAssignedReviewerEditor', + ], + manuscript, ) - permission = permission - ? true - : await this.canReadatLeastOneFragmentOfCollection(collection, [ - 'isAssignedReviewerEditor', - ]) + permission = permission ? true : await this.isAuthor(manuscript) - permission = permission ? true : await this.isAuthor(collection) return permission } - async canReadatLeastOneFragmentOfCollection(collection, teamMembers) { - const permission = await Promise.all( - collection.fragments.map(async fragmentId => - this.checkTeamMembers(teamMembers, { id: fragmentId }), - ), - ) - return permission.includes(true) - } - /** * Checks if a user can list users * @@ -216,7 +203,7 @@ class XpubCollabraMode { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) return true } @@ -231,7 +218,7 @@ class XpubCollabraMode { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) if (this.user.id === this.object.id) { return true @@ -252,7 +239,7 @@ class XpubCollabraMode { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) const fragment = this.object let permission = this.isAuthor(fragment) @@ -283,43 +270,45 @@ class XpubCollabraMode { } /** - * Checks if a user can list collections + * Checks if a user can list Manuscripts * * @returns {boolean} */ - async canListCollections() { - if (!this.isAuthenticated()) { - return false - } - this.user = await this.context.models.User.find(this.userId) - - return { - filter: async collections => { - const filteredCollections = await Promise.all( - collections.map(async collection => { - let condition = await this.checkTeamMembers( - [ - 'isAssignedSeniorEditor', - 'isAssignedHandlingEditor', - 'isManagingEditor', - ], - collection, - ) - condition = condition - ? true - : await this.canReadatLeastOneFragmentOfCollection(collection, [ - 'isAssignedReviewerEditor', - ]) - - condition = condition ? true : await this.isAuthor(collection) - return condition ? collection : false - }), - ) - - return filteredCollections.filter(collection => collection) - }, - } - } + // async canListManuscripts() { + // if (!this.isAuthenticated()) { + // return false + // } + + // this.user = await getUserAndTeams(this.userId, this.context) + + // return { + // filter: async manuscripts => { + // const filteredManuscripts = await Promise.all( + // manuscripts.map(async manuscript => { + // let condition = await this.checkTeamMembers( + // [ + // 'isAssignedSeniorEditor', + // 'isAssignedHandlingEditor', + // 'isManagingEditor', + // 'isAssignedReviewerEditor', + // ], + // manuscript, + // ) + // // condition = condition + // // ? true + // // : await this.canReadatLeastOneFragmentOfCollection(collection, [ + // // 'isAssignedReviewerEditor', + // // ]) + + // condition = condition ? true : await this.isAuthor(manuscript) + // return condition ? manuscript : false + // }), + // ) + + // return filteredManuscripts.filter(manuscript => manuscript) + // }, + // } + // } /** * Checks if a user can create fragments @@ -343,7 +332,7 @@ class XpubCollabraMode { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) const { collection } = this.object if (collection) { @@ -367,7 +356,7 @@ class XpubCollabraMode { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) const { collection } = this.object if (collection) { @@ -396,13 +385,13 @@ class XpubCollabraMode { * Checks if a user can create a team * * @returns {boolean} - * @memberof XpubCollabraMode + * @memberof SimpleJMode */ async canCreateTeam() { if (!this.isAuthenticated()) { return false } - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) const { role, object } = this.object.team if (role === 'handlingEditor') { @@ -412,16 +401,6 @@ class XpubCollabraMode { return false } - /** - * Checks if a user can delete a collection - * - * @returns {boolean} - */ - async canDeleteCollection() { - this.user = await this.context.models.User.find(this.userId) - return this.isAuthor(this.object) && this.object.status !== 'revising' - } - /** * Checks if a user can read a team * @@ -463,20 +442,20 @@ class XpubCollabraMode { } /** - * Checks if a user can update a fragment + * Checks if a user can update a manuscript * * @returns {boolean} */ - async canUpdateFragment() { + async canUpdateManuscript() { const { update, current } = this.object - this.user = await this.context.models.User.find(this.userId) - const collection = await this.context.models.Collection.find( - current.collections[0], - ) + this.user = await getUserAndTeams(this.userId, this.context) + const collection = await this.context.models.Manuscript.find(current.id) + const schemaEditors = ['decision', 'id', 'reviewers'] const schemaReviewers = ['id', 'reviewers'] - let permission = (await this.isAuthor(current)) && !current.submitted + let permission = + (await this.isAuthor(current)) && current.status !== 'submitted' permission = permission ? true @@ -495,32 +474,32 @@ class XpubCollabraMode { } /** - * Checks if a user can update collection + * Checks if a user can update manuscript * * @returns {boolean} */ - async canUpdateCollection() { - this.user = await this.context.models.User.find(this.userId) - const { current } = this.object - if (current) { - return this.checkTeamMembers( - ['isAuthor', 'isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - current, - ) - } - return false - } + // async canUpdateManuscript() { + // this.user = await getUserAndTeams(this.userId, this.context) + // const { current } = this.object + // if (current) { + // return this.checkTeamMembers( + // ['isAuthor', 'isAssignedSeniorEditor', 'isAssignedHandlingEditor'], + // current, + // ) + // } + // return false + // } /** - * Checks if a user can delete fragment + * Checks if a user can delete Manuscript * * @returns {boolean} */ - async canDeleteFragment() { - this.user = await this.context.models.User.find(this.userId) - const fragment = this.object + async canDeleteManuscript() { + this.user = await getUserAndTeams(this.userId, this.context) + const manuscript = this.object - return this.isAuthor(fragment) && !fragment.submitted + return this.isAuthor(manuscript) && manuscript.status !== 'submitted' } /** @@ -529,7 +508,7 @@ class XpubCollabraMode { * @returns {boolean} */ async canMakeInvitation() { - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) const { current } = this.object if (current) { return this.checkTeamMembers( @@ -540,22 +519,33 @@ class XpubCollabraMode { return false } + async isAllowedToReview(object) { + this.user = await getUserAndTeams(this.userId, this.context) + const permission = await this.isAssignedReviewerEditor({ + id: object.manuscriptId, + }) + return permission + } + async canViewMySubmissionSection() { - this.user = await this.context.models.User.find(this.userId) - const collection = await Promise.all( - this.object.map(async collection => this.isAuthor(collection)), + this.user = await getUserAndTeams(this.userId, this.context) + const manuscripts = await Promise.all( + this.object.map(async manuscript => this.isAuthor(manuscript)), ) - return collection.some(collection => collection) + + return manuscripts.some(manuscript => manuscript) } async canViewReviewSection() { - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) + const collection = await Promise.all( - this.object.map(async collection => { - const permission = await this.canReadatLeastOneFragmentOfCollection( - collection, + this.object.map(async manuscript => { + const permission = await this.checkTeamMembers( ['isAssignedReviewerEditor'], + manuscript, ) + return permission }), ) @@ -563,46 +553,46 @@ class XpubCollabraMode { } async canViewManuscripts() { - this.user = await this.context.models.User.find(this.userId) - const collection = await Promise.all( + this.user = await getUserAndTeams(this.userId, this.context) + const manuscripts = await Promise.all( this.object.map( - async collection => + async manuscript => (await this.checkTeamMembers( ['isAdmin', 'isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - collection, + manuscript, )) && - (collection.status === 'revising' || - collection.status === 'submitted'), + (manuscript.status === 'revising' || + manuscript.status === 'submitted'), ), ) - return collection.some(collection => collection) + return manuscripts.some(collection => collection) } async canViewPage() { - this.user = await this.context.models.User.find(this.userId) + this.user = await getUserAndTeams(this.userId, this.context) const { path, params } = this.object if (path === '/teams') { return !!this.isAdmin() } - if (path === '/projects/:project/versions/:version/submit') { + if (path === '/journals/:journal/versions/:version/submit') { return this.checkPageSubmit(params) } - if (path === '/projects/:project/versions/:version/reviews/:review') { + if (path === '/journals/:journal/versions/:version/reviews/:review') { return this.checkPageReviews(params) } if ( - path === '/projects/:project/versions/:version/review' || - path === '/projects/:project/versions/:version/reviewers' + path === '/journals/:journal/versions/:version/review' || + path === '/journals/:journal/versions/:version/reviewers' ) { return this.checkPageReview(params) } - if (path === '/projects/:project/versions/:version/decisions/:decision') { + if (path === '/journals/:journal/versions/:version/decisions/:decision') { return this.checkPageDecision(params) } @@ -613,16 +603,20 @@ class XpubCollabraMode { const collection = this.context.models.Collection.find(params.project) let permission = await this.isAuthor(collection) - permission = permission - ? true - : await !this.canReadatLeastOneFragmentOfCollection(collection, [ - 'isAssignedReviewerEditor', - ]) + // permission = permission + // ? true + // : await !this.canReadatLeastOneFragmentOfCollection(collection, [ + // 'isAssignedReviewerEditor', + // ]) permission = permission ? true : await this.checkTeamMembers( - ['isAssignedSeniorEditor', 'isAssignedHandlingEditor'], + [ + 'isAssignedSeniorEditor', + 'isAssignedHandlingEditor', + 'isAssignedReviewerEditor', + ], collection, ) @@ -670,109 +664,61 @@ class XpubCollabraMode { } } +const getUserAndTeams = async (userId, context) => { + const user = await context.models.User.find(userId) + if (!user) return false + // If it runs on the client, it will have teams in there already. + // On the server, we need to add the team ids. + if (!user.teams && user.$relatedQuery) { + user.teams = (await user.$relatedQuery('teams')).map(team => team.id) + } + + return user +} + module.exports = { // This runs before all other authorization queries and is used here // to allow admin to do everything before: async (userId, operation, object, context) => { - const user = await context.models.User.find(userId) + if (!userId) return false + + const user = await getUserAndTeams(userId, context) if (!user) return false // we need to introduce a new Role Managing Editor // currently we take for granted that an admin is the Managing Editor // Temporally we need this if statement to prevent admin from seeing // review and submission section on dashboard (ME permissions) - if ( - operation === 'can view review section' || - operation === 'can view my submission section' || - operation === 'can view my manuscripts section' - ) - return false - + // if ( + // operation === 'can view review section' || + // operation === 'can view my submission section' || + // operation === 'can view my manuscripts section' + // ) + // return false return user.admin }, - GET: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - - // GET /api/collections - if (mode.object && mode.object.path === '/collections') { - return mode.canListCollections() - } - - // GET /api/users - if (mode.object && mode.object.path === '/users') { - return mode.canListUsers() - } - - // GET /api/fragments - if (mode.object && mode.object.path === '/fragments') { - return mode.canListFragments() - } + create: (userId, operation, object, context) => true, + update: async (userId, operation, object, context) => { + const mode = new SimpleJMode(userId, operation, object, context) - // GET /api/teams - if (mode.object && mode.object.path === '/teams') { - return mode.canListTeams() - } - - // GET /api/collection - if (mode.object && mode.object.type === 'collection') { - return mode.canReadCollection() - } - - // GET /api/fragment - if (mode.object && mode.object.type === 'fragment') { - return mode.canReadFragment() - } - - // GET /api/team - if (object && object.type === 'team') { - return mode.canReadTeam() - } - - // GET /api/user - if (mode.object && mode.object.type === 'user') { - return mode.canReadUser() - } - - return false - }, - POST: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - // POST /api/collections - if (mode.object && mode.object.path === '/collections') { - return mode.canCreateCollection() - } - - // POST /api/users - if (mode.object && mode.object.path === '/users') { - return mode.canCreateUser() - } - - // POST /api/fragments - if (mode.object && mode.object.path === '/fragments') { - return mode.canCreateFragment() - } - - // POST /api/collections/:collectionId/fragments if ( - mode.object && - mode.object.path === '/collections/:collectionId/fragments' + mode.object === 'Manuscript' || + mode.object === 'Review' || + mode.object === 'Team' ) { - return mode.canCreateFragmentInACollection() + return true } - // POST /api/teams - if (mode.object && mode.object.path === '/teams') { - return mode.canCreateTeam() + if (mode.object.current.type === 'team') { + return true } - return false - }, - PATCH: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + if (mode.object.current.type === 'Review') { + return mode.isAllowedToReview(mode.object.current) + } - // PATCH /api/collections/:id - if (mode.object.current && mode.object.current.type === 'collection') { - return mode.canUpdateCollection() + if (mode.object.current.type === 'Manuscript') { + return mode.canUpdateManuscript() } // PATCH /api/users/:id @@ -780,94 +726,74 @@ module.exports = { return mode.canUpdateUser() } - // PATCH /api/fragments/:id - if (mode.object.current && mode.object.current.type === 'fragment') { - return mode.canUpdateFragment() - } - - // PATCH /api/teams/:id - if (mode.object.current && mode.object.current.type === 'team') { - return mode.canUpdateTeam() - } - return false }, - DELETE: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - - // DELETE /api/collections/:id - if (object && object.type === 'collection') { - return mode.canDeleteCollection() - } + delete: (userId, operation, object, context) => { + const mode = new SimpleJMode(userId, operation, object, context) - // DELETE /api/users/:id if (object && object.type === 'users') { return mode.canDeleteUser() } - // DELETE /api/fragments/:id - if (object && object.type === 'fragment') { - return mode.canDeleteFragment() + if (object === 'Manuscript' || object.type === 'Manuscript') { + return mode.isAuthor(object) } - // DELETE /api/teams/:id - if (object && object.type === 'teams') { - return mode.canDeleteTeam() + if (object === 'Team' || object.type === 'Team') { + return true + // return mode.canDeleteTeam() } return false }, - // Example of a specific authorization query. Notice how easy it is to respond to this. - 'list collections': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canListCollections() - }, 'can view my submission section': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + const mode = new SimpleJMode(userId, operation, object, context) return mode.canViewMySubmissionSection() }, 'can view my manuscripts section': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + const mode = new SimpleJMode(userId, operation, object, context) return mode.canViewManuscripts() }, 'can view review section': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + const mode = new SimpleJMode(userId, operation, object, context) return mode.canViewReviewSection() }, - 'can delete collection': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canDeleteCollection() + 'can delete manuscript': (userId, operation, object, context) => { + const mode = new SimpleJMode(userId, operation, object, context) + return mode.canDeleteManuscript() }, 'can view page': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + const mode = new SimpleJMode(userId, operation, object, context) return mode.canViewPage() }, 'can view only admin': () => false, - create: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + read: async (userId, operation, object, context) => { + const mode = new SimpleJMode(userId, operation, object, context) - if (object === 'collections' || object.type === 'collection') { - return mode.canCreateCollection() + if (object === 'Manuscript' || object === 'Review') { + return true } - return false - }, - read: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) + if (object.type === 'Review') { + return mode.isAllowedToReview(object) + } - if (object === 'collections') { - return mode.canListCollections() + if (object.type === 'Manuscript') { + return mode.canReadManuscript() + } + if (object.type === 'team' || object === 'Team') { + return mode.canReadTeam() } - if (object.type === 'collection') { - return mode.canReadCollection() + if (object.constructor.name === 'TeamMember') { + return true } if (object === 'users') { return mode.canListUsers() } - if (object.type === 'user') { + if (object.type === 'user' || object === 'User') { return mode.canReadUser() } diff --git a/config/authsomeGraphql.js b/config/authsomeGraphql.js deleted file mode 100644 index e1ac0d4feb..0000000000 --- a/config/authsomeGraphql.js +++ /dev/null @@ -1,789 +0,0 @@ -const { pickBy } = require('lodash') - -class XpubCollabraMode { - /** - * Creates a new instance of XpubCollabraMode - * - * @param {string} userId A user's UUID - * @param {string} operation The operation you're authorizing for - * @param {any} object The object of authorization - * @param {any} context Context for authorization, e.g. database access - * @returns {string} - */ - constructor(userId, operation, object, context) { - this.userId = userId - this.operation = XpubCollabraMode.mapOperation(operation) - this.object = object - this.context = context - } - - /** - * Maps operations from HTTP verbs to semantic verbs - * - * @param {any} operation - * @returns {string} - */ - static mapOperation(operation) { - const operationMap = { - GET: 'read', - POST: 'create', - PATCH: 'update', - DELETE: 'delete', - } - - return operationMap[operation] ? operationMap[operation] : operation - } - - /** - * Checks if user is a member of a team of a certain type for a certain object - * - * @param {any} role - * @param {any} object - * @returns {boolean} - */ - async isTeamMember(role, object) { - if (!this.user || !Array.isArray(this.user.teams)) { - return false - } - - let membershipCondition - if (object) { - // We're asking if a user is a member of a team for a specific object - membershipCondition = team => { - // TODO: This needs to be fixed... - const objectId = team.objectId || (team.object && team.object.objectId) - return team.role === role && objectId === object.id - } - } else { - // We're asking if a user is a member of a global team - membershipCondition = team => team.role === role && team.global - } - - const memberships = await Promise.all( - this.user.teams.map(async teamId => { - const id = teamId.id ? teamId.id : teamId - const team = await this.context.models.Team.find(id) - if (!team) return [false] - - return membershipCondition(team) - }), - ) - - return memberships.includes(true) - } - - /** - * Returns permissions for unauthenticated users - * - * @param {any} operation - * @param {any} object - * @returns {boolean} - */ - unauthenticatedUser(object) { - return this.operation === 'something public' - } - - /** - * Checks if the user is an admin, as represented with the owners - * relationship - * - * @returns {boolean} - */ - isAdmin() { - return this.user && this.user.admin - } - - /** - * Checks if user is a author editor (member of a team of type author) for an object - * - * @returns {boolean} - */ - isAuthor(object) { - return this.isTeamMember('author', object) - } - - /** - * Checks if user is a handling editor (member of a team of type handling editor) for an object - * - * @returns {boolean} - */ - isAssignedHandlingEditor(object) { - return this.isTeamMember('handlingEditor', object) - } - - /** - * Checks if user is a senior editor (member of a team of type senior editor) for an object - * - * @returns {boolean} - */ - isAssignedSeniorEditor(object) { - return this.isTeamMember('seniorEditor', object) - } - - /** - * Checks if user is a senior editor (member of a team of type senior editor) for an object - * - * @returns {boolean} - */ - isManagingEditor(object) { - return this.isTeamMember('managingEditor', object) - } - - /** - * Checks if user is a reviewer editor (member of a team of type reviewer editor) for an object - * - * @returns {boolean} - */ - isAssignedReviewerEditor(object) { - return this.isTeamMember('reviewerEditor', object) - } - - /** - * Checks if userId is present, indicating an authenticated user - * - * @param {any} userId - * @returns {boolean} - */ - isAuthenticated() { - return !!this.userId - } - - /** - * Checks if a user can create a collection. - * - * @returns {boolean} - */ - canCreateCollection() { - return this.isAuthenticated() - } - - /** - * Checks if a user can read a specific collection - * - * @returns {boolean} - * - */ - async canReadManuscript() { - if (!this.isAuthenticated()) { - return false - } - - this.user = await this.context.models.User.find(this.userId) - - const manuscript = this.object - - // TODO: Enable more team types - // if (await this.isManagingEditor(manuscript)) { - // return true - // } - - let permission = await this.checkTeamMembers( - [ - 'isAssignedSeniorEditor', - 'isAssignedHandlingEditor', - 'isAssignedReviewerEditor', - ], - manuscript, - ) - - permission = permission ? true : await this.isAuthor(manuscript) - - return permission - } - - /** - * Checks if a user can list users - * - * @returns {boolean} - */ - async canListUsers() { - if (!this.isAuthenticated()) { - return false - } - - this.user = await this.context.models.User.find(this.userId) - - return true - } - - /** - * Checks if a user can read a specific user - * - * @returns {boolean} - */ - async canReadUser() { - if (!this.isAuthenticated()) { - return false - } - - this.user = await this.context.models.User.find(this.userId) - - if (this.user.id === this.object.id) { - return true - } - return { - filter: user => - pickBy(user, (_, key) => ['id', 'username', 'type'].includes(key)), - } - } - - /** - * Checks if a user can read a fragment - * - * @returns {boolean} - */ - async canReadFragment() { - if (!this.isAuthenticated()) { - return false - } - - this.user = await this.context.models.User.find(this.userId) - - const fragment = this.object - let permission = this.isAuthor(fragment) - - permission = permission - ? true - : await this.isAssignedReviewerEditor(fragment) - - permission = permission - ? true - : await this.checkTeamMembers( - [ - 'isAssignedSeniorEditor', - 'isAssignedHandlingEditor', - 'isManagingEditor', - ], - { id: fragment.collections[0] }, - ) - - // permission = permission - // ? true - // : await this.isAssignedManagingEditor(fragment) - // Caveat: this means every logged-in user can read every fragment (but needs its UUID) - // Ideally we'd check if the fragment (version) belongs to a collection (project) - // where the user is a member of a team with the appropriate rights. However there is no - // link from a fragment back to a collection at this point. Something to keep in mind! - return permission - } - - /** - * Checks if a user can list Manuscripts - * - * @returns {boolean} - */ - // async canListManuscripts() { - // if (!this.isAuthenticated()) { - // return false - // } - - // this.user = await this.context.models.User.find(this.userId) - - // return { - // filter: async manuscripts => { - // const filteredManuscripts = await Promise.all( - // manuscripts.map(async manuscript => { - // let condition = await this.checkTeamMembers( - // [ - // 'isAssignedSeniorEditor', - // 'isAssignedHandlingEditor', - // 'isManagingEditor', - // 'isAssignedReviewerEditor', - // ], - // manuscript, - // ) - // // condition = condition - // // ? true - // // : await this.canReadatLeastOneFragmentOfCollection(collection, [ - // // 'isAssignedReviewerEditor', - // // ]) - - // condition = condition ? true : await this.isAuthor(manuscript) - // return condition ? manuscript : false - // }), - // ) - - // return filteredManuscripts.filter(manuscript => manuscript) - // }, - // } - // } - - /** - * Checks if a user can create fragments - * - * @returns {boolean} - */ - canCreateFragment() { - if (!this.isAuthenticated()) { - return false - } - return true - } - - /** - * Checks if a user can create a fragment in a specific collection - * - * @returns {boolean} - */ - async canCreateFragmentInACollection() { - if (!this.isAuthenticated()) { - return false - } - - this.user = await this.context.models.User.find(this.userId) - - const { collection } = this.object - if (collection) { - const permission = await this.checkTeamMembers( - ['isAuthor', 'isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - collection, - ) - return permission - } - - return false - } - - /** - * Checks if a user can create a fragment in a specific collection - * - * @returns {boolean} - */ - async canUpdateFragmentInACollection() { - if (!this.isAuthenticated()) { - return false - } - - this.user = await this.context.models.User.find(this.userId) - - const { collection } = this.object - if (collection) { - const permission = - this.isAuthor(collection) || - (await this.isAssignedHandlingEditor(collection)) || - (await this.isAssignedSeniorEditor(collection)) || - (await this.isAssignedReviewerEditor(collection)) - return permission - } - - return false - } - - /** - * Checks if a user can be created - * - * @returns {boolean} - */ - // eslint-disable-next-line - canCreateUser() { - return true - } - - /** - * Checks if a user can create a team - * - * @returns {boolean} - * @memberof XpubCollabraMode - */ - async canCreateTeam() { - if (!this.isAuthenticated()) { - return false - } - this.user = await this.context.models.User.find(this.userId) - - const { role, object } = this.object.team - if (role === 'handlingEditor') { - return this.isAssignedSeniorEditor(object) - } - - return false - } - - /** - * Checks if a user can read a team - * - * @returns {boolean} - */ - // eslint-disable-next-line - async canReadTeam() { - return true - } - - /** - * Checks if a user can list a team - * - * @returns {boolean} - */ - // eslint-disable-next-line - async canListTeam() { - return true - } - - /** - * Checks if a user can lists team - * - * @returns {boolean} - */ - // eslint-disable-next-line - async canListTeams() { - return true - } - - /** - * Checks if a user can lists fragments - * - * @returns {boolean} - */ - // eslint-disable-next-line - async canListFragments() { - return true - } - - /** - * Checks if a user can update a manuscript - * - * @returns {boolean} - */ - async canUpdateManuscript() { - const { update, current } = this.object - this.user = await this.context.models.User.find(this.userId) - const collection = await this.context.models.Manuscript.find(current.id) - - const schemaEditors = ['decision', 'id', 'reviewers'] - const schemaReviewers = ['id', 'reviewers'] - - let permission = - (await this.isAuthor(current)) && current.status !== 'submitted' - - permission = permission - ? true - : (await this.checkTeamMembers( - ['isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - collection, - )) && - Object.keys(update).every(value => schemaEditors.indexOf(value) >= 0) - - permission = permission - ? true - : (await this.isAssignedReviewerEditor(current)) && - Object.keys(update).every(value => schemaReviewers.indexOf(value) >= 0) - - return permission - } - - /** - * Checks if a user can update manuscript - * - * @returns {boolean} - */ - // async canUpdateManuscript() { - // this.user = await this.context.models.User.find(this.userId) - // const { current } = this.object - // if (current) { - // return this.checkTeamMembers( - // ['isAuthor', 'isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - // current, - // ) - // } - // return false - // } - - /** - * Checks if a user can delete Manuscript - * - * @returns {boolean} - */ - async canDeleteManuscript() { - this.user = await this.context.models.User.find(this.userId) - const manuscript = this.object - - return this.isAuthor(manuscript) && manuscript.status !== 'submitted' - } - - /** - * Checks if editor can invite Reviewers - * - * @returns {boolean} - */ - async canMakeInvitation() { - this.user = await this.context.models.User.find(this.userId) - const { current } = this.object - if (current) { - return this.checkTeamMembers( - ['isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - current, - ) - } - return false - } - - async isAllowedToReview(object) { - this.user = await this.context.models.User.query() - .findById(this.userId) - .eager('teams') - const permission = await this.isAssignedReviewerEditor({ - id: object.manuscriptId, - }) - return permission - } - - async canViewMySubmissionSection() { - this.user = await this.context.models.User.find(this.userId) - const manuscripts = await Promise.all( - this.object.map(async manuscript => this.isAuthor(manuscript)), - ) - - return manuscripts.some(manuscript => manuscript) - } - - async canViewReviewSection() { - this.user = await this.context.models.User.find(this.userId) - - const collection = await Promise.all( - this.object.map(async manuscript => { - const permission = await this.checkTeamMembers( - ['isAssignedReviewerEditor'], - manuscript, - ) - - return permission - }), - ) - return collection.some(collection => collection) - } - - async canViewManuscripts() { - this.user = await this.context.models.User.find(this.userId) - const manuscripts = await Promise.all( - this.object.map( - async manuscript => - (await this.checkTeamMembers( - ['isAdmin', 'isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - manuscript, - )) && - (manuscript.status === 'revising' || - manuscript.status === 'submitted'), - ), - ) - - return manuscripts.some(collection => collection) - } - - async canViewPage() { - this.user = await this.context.models.User.find(this.userId) - const { path, params } = this.object - - if (path === '/teams') { - return !!this.isAdmin() - } - - if (path === '/journals/:journal/versions/:version/submit') { - return this.checkPageSubmit(params) - } - - if (path === '/journals/:journal/versions/:version/reviews/:review') { - return this.checkPageReviews(params) - } - - if ( - path === '/journals/:journal/versions/:version/review' || - path === '/journals/:journal/versions/:version/reviewers' - ) { - return this.checkPageReview(params) - } - - if (path === '/journals/:journal/versions/:version/decisions/:decision') { - return this.checkPageDecision(params) - } - - return true - } - - async checkPageSubmit(params) { - const collection = this.context.models.Collection.find(params.project) - let permission = await this.isAuthor(collection) - - // permission = permission - // ? true - // : await !this.canReadatLeastOneFragmentOfCollection(collection, [ - // 'isAssignedReviewerEditor', - // ]) - - permission = permission - ? true - : await this.checkTeamMembers( - [ - 'isAssignedSeniorEditor', - 'isAssignedHandlingEditor', - 'isAssignedReviewerEditor', - ], - collection, - ) - - return permission - } - - async checkPageDecision(params) { - const collection = this.context.models.Collection.find(params.project) - - if (this.isAuthor(collection)) return false - - const permission = await this.checkTeamMembers( - ['isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - collection, - ) - - return permission - } - - async checkPageReviews(params) { - const fragment = this.context.models.Fragment.find(params.version) - - const permission = await this.checkTeamMembers( - ['isAssignedReviewerEditor'], - fragment, - ) - - return permission - } - - async checkPageReview(params) { - const collection = this.context.models.Collection.find(params.project) - - const permission = await this.checkTeamMembers( - ['isAssignedSeniorEditor', 'isAssignedHandlingEditor'], - collection, - ) - - return permission - } - - async checkTeamMembers(team, object) { - const permission = await Promise.all(team.map(t => this[t](object))) - return permission.includes(true) - } -} - -module.exports = { - // This runs before all other authorization queries and is used here - // to allow admin to do everything - before: async (userId, operation, object, context) => { - if (!userId) return false - - const user = await context.models.User.find(userId) - if (!user) return false - - // we need to introduce a new Role Managing Editor - // currently we take for granted that an admin is the Managing Editor - // Temporally we need this if statement to prevent admin from seeing - // review and submission section on dashboard (ME permissions) - // if ( - // operation === 'can view review section' || - // operation === 'can view my submission section' || - // operation === 'can view my manuscripts section' - // ) - // return false - return user.admin - }, - create: (userId, operation, object, context) => true, - update: async (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - - if ( - mode.object === 'Manuscript' || - mode.object === 'Review' || - mode.object === 'Team' - ) { - return true - } - - if (mode.object.current.type === 'team') { - return true - } - - if (mode.object.current.type === 'Review') { - return mode.isAllowedToReview(mode.object.current) - } - - if (mode.object.current.type === 'Manuscript') { - return mode.canUpdateManuscript() - } - - // PATCH /api/users/:id - if (mode.object.current && mode.object.current.type === 'user') { - return mode.canUpdateUser() - } - - return false - }, - delete: (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - - if (object && object.type === 'users') { - return mode.canDeleteUser() - } - - if (object === 'Manuscript' || object.type === 'Manuscript') { - return mode.isAuthor(object) - } - - if (object === 'Team' || object.type === 'Team') { - return mode.canDeleteTeam() - } - - return false - }, - 'can view my submission section': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canViewMySubmissionSection() - }, - 'can view my manuscripts section': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canViewManuscripts() - }, - 'can view review section': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canViewReviewSection() - }, - 'can delete manuscript': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canDeleteManuscript() - }, - 'can view page': (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - return mode.canViewPage() - }, - 'can view only admin': () => false, - read: async (userId, operation, object, context) => { - const mode = new XpubCollabraMode(userId, operation, object, context) - - if (object === 'Manuscript' || object === 'Review') { - return true - } - - if (object.type === 'Review') { - return mode.isAllowedToReview(object) - } - - if (object.type === 'Manuscript') { - return mode.canReadManuscript() - } - if (object.type === 'team' || object === 'Team') { - return mode.canReadTeam() - } - - if (object.constructor.name === 'TeamMember') { - return true - } - - if (object === 'users') { - return mode.canListUsers() - } - - if (object.type === 'user' || object === 'User') { - return mode.canReadUser() - } - - return false - }, -} diff --git a/config/default.js b/config/default.js index 13294c1dd1..5ba4fcd8fd 100644 --- a/config/default.js +++ b/config/default.js @@ -4,7 +4,7 @@ const logger = require('winston') module.exports = { authsome: { - mode: path.resolve(__dirname, 'authsomeGraphql.js'), + mode: path.resolve(__dirname, 'authsome.js'), teams: { seniorEditor: { name: 'Senior Editors', diff --git a/cypress/integration/upload_spec.js b/cypress/integration/upload_spec.js index 4db69798b1..4c04395442 100644 --- a/cypress/integration/upload_spec.js +++ b/cypress/integration/upload_spec.js @@ -1,14 +1,45 @@ +const login = (username, password = 'password') => { + cy.get('input[name="username"]').type(username) + cy.get('input[name="password"]').type(password) + cy.get('button[type="submit"]').click() +} + +const doReview = (username, note, confidential, recommendation) => { + // 1. Login + login(username) + + // 2. Accept and do the review + cy.get('[data-testid=accept-review]').click() + cy.contains('Do Review').click() + + cy.get('[placeholder*="Enter your review"] div[contenteditable="true"]') + .focus() + .type(note) + .blur() + cy.wait(1000) + cy.get( + '[placeholder*="Enter a confidential note"] div[contenteditable="true"]', + ) + .focus() + .type(confidential) + .blur() + cy.wait(1000) + // 0 == accept, 1 == revise, 2 == reject + cy.get(`[class*=Radio__Label]:nth(${recommendation})`).click() + cy.get('button[type=submit]').click() + + // 3. Logout + cy.get('nav button').click() +} + describe('PDF submission test', () => { it('can upload and submit a PDF', () => { cy.visit('/dashboard') - const username = 'admin' - const password = 'password' - - cy.get('input[name="username"]').type(username) - cy.get('input[name="password"]').type(password) - cy.get('button[type="submit"]').click() + // 1. Log in as author + login('author') + // 2. Submit a PDF cy.fixture('test-pdf.pdf', 'base64').then(fileContent => { cy.get('[data-testid="dropzone"]').upload( { fileContent, fileName: 'test-pdf.pdf', mimeType: 'application/pdf' }, @@ -18,14 +49,15 @@ describe('PDF submission test', () => { cy.get('body').contains('Submission information') cy.get('[data-testid="meta.title"]').contains('test pdf') + cy.get('[data-testid="meta.title"] div[contenteditable="true"]') + .click() + .type('{selectall}{del}A Manuscript For The Ages') - cy.get('[data-testid="meta.title"]').type( - '{selectall}{del}A Manuscript For The Ages', - ) - - cy.get('[data-testid="meta.abstract"]').type(`{selectall}{del} - Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem. - `) + cy.get('[data-testid="meta.abstract"] div[contenteditable="true"]') + .click() + .type( + `{selectall}{del}Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem.`, + ) cy.get('[data-testid="meta.keywords"]').type('quantum, machines, nature') @@ -58,17 +90,96 @@ describe('PDF submission test', () => { cy.get('[data-testid="suggestions.editors.suggested"]').type('John Ode') cy.get('[data-testid="suggestions.editors.opposed"]').type('Gina Ode') - cy.get('[name="meta.notes.0.content"] div[contenteditable="true"]').type( - 'This work was supported by the Trust [grant numbers 393,295]; the Natural Environment Research Council [grant number 49493]; and the Economic and Social Research Council [grant number 30304]', - ) - cy.get('[name="meta.notes.1.content"] div[contenteditable="true"]').type( - 'This is extremely divisive work, choose reviewers with care.', - ) + cy.get('[name="meta.notes.0.content"] div[contenteditable="true"]') + .click() + .type( + 'This work was supported by the Trust [grant numbers 393,295]; the Natural Environment Research Council [grant number 49493].', + ) + cy.get('[name="meta.notes.1.content"] div[contenteditable="true"]') + .click() + .type('This is extremely divisive work, choose reviewers with care.') cy.get('form button:last').click() cy.get('button[type="submit"]').click() cy.visit('/dashboard') cy.contains('A Manuscript For The Ages') + + // 3. Logout + cy.get('nav button').click() + + // 4. And login as admin + login('admin', 'password') + + cy.get('[data-testid="control-panel"]').click() + + // 5. Assign senior editor + // TODO: Find a way to not match by partial class + cy.get('[class*="AssignEditor"] [class*="Menu__Root"]:first').click() + cy.get( + '[class*="AssignEditor"] [class*="Menu__Root"]:first [role="option"]:nth(1)', + ).click() + + // 6. Assign handling editor + cy.get('[class*="AssignEditor"] [class*="Menu__Root"]:nth(1)').click() + cy.get( + '[class*="AssignEditor"] [class*="Menu__Root"]:nth(1) [role="option"]:nth(2)', + ).click() + + // 7. Logout + cy.get('nav button').click() + + // 8. And login as handling editor + login('heditor') + + cy.get('[data-testid="control-panel"]').click() + + // 9. Assign reviewers + cy.get('[class*="AssignEditorsReviewers"] a').click() + + cy.get('.Select-control').click() + cy.get('.Select-menu[role="listbox"] [role="option"]:nth(3)').click() + cy.get('button[type="submit"]').click() + cy.get('[class*="Reviewer__"]').should('have.length', 1) + + cy.get('.Select-control').click() + cy.get('.Select-menu[role="listbox"] [role="option"]:nth(4)').click() + cy.get('button[type="submit"]').click() + cy.get('[class*="Reviewer__"]').should('have.length', 2) + + cy.get('.Select-control').click() + cy.get('.Select-menu[role="listbox"] [role="option"]:nth(5)').click() + cy.get('button[type="submit"]').click() + cy.get('[class*="Reviewer__"]').should('have.length', 3) + + // 10. Check that 3 reviewers are invited + cy.contains('SimpleJ').click() + cy.get('[data-testid="invited"]').contains('3') + + // 11. Logout + cy.get('nav button').click() + + doReview( + 'reviewer1', + 'Great research into CC bases in the ky289 variant are mutated to TC which results in the truncation of the SAD-1.', + 'Not too bad.', + 0, + ) + doReview( + 'reviewer2', + 'Mediocre analysis of Iron-Sulfur ClUster assembly enzyme homolog.', + 'It is so so.', + 1, + ) + doReview( + 'reviewer3', + 'mTOR-Is positively influence the occurrence and course of certain tumors after solid organ transplantation.', + 'It is not good.', + 2, + ) + + // 12. Log in as handling editor + login('heditor') + cy.get('[data-testid="control-panel"]').click() }) }) diff --git a/scripts/clearAndSeed.js b/scripts/clearAndSeed.js index 61f0379d98..b9e43a6e1b 100644 --- a/scripts/clearAndSeed.js +++ b/scripts/clearAndSeed.js @@ -18,6 +18,36 @@ const seed = async () => { password: 'password', }).save() + await new User({ + username: 'seditor', + email: 'simone@example.com', + password: 'password', + }).save() + + await new User({ + username: 'heditor', + email: 'hector@example.com', + password: 'password', + }).save() + + await new User({ + username: 'reviewer1', + email: 'regina@example.com', + password: 'password', + }).save() + + await new User({ + username: 'reviewer2', + email: 'robert@example.com', + password: 'password', + }).save() + + await new User({ + username: 'reviewer3', + email: 'remionne@example.com', + password: 'password', + }).save() + await new Journal({ title: 'My Journal', }).save() diff --git a/server/manuscript/src/resolvers.js b/server/manuscript/src/resolvers.js index ba25fd8782..5a87fdbd28 100644 --- a/server/manuscript/src/resolvers.js +++ b/server/manuscript/src/resolvers.js @@ -123,8 +123,9 @@ const resolvers = { }, async submitManuscript(_, { id, input }, ctx) { const data = JSON.parse(input) - const manuscript = await ctx.connectors.Manuscript.fetchOne(id, ctx) + const manuscript = await ctx.connectors.Manuscript.fetchOne(id, ctx) + console.log(manuscript, 'hello!') const update = merge({}, manuscript, data) // eslint-disable-next-line const previousVersion = await new ctx.connectors.Manuscript.model( diff --git a/server/manuscript/src/typeDefs.js b/server/manuscript/src/typeDefs.js index b620bfeb79..26c8cbdcc5 100644 --- a/server/manuscript/src/typeDefs.js +++ b/server/manuscript/src/typeDefs.js @@ -80,7 +80,7 @@ const typeDefs = ` type ManuscriptMeta { title: String! - source: String! + source: String articleType: String declarations: Declarations articleSections: [String] -- GitLab