diff --git a/database/initialization-scripts/schema.sql b/database/initialization-scripts/schema.sql
index 6d8b94db..0afe93e2 100644
--- a/database/initialization-scripts/schema.sql
+++ b/database/initialization-scripts/schema.sql
@@ -593,3 +593,47 @@ CREATE TABLE response_versions (
PRIMARY KEY(response_id, version)
);
+/******************************************************************************
+ * Permissions
+ ******************************************************************************/
+
+
+CREATE TABLE permissions (
+ id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
+ entity varchar(512) NOT NULL,
+ action varchar(512) NOT NULL,
+
+ user_id bigint REFERENCES users(id) DEFAULT NULL,
+ role_id uuid REFERENCES roles(id) DEFAULT NULL,
+
+ paper_id bigint REFERENCES papers(id) DEFAULT NULL,
+ paper_version_id uuid REFERENCES paper_versions(id) DEFAULT NULL,
+ event_id bigint REFERENCES paper_events(id) DEFAULT NULL,
+ review_id bigint REFERENCES reviews(id) DEFAULT NULL,
+ paper_comment_id bigint REFERENCES paper_comments(id) DEFAULT NULL,
+ submission_id bigint REFERENCES journal_submissions(id) DEFAULT NULL,
+ journal_id bigint REFERENCES journals(id) DEFAULT NULL,
+
+ created_date timestamptz,
+ updated_date timestamptz
+);
+
+CREATE TABLE roles (
+ id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
+ name varchar(1024) NOT NULL,
+ description varchar(1024) NOT NULL,
+
+ journal_id bigint REFERENCES journals(id) DEFAULT NULL,
+ paper_id bigint REFERENCES papers(id) DEFAULT NULL,
+
+ created_date timestamptz,
+ updated_date timestamptz
+);
+INSERT INTO roles (name, description) VALUES ('public', 'The general public.');
+
+CREATE TABLE user_roles (
+ role_id uuid REFERENCS roles(id) NOT NULL,
+ user_id bigint REFERENCES users(id) NOT NULL,
+
+ created_date timestamptz
+);
diff --git a/documentation/permission-system.md b/documentation/permission-system.md
new file mode 100644
index 00000000..84fffab5
--- /dev/null
+++ b/documentation/permission-system.md
@@ -0,0 +1,54 @@
+# Entity:Action Permission System
+
+JournalHub uses an Entity:Action permission system where `action` is granted to
+`user` on `entity`. This has a number of benefits and some costs.
+
+The benefits are:
+
+* Querying for permissions is fast, cheap, and easy.
+* High level of flexibility in terms of what `action` we can define. Eg. `identify`
+* Enables user defined permission models.
+
+The costs are:
+
+* Granting permissions is difficult and expensive. We need to make sure we're thorough.
+
+## The Entities
+
+The top level entities are defined by the controller/DAO combinations. Any
+entity that has both a controller and DAO is considered a top-level entities.
+Permissions can also be granted to sub-entities using `:` as a separator. For
+example, to grant a permission on a Paper Author entity, you would use
+`Paper:author`. Top level entities are always capitalized, while sub-entities
+are always lower case.
+
+Currently, the top level entities are:
+
+* Field
+* File
+* Journal
+* JournalSubmission
+* Notification
+* PaperComment
+* Paper
+* PaperEvent
+* PaperVersion
+* Review
+* Token
+* User
+
+## Actions
+
+The actions that may be granted correspond to the basic CRUD actions
+and enable each of the REST endpoints.
+
+They are:
+
+* `create` enabling `POST`
+* `read` enabling `GET`
+* `update` enabling `PATCH`
+* `delete` enabling `DELETE`
+
+Additional actions that don't correspond to the basic CRUD are:
+
+* `identify` allowing a user to identify an anonymous individual
diff --git a/packages/backend/daos/DAO.js b/packages/backend/daos/DAO.js
new file mode 100644
index 00000000..3caa0607
--- /dev/null
+++ b/packages/backend/daos/DAO.js
@@ -0,0 +1,82 @@
+/******************************************************************************
+ *
+ * JournalHub -- Universal Scholarly Publishing
+ * Copyright (C) 2022 - 2024 Daniel Bingham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ ******************************************************************************/
+
+export class DAO {
+
+ constructor(core) {
+ this.core = core
+ }
+
+
+ /**
+ * @return Promise
+ */
+ async insert(entityName, table, fieldMap, entities) {
+ if ( ! Array.isArray(entities) ) {
+ entities = [ entities ]
+ }
+
+ let columns = '('
+ for (const [field, meta] of Object.entries(fieldMap)) {
+ columns += ( columns == '(' ? '' : ', ') + field
+ }
+
+ if ( columns == '(' ) {
+ throw new DAOError('missing-fields',
+ `Empty field map sent to DAO::insert().`)
+ }
+
+ columns += ', created_date, updated_date)'
+
+ let rows = ''
+ let params = []
+ for(const entity of entities) {
+ let row = '('
+ for(const [field, meta] of Object.entries(fieldMap)) {
+ if ( meta.required && ! ( meta.key in entity ) ) {
+ throw new DAOError('missing-field',
+ `Required '${meta.key}' not found in ${entityName}.`)
+ }
+
+ params.push(( entity[meta.key] ? entity[meta.key] : null ))
+ row += ( row == '(' ? '' : ', ') + `$${params.length}`
+ }
+ row += ', now(), now())'
+
+ if ( rows !== '' ) {
+ rows += ', ' + row
+ } else {
+ rows += row
+ }
+ }
+
+
+ let sql = `
+ INSERT INTO ${table} ${columns}
+ VALUES ${rows}`
+
+ await this.core.database.query(sql, params)
+ }
+
+ async update(entityName, table, fieldMap, entities) {
+
+
+ }
+}
diff --git a/packages/backend/daos/PermissionDAO.js b/packages/backend/daos/PermissionDAO.js
new file mode 100644
index 00000000..482a2c46
--- /dev/null
+++ b/packages/backend/daos/PermissionDAO.js
@@ -0,0 +1,167 @@
+/******************************************************************************
+ *
+ * JournalHub -- Universal Scholarly Publishing
+ * Copyright (C) 2022 - 2024 Daniel Bingham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ ******************************************************************************/
+import { DAO } from './DAO'
+
+export class PermissionDAO extends DAO {
+
+ constructor(core) {
+ super(core)
+
+ this.fieldMap = {
+ 'id': {
+ required: false,
+ key: 'id'
+ },
+ 'entity': {
+ required: true,
+ key: 'entity'
+ },
+ 'action': {
+ required: true,
+ key: 'action'
+ },
+ 'user_id': {
+ required: false,
+ key: 'userId'
+ },
+ 'role_id': {
+ required: false,
+ key: 'roleId'
+ },
+ 'paper_id': {
+ rquired: false,
+ key: 'paperId'
+ },
+ 'paper_version_id': {
+ required: false,
+ key: 'paperVersionId'
+ },
+ 'event_id': {
+ required: false,
+ key: 'event_id'
+ },
+ 'review_id': {
+ required: false,
+ key: 'review_id'
+ },
+ 'paper_comment_id': {
+ required: false,
+ key: 'paperCommentId'
+ },
+ 'submission_id': {
+ required: false,
+ key: 'submissionId'
+ },
+ 'journal_id': {
+ required: false,
+ key: 'journalId'
+ }
+ }
+
+ }
+
+ /*
+ * @return {string}
+ */
+ getPermissionsSelectionString() {
+ return `
+ permissions.id as "Permission_id",
+ permissions.entity as "Permission_entity",
+ permissions.action as "Permission_action",
+ permissions.user_id as "Permission_userId",
+ permissions.role_id as "Permission_roleId",
+ permissions.paper_id as "Permission_paperId",
+ permissions.paper_version_id as "Permission_paperVersionId",
+ permissions.event_id as "Permission_eventId",
+ permissions.review_id as "Permission_reviewId",
+ permissions.paper_comment_id as "Permission_paperCommentId",
+ permissions.submission_id as "Permission_submissionId",
+ permissions.journal_id as "Permission_journalId",
+ permissions.created_date as "Permission_createdDate",
+ permissions.updated_date as "Permission_updatedDate"
+ `
+ }
+
+ /**
+ * @return {any}
+ */
+ hydratePermission(row) {
+ return {
+ id: row.Permissions_id,
+ entity: row.Permission_entity,
+ action: row.Permission_action,
+ userId: row.Permission_userId,
+ roleId: row.Permission_roleId,
+ paperId: row.Permission_paperId,
+ paperVersionId: row.Permission_paperVersionId,
+ eventId: row.Permission_eventId,
+ reviewId: row.Permission_reviewId,
+ paperCommentId: row.Permission_paperCommentId,
+ submissionId: row.Permission_submissionId,
+ journalId: row.Permission_journalId,
+ createdDate: row.Permission_createdDate,
+ updatedDate: row.Permission_updatedDate
+ }
+ }
+
+ /**
+ * @return dictionary: { [id: string]: any, list: any[]}
+ */
+ hydratePermissions(rows) {
+ const dictionary = {}
+ const list = []
+
+ for(const row of rows) {
+ dictionary[row.Permission_id] = this.hydratePermission(row)
+ list.push(row.Permission_id)
+ }
+
+ return { dictionary: dictionary, list: list }
+ }
+
+ /**
+ * @param {string} where
+ * @param {any[]} params
+ *
+ * @return {Promise}
+ */
+ async selectPermissions(where, params) {
+ where = where ? `WHERE ${where}` : ''
+ params = params ? params : []
+
+ const sql = `
+ SELECT
+ ${this.getPermissionsSelectionString()}
+ FROM permissions
+ LEFT OUTER JOIN user_roles ON user_roles.role_id = permissions.role_id
+ ${where}
+ `
+
+ const results = await this.core.database.query(sql, params)
+ return this.hydratePermissions(results.rows)
+ }
+
+ /**
+ * @return {Promise}
+ */
+ async insertPermissions(permissions) {
+ await this.insert('Permission', 'permissions', this.fieldMap, permissions)
+ }
+}
diff --git a/packages/backend/daos/RoleDAO.js b/packages/backend/daos/RoleDAO.js
new file mode 100644
index 00000000..e11c3d0a
--- /dev/null
+++ b/packages/backend/daos/RoleDAO.js
@@ -0,0 +1,148 @@
+/******************************************************************************
+ *
+ * JournalHub -- Universal Scholarly Publishing
+ * Copyright (C) 2022 - 2024 Daniel Bingham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ ******************************************************************************/
+
+import { DAO } from './DAO'
+
+export class RoleDAO extends DAO {
+ constructor(core) {
+ super(core)
+
+ this.fieldMap = {
+ 'roles': {
+ 'id': {
+ required: false,
+ key: 'id'
+ },
+ 'name': {
+ required: true,
+ key: 'name'
+ },
+ 'description': {
+ required: true,
+ key: 'description'
+ },
+ 'journal_id': {
+ required: false,
+ key: 'journalId'
+ },
+ 'paper_id': {
+ required: false,
+ key: 'paperId'
+ }
+ },
+ 'user_roles': {
+ 'user_id': {
+ required: true,
+ key: 'userId'
+ },
+ 'role_id': {
+ required: true,
+ key: 'roleId'
+ }
+ }
+ }
+ }
+
+ /**
+ * @return string
+ */
+ getRoleSelectionString() {
+ return `
+ roles.id as "Role_id",
+ roles.name as "Role_name",
+ roles.description as "Role_description",
+ roles.journal_id as "Role_journalId",
+ roles.paper_id as "Role_paperId",
+ roles.created_date as "Role_createdDate",
+ roles.updated_date as "Role_updatedDate"
+ `
+ }
+
+ /**
+ * @return any
+ */
+ hydrateRole(row) {
+ return {
+ id: row.Role_id,
+ name: row.Role_name,
+ description: row.Role_description,
+ journalId: row.Role_journalId,
+ paperId: row.Role_paperId,
+ createdDate: row.Role_createdDate,
+ updatedDate: row.Role_updatedDate
+ }
+ }
+
+ /**
+ * @return { dictionary: { [id: string]: any }, list: number[] }
+ */
+ hydrateRoles(rows) {
+ const dictionary = {}
+ const list = []
+
+ for(const row of rows) {
+ dictionary[row.Role_id] = this.hydrateRole(row)
+ list.push(row.Role_id)
+ }
+
+ return { dictionary: dictionary, list: list }
+ }
+
+
+ /**
+ * @return Promise<{ dictionary: [id: string]: any, list: number[] }>
+ */
+ async selectRoles(where, params) {
+ where = where ? `WHERE ${where}` : ''
+ params = params ? params : []
+
+ const results = await this.core.database.query(`
+ SELECT
+ ${this.getRoleSelectionString()}
+ FROM roles
+ ${where}
+ `, params)
+
+ return this.hydrateRoles(results.rows)
+ }
+
+ /**
+ * @return Promise
+ */
+ async getRole(id) {
+ const results = await this.selectRoles(`roles.id = $1`, [ id ])
+ return results.dictionary[id]
+ }
+
+
+ /**
+ * @return Promise
+ */
+ async insertRoles(roles) {
+ await this.insert('Role', 'roles', this.fieldMap['roles'], roles)
+ }
+
+ /**
+ * @return Promise
+ **/
+ async insertUserRoles(userRoles) {
+ await this.insert('UserRole', 'user_roles', this.fieldMap['userRoles'], userRoles)
+ }
+}
diff --git a/packages/backend/errors/ServiceError.js b/packages/backend/errors/ServiceError.js
index 89937d3b..4f6e2573 100644
--- a/packages/backend/errors/ServiceError.js
+++ b/packages/backend/errors/ServiceError.js
@@ -1,3 +1,23 @@
+/******************************************************************************
+ *
+ * JournalHub -- Universal Scholarly Publishing
+ * Copyright (C) 2022 - 2024 Daniel Bingham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ ******************************************************************************/
+
module.exports = class ServiceError extends Error {
constructor(type, message) {
super(message)
diff --git a/packages/backend/index.js b/packages/backend/index.js
index b96d848f..0ed1f0b0 100644
--- a/packages/backend/index.js
+++ b/packages/backend/index.js
@@ -30,8 +30,10 @@ exports.OpenAlexService = require('./services/OpenAlexService')
exports.PageMetadataService = require('./services/PageMetadataService')
exports.PaperEventService = require('./services/PaperEventService')
exports.PaperService = require('./services/PaperService')
+exports.PermissionService = require('./services/PermissionsService')
exports.ReputationGenerationService = require('./services/ReputationGenerationService')
exports.ReputationPermissionService = require('./services/ReputationPermissionService')
+exports.RoleService = require('./services/RoleService')
exports.S3FileService = require('./services/S3FileService')
exports.ServerSideRenderingService = require('./services/ServerSideRenderingService')
exports.SessionService = require('./services/SessionService')
diff --git a/packages/backend/services/PermissionService.js b/packages/backend/services/PermissionService.js
new file mode 100644
index 00000000..296f28d5
--- /dev/null
+++ b/packages/backend/services/PermissionService.js
@@ -0,0 +1,148 @@
+/******************************************************************************
+ *
+ * JournalHub -- Universal Scholarly Publishing
+ * Copyright (C) 2022 - 2024 Daniel Bingham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ ******************************************************************************/
+
+const PermissionDAO = requre('../daos/PermissionDAO')
+
+const ServiceError = require('../errors/ServiceError')
+
+module.exports = class PermissionService {
+ constructor(core) {
+ this.core = core
+
+ this.permissionDAO = new PermissionDAO(this.core)
+
+ this.publicRoleId = null
+ }
+
+ /**
+ * @return {Promise}
+ */
+ async getPublicRoleId() {
+ if ( this.publicRoleId !== null ) {
+ return this.publicRoleId
+ }
+
+ const results = await this.core.database.query(
+ `SELECT id FROM roles WHERE name='public'`,
+ []
+ )
+
+ if ( results.rows.length <= 0 ) {
+ throw new ServiceError('missing-public', 'Failed to find the public role id!')
+ }
+
+ this.publicRoleId = results.rows[0].id
+ return this.publicRoleId
+ }
+
+ addContextSQL(query, context) {
+ const contextMap = {
+ paperId: 'paper_id',
+ paperVersionId: 'paper_version_id',
+ eventId: 'event_id',
+ reviewId: 'review_id',
+ paperCommentId: 'paper_comment_id',
+ submissionId: 'submission_id',
+ journalId: 'journal_id'
+ }
+ for(const [key, value] of Object.entries(context)) {
+ if ( ! ( key in contextMap ) ) {
+ throw new ServiceError('invalid-context',
+ `Invalid context '${key}'.`)
+ }
+
+ query.where += ` AND ${contextMap[key]} = ${query.params.length+1}`
+ query.params.push(value)
+ }
+ return query
+ }
+
+ /**
+ * Can `user` perform `action` on `entity` identified by `context.
+ *
+ * @returns {Promise} True if the `user` can perform `action` on `entity`
+ * identified by `context`, false otherwise.
+ */
+ async can(user, action, entity, context) {
+ const query = {
+ where: 'permissions.entity = $1 and permissions.action = $2',
+ params: [ entity, action ]
+ }
+
+ const publicRoleId = await this.getPublicRoleId()
+
+ if ( user ) {
+ query.where += ` AND
+ ( permissions.user_id = $${query.params.length+1}
+ OR user_roles.user_id = $${query.params.length+1}
+ OR permissions.role_id = $${query.params.length+2}
+ )`
+ query.params.push(user.id)
+ query.params.push(publicRoleId)
+ } else {
+ query.where += ` AND permissions.role_id = $${query.params.length+1}`
+ query.params.push(publicRoleId)
+ }
+
+ this.addContextSQL(query, context)
+
+ const results = await this.permissionDAO.selectPermissions(query.where, query.params)
+
+ return results.list.length > 0
+ }
+
+ /**
+ * @returns {Promise}
+ */
+ async get(user, entity, action, context) {
+ const query = {
+ where: '',
+ params: []
+ }
+
+ const publicRoleId = await this.getPublicRoleId()
+
+ if ( user ) {
+ query.where += '(permissions.user_id = $1 OR user_roles.user_id = $1 OR permissions.role_id = $2)'
+ query.params.push(user.id, publicRoleId)
+ } else {
+ query.where += 'permissions.role_id = $1'
+ query.params.push(publicRoleId)
+ }
+
+ if ( entity && entity !== '*' ) {
+ query.params.push(entity)
+ query.where += ` AND permissions.entity = $${query.params.length}`
+ }
+ if ( action && action !== '*' ) {
+ query.params.push(action)
+ query.where += ` AND permissions.action = $${query.params.length}`
+ }
+
+ this.addContextSQL(query, context)
+
+ const results = await this.permissionDAO.selectPermissions(query.where, query.params)
+ return results.list.map((id) => results.dictionary[id])
+ }
+
+ async grant(permissions) {
+ await this.insertPermissions(permissions)
+ }
+}
diff --git a/packages/backend/services/RoleService.js b/packages/backend/services/RoleService.js
new file mode 100644
index 00000000..e419c60c
--- /dev/null
+++ b/packages/backend/services/RoleService.js
@@ -0,0 +1,125 @@
+/******************************************************************************
+ *
+ * JournalHub -- Universal Scholarly Publishing
+ * Copyright (C) 2022 - 2024 Daniel Bingham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ ******************************************************************************/
+
+import { Uuid } from 'uuid'
+
+import ServiceError from '../errors/ServiceError'
+
+import { RoleDAO } from '../daos/RoleDAO'
+
+import { PermissionService } from './PermissionService'
+
+export class RoleService {
+
+ constructor(core) {
+ this.core = core
+
+ this.roleDAO = new RoleDAO(core)
+
+ this.permissionService = new PermissionService(core)
+ }
+
+ /**
+ * Grant a role to a user.
+ *
+ * @param {string} role The role name.
+ * @param {number} userId The id of the user.
+ * @param {Object} context The required context for the role being granted.
+ *
+ * @return {Promise}
+ */
+ async grant(role, userId, context) {
+ const sql = `SELECT id FROM roles WHERE name = $1`
+ const params = [ role ]
+
+ if ( context.paperId ) {
+ sql += ` AND paper_id = $2`
+ params.push(context.paperId)
+ } else if ( context.journalId ) {
+ sql += ` AND journal_id = $2`
+ params.push(context.journalId)
+ } else {
+ throw new ServiceError('missing-context',
+ `Roles may only be granted on papers or journals.`)
+ }
+
+ const roleResults = await this.database.query(sql, params)
+
+ if ( roleResults.rows.length <= 0 ) {
+ throw new ServiceError('missing-role',
+ `No Role named ${role} exists for context.`)
+ } else if ( roleResults.rows.length > 1 ) {
+ throw new ServiceError('invalid-state',
+ `Multiple Roles named ${role} exist for context!`)
+ }
+
+ const id = roleResults.rows[0].id
+
+ await this.roleDAO.insertUserRoles({ roleId: id, userId: userId })
+
+ return true
+ }
+
+ /**
+ * Create the initial roles for a paper and grant the initial permissions for those roles.
+ *
+ * @param {number} paperId
+ *
+ * @return {Promise}
+ */
+ async createPaperRoles(paperId) {
+ const correspondingAuthorId = Uuid.v4()
+ const authorId = Uuid.v4()
+
+ await this.roleDAO.insertRoles([
+ {
+ id: correspondingAuthorId,
+ name: 'Corresponding Author',
+ description: `One of this paper's corresponding authors.`,
+ paperId: paperId
+ },
+ {
+ id: authorId,
+ name: 'Author',
+ description: `One of this paper's authors.`,
+ paperId: paperId
+ }
+ ])
+
+ await this.permissionService.grant([
+ { entity:'Paper', action:'update', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'Paper', action:'read', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'Paper', action:'delete', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'Paper', action:'grant', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'PaperVersion', action:'create', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'PaperVersion', action:'read', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'PaperVersion', action:'update', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'PaperVersion', action:'delete', roleId:correspondingAuthorId, paperId:paperId },
+ { entity:'PaperVersion', action:'grant', roleId:correspondingAuthorId, paperId:paperId }
+ ])
+
+ await this.permissionService.grant([
+ { entity:'Paper', action:'read', roleId:authorId, paperId:paperId },
+ { entity:'PaperVersion', action:'create', roleId:authorId, paperId:paperId },
+ { entity:'PaperVersion', action:'read', roleId:authorId, paperId:paperId }
+ ])
+ }
+}
+
diff --git a/packages/backend/services/SubmissionService.js b/packages/backend/services/SubmissionService.js
index f27cf281..514e511c 100644
--- a/packages/backend/services/SubmissionService.js
+++ b/packages/backend/services/SubmissionService.js
@@ -61,6 +61,12 @@ module.exports = class SubmissionService {
this.logger = core.logger
}
+ /**
+ * @param {Object} user
+ * @param {number} paperId
+ *
+ * @return {Promise