From cf94319f302ed10d3dcbc0acfc5486eb3a0ec773 Mon Sep 17 00:00:00 2001 From: tri10 Date: Fri, 10 Dec 2021 11:06:38 +0530 Subject: [PATCH 01/88] fix(BB-528); index author in ES --- .../common/entity-search-field-option.js | 1 + src/client/entity-editor/common/entity.tsx | 6 ++-- .../entity-editor/common/linked-entity.tsx | 5 +-- src/common/helpers/search.js | 33 ++++++++++++++++--- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/client/entity-editor/common/entity-search-field-option.js b/src/client/entity-editor/common/entity-search-field-option.js index d4bc0956c5..f1a6f6d735 100644 --- a/src/client/entity-editor/common/entity-search-field-option.js +++ b/src/client/entity-editor/common/entity-search-field-option.js @@ -70,6 +70,7 @@ class EntitySearchFieldOption extends React.Component { const languageId = _.get(entity, ['defaultAlias', 'languageId']); const language = this.props.languageOptions.find(index => index.value === languageId); return { + author: entity.author ?? null, disambiguation: _.get(entity, ['disambiguation', 'comment']), id, language: language && language.label, diff --git a/src/client/entity-editor/common/entity.tsx b/src/client/entity-editor/common/entity.tsx index 34b6f2f6f4..9b1e44c761 100644 --- a/src/client/entity-editor/common/entity.tsx +++ b/src/client/entity-editor/common/entity.tsx @@ -22,6 +22,7 @@ import {genEntityIconHTMLElement} from '../../helpers/entity'; type EntityProps = { + author: string | null, disambiguation?: string | null | undefined, language?: string, link?: string | false, @@ -31,7 +32,7 @@ type EntityProps = { }; function Entity( - {disambiguation, language, link, text, type, unnamedText}: EntityProps + {disambiguation, language, link, text, type, unnamedText, author}: EntityProps ) { const nameComponent = text || {unnamedText}; const contents = ( @@ -41,7 +42,8 @@ function Entity( } {nameComponent} { - disambiguation && + author ? ({author}) : + disambiguation && ({disambiguation}) } { diff --git a/src/client/entity-editor/common/linked-entity.tsx b/src/client/entity-editor/common/linked-entity.tsx index 27685e2f99..85b894ce8c 100644 --- a/src/client/entity-editor/common/linked-entity.tsx +++ b/src/client/entity-editor/common/linked-entity.tsx @@ -78,7 +78,7 @@ class LinkedEntity extends React.Component { render() { const option = this.getSafeOptionValue(this.props.option); - const {disambiguation, text, type, unnamedText, language} = option; + const {disambiguation, text, type, unnamedText, language, author} = option; const nameComponent = text || {unnamedText}; @@ -91,7 +91,8 @@ class LinkedEntity extends React.Component { {nameComponent}    { - disambiguation && + author ? ({author}) : + disambiguation && ({disambiguation}) } {' '} diff --git a/src/common/helpers/search.js b/src/common/helpers/search.js index 7aaab48f3e..cfaf83b8e5 100644 --- a/src/common/helpers/search.js +++ b/src/common/helpers/search.js @@ -33,9 +33,15 @@ const _maxJitter = 75; let _client = null; +async function getEntityWithAlias(orm, bbid, type) { + const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, bbid, null); + const model = commonUtils.getEntityModelByType(orm, type); + return model.forge({bbid: redirectBbid}) + .fetch({require: false, withRelated: ['defaultAlias']}); +} + async function _fetchEntityModelsForESResults(orm, results) { const {Area, Editor, UserCollection} = orm; - if (!results.hits) { return null; } @@ -88,7 +94,7 @@ async function _fetchEntityModelsForESResults(orm, results) { const entity = await model.forge({bbid: entityStub.bbid}) .fetch({require: false, withRelated: ['defaultAlias.language', 'disambiguation', 'aliasSet.aliases', 'identifierSet.identifiers']}); - return entity?.toJSON(); + return {...entity?.toJSON(), author: entityStub.author ?? null}; })).catch(err => log.error(err)); return processedResults; } @@ -269,6 +275,10 @@ export async function generateIndex(orm) { }, type: 'object' }, + author: { + analyzer: 'trigrams', + type: 'text' + }, 'disambiguation.comment': { analyzer: 'trigrams', type: 'text' @@ -357,7 +367,7 @@ export async function generateIndex(orm) { {model: EditionGroup, relations: ['editionGroupType']}, {model: Publisher, relations: ['publisherType', 'area']}, {model: Series, relations: ['seriesOrderingType']}, - {model: Work, relations: ['workType']} + {model: Work, relations: ['workType', 'relationshipSet.relationships.type']} ]; // Update the indexed entries for each entity type @@ -374,8 +384,22 @@ export async function generateIndex(orm) { const entityLists = await Promise.all(behaviorPromise); const listIndexes = []; - for (const entityList of entityLists) { + for (const [index, entityList] of entityLists.entries()) { const listArray = entityList.toJSON(); + if (index === (entityLists.length - 1)) { + for (const workEntity of listArray) { + if (workEntity.relationshipSetId) { + for (const relationship of workEntity.relationshipSet.relationships) { + if (relationship.typeId === 8) { + // eslint-disable-next-line no-await-in-loop + const source = await getEntityWithAlias(orm, relationship.sourceBbid, 'Author'); + workEntity.author = source ? source.toJSON().defaultAlias.name : null; + break; + } + } + } + } + } listIndexes.push(_processEntityListForBulk(listArray)); } await Promise.all(listIndexes); @@ -528,7 +552,6 @@ export async function init(orm, options) { if (mainIndexExists) { return null; } - return generateIndex(orm); } catch (error) { From ea648cb3e92defa28f003f7a5a255fbc0bc378c2 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 23 Jun 2022 19:03:20 +0200 Subject: [PATCH 02/88] feat(search): Refactor work author indexing Use the Collection of Author models that we already fetched instead of fetching the Author entities again from the DB Also support multiple authors --- .../common/entity-search-field-option.js | 2 +- src/client/entity-editor/common/entity.tsx | 6 +-- .../entity-editor/common/linked-entity.tsx | 16 +++--- src/common/helpers/search.js | 52 ++++++++++--------- 4 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/client/entity-editor/common/entity-search-field-option.js b/src/client/entity-editor/common/entity-search-field-option.js index 15c8136f5d..3c5698ac87 100644 --- a/src/client/entity-editor/common/entity-search-field-option.js +++ b/src/client/entity-editor/common/entity-search-field-option.js @@ -75,7 +75,7 @@ class EntitySearchFieldOption extends React.Component { (index) => index.value === languageId ); return { - author: entity.author ?? null, + authors: entity.authors?.join(', ') ?? null, disambiguation: _.get(entity, ['disambiguation', 'comment']), id, language: language && language.label, diff --git a/src/client/entity-editor/common/entity.tsx b/src/client/entity-editor/common/entity.tsx index 9b1e44c761..8e3bc7e25c 100644 --- a/src/client/entity-editor/common/entity.tsx +++ b/src/client/entity-editor/common/entity.tsx @@ -22,7 +22,7 @@ import {genEntityIconHTMLElement} from '../../helpers/entity'; type EntityProps = { - author: string | null, + authors: string | null, disambiguation?: string | null | undefined, language?: string, link?: string | false, @@ -32,7 +32,7 @@ type EntityProps = { }; function Entity( - {disambiguation, language, link, text, type, unnamedText, author}: EntityProps + {disambiguation, language, link, text, type, unnamedText, authors}: EntityProps ) { const nameComponent = text || {unnamedText}; const contents = ( @@ -42,7 +42,7 @@ function Entity( } {nameComponent} { - author ? ({author}) : + authors ? ({authors}) : disambiguation && ({disambiguation}) } diff --git a/src/client/entity-editor/common/linked-entity.tsx b/src/client/entity-editor/common/linked-entity.tsx index ee91541f26..2dedbb9b31 100644 --- a/src/client/entity-editor/common/linked-entity.tsx +++ b/src/client/entity-editor/common/linked-entity.tsx @@ -80,7 +80,7 @@ class LinkedEntity extends React.Component { render() { const option = this.getSafeOptionValue(this.props.data); - const {disambiguation, text, type, unnamedText, language, author} = option; + const {disambiguation, text, type, unnamedText, language, authors} = option; const nameComponent = text || {unnamedText}; return (
{ }   {nameComponent} -    - { - author ? ({author}) : - disambiguation && - ({disambiguation}) + {disambiguation && + +  ({disambiguation}) + + } + {authors && + +  — ({authors}) + } {' '} diff --git a/src/common/helpers/search.js b/src/common/helpers/search.js index 796b6bcfb3..435f93926f 100644 --- a/src/common/helpers/search.js +++ b/src/common/helpers/search.js @@ -34,13 +34,6 @@ const _maxJitter = 75; let _client = null; -async function getEntityWithAlias(orm, bbid, type) { - const redirectBbid = await orm.func.entity.recursivelyGetRedirectBBID(orm, bbid, null); - const model = commonUtils.getEntityModelByType(orm, type); - return model.forge({bbid: redirectBbid}) - .fetch({require: false, withRelated: ['defaultAlias']}); -} - async function _fetchEntityModelsForESResults(orm, results) { const {Area, Editor, UserCollection} = orm; if (!results.hits) { @@ -95,7 +88,7 @@ async function _fetchEntityModelsForESResults(orm, results) { const entity = await model.forge({bbid: entityStub.bbid}) .fetch({require: false, withRelated: ['defaultAlias.language', 'disambiguation', 'aliasSet.aliases', 'identifierSet.identifiers']}); - return {...entity?.toJSON(), author: entityStub.author ?? null}; + return {...entity?.toJSON(), authors: entityStub.authors ?? null}; })).catch(err => log.error(err)); return processedResults; } @@ -275,7 +268,7 @@ export async function generateIndex(orm) { }, type: 'object' }, - author: { + authors: { analyzer: 'trigrams', type: 'text' }, @@ -382,24 +375,35 @@ export async function generateIndex(orm) { }) ); const entityLists = await Promise.all(behaviorPromise); - + /* eslint-disable @typescript-eslint/no-unused-vars */ + const [authorsCollection, + editionCollection, + editionGroupCollection, + publisherCollection, + seriesCollection, + workCollection] = entityLists; + /* eslint-enable @typescript-eslint/no-unused-vars */ const listIndexes = []; - for (const [index, entityList] of entityLists.entries()) { - const listArray = entityList.toJSON(); - if (index === (entityLists.length - 1)) { - for (const workEntity of listArray) { - if (workEntity.relationshipSetId) { - for (const relationship of workEntity.relationshipSet.relationships) { - if (relationship.typeId === 8) { - // eslint-disable-next-line no-await-in-loop - const source = await getEntityWithAlias(orm, relationship.sourceBbid, 'Author'); - workEntity.author = source ? source.toJSON().defaultAlias.name : null; - break; - } - } + + workCollection.forEach(workEntity => { + const relationshipSet = workEntity.related('relationshipSet'); + if (relationshipSet) { + const authorWroteWorkRels = relationshipSet.related('relationships')?.filter(relationshipModel => relationshipModel.get('typeId') === 8); + const authorNames = authorWroteWorkRels.map(relationshipModel => { + // Search for the Author in the already fetched BookshelfJS Collection + const source = authorsCollection.get(relationshipModel.get('sourceBbid')); + if (!source) { + // This shouldn't happen, but just in case + return null; } - } + return source.toJSON().defaultAlias.name; + }); + workEntity.set('authors', authorNames); } + }); + // Index all the entities + for (const entityList of entityLists) { + const listArray = entityList.toJSON(); listIndexes.push(_processEntityListForBulk(listArray)); } await Promise.all(listIndexes); From a2cd2918da233954b16725948ca16e1af1a63561 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Mon, 29 May 2023 16:37:52 +0530 Subject: [PATCH 03/88] SQL: Schema Changes for Administration System --- sql/migrations/2023-05-29-admin_logs/down.sql | 6 +++++ sql/migrations/2023-05-29-admin_logs/up.sql | 25 +++++++++++++++++++ .../2023-05-29-user_privileges/up.sql | 5 ++++ sql/schemas/bookbrainz.sql | 20 +++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 sql/migrations/2023-05-29-admin_logs/down.sql create mode 100644 sql/migrations/2023-05-29-admin_logs/up.sql create mode 100644 sql/migrations/2023-05-29-user_privileges/up.sql diff --git a/sql/migrations/2023-05-29-admin_logs/down.sql b/sql/migrations/2023-05-29-admin_logs/down.sql new file mode 100644 index 0000000000..0af4514026 --- /dev/null +++ b/sql/migrations/2023-05-29-admin_logs/down.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +DROP TABLE IF EXISTS bookbrainz.admin_log; +DROP TYPE IF EXISTS bookbrainz.admin_action_type; + +COMMIT; \ No newline at end of file diff --git a/sql/migrations/2023-05-29-admin_logs/up.sql b/sql/migrations/2023-05-29-admin_logs/up.sql new file mode 100644 index 0000000000..f91d5ac401 --- /dev/null +++ b/sql/migrations/2023-05-29-admin_logs/up.sql @@ -0,0 +1,25 @@ +BEGIN TRANSACTION; + +CREATE TYPE bookbrainz.admin_action_type AS ENUM ( + 'Change Privileges' +); + +COMMIT; + +BEGIN TRANSACTION; + +CREATE TABLE bookbrainz.admin_log ( + id SERIAL PRIMARY KEY, + user1_id INT NOT NULL, + user2_id INT NOT NULL, + old_privs INT, + new_privs INT, + action_type bookbrainz.admin_action_type NOT NULL, + time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC'::TEXT, now()), + note VARCHAR NOT NULL +); + +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user1_id) REFERENCES bookbrainz.editor(id); +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user2_id) REFERENCES bookbrainz.editor(id); + +COMMIT; \ No newline at end of file diff --git a/sql/migrations/2023-05-29-user_privileges/up.sql b/sql/migrations/2023-05-29-user_privileges/up.sql new file mode 100644 index 0000000000..95982a4a2b --- /dev/null +++ b/sql/migrations/2023-05-29-user_privileges/up.sql @@ -0,0 +1,5 @@ +BEGIN TRANSACTION; + +ALTER TABLE bookbrainz.editor ADD COLUMN privs INT NOT NULL DEFAULT 1; + +COMMIT; \ No newline at end of file diff --git a/sql/schemas/bookbrainz.sql b/sql/schemas/bookbrainz.sql index e0fdbe4755..26b89cd504 100644 --- a/sql/schemas/bookbrainz.sql +++ b/sql/schemas/bookbrainz.sql @@ -20,6 +20,10 @@ CREATE TYPE bookbrainz.external_service_oauth_type AS ENUM ( 'critiquebrainz' ); +CREATE TYPE bookbrainz.admin_action_type AS ENUM ( + 'Change Privileges' +); + CREATE TABLE bookbrainz.editor_type ( id SERIAL PRIMARY KEY, label VARCHAR(255) NOT NULL CHECK (label <> '') @@ -46,6 +50,8 @@ ALTER TABLE bookbrainz.editor ADD FOREIGN KEY (gender_id) REFERENCES musicbrainz ALTER TABLE bookbrainz.editor ADD FOREIGN KEY (type_id) REFERENCES bookbrainz.editor_type (id); ALTER TABLE bookbrainz.editor ADD FOREIGN KEY (area_id) REFERENCES musicbrainz.area (id) DEFERRABLE; +ALTER TABLE bookbrainz.editor ADD COLUMN privs INT NOT NULL DEFAULT 1; + CREATE TABLE bookbrainz.editor__language ( editor_id INT, language_id INT, @@ -56,6 +62,20 @@ CREATE TABLE bookbrainz.editor__language ( ) ); +CREATE TABLE bookbrainz.admin_log ( + id SERIAL PRIMARY KEY, + user1_id INT NOT NULL, + user2_id INT NOT NULL, + old_privs INT, + new_privs INT, + action_type bookbrainz.admin_action_type NOT NULL, + time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC'::TEXT, now()), + note VARCHAR NOT NULL +); + +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user1_id) REFERENCES bookbrainz.editor (id); +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user2_id) REFERENCES bookbrainz.editor (id); + CREATE TABLE bookbrainz.entity ( bbid UUID PRIMARY KEY DEFAULT public.uuid_generate_v4(), type bookbrainz.entity_type NOT NULL From 7ba5dd0cb9635e74535d226fbdebf8e3131313c1 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 6 Jun 2023 15:51:50 +0530 Subject: [PATCH 04/88] Change column names --- sql/migrations/2023-05-29-admin_logs/up.sql | 8 ++++---- sql/schemas/bookbrainz.sql | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/sql/migrations/2023-05-29-admin_logs/up.sql b/sql/migrations/2023-05-29-admin_logs/up.sql index f91d5ac401..eff0fd4130 100644 --- a/sql/migrations/2023-05-29-admin_logs/up.sql +++ b/sql/migrations/2023-05-29-admin_logs/up.sql @@ -10,8 +10,8 @@ BEGIN TRANSACTION; CREATE TABLE bookbrainz.admin_log ( id SERIAL PRIMARY KEY, - user1_id INT NOT NULL, - user2_id INT NOT NULL, + admin_id INT NOT NULL, + target_user_id INT NOT NULL, old_privs INT, new_privs INT, action_type bookbrainz.admin_action_type NOT NULL, @@ -19,7 +19,7 @@ CREATE TABLE bookbrainz.admin_log ( note VARCHAR NOT NULL ); -ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user1_id) REFERENCES bookbrainz.editor(id); -ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user2_id) REFERENCES bookbrainz.editor(id); +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (admin_id) REFERENCES bookbrainz.editor(id); +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (target_user_id) REFERENCES bookbrainz.editor(id); COMMIT; \ No newline at end of file diff --git a/sql/schemas/bookbrainz.sql b/sql/schemas/bookbrainz.sql index 26b89cd504..e23f050abf 100644 --- a/sql/schemas/bookbrainz.sql +++ b/sql/schemas/bookbrainz.sql @@ -41,6 +41,7 @@ CREATE TABLE bookbrainz.editor ( type_id INT NOT NULL, gender_id INT, area_id INT, + privs INT NOT NULL DEFAULT 1, revisions_applied INT NOT NULL DEFAULT 0 CHECK (revisions_applied >= 0), revisions_reverted INT NOT NULL DEFAULT 0 CHECK (revisions_reverted >= 0), total_revisions INT NOT NULL DEFAULT 0 CHECK (total_revisions >= 0), @@ -50,8 +51,6 @@ ALTER TABLE bookbrainz.editor ADD FOREIGN KEY (gender_id) REFERENCES musicbrainz ALTER TABLE bookbrainz.editor ADD FOREIGN KEY (type_id) REFERENCES bookbrainz.editor_type (id); ALTER TABLE bookbrainz.editor ADD FOREIGN KEY (area_id) REFERENCES musicbrainz.area (id) DEFERRABLE; -ALTER TABLE bookbrainz.editor ADD COLUMN privs INT NOT NULL DEFAULT 1; - CREATE TABLE bookbrainz.editor__language ( editor_id INT, language_id INT, @@ -64,8 +63,8 @@ CREATE TABLE bookbrainz.editor__language ( CREATE TABLE bookbrainz.admin_log ( id SERIAL PRIMARY KEY, - user1_id INT NOT NULL, - user2_id INT NOT NULL, + admin_id INT NOT NULL, + target_user_id INT NOT NULL, old_privs INT, new_privs INT, action_type bookbrainz.admin_action_type NOT NULL, @@ -73,8 +72,8 @@ CREATE TABLE bookbrainz.admin_log ( note VARCHAR NOT NULL ); -ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user1_id) REFERENCES bookbrainz.editor (id); -ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (user2_id) REFERENCES bookbrainz.editor (id); +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (admin_id) REFERENCES bookbrainz.editor (id); +ALTER TABLE bookbrainz.admin_log ADD FOREIGN KEY (target_user_id) REFERENCES bookbrainz.editor (id); CREATE TABLE bookbrainz.entity ( bbid UUID PRIMARY KEY DEFAULT public.uuid_generate_v4(), From e9309b102b0e3e45d55a02fc094a38b1ee5102a6 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 13 Jun 2023 17:36:06 +0530 Subject: [PATCH 05/88] feat(Admin System): Edit privileges --- .../components/pages/admin-panel-search.tsx | 166 ++++++++++++++++++ .../pages/parts/admin-panel-search-field.tsx | 128 ++++++++++++++ .../parts/admin-panel-search-results.tsx | 157 +++++++++++++++++ .../pages/parts/privilege-badges.js | 46 +++++ .../pages/parts/privs-edit-modal.js | 149 ++++++++++++++++ src/client/containers/layout.js | 23 ++- src/client/controllers/admin-panel.js | 51 ++++++ src/common/helpers/privileges-utils.ts | 82 +++++++++ src/server/routes.js | 2 + src/server/routes/adminPanel.js | 107 +++++++++++ src/server/routes/editor.js | 20 +++ .../icons/shield-check-orange-filled.svg | 21 +++ static/images/icons/shield-grey-center.svg | 18 ++ static/images/icons/shield-orange-center.svg | 18 ++ static/images/icons/shield-white-center.svg | 18 ++ webpack.client.js | 1 + 16 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 src/client/components/pages/admin-panel-search.tsx create mode 100644 src/client/components/pages/parts/admin-panel-search-field.tsx create mode 100644 src/client/components/pages/parts/admin-panel-search-results.tsx create mode 100644 src/client/components/pages/parts/privilege-badges.js create mode 100644 src/client/components/pages/parts/privs-edit-modal.js create mode 100644 src/client/controllers/admin-panel.js create mode 100644 src/common/helpers/privileges-utils.ts create mode 100644 src/server/routes/adminPanel.js create mode 100644 static/images/icons/shield-check-orange-filled.svg create mode 100644 static/images/icons/shield-grey-center.svg create mode 100644 static/images/icons/shield-orange-center.svg create mode 100644 static/images/icons/shield-white-center.svg diff --git a/src/client/components/pages/admin-panel-search.tsx b/src/client/components/pages/admin-panel-search.tsx new file mode 100644 index 0000000000..5634b8005a --- /dev/null +++ b/src/client/components/pages/admin-panel-search.tsx @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as React from 'react'; +import AdminPanelSearchField from './parts/admin-panel-search-field'; +import AdminPanelSearchResults from './parts/admin-panel-search-results'; +import {Card} from 'react-bootstrap'; +import PagerElement from './parts/pager'; +import PropTypes from 'prop-types'; + +type Props = { + from?: number, + initialResults?: any[], + nextEnabled: boolean, + query?: string, + resultsPerPage?: number, + user: Record +}; + +type State = { + query: string | null | undefined; + results: any[]; +}; + +class AdminPanelSearchPage extends React.Component { + static displayName = 'AdminPanelSearchPage'; + + static propTypes = { + from: PropTypes.number, + initialResults: PropTypes.array, + nextEnabled: PropTypes.bool.isRequired, + query: PropTypes.string, + resultsPerPage: PropTypes.number, + user: PropTypes.object.isRequired + }; + + static defaultProps = { + from: 0, + initialResults: [], + query: '', + resultsPerPage: 20 + }; + + /** + * Initializes component state to default values and binds class + * methods to proper context so that they can be directly invoked + * without explicit binding. + * + * @param {object} props - Properties object passed down from parents. + */ + constructor(props) { + super(props); + + this.state = { + query: props.query, + results: props.initialResults + }; + + this.paginationUrl = './admin-panel/search'; + } + + paginationUrl: string; + + /** + * Gets user text query from the browser's URL search parameters and + * sets it in the state to be passed down to AdminPanelSearchField and Pager components + * + * @param {string} query - Query string entered by user. + */ + handleSearch = (query: string) => { + this.setState({query}); + }; + + /** + * The Pager component deals with fetching the query from the server. + * We use this callback to set the results on this component's state. + * + * @param {array} newResults - The array of results from the query + */ + searchResultsCallback = (newResults: any[]) => { + this.setState({results: newResults}); + }; + + /** + * The Pager component is set up to react to browser history navigation (prev/next buttons), + * and we use this callback to set the query and type on this component's state. + * + * @param {URLSearchParams} searchParams - The URL search parameters passed up from the pager component + */ + searchParamsChangeCallback = (searchParams: URLSearchParams) => { + let query; + if (searchParams.has('q')) { + query = searchParams.get('q'); + } + if (query === this.state.query) { + return; + } + this.handleSearch(query); + }; + + /** + * Renders the component: Search bar with results table located vertically + * below it. + * + * @returns {object} - JSX to render. + */ + render() { + const {query, results} = this.state; + const querySearchParams = `q=${query}&type=editor`; + return ( + + + Admin Panel + + +
+ + + +
+ {results.length === 0 && query.length !== 0 && +
+
+

+ No results found +

+
} +
+
+
+
+ ); + } +} + +export default AdminPanelSearchPage; diff --git a/src/client/components/pages/parts/admin-panel-search-field.tsx b/src/client/components/pages/parts/admin-panel-search-field.tsx new file mode 100644 index 0000000000..972c11c6b5 --- /dev/null +++ b/src/client/components/pages/parts/admin-panel-search-field.tsx @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as React from 'react'; +import * as bootstrap from 'react-bootstrap'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import {faSearch} from '@fortawesome/free-solid-svg-icons'; + +const {Button, Col, InputGroup, Form, Row} = bootstrap; + +const SearchButton = ( + +); + +const updateDelay = 1000; + +type AdminPanelSearchFieldState = { + query: string +}; +type AdminPanelSearchFieldProps = { + onSearch: (query: string) => void, + query?: string +}; + +class AdminPanelSearchField extends React.Component { + static displayName = 'AdminPanelSearchField'; + + static propTypes = { + onSearch: PropTypes.func.isRequired, + query: PropTypes.string + }; + + static defaultProps = { + query: '' + }; + + constructor(props: AdminPanelSearchFieldProps) { + super(props); + + this.state = { + query: props.query || '' + }; + this.debouncedTriggerOnSearch = _.debounce(this.triggerOnSearch, updateDelay, {}); + } + + // If search term is changed outside this component (for example browser navigation), + // reflects those changes + componentDidUpdate(prevProps: AdminPanelSearchFieldProps) { + if (prevProps.query !== this.props.query) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({query: this.props.query}); + } + } + + debouncedTriggerOnSearch: () => void; + + triggerOnSearch() { + const {query} = this.state; + this.props.onSearch(query); + } + + handleSubmit = event => { + event.preventDefault(); + event.stopPropagation(); + this.triggerOnSearch(); + }; + + handleChange = event => { + if (!event.target.value.match(/^ +$/) && event.target.value !== this.state.query) { + this.setState({query: event.target.value}, this.debouncedTriggerOnSearch); + } + }; + + render() { + return ( + + +
+ + + + + {SearchButton} + + + +
+ +
+ ); + } +} + +export default AdminPanelSearchField; diff --git a/src/client/components/pages/parts/admin-panel-search-results.tsx b/src/client/components/pages/parts/admin-panel-search-results.tsx new file mode 100644 index 0000000000..37f4eeade5 --- /dev/null +++ b/src/client/components/pages/parts/admin-panel-search-results.tsx @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import PrivilegeBadges from './privilege-badges'; +import PrivsEditModal from './privs-edit-modal'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {faPencilAlt} from '@fortawesome/free-solid-svg-icons'; +import {genEntityIconHTMLElement} from '../../../helpers/entity'; +import {getPrivilegeShieldIcon} from '../../../../common/helpers/privileges-utils'; + +const {Badge, Button, Table} = bootstrap; + +type AdminPanelSearchResultsState = { + selectedUser?: Record, + showModal: boolean +}; +type AdminPanelSearchResultsProps = { + results?: any[], + user: Record +}; + + +/** + * Renders the document and displays the 'AdminPanelSearchResults' page. + * @returns {ReactElement} a HTML document which displays the AdminPanelSearchResults. + * @param {object} props - Properties passed to the component. + */ +class AdminPanelSearchResults extends React.Component { + static displayName = 'AdminPanelSearchResults'; + + static propTypes = { + results: PropTypes.array, + user: PropTypes.object.isRequired + }; + + static defaultProps = { + results: null + }; + + constructor(props) { + super(props); + + this.state = { + selectedUser: null, + showModal: false + }; + this.onCloseModal = this.onCloseModal.bind(this); + } + + onCloseModal() { + this.setState({showModal: false}); + } + + openPrivsEditModal(user) { + this.setState({ + selectedUser: user, + showModal: true + }); + } + + render() { + const noResults = !this.props.results || this.props.results.length === 0; + + const results = this.props.results.map((result) => { + if (!result) { + return null; + } + const name = result.defaultAlias ? result.defaultAlias.name : + '(unnamed)'; + const link = `/editor/${result.bbid}`; + + /* eslint-disable react/jsx-no-bind */ + return ( + + +
+ {genEntityIconHTMLElement('editor')} + {name} + + + + + + + + + + ); + }); + let tableCssClasses = 'table table-striped'; + if (noResults) { + return null; + } + return ( +
+ { + this.state.showModal && + ( +
+ +
+ ) + } +

+ Search Results +

+
+ + + + + + + + + {results} + +
NamePrivileges +
+
+ ); + } +} + +export default AdminPanelSearchResults; diff --git a/src/client/components/pages/parts/privilege-badges.js b/src/client/components/pages/parts/privilege-badges.js new file mode 100644 index 0000000000..3acc80a2db --- /dev/null +++ b/src/client/components/pages/parts/privilege-badges.js @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {getBadgeVariantFromTitle, getPrivilegeTitlesArray} from '../../../../common/helpers/privileges-utils'; +import {Badge} from 'react-bootstrap'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +function PrivilegeBadges({privs}) { + const privTitles = getPrivilegeTitlesArray(privs); + const privilegeListComp = privTitles.map(title => ( + + + {title} + + {' '} + + )); + return ( +
{privilegeListComp}
+ ); +} + +PrivilegeBadges.displayName = 'PrivilegeBadges'; +PrivilegeBadges.propTypes = { + privs: PropTypes.number.isRequired +}; + +export default PrivilegeBadges; diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js new file mode 100644 index 0000000000..41003040a0 --- /dev/null +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import {PrivilegeTypeBits, getPrivilegeShieldIcon, getPrivilegeTitleFromBit} from '../../../../common/helpers/privileges-utils'; +import {faPencilAlt, faTimes} from '@fortawesome/free-solid-svg-icons'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import PrivilegeBadges from './privilege-badges'; +import PropTypes from 'prop-types'; +import React from 'react'; + +const {Button, Form, Modal} = bootstrap; + +class PrivsEditModal extends React.Component { + constructor(props) { + super(props); + this.state = { + privs: props.targetUser.privs ? props.targetUser.privs : null, + submittable: false + }; + + this.handleBitChange = this.handleBitChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + async handleSubmit() { + const {privs} = this.state; + const oldPrivs = this.props.targetUser.privs; + if (privs === oldPrivs) { + return; + } + + const data = { + adminId: this.props.adminId, + newPrivs: privs, + oldPrivs, + targetUserId: this.props.targetUser.id + }; + + try { + const response = await fetch('/editor/privs/edit/handler', { + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + method: 'POST' + }); + if (!response.ok) { + const {error} = await response.json(); + throw new Error(error ?? response.statusText); + } + window.location.reload(); + } + catch (err) { + throw new Error(err); + } + } + + /* eslint-disable no-bitwise */ + handleBitChange(bit) { + const newPrivs = this.state.privs ^ (1 << bit); + if (this.props.targetUser.privs !== newPrivs) { + this.setState({ + privs: newPrivs, + submittable: true + }); + } + else { + this.setState({ + privs: newPrivs, + submittable: false + }); + } + } + + /* eslint-disable react/jsx-no-bind */ + render() { + const link = `/editor/${this.props.targetUser.bbid}`; + + const switches = Object.values(PrivilegeTypeBits).map(bit => ( + this.handleBitChange(bit)} + /> + )); + + return ( + + + + {' '} + + {this.props.targetUser.defaultAlias.name} + + + + + +
+
+ {switches} +
+
+ + + + +
+ ); + } +} + + +PrivsEditModal.displayName = 'PrivsEditModal'; +PrivsEditModal.propTypes = { + adminId: PropTypes.number.isRequired, + handleCloseModal: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + targetUser: PropTypes.object.isRequired +}; + +export default PrivsEditModal; diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index b727d8e16c..7cd5205169 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -24,7 +24,7 @@ import * as bootstrap from 'react-bootstrap'; import { faChartLine, faGripVertical, faLink, faListUl, faPlus, faQuestionCircle, - faSearch, faSignInAlt, faSignOutAlt, faTrophy, faUserCircle + faSearch, faShieldHalved, faSignInAlt, faSignOutAlt, faTrophy, faUserCircle, faUserGear } from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import Footer from './../components/footer'; @@ -123,12 +123,33 @@ class Layout extends React.Component { ); + const privilegesDropdownTitle = ( + + + {' Privileges'} + + ); + const disableSignUp = this.props.disableSignUp ? {disabled: true} : {}; return (
) diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 92c5e6a40a..a3c4bb951b 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -60,11 +60,15 @@ class PrivsEditModal extends React.Component { }, method: 'POST' }); + if (!response.ok) { const {error} = await response.json(); throw new Error(error ?? response.statusText); } - window.location.reload(); + let updatedUserData = this.props.targetUser; + updatedUserData.privs = privs; + this.props.updatePrivsOnResultsList(updatedUserData); + this.props.handleCloseModal(); } catch (err) { throw new Error(err); @@ -143,7 +147,8 @@ PrivsEditModal.propTypes = { adminId: PropTypes.number.isRequired, handleCloseModal: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, - targetUser: PropTypes.object.isRequired + targetUser: PropTypes.object.isRequired, + updatePrivsOnResultsList: PropTypes.func.isRequired }; export default PrivsEditModal; From fe32c171d7d09ad630da86ee453cadedef4f8240 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Wed, 21 Jun 2023 17:03:42 +0530 Subject: [PATCH 08/88] pull a new copy of results when privs updated --- .../components/pages/admin-panel-search.tsx | 27 ++++++++++--------- .../parts/admin-panel-search-results.tsx | 6 ++--- src/client/components/pages/parts/pager.js | 7 ++++- .../pages/parts/privs-edit-modal.js | 6 ++--- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/client/components/pages/admin-panel-search.tsx b/src/client/components/pages/admin-panel-search.tsx index f0b1aeffc4..72cad0c17e 100644 --- a/src/client/components/pages/admin-panel-search.tsx +++ b/src/client/components/pages/admin-panel-search.tsx @@ -22,7 +22,6 @@ import AdminPanelSearchResults from './parts/admin-panel-search-results'; import {Card} from 'react-bootstrap'; import PagerElement from './parts/pager'; import PropTypes from 'prop-types'; -import {findIndex} from 'lodash'; type Props = { from?: number, @@ -36,6 +35,7 @@ type Props = { type State = { query: string | null | undefined; results: any[]; + updateResultsTrigger: number; }; class AdminPanelSearchPage extends React.Component { @@ -69,7 +69,8 @@ class AdminPanelSearchPage extends React.Component { this.state = { query: props.query, - results: props.initialResults + results: props.initialResults, + updateResultsTrigger: 0 }; this.paginationUrl = './admin-panel/search'; @@ -116,17 +117,16 @@ class AdminPanelSearchPage extends React.Component { /** * When we update the privileges of the user, we need to update the information in the results page - * - * @param {Record} updatedUserData - The modified user after privileges change + * The updateResultsTrigger is passed as a prop in the Pager Component, whenever we want to + * get updated results, we need to just use the triggerSearch function in the Pager Component. In order to + * trigger that function without any change in query, we can just update this prop by flipping it between 0 and 1. + * This will trigger the componentDidUpdate lifecycle method in Pager component and then we can run the + * triggerSearch function */ - updatePrivsOnResultsList = (updatedUserData: Record) => { - const {results} = this.state; - const selectedIndex = findIndex(results, {id: updatedUserData.id}); - const updatedUsers = results; - updatedUsers[selectedIndex] = updatedUserData; - + updateResultsOnPrivsChange = () => { + const {updateResultsTrigger: trigger} = this.state; this.setState({ - results: updatedUsers + updateResultsTrigger: 1 - trigger }); }; @@ -137,7 +137,7 @@ class AdminPanelSearchPage extends React.Component { * @returns {object} - JSX to render. */ render() { - const {query, results} = this.state; + const {query, results, updateResultsTrigger} = this.state; const querySearchParams = `q=${query}&type=editor`; return ( @@ -152,7 +152,7 @@ class AdminPanelSearchPage extends React.Component { /> { searchParamsChangeCallback={this.searchParamsChangeCallback} searchResultsCallback={this.searchResultsCallback} size={this.props.resultsPerPage} + updateResultsTrigger={updateResultsTrigger} />
{results.length === 0 && query.length !== 0 && diff --git a/src/client/components/pages/parts/admin-panel-search-results.tsx b/src/client/components/pages/parts/admin-panel-search-results.tsx index 161ea88356..2e1db27c55 100644 --- a/src/client/components/pages/parts/admin-panel-search-results.tsx +++ b/src/client/components/pages/parts/admin-panel-search-results.tsx @@ -34,7 +34,7 @@ type AdminPanelSearchResultsState = { }; type AdminPanelSearchResultsProps = { results?: any[], - updatePrivsOnResultsList: (user: Record) => void, + updateResultsOnPrivsChange: () => void, user: Record }; @@ -49,7 +49,7 @@ class AdminPanelSearchResults extends React.Component
) diff --git a/src/client/components/pages/parts/pager.js b/src/client/components/pages/parts/pager.js index c7018fd2e3..2ae18a7132 100644 --- a/src/client/components/pages/parts/pager.js +++ b/src/client/components/pages/parts/pager.js @@ -60,6 +60,10 @@ class PagerElement extends React.Component { // eslint-disable-next-line react/no-did-update-set-state this.setState({from: 0}, this.triggerSearch); } + + if (prevProps.updateResultsTrigger !== this.props.updateResultsTrigger) { + this.triggerSearch(this.state.from, this.state.size); + } } componentWillUnmount() { @@ -200,7 +204,8 @@ PagerElement.propTypes = { results: PropTypes.array, searchParamsChangeCallback: PropTypes.func, searchResultsCallback: PropTypes.func.isRequired, - size: PropTypes.number + size: PropTypes.number, + updateResultsTrigger: PropTypes.number.isRequired }; PagerElement.defaultProps = { from: 0, diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index a3c4bb951b..46903c31e8 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -65,9 +65,7 @@ class PrivsEditModal extends React.Component { const {error} = await response.json(); throw new Error(error ?? response.statusText); } - let updatedUserData = this.props.targetUser; - updatedUserData.privs = privs; - this.props.updatePrivsOnResultsList(updatedUserData); + this.props.updateResultsOnPrivsChange(); this.props.handleCloseModal(); } catch (err) { @@ -148,7 +146,7 @@ PrivsEditModal.propTypes = { handleCloseModal: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, targetUser: PropTypes.object.isRequired, - updatePrivsOnResultsList: PropTypes.func.isRequired + updateResultsOnPrivsChange: PropTypes.func.isRequired }; export default PrivsEditModal; From af418505e78a54c3e15a77f53dc9df61876534af Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 22 Jun 2023 22:24:17 +0530 Subject: [PATCH 09/88] privilege-utils: store privileges information in objects --- .../parts/admin-panel-search-results.tsx | 2 +- .../pages/parts/privilege-badges.js | 12 +-- .../pages/parts/privs-edit-modal.js | 14 +-- src/common/helpers/privileges-utils.ts | 91 +++++++++++-------- 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/src/client/components/pages/parts/admin-panel-search-results.tsx b/src/client/components/pages/parts/admin-panel-search-results.tsx index 2e1db27c55..d42e143243 100644 --- a/src/client/components/pages/parts/admin-panel-search-results.tsx +++ b/src/client/components/pages/parts/admin-panel-search-results.tsx @@ -26,7 +26,7 @@ import {faPencilAlt} from '@fortawesome/free-solid-svg-icons'; import {genEntityIconHTMLElement} from '../../../helpers/entity'; import {getPrivilegeShieldIcon} from '../../../../common/helpers/privileges-utils'; -const {Badge, Button, Table} = bootstrap; +const {Button, Table} = bootstrap; type AdminPanelSearchResultsState = { selectedUser?: Record, diff --git a/src/client/components/pages/parts/privilege-badges.js b/src/client/components/pages/parts/privilege-badges.js index 3acc80a2db..8cca6f7995 100644 --- a/src/client/components/pages/parts/privilege-badges.js +++ b/src/client/components/pages/parts/privilege-badges.js @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {getBadgeVariantFromTitle, getPrivilegeTitlesArray} from '../../../../common/helpers/privileges-utils'; +import {getBadgeVariantFromBit, getPrivilegeBitsArray, getPrivilegeTitleFromBit} from '../../../../common/helpers/privileges-utils'; import {Badge} from 'react-bootstrap'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; @@ -24,11 +24,11 @@ import React from 'react'; function PrivilegeBadges({privs}) { - const privTitles = getPrivilegeTitlesArray(privs); - const privilegeListComp = privTitles.map(title => ( - - - {title} + const privBits = getPrivilegeBitsArray(privs); + const privilegeListComp = privBits.map(bit => ( + + + {getPrivilegeTitleFromBit(bit)} {' '} diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 46903c31e8..78db6e4ddc 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -17,7 +17,7 @@ */ import * as bootstrap from 'react-bootstrap'; -import {PrivilegeTypeBits, getPrivilegeShieldIcon, getPrivilegeTitleFromBit} from '../../../../common/helpers/privileges-utils'; +import {PrivilegeTypes, getPrivilegeShieldIcon, getPrivilegeTitleFromBit} from '../../../../common/helpers/privileges-utils'; import {faPencilAlt, faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PrivilegeBadges from './privilege-badges'; @@ -94,14 +94,14 @@ class PrivsEditModal extends React.Component { render() { const link = `/editor/${this.props.targetUser.bbid}`; - const switches = Object.values(PrivilegeTypeBits).map(bit => ( + const switches = Object.values(PrivilegeTypes).map(priv => ( this.handleBitChange(bit)} + onChange={() => this.handleBitChange(priv.bit)} /> )); diff --git a/src/common/helpers/privileges-utils.ts b/src/common/helpers/privileges-utils.ts index 00b67d1d82..d75174d918 100644 --- a/src/common/helpers/privileges-utils.ts +++ b/src/common/helpers/privileges-utils.ts @@ -1,21 +1,50 @@ /* eslint-disable sort-keys */ -export const PrivilegeTypes = { - ADMIN_PRIV: 16, - REINDEX_SEARCH_SERVER_PRIV: 8, - RELATIONSHIP_TYPE_EDITOR_PRIV: 4, - IDENTIFIER_TYPE_EDITOR_PRIV: 2, - ENTITY_EDITING_PRIV: 1 +export const PRIVILEGE_PROPERTIES = { + 0: { + title: 'Entity Editor', + badgeVariant: 'secondary' + }, + 1: { + title: 'Identifier Type Editor', + badgeVariant: 'warning' + }, + 2: { + title: 'Relationship Type Editor', + badgeVariant: 'info' + }, + 3: { + title: 'Reindex Search Engine', + badgeVariant: 'succes' + }, + 4: { + title: 'Administrator', + badgeVariant: 'danger' + } }; -export const PrivilegeTypeBits = { - ADMIN_PRIV: 4, - REINDEX_SEARCH_SERVER_PRIV: 3, - RELATIONSHIP_TYPE_EDITOR_PRIV: 2, - IDENTIFIER_TYPE_EDITOR_PRIV: 1, - ENTITY_EDITING_PRIV: 0 +export const PrivilegeTypes = { + ADMIN_PRIV: { + bit: 4, + value: 16 + }, + REINDEX_SEARCH_SERVER_PRIV: { + bit: 3, + value: 8 + }, + RELATIONSHIP_TYPE_EDITOR_PRIV: { + bit: 2, + value: 4 + }, + IDENTIFIER_TYPE_EDITOR_PRIV: { + bit: 1, + value: 2 + }, + ENTITY_EDITING_PRIV: { + bit: 0, + value: 1 + } }; - /* eslint-disable no-bitwise */ /** * Retrieves the icon for the shield depending on the privileges that the user has @@ -25,7 +54,7 @@ export const PrivilegeTypeBits = { */ export function getPrivilegeShieldIcon(privs: number) { // if the user has admin privilege - if (privs & PrivilegeTypes.ADMIN_PRIV) { + if (privs & PrivilegeTypes.ADMIN_PRIV.value) { return '/images/icons/shield-check-orange-filled.svg'; } // if the user has some special privileges, but not the admin privilege @@ -41,42 +70,26 @@ export function getPrivilegeShieldIcon(privs: number) { } export function getPrivilegeTitleFromBit(bit: number) { - const privTypes = { - 0: 'Entity Editor', - 1: 'Identifier Type Editor', - 2: 'Relationship Type Editor', - 3: 'Reindex Search Engine', - 4: 'Administrator' - }; - - return privTypes[bit]; + return PRIVILEGE_PROPERTIES[bit].title; } -export function getBadgeVariantFromTitle(title: string) { - const variants = { - 'Entity Editor': 'secondary', - 'Identifier Type Editor': 'warning', - 'Relationship Type Editor': 'info', - 'Reindex Search Engine': 'success', - Administrator: 'danger' - }; - - return variants[title]; +export function getBadgeVariantFromBit(bit: number) { + return PRIVILEGE_PROPERTIES[bit].badgeVariant; } /** - * Retrieves the titles of all the privileges contained in the privs variable + * Retrieves the bits of all the privileges set in the privs variable * * @param {number} privs - the privileges of the user * @throws {Error} Throws a custom error if there is some unsupported privilege type - * @returns {Array} - returns an array of containing the titles of all the privileges the user has + * @returns {Array} - returns an array of containing the bits of all the privileges the user has set */ -export function getPrivilegeTitlesArray(privs: number): any { - const PrivTitles = Object.values(PrivilegeTypeBits).filter(bit => privs & (1 << bit)).map(bit => getPrivilegeTitleFromBit(bit)); - const maxBits = Object.keys(PrivilegeTypeBits).length; +export function getPrivilegeBitsArray(privs: number): any { + const PrivBits = Object.values(PrivilegeTypes).filter(priv => privs & (1 << priv.bit)).map(priv => priv.bit); + const maxBits = Object.keys(PrivilegeTypes).length; if (privs >= (1 << maxBits)) { throw new Error(`Unsupported set of Privileges: '${privs}'`); } - return PrivTitles; + return PrivBits; } From 89044cd5090d086a40a05a32f86f4e7cc306894c Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sat, 24 Jun 2023 11:52:29 +0530 Subject: [PATCH 10/88] fix typo and lint-warnings --- src/client/components/pages/admin-panel-search.tsx | 1 + .../components/pages/parts/admin-panel-search-field.tsx | 1 + .../components/pages/parts/admin-panel-search-results.tsx | 3 ++- src/client/components/pages/parts/privilege-badges.js | 1 - src/client/components/pages/parts/privs-edit-modal.js | 1 + src/common/helpers/privileges-utils.ts | 2 +- src/server/routes/adminPanel.js | 5 ++--- src/server/routes/editor.js | 2 +- 8 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client/components/pages/admin-panel-search.tsx b/src/client/components/pages/admin-panel-search.tsx index 72cad0c17e..18fbf9f3c6 100644 --- a/src/client/components/pages/admin-panel-search.tsx +++ b/src/client/components/pages/admin-panel-search.tsx @@ -23,6 +23,7 @@ import {Card} from 'react-bootstrap'; import PagerElement from './parts/pager'; import PropTypes from 'prop-types'; + type Props = { from?: number, initialResults?: any[], diff --git a/src/client/components/pages/parts/admin-panel-search-field.tsx b/src/client/components/pages/parts/admin-panel-search-field.tsx index 972c11c6b5..013ed826ff 100644 --- a/src/client/components/pages/parts/admin-panel-search-field.tsx +++ b/src/client/components/pages/parts/admin-panel-search-field.tsx @@ -24,6 +24,7 @@ import PropTypes from 'prop-types'; import _ from 'lodash'; import {faSearch} from '@fortawesome/free-solid-svg-icons'; + const {Button, Col, InputGroup, Form, Row} = bootstrap; const SearchButton = ( diff --git a/src/client/components/pages/parts/admin-panel-search-results.tsx b/src/client/components/pages/parts/admin-panel-search-results.tsx index d42e143243..296a045596 100644 --- a/src/client/components/pages/parts/admin-panel-search-results.tsx +++ b/src/client/components/pages/parts/admin-panel-search-results.tsx @@ -26,6 +26,7 @@ import {faPencilAlt} from '@fortawesome/free-solid-svg-icons'; import {genEntityIconHTMLElement} from '../../../helpers/entity'; import {getPrivilegeShieldIcon} from '../../../../common/helpers/privileges-utils'; + const {Button, Table} = bootstrap; type AdminPanelSearchResultsState = { @@ -112,7 +113,7 @@ class AdminPanelSearchResults extends React.Component ); }); - let tableCssClasses = 'table table-striped'; + const tableCssClasses = 'table table-striped'; if (noResults) { return null; } diff --git a/src/client/components/pages/parts/privilege-badges.js b/src/client/components/pages/parts/privilege-badges.js index 8cca6f7995..ec99c2145e 100644 --- a/src/client/components/pages/parts/privilege-badges.js +++ b/src/client/components/pages/parts/privilege-badges.js @@ -18,7 +18,6 @@ import {getBadgeVariantFromBit, getPrivilegeBitsArray, getPrivilegeTitleFromBit} from '../../../../common/helpers/privileges-utils'; import {Badge} from 'react-bootstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; import React from 'react'; diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 78db6e4ddc..0d00fe65e3 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -24,6 +24,7 @@ import PrivilegeBadges from './privilege-badges'; import PropTypes from 'prop-types'; import React from 'react'; + const {Button, Form, Modal} = bootstrap; class PrivsEditModal extends React.Component { diff --git a/src/common/helpers/privileges-utils.ts b/src/common/helpers/privileges-utils.ts index d75174d918..0d202ed160 100644 --- a/src/common/helpers/privileges-utils.ts +++ b/src/common/helpers/privileges-utils.ts @@ -14,7 +14,7 @@ export const PRIVILEGE_PROPERTIES = { }, 3: { title: 'Reindex Search Engine', - badgeVariant: 'succes' + badgeVariant: 'success' }, 4: { title: 'Administrator', diff --git a/src/server/routes/adminPanel.js b/src/server/routes/adminPanel.js index 0b6b0d6a3d..947e98abe1 100644 --- a/src/server/routes/adminPanel.js +++ b/src/server/routes/adminPanel.js @@ -16,13 +16,12 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import * as auth from '../helpers/auth'; import * as commonUtils from '../../common/helpers/utils'; import * as handler from '../helpers/handler'; import * as propHelpers from '../../client/helpers/props'; import * as search from '../../common/helpers/search'; -import {keys as _keys, snakeCase as _snakeCase, isNil} from 'lodash'; +import {snakeCase as _snakeCase, isNil} from 'lodash'; import {escapeProps, generateProps} from '../helpers/props'; import AdminPanelSearchPage from '../../client/components/pages/admin-panel-search'; import Layout from '../../client/containers/layout'; @@ -53,7 +52,7 @@ router.get('/', async (req, res, next) => { if (query) { // get 1 more results to check nextEnabled const searchResponse = await search.searchByName(orm, query, _snakeCase(type), size + 1, from); - const {results: entities, total} = searchResponse; + const {results: entities} = searchResponse; searchResults = { initialResults: entities.filter(entity => !isNil(entity)), query diff --git a/src/server/routes/editor.js b/src/server/routes/editor.js index 143c9885e8..55bf944964 100644 --- a/src/server/routes/editor.js +++ b/src/server/routes/editor.js @@ -186,7 +186,7 @@ router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, async (req, r 'No change to Privileges', req )); } - const modifiedEditor = await editor.save({privs: req.body.newPrivs}); + await editor.save({privs: req.body.newPrivs}); return res.status(200).send(); } catch (err) { From 13add37a9fb40f0a264bf75a4f394cd66725a40c Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 27 Jun 2023 20:15:46 +0530 Subject: [PATCH 11/88] add edit privs button in place of icon --- .../components/pages/admin-panel-search.tsx | 16 +++++++--------- .../parts/admin-panel-search-results.tsx | 19 ++++++++----------- src/client/components/pages/parts/pager.js | 7 +------ .../pages/parts/privs-edit-modal.js | 8 ++------ src/client/containers/layout.js | 8 ++++---- src/client/stylesheets/style.scss | 6 +++++- 6 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/client/components/pages/admin-panel-search.tsx b/src/client/components/pages/admin-panel-search.tsx index 18fbf9f3c6..9999e99083 100644 --- a/src/client/components/pages/admin-panel-search.tsx +++ b/src/client/components/pages/admin-panel-search.tsx @@ -36,7 +36,6 @@ type Props = { type State = { query: string | null | undefined; results: any[]; - updateResultsTrigger: number; }; class AdminPanelSearchPage extends React.Component { @@ -70,15 +69,17 @@ class AdminPanelSearchPage extends React.Component { this.state = { query: props.query, - results: props.initialResults, - updateResultsTrigger: 0 + results: props.initialResults }; this.paginationUrl = './admin-panel/search'; + this.pagerRef = React.createRef(); } paginationUrl: string; + pagerRef: any; + /** * Gets user text query from the browser's URL search parameters and * sets it in the state to be passed down to AdminPanelSearchField and Pager components @@ -125,10 +126,7 @@ class AdminPanelSearchPage extends React.Component { * triggerSearch function */ updateResultsOnPrivsChange = () => { - const {updateResultsTrigger: trigger} = this.state; - this.setState({ - updateResultsTrigger: 1 - trigger - }); + this.pagerRef?.current?.triggerSearch(); }; /** @@ -138,7 +136,7 @@ class AdminPanelSearchPage extends React.Component { * @returns {object} - JSX to render. */ render() { - const {query, results, updateResultsTrigger} = this.state; + const {query, results} = this.state; const querySearchParams = `q=${query}&type=editor`; return ( @@ -161,11 +159,11 @@ class AdminPanelSearchPage extends React.Component { nextEnabled={this.props.nextEnabled} paginationUrl={this.paginationUrl} querySearchParams={querySearchParams} + ref={this.pagerRef} results={results} searchParamsChangeCallback={this.searchParamsChangeCallback} searchResultsCallback={this.searchResultsCallback} size={this.props.resultsPerPage} - updateResultsTrigger={updateResultsTrigger} />
{results.length === 0 && query.length !== 0 && diff --git a/src/client/components/pages/parts/admin-panel-search-results.tsx b/src/client/components/pages/parts/admin-panel-search-results.tsx index 296a045596..0770162bbc 100644 --- a/src/client/components/pages/parts/admin-panel-search-results.tsx +++ b/src/client/components/pages/parts/admin-panel-search-results.tsx @@ -17,13 +17,10 @@ */ import * as bootstrap from 'react-bootstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import PrivilegeBadges from './privilege-badges'; import PrivsEditModal from './privs-edit-modal'; import PropTypes from 'prop-types'; import React from 'react'; -import {faPencilAlt} from '@fortawesome/free-solid-svg-icons'; -import {genEntityIconHTMLElement} from '../../../helpers/entity'; import {getPrivilegeShieldIcon} from '../../../../common/helpers/privileges-utils'; @@ -94,7 +91,7 @@ class AdminPanelSearchResults extends React.Component - {genEntityIconHTMLElement('editor')} + {name} @@ -103,11 +100,11 @@ class AdminPanelSearchResults extends React.Component @@ -133,7 +130,7 @@ class AdminPanelSearchResults extends React.Component ) } -

+

Search Results


@@ -143,9 +140,9 @@ class AdminPanelSearchResults extends React.Component - Name - Privileges - + Name + Privileges + diff --git a/src/client/components/pages/parts/pager.js b/src/client/components/pages/parts/pager.js index 2ae18a7132..c7018fd2e3 100644 --- a/src/client/components/pages/parts/pager.js +++ b/src/client/components/pages/parts/pager.js @@ -60,10 +60,6 @@ class PagerElement extends React.Component { // eslint-disable-next-line react/no-did-update-set-state this.setState({from: 0}, this.triggerSearch); } - - if (prevProps.updateResultsTrigger !== this.props.updateResultsTrigger) { - this.triggerSearch(this.state.from, this.state.size); - } } componentWillUnmount() { @@ -204,8 +200,7 @@ PagerElement.propTypes = { results: PropTypes.array, searchParamsChangeCallback: PropTypes.func, searchResultsCallback: PropTypes.func.isRequired, - size: PropTypes.number, - updateResultsTrigger: PropTypes.number.isRequired + size: PropTypes.number }; PagerElement.defaultProps = { from: 0, diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 0d00fe65e3..516764eec9 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -93,8 +93,6 @@ class PrivsEditModal extends React.Component { /* eslint-disable react/jsx-no-bind */ render() { - const link = `/editor/${this.props.targetUser.bbid}`; - const switches = Object.values(PrivilegeTypes).map(priv => ( - {' '} - - {this.props.targetUser.defaultAlias.name} - + + {this.props.targetUser.defaultAlias.name} diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 7cd5205169..58386ca9d0 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -125,8 +125,8 @@ class Layout extends React.Component { const privilegesDropdownTitle = ( - - {' Privileges'} + + Privileges ); @@ -146,8 +146,8 @@ class Layout extends React.Component { onToggle={this.handleDropdownToggle} > - - {' Admin Panel'} + + Admin Panel Date: Tue, 27 Jun 2023 20:26:39 +0530 Subject: [PATCH 12/88] use search-results css class on search-results too --- src/client/components/pages/parts/search-results.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/components/pages/parts/search-results.js b/src/client/components/pages/parts/search-results.js index e4a37a157b..afd1fb7900 100644 --- a/src/client/components/pages/parts/search-results.js +++ b/src/client/components/pages/parts/search-results.js @@ -219,7 +219,7 @@ class SearchResults extends React.Component { } { !this.props.condensed && -

+

Search Results

} From b3ef8bda50307fe362585046e4fb5bab6105a242 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 27 Jun 2023 20:35:09 +0530 Subject: [PATCH 13/88] fix a small mistake --- src/client/components/pages/parts/search-results.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/components/pages/parts/search-results.js b/src/client/components/pages/parts/search-results.js index afd1fb7900..c7a5cff191 100644 --- a/src/client/components/pages/parts/search-results.js +++ b/src/client/components/pages/parts/search-results.js @@ -219,7 +219,7 @@ class SearchResults extends React.Component { } { !this.props.condensed && -

+

Search Results

} From d75514b97f1f85a1fdb03edfa8f28aec64079c66 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 27 Jun 2023 22:24:45 +0530 Subject: [PATCH 14/88] define pagerRef type explicitly --- src/client/components/pages/admin-panel-search.tsx | 4 ++-- .../components/pages/parts/admin-panel-search-results.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/components/pages/admin-panel-search.tsx b/src/client/components/pages/admin-panel-search.tsx index 9999e99083..0c2e4171d0 100644 --- a/src/client/components/pages/admin-panel-search.tsx +++ b/src/client/components/pages/admin-panel-search.tsx @@ -73,12 +73,12 @@ class AdminPanelSearchPage extends React.Component { }; this.paginationUrl = './admin-panel/search'; - this.pagerRef = React.createRef(); + this.pagerRef = React.createRef(); } paginationUrl: string; - pagerRef: any; + pagerRef: React.RefObject; /** * Gets user text query from the browser's URL search parameters and diff --git a/src/client/components/pages/parts/admin-panel-search-results.tsx b/src/client/components/pages/parts/admin-panel-search-results.tsx index 0770162bbc..8f8deee181 100644 --- a/src/client/components/pages/parts/admin-panel-search-results.tsx +++ b/src/client/components/pages/parts/admin-panel-search-results.tsx @@ -142,7 +142,7 @@ class AdminPanelSearchResults extends React.Component Name Privileges - + From 1637d609ba5624befc173f106ba01316d1977fcf Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Mon, 3 Jul 2023 15:13:08 +0530 Subject: [PATCH 15/88] add middleware for securing routes according to privs --- src/common/helpers/error.js | 18 ++++++++++++++++++ src/server/helpers/auth.js | 21 +++++++++++++++++++++ src/server/routes/adminPanel.js | 8 ++++++-- src/server/routes/editor.js | 5 ++++- src/server/routes/entity/author.js | 19 +++++++++++-------- src/server/routes/entity/edition-group.js | 18 ++++++++++-------- src/server/routes/entity/edition.ts | 19 +++++++++++-------- src/server/routes/entity/publisher.js | 19 +++++++++++-------- src/server/routes/entity/series.js | 19 +++++++++++-------- src/server/routes/entity/work.js | 19 +++++++++++-------- src/server/routes/merge.ts | 12 +++++++----- src/server/routes/reviews.js | 5 ++++- src/server/routes/unifiedform.ts | 9 ++++++--- 13 files changed, 131 insertions(+), 60 deletions(-) diff --git a/src/common/helpers/error.js b/src/common/helpers/error.js index 3b03d2983e..08fc3b71f4 100644 --- a/src/common/helpers/error.js +++ b/src/common/helpers/error.js @@ -126,6 +126,24 @@ export class PermissionDeniedError extends PathError { } } +export class NotAuthorizedError extends PathError { + static get defaultMessage() { + return 'You do not have permission to access this route'; + } + + static get status() { + return status.FORBIDDEN; + } + + static detailedMessage(req) { + return [ + `You do not have permission to access the following path: + ${req.path}`, + 'Please make sure you have the privileges to access the route!' + ]; + } +} + function _logError(err) { log.error(err); } diff --git a/src/server/helpers/auth.js b/src/server/helpers/auth.js index 412fc9184b..44432d29f6 100644 --- a/src/server/helpers/auth.js +++ b/src/server/helpers/auth.js @@ -172,3 +172,24 @@ export function isAuthenticatedForCollectionView(req, res, next) { 'You do not have permission to view this collection', req ); } + +export function isAuthorized(flag) { + return async (req, res, next) => { + try { + const {Editor} = req.app.locals.orm; + const latestPrivs = await Editor.query({where: {id: req.user.id}}) + .fetch({require: true}) + .then(editor => editor.get('privs')); + /* eslint-disable no-bitwise */ + if (latestPrivs & flag) { + return next(); + } + throw new error.NotAuthorizedError( + 'You do not have the privilege to access this route', req + ); + } + catch (err) { + return next(err); + } + }; +} diff --git a/src/server/routes/adminPanel.js b/src/server/routes/adminPanel.js index 947e98abe1..fbf5671b55 100644 --- a/src/server/routes/adminPanel.js +++ b/src/server/routes/adminPanel.js @@ -16,6 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +import * as auth from '../helpers/auth'; import * as commonUtils from '../../common/helpers/utils'; import * as handler from '../helpers/handler'; import * as propHelpers from '../../client/helpers/props'; @@ -25,19 +26,22 @@ import {snakeCase as _snakeCase, isNil} from 'lodash'; import {escapeProps, generateProps} from '../helpers/props'; import AdminPanelSearchPage from '../../client/components/pages/admin-panel-search'; import Layout from '../../client/containers/layout'; +import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import express from 'express'; import target from '../templates/target'; +const ADMIN = PrivilegeTypes.ADMIN_PRIV.value; + const router = express.Router(); /** * Generates React markup for the search page that is rendered by the user's * browser. */ -router.get('/', async (req, res, next) => { +router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req, res, next) => { const {orm} = req.app.locals; const query = req.query.q ?? ''; const type = 'editor'; @@ -90,7 +94,7 @@ router.get('/', async (req, res, next) => { } }); -router.get('/search', (req, res) => { +router.get('/search', auth.isAuthenticated, auth.isAuthorized(ADMIN), (req, res) => { const {orm} = req.app.locals; const query = req.query.q; const type = 'editor'; diff --git a/src/server/routes/editor.js b/src/server/routes/editor.js index 55bf944964..2cadb34e95 100644 --- a/src/server/routes/editor.js +++ b/src/server/routes/editor.js @@ -31,6 +31,7 @@ import CollectionsPage from '../../client/components/pages/collections'; import EditorContainer from '../../client/containers/editor'; import EditorRevisionPage from '../../client/components/pages/editor-revision'; import Layout from '../../client/containers/layout'; +import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; import ProfileForm from '../../client/components/forms/profile'; import ProfileTab from '../../client/components/pages/parts/editor-profile'; import React from 'react'; @@ -42,6 +43,8 @@ import {getOrderedRevisionForEditorPage} from '../helpers/revisions'; import target from '../templates/target'; +const ADMIN = PrivilegeTypes.ADMIN_PRIV.value; + const router = express.Router(); router.get('/edit', auth.isAuthenticated, async (req, res, next) => { @@ -174,7 +177,7 @@ router.post('/edit/handler', auth.isAuthenticatedForHandler, (req, res) => { handler.sendPromiseResult(res, runAsync(), search.indexEntity); }); -router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, async (req, res, next) => { +router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ADMIN), async (req, res, next) => { const {Editor} = req.app.locals.orm; try { const editor = await Editor diff --git a/src/server/routes/entity/author.js b/src/server/routes/entity/author.js index 0ee99b3a1c..02025c87e7 100644 --- a/src/server/routes/entity/author.js +++ b/src/server/routes/entity/author.js @@ -30,6 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; +import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -86,6 +87,8 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'author', transformNewForm, additionalAuthorProps, true ); +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + /** **************************** *********** Routes ************ *******************************/ @@ -94,7 +97,7 @@ const router = express.Router(); // Creation router.get( - '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, async (req, res) => { @@ -134,7 +137,7 @@ router.get( ); router.post( - '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, async (req, res) => { @@ -173,7 +176,7 @@ router.post( } ); -router.post('/create/handler', auth.isAuthenticatedForHandler, +router.post('/create/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); /* If the route specifies a BBID, make sure it does not redirect to another bbid then load the corresponding entity */ @@ -203,7 +206,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipedi entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -212,7 +215,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { }); router.post( - '/:bbid/delete/handler', auth.isAuthenticatedForHandler, + '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), (req, res) => { const {orm} = req.app.locals; const {AuthorHeader, AuthorRevision} = orm; @@ -320,7 +323,7 @@ export function authorToFormState(author) { router.get( - '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, @@ -341,10 +344,10 @@ router.get( ); -router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); -router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), mergeHandler); export default router; diff --git a/src/server/routes/entity/edition-group.js b/src/server/routes/entity/edition-group.js index 1bcd8e8894..2462444e2d 100644 --- a/src/server/routes/entity/edition-group.js +++ b/src/server/routes/entity/edition-group.js @@ -30,6 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; +import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -82,6 +83,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'editionGroup', transformNewForm, 'typeId', true ); +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; /** **************************** *********** Routes ************ @@ -91,7 +93,7 @@ const router = express.Router(); // Creation router.get( - '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadEditionGroupTypes, middleware.loadRelationshipTypes, async (req, res) => { @@ -130,7 +132,7 @@ router.get( router.post( - '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadEditionGroupTypes, middleware.loadRelationshipTypes, async (req, res) => { const entity = await utils.parseInitialState(req, 'editionGroup'); @@ -155,7 +157,7 @@ router.post( } ); -router.post('/create/handler', auth.isAuthenticatedForHandler, +router.post('/create/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); /* If the route specifies a BBID, make sure it does not redirect to another bbid then load the corresponding entity */ @@ -194,7 +196,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipedi entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -203,7 +205,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { }); router.post( - '/:bbid/delete/handler', auth.isAuthenticatedForHandler, + '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), (req, res) => { const {orm} = req.app.locals; const {EditionGroupHeader, EditionGroupRevision} = orm; @@ -324,7 +326,7 @@ export function editionGroupToFormState(editionGroup) { } router.get( - '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionGroupTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, (req, res) => { @@ -341,10 +343,10 @@ router.get( } ); -router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); -router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), mergeHandler); export default router; diff --git a/src/server/routes/entity/edition.ts b/src/server/routes/entity/edition.ts index fb645d00be..516c69bd5d 100644 --- a/src/server/routes/entity/edition.ts +++ b/src/server/routes/entity/edition.ts @@ -31,6 +31,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; +import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; import {RelationshipTypes} from '../../../client/entity-editor/relationship-editor/types'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; @@ -132,6 +133,8 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'edition', transformNewForm, additionalEditionProps, true ); +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + /** **************************** *********** Routes ************* *******************************/ @@ -140,7 +143,7 @@ const router = express.Router(); // Creation router.get( - '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadRelationshipTypes, (req:PassportRequest, res, next) => { @@ -247,7 +250,7 @@ router.get( ); router.post( - '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadRelationshipTypes, async (req:PassportRequest, res, next) => { @@ -296,7 +299,7 @@ router.post( } ); -router.post('/create/handler', auth.isAuthenticatedForHandler, +router.post('/create/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); /* If the route specifies a BBID, make sure it does not redirect to another bbid then load the corresponding entity */ @@ -349,7 +352,7 @@ router.get('/:bbid/revisions/revisions', (req:PassportRequest, res, next) => { }); -router.get('/:bbid/delete', auth.isAuthenticated, (req:PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req:PassportRequest, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -358,7 +361,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req:PassportRequest, res, nex }); router.post( - '/:bbid/delete/handler', auth.isAuthenticatedForHandler, + '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), (req:PassportRequest, res) => { const {orm} = req.app.locals; const {EditionHeader, EditionRevision} = orm; @@ -498,7 +501,7 @@ export function editionToFormState(edition) { } router.get( - '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, @@ -516,10 +519,10 @@ router.get( } ); -router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); -router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), mergeHandler); export default router; diff --git a/src/server/routes/entity/publisher.js b/src/server/routes/entity/publisher.js index c32775676d..59ddf45639 100644 --- a/src/server/routes/entity/publisher.js +++ b/src/server/routes/entity/publisher.js @@ -30,6 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; +import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -82,6 +83,8 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'publisher', transformNewForm, additionalPublisherProps, true ); +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + /** **************************** *********** Routes ************* *******************************/ @@ -91,7 +94,7 @@ const router = express.Router(); // Creation router.get( - '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadPublisherTypes, middleware.loadRelationshipTypes, async (req, res) => { @@ -130,7 +133,7 @@ router.get( router.post( - '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadPublisherTypes, middleware.loadRelationshipTypes, async (req, res) => { @@ -163,7 +166,7 @@ router.post( ); -router.post('/create/handler', auth.isAuthenticatedForHandler, +router.post('/create/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); @@ -215,7 +218,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipedi .catch(next); }); -router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -224,7 +227,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { }); router.post( - '/:bbid/delete/handler', auth.isAuthenticatedForHandler, + '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), (req, res) => { const {orm} = req.app.locals; const {PublisherHeader, PublisherRevision} = orm; @@ -328,7 +331,7 @@ export function publisherToFormState(publisher) { } router.get( - '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadPublisherTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, (req, res) => { @@ -346,10 +349,10 @@ router.get( ); -router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); -router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), mergeHandler); export default router; diff --git a/src/server/routes/entity/series.js b/src/server/routes/entity/series.js index 4c167fd3d7..7579a911e3 100644 --- a/src/server/routes/entity/series.js +++ b/src/server/routes/entity/series.js @@ -30,6 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; +import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -79,6 +80,8 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'series', transformNewForm, additionalSeriesProps, true ); +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + /** **************************** *********** Routes ************ *******************************/ @@ -87,7 +90,7 @@ const router = express.Router(); // Creation router.get( - '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, async (req, res) => { @@ -125,7 +128,7 @@ router.get( ); router.post( - '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, async (req, res) => { const entity = await utils.parseInitialState(req, 'series'); @@ -163,10 +166,10 @@ router.post( } ); -router.post('/create/handler', auth.isAuthenticatedForHandler, +router.post('/create/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); -router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), mergeHandler); /* If the route specifies a BBID, make sure it does not redirect to another bbid then load the corresponding entity */ @@ -202,7 +205,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadSeriesIt entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -211,7 +214,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { }); router.post( - '/:bbid/delete/handler', auth.isAuthenticatedForHandler, + '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), (req, res) => { const {orm} = req.app.locals; const {SeriesHeader, SeriesRevision} = orm; @@ -318,7 +321,7 @@ export function seriesToFormState(series) { } router.get( - '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadSeriesOrderingTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, (req, res) => { @@ -335,7 +338,7 @@ router.get( } ); -router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); export default router; diff --git a/src/server/routes/entity/work.js b/src/server/routes/entity/work.js index c2dbcd0e92..82638b1f76 100644 --- a/src/server/routes/entity/work.js +++ b/src/server/routes/entity/work.js @@ -31,6 +31,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; +import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; import {RelationshipTypes} from '../../../client/entity-editor/relationship-editor/types'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; @@ -80,6 +81,8 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'work', transformNewForm, 'typeId', true ); +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + /** **************************** *********** Routes ************* *******************************/ @@ -89,7 +92,7 @@ const router = express.Router(); // Creation router.get( - '/create', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadRelationshipTypes, (req, res, next) => { @@ -162,7 +165,7 @@ router.get( ); router.post( - '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, middleware.loadIdentifierTypes, + '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadRelationshipTypes, async (req, res, next) => { @@ -196,7 +199,7 @@ router.post( } ); -router.post('/create/handler', auth.isAuthenticatedForHandler, +router.post('/create/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); @@ -227,7 +230,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipedi entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -236,7 +239,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, (req, res, next) => { }); router.post( - '/:bbid/delete/handler', auth.isAuthenticatedForHandler, + '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), (req, res) => { const {orm} = req.app.locals; const {WorkHeader, WorkRevision} = orm; @@ -339,7 +342,7 @@ export function workToFormState(work) { } router.get( - '/:bbid/edit', auth.isAuthenticated, middleware.loadIdentifierTypes, + '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadWorkTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, (req, res) => { @@ -356,10 +359,10 @@ router.get( } ); -router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), createOrEditHandler); -router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, +router.post('/:bbid/merge/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), mergeHandler); export default router; diff --git a/src/server/routes/merge.ts b/src/server/routes/merge.ts index f362ece797..beec4cf9bb 100644 --- a/src/server/routes/merge.ts +++ b/src/server/routes/merge.ts @@ -30,7 +30,7 @@ import { entityMergeMarkup, generateEntityMergeProps } from '../helpers/entityRouteUtils'; - +import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../helpers/props'; import express from 'express'; @@ -43,6 +43,8 @@ type PassportRequest = express.Request & { session: any }; +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + const router = express.Router(); function entitiesToFormState(entities: any[]) { @@ -256,7 +258,7 @@ async function getEntityByBBID(orm, transacting, bbid) { } -router.get('/add/:bbid', auth.isAuthenticated, +router.get('/add/:bbid', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), async (req: PassportRequest, res, next) => { const {orm}: {orm?: any} = req.app.locals; let {mergeQueue} = req.session; @@ -312,7 +314,7 @@ router.get('/add/:bbid', auth.isAuthenticated, return res.redirect(req.headers.referer); }); -router.get('/remove/:bbid', auth.isAuthenticated, +router.get('/remove/:bbid', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res) => { const {mergeQueue} = req.session; if (!mergeQueue || _.isNil(req.params.bbid)) { @@ -331,13 +333,13 @@ router.get('/remove/:bbid', auth.isAuthenticated, res.redirect(req.headers.referer); }); -router.get('/cancel', auth.isAuthenticated, +router.get('/cancel', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res) => { req.session.mergeQueue = null; res.redirect(req.headers.referer); }); -router.get('/submit/:targetBBID?', auth.isAuthenticated, +router.get('/submit/:targetBBID?', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadRelationshipTypes, async (req: PassportRequest, res, next) => { diff --git a/src/server/routes/reviews.js b/src/server/routes/reviews.js index 616d72af8a..cf5d539029 100644 --- a/src/server/routes/reviews.js +++ b/src/server/routes/reviews.js @@ -19,9 +19,12 @@ import * as auth from '../helpers/auth'; import * as cbHelper from '../helpers/critiquebrainz'; +import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; import express from 'express'; +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + const router = express.Router(); router.get('/:entityType/:bbid/reviews', async (req, res) => { @@ -30,7 +33,7 @@ router.get('/:entityType/:bbid/reviews', async (req, res) => { res.json(reviews); }); -router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, async (req, res) => { +router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), async (req, res) => { const editorId = req.user.id; const {orm} = req.app.locals; diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index d50e195a64..7148a4694c 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -1,15 +1,18 @@ import * as middleware from '../helpers/middleware'; import {createEntitiesHandler, generateUnifiedProps, unifiedFormMarkup} from '../helpers/entityRouteUtils'; -import {isAuthenticated, isAuthenticatedForHandler} from '../helpers/auth'; +import {isAuthenticated, isAuthenticatedForHandler, isAuthorized} from '../helpers/auth'; +import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; import {escapeProps} from '../helpers/props'; import express from 'express'; import target from '../templates/target'; +const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; + type PassportRequest = express.Request & {user: any, session: any}; const router = express.Router(); -router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, +router.get('/create', isAuthenticated, isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadEditionGroupTypes, middleware.loadSeriesOrderingTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadPublisherTypes, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { @@ -27,6 +30,6 @@ router.get('/create', isAuthenticated, middleware.loadIdentifierTypes, })); }); -router.post('/create/handler', isAuthenticatedForHandler, createEntitiesHandler); +router.post('/create/handler', isAuthenticatedForHandler, isAuthorized(ENTITY_EDITOR), createEntitiesHandler); export default router; From 6952c13a0483d2210c736bd4e0f8f1ad51a456a0 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 4 Jul 2023 00:35:21 +0530 Subject: [PATCH 16/88] add tests for middleware --- test/src/server/routes/editor.js | 59 +++++++++++++++++++ test/src/server/routes/entity/author.js | 44 +++++++++++++- .../src/server/routes/entity/edition-group.js | 44 +++++++++++++- test/src/server/routes/entity/edition.js | 42 +++++++++++++ test/src/server/routes/entity/publisher.js | 42 +++++++++++++ test/src/server/routes/entity/series.js | 42 +++++++++++++ test/src/server/routes/entity/work.js | 42 +++++++++++++ test/src/server/routes/merge.js | 41 ++++++++++++- test/src/server/routes/unifiedform.js | 28 ++++++++- test/test-helpers/create-entities.js | 3 +- 10 files changed, 382 insertions(+), 5 deletions(-) diff --git a/test/src/server/routes/editor.js b/test/src/server/routes/editor.js index a82733ff52..0111290a1e 100644 --- a/test/src/server/routes/editor.js +++ b/test/src/server/routes/editor.js @@ -1,6 +1,7 @@ /* eslint-disable sort-keys */ import {createEditor, truncateEntities} from '../../../test-helpers/create-entities'; +import app from '../../../../src/server/app'; import chai from 'chai'; import chaiHttp from 'chai-http'; import {getEditorActivity} from '../../../../src/server/routes/editor.js'; @@ -9,6 +10,11 @@ import orm from '../../../bookbrainz-data'; chai.use(chaiHttp); const {expect} = chai; +const {Editor} = orm; + +const targetUserId = 1; +const oldPrivs = 1; +const newPrivs = 3; describe('getEditorActivity', () => { @@ -132,3 +138,56 @@ describe('getEditorActivity', () => { } }); }); + +describe('Editor with Administrator priv', () => { + let agent; + beforeEach(async () => { + await createEditor(123456, 16); + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + afterEach(truncateEntities); + + it('should be able to edit privs of an editor', async () => { + await createEditor(targetUserId, oldPrivs); + const data = { + targetUserId, + newPrivs + }; + + const res = await agent.post('/editor/privs/edit/handler').send(data); + const latestPrivsOfTargetUser = await Editor.query({where: {id: targetUserId}}) + .fetch({require: true}) + .then(editor => editor.get('privs')); + expect(res.ok).to.be.true; + expect(res).to.have.status(200); + expect(latestPrivsOfTargetUser).to.be.equal(newPrivs); + }); +}); + +describe('Editor without Administrator priv', () => { + let agent; + beforeEach(async () => { + await createEditor(123456, 15); + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + afterEach(truncateEntities); + + it('should not be able to edit privs of an editor', async () => { + await createEditor(targetUserId, oldPrivs); + const data = { + targetUserId, + newPrivs + }; + + const res = await agent.post('/editor/privs/edit/handler').send(data); + const latestPrivsOfTargetUser = await Editor.query({where: {id: targetUserId}}) + .fetch({require: true}) + .then(editor => editor.get('privs')); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + expect(latestPrivsOfTargetUser).to.be.equal(oldPrivs); + }); +}); diff --git a/test/src/server/routes/entity/author.js b/test/src/server/routes/entity/author.js index 28f83aab17..ecd59561e3 100644 --- a/test/src/server/routes/entity/author.js +++ b/test/src/server/routes/entity/author.js @@ -8,7 +8,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Author routes', () => { +describe('Author routes with entity editing priv', () => { const aBBID = getRandomUUID(); const inValidBBID = 'have-you-seen-the-fnords'; let agent; @@ -75,4 +75,46 @@ describe('Author routes', () => { }); }); +describe('Author routes without entity editing priv', () => { + const aBBID = getRandomUUID(); + let agent; + before(async () => { + await createAuthor(aBBID); + await createEditor(123456, 0); + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw an error if trying to open author create page', async () => { + const res = await agent + .get('/author/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error trying to edit an existing author', async () => { + const res = await agent + .get(`/author/${aBBID}/edit`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error when trying to delete an existing author', async () => { + const res = await agent + .get(`/author/${aBBID}/delete`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw not authorized error while seeding author', async () => { + const data = seedInitialState; + const res = await agent.post('/author/create').set('Origin', `http://127.0.0.1:${agent.app.address().port}`).send(data); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); + diff --git a/test/src/server/routes/entity/edition-group.js b/test/src/server/routes/entity/edition-group.js index 6f5e41b3df..759ef9e757 100644 --- a/test/src/server/routes/entity/edition-group.js +++ b/test/src/server/routes/entity/edition-group.js @@ -8,7 +8,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Edition Group routes', () => { +describe('Edition Group routes with entity editing priv', () => { const aBBID = getRandomUUID(); const inValidBBID = 'have-you-seen-the-fnords'; let agent; @@ -71,3 +71,45 @@ describe('Edition Group routes', () => { expect(res).to.have.status(200); }); }); + +describe('Edition Group routes without entity editing priv', () => { + const aBBID = getRandomUUID(); + let agent; + before(async () => { + await createEditionGroup(aBBID); + await createEditor(123456, 0); + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw an error if trying to open edition-group create page', async () => { + const res = await agent + .get('/edition-group/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error trying to edit an existing edition-group', async () => { + const res = await agent + .get(`/edition-group/${aBBID}/edit`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error when trying to delete an existing edition-group', async () => { + const res = await agent + .get(`/edition-group/${aBBID}/delete`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw not authorized error while seeding edition-group', async () => { + const data = seedInitialState; + const res = await agent.post('/edition-group/create').set('Origin', `http://127.0.0.1:${agent.app.address().port}`).send(data); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); diff --git a/test/src/server/routes/entity/edition.js b/test/src/server/routes/entity/edition.js index 9bfb0ad734..270eab4faa 100644 --- a/test/src/server/routes/entity/edition.js +++ b/test/src/server/routes/entity/edition.js @@ -77,3 +77,45 @@ describe('Edition routes', () => { }); }); +describe('Edition routes without entity editing priv', () => { + const aBBID = getRandomUUID(); + let agent; + before(async () => { + await createEdition(aBBID); + await createEditor(123456, 0); + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw an error if trying to open edition create page', async () => { + const res = await agent + .get('/edition/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error trying to edit an existing edition', async () => { + const res = await agent + .get(`/edition/${aBBID}/edit`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error when trying to delete an existing edition', async () => { + const res = await agent + .get(`/edition/${aBBID}/delete`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw not authorized error while seeding edition', async () => { + const data = seedInitialState; + const res = await agent.post('/edition/create').set('Origin', `http://127.0.0.1:${agent.app.address().port}`).send(data); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); + diff --git a/test/src/server/routes/entity/publisher.js b/test/src/server/routes/entity/publisher.js index d3932270e9..87a76af9e7 100644 --- a/test/src/server/routes/entity/publisher.js +++ b/test/src/server/routes/entity/publisher.js @@ -74,3 +74,45 @@ describe('Publisher routes', () => { expect(res).to.have.status(200); }); }); + +describe('Publisher routes without entity editing priv', () => { + const aBBID = getRandomUUID(); + let agent; + before(async () => { + await createPublisher(aBBID); + await createEditor(123456, 0); + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw an error if trying to open publisher create page', async () => { + const res = await agent + .get('/publisher/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error trying to edit an existing publisher', async () => { + const res = await agent + .get(`/publisher/${aBBID}/edit`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error when trying to delete an existing publisher', async () => { + const res = await agent + .get(`/publisher/${aBBID}/delete`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw not authorized error while seeding publisher', async () => { + const data = seedInitialState; + const res = await agent.post('/publisher/create').set('Origin', `http://127.0.0.1:${agent.app.address().port}`).send(data); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); diff --git a/test/src/server/routes/entity/series.js b/test/src/server/routes/entity/series.js index de1b0db285..ecd4eef792 100644 --- a/test/src/server/routes/entity/series.js +++ b/test/src/server/routes/entity/series.js @@ -91,3 +91,45 @@ describe('Series routes', () => { expect(res).to.have.status(200); }); }); + +describe('Series routes without entity editing priv', () => { + const aBBID = getRandomUUID(); + let agent; + before(async () => { + await createSeries(aBBID); + await createEditor(123456, 0); + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw an error if trying to open series create page', async () => { + const res = await agent + .get('/series/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error trying to edit an existing series', async () => { + const res = await agent + .get(`/series/${aBBID}/edit`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error when trying to delete an existing series', async () => { + const res = await agent + .get(`/series/${aBBID}/delete`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw not authorized error while seeding series', async () => { + const data = seedInitialState; + const res = await agent.post('/series/create').set('Origin', `http://127.0.0.1:${agent.app.address().port}`).send(data); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); diff --git a/test/src/server/routes/entity/work.js b/test/src/server/routes/entity/work.js index 28c6f3dbdf..b220bbc593 100644 --- a/test/src/server/routes/entity/work.js +++ b/test/src/server/routes/entity/work.js @@ -73,3 +73,45 @@ describe('Work routes', () => { expect(res).to.have.status(200); }); }); + +describe('Work routes without entity editing priv', () => { + const aBBID = getRandomUUID(); + let agent; + before(async () => { + await createWork(aBBID); + await createEditor(123456, 0); + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw an error if trying to open work create page', async () => { + const res = await agent + .get('/work/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error trying to edit an existing work', async () => { + const res = await agent + .get(`/work/${aBBID}/edit`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an error when trying to delete an existing work', async () => { + const res = await agent + .get(`/work/${aBBID}/delete`); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw not authorized error while seeding work', async () => { + const data = seedInitialState; + const res = await agent.post('/work/create').set('Origin', `http://127.0.0.1:${agent.app.address().port}`).send(data); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); diff --git a/test/src/server/routes/merge.js b/test/src/server/routes/merge.js index dae2b0054e..b35ab436b6 100644 --- a/test/src/server/routes/merge.js +++ b/test/src/server/routes/merge.js @@ -8,7 +8,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Merge routes', () => { +describe('Merge routes with entity editing priv', () => { describe('/add route', () => { const aBBID = getRandomUUID(); const bBBID = getRandomUUID(); @@ -172,3 +172,42 @@ describe('Merge routes', () => { }); }); }); + +describe('Merge routes without entity editing priv', () => { + describe('/add route', () => { + const aBBID = getRandomUUID(); + const bBBID = getRandomUUID(); + const nonExistingEntity = getRandomUUID(); + const inValidBBID = 'have-you-seen-the-fnords'; + let agent; + before(async () => { + await createAuthor(aBBID); + await createWork(bBBID); + await createEditor(123456, 0); + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + after(truncateEntities); + + it('should throw a 403 error if adding an invalid BBID', async () => { + const res = await agent.get(`/merge/add/${inValidBBID}`); + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw an 404 error if BBID is absent (invalid route)', async () => { + const res = await agent.get('/merge/add/'); + expect(res).to.have.status(404); + }); + it('should throw a 403 error if entity does not exist', async () => { + const res = await agent.get(`/merge/add/${nonExistingEntity}`); + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + it('should throw 403 error when trying to add an entity to the merge queue', async () => { + const res = await agent.get(`/merge/add/${aBBID}`) + .set('referer', `/author/${aBBID}`); + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); + }); +}); diff --git a/test/src/server/routes/unifiedform.js b/test/src/server/routes/unifiedform.js index 48fdf5f442..2fd16470fe 100644 --- a/test/src/server/routes/unifiedform.js +++ b/test/src/server/routes/unifiedform.js @@ -34,7 +34,7 @@ function testDefaultAlias(entity, languageId) { return areKeysEqual(expectedDefaultAlias, actualDefaultAlias); } -describe('Unified form routes', () => { +describe('Unified form routes with entity editing priv', () => { let agent; let newLanguage; let newRelationshipType; @@ -309,3 +309,29 @@ describe('Unified form routes', () => { expect(res).to.have.status(400); }); }); + + +describe('Unified form routes without entity editing priv', () => { + let agent; + before(async () => { + try { + await createEditor(123456, 0); + } + catch (error) { + // console.log(error); + } + // Log in; use agent to use logged in session + agent = await chai.request.agent(app); + await agent.get('/cb'); + }); + + after(truncateEntities); + + it('should throw an error if trying to open unified form create page', async () => { + const res = await agent + .get('/create'); + expect(res.ok).to.be.false; + expect(res).to.have.status(403); + expect(res.res.statusMessage).to.equal('You do not have the privilege to access this route'); + }); +}); diff --git a/test/test-helpers/create-entities.js b/test/test-helpers/create-entities.js index a62e1881aa..4b12c02b96 100644 --- a/test/test-helpers/create-entities.js +++ b/test/test-helpers/create-entities.js @@ -144,7 +144,7 @@ const entityAttribs = { revisionId: 1 }; -export function createEditor(editorId) { +export function createEditor(editorId, privs = 1) { return orm.bookshelf.knex.transaction(async (transacting) => { const editorType = await new EditorType(editorTypeAttribs) .save(null, {method: 'insert', transacting}); @@ -155,6 +155,7 @@ export function createEditor(editorId) { editorAttribs.genderId = gender.id; editorAttribs.typeId = editorType.id; editorAttribs.name = internet.userName(); + editorAttribs.privs = privs; editorAttribs.metabrainzUserId = random.number(); editorAttribs.cachedMetabrainzName = editorAttribs.name; From 5d567d52b5b3dcbd06d43b9357b203900399df48 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 4 Jul 2023 22:34:28 +0530 Subject: [PATCH 17/88] expect a 403 error on privs edit modal --- src/client/components/pages/parts/privs-edit-modal.js | 4 +++- src/common/helpers/error.js | 2 +- src/server/helpers/auth.js | 7 +++---- test/src/server/routes/entity/edition.js | 2 +- test/src/server/routes/entity/publisher.js | 2 +- test/src/server/routes/entity/series.js | 2 +- test/src/server/routes/entity/work.js | 2 +- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 516764eec9..f60aea3749 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -61,8 +61,10 @@ class PrivsEditModal extends React.Component { }, method: 'POST' }); - if (!response.ok) { + if (response.status === 403) { + throw new Error(response.statusText); + } const {error} = await response.json(); throw new Error(error ?? response.statusText); } diff --git a/src/common/helpers/error.js b/src/common/helpers/error.js index 08fc3b71f4..f02f069f64 100644 --- a/src/common/helpers/error.js +++ b/src/common/helpers/error.js @@ -138,7 +138,7 @@ export class NotAuthorizedError extends PathError { static detailedMessage(req) { return [ `You do not have permission to access the following path: - ${req.path}`, + ${req.originalUrl}`, 'Please make sure you have the privileges to access the route!' ]; } diff --git a/src/server/helpers/auth.js b/src/server/helpers/auth.js index 44432d29f6..4b85d08e13 100644 --- a/src/server/helpers/auth.js +++ b/src/server/helpers/auth.js @@ -177,11 +177,10 @@ export function isAuthorized(flag) { return async (req, res, next) => { try { const {Editor} = req.app.locals.orm; - const latestPrivs = await Editor.query({where: {id: req.user.id}}) - .fetch({require: true}) - .then(editor => editor.get('privs')); + const editor = await Editor.query({where: {id: req.user.id}}) + .fetch({require: true}); /* eslint-disable no-bitwise */ - if (latestPrivs & flag) { + if (editor.get('privs') & flag) { return next(); } throw new error.NotAuthorizedError( diff --git a/test/src/server/routes/entity/edition.js b/test/src/server/routes/entity/edition.js index 270eab4faa..2436e0de8c 100644 --- a/test/src/server/routes/entity/edition.js +++ b/test/src/server/routes/entity/edition.js @@ -7,7 +7,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Edition routes', () => { +describe('Edition routes with entity editing priv', () => { const aBBID = getRandomUUID(); const inValidBBID = 'have-you-seen-the-fnords'; let agent; diff --git a/test/src/server/routes/entity/publisher.js b/test/src/server/routes/entity/publisher.js index 87a76af9e7..0656930f38 100644 --- a/test/src/server/routes/entity/publisher.js +++ b/test/src/server/routes/entity/publisher.js @@ -8,7 +8,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Publisher routes', () => { +describe('Publisher routes with entity editing priv', () => { const aBBID = getRandomUUID(); const inValidBBID = 'have-you-seen-the-fnords'; let agent; diff --git a/test/src/server/routes/entity/series.js b/test/src/server/routes/entity/series.js index ecd4eef792..246db746f0 100644 --- a/test/src/server/routes/entity/series.js +++ b/test/src/server/routes/entity/series.js @@ -27,7 +27,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Series routes', () => { +describe('Series routes with entity editing priv', () => { const aBBID = getRandomUUID(); const inValidBBID = 'have-you-seen-the-fnords'; let agent; diff --git a/test/src/server/routes/entity/work.js b/test/src/server/routes/entity/work.js index b220bbc593..15a9f3a886 100644 --- a/test/src/server/routes/entity/work.js +++ b/test/src/server/routes/entity/work.js @@ -8,7 +8,7 @@ import chaiHttp from 'chai-http'; chai.use(chaiHttp); const {expect} = chai; -describe('Work routes', () => { +describe('Work routes with entity editing priv', () => { const aBBID = getRandomUUID(); const inValidBBID = 'have-you-seen-the-fnords'; let agent; From a6e115c25fde74b528595b864d0c9a869bdad822 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 6 Jul 2023 20:03:14 +0530 Subject: [PATCH 18/88] change files to ts to increase type validation --- src/common/helpers/privileges-utils.ts | 20 +++++++++---- src/server/helpers/{auth.js => auth.ts} | 5 ++-- .../routes/{adminPanel.js => adminPanel.tsx} | 14 ++++++--- src/server/routes/{editor.js => editor.tsx} | 30 +++++++++++-------- .../routes/entity/{author.js => author.ts} | 26 +++++++++------- .../{edition-group.js => edition-group.ts} | 29 ++++++++++-------- src/server/routes/entity/edition.ts | 4 +-- .../entity/{publisher.js => publisher.ts} | 26 +++++++++------- .../routes/entity/{series.js => series.ts} | 28 ++++++++++------- src/server/routes/entity/{work.js => work.ts} | 27 ++++++++++------- src/server/routes/merge.ts | 4 +-- src/server/routes/{reviews.js => reviews.ts} | 12 +++++--- src/server/routes/unifiedform.ts | 4 +-- test/src/server/routes/editor.js | 2 +- 14 files changed, 140 insertions(+), 91 deletions(-) rename src/server/helpers/{auth.js => auth.ts} (96%) rename src/server/routes/{adminPanel.js => adminPanel.tsx} (92%) rename src/server/routes/{editor.js => editor.tsx} (95%) rename src/server/routes/entity/{author.js => author.ts} (94%) rename src/server/routes/entity/{edition-group.js => edition-group.ts} (93%) rename src/server/routes/entity/{publisher.js => publisher.ts} (93%) rename src/server/routes/entity/{series.js => series.ts} (93%) rename src/server/routes/entity/{work.js => work.ts} (94%) rename src/server/routes/{reviews.js => reviews.ts} (87%) diff --git a/src/common/helpers/privileges-utils.ts b/src/common/helpers/privileges-utils.ts index 0d202ed160..fa1137f281 100644 --- a/src/common/helpers/privileges-utils.ts +++ b/src/common/helpers/privileges-utils.ts @@ -22,26 +22,34 @@ export const PRIVILEGE_PROPERTIES = { } }; +export enum PrivilegeType { + ADMIN = 16, + REINDEX_SEARCH_SERVER = 8, + RELATIONSHIP_TYPE_EDITOR = 4, + IDENTIFIER_TYPE_EDITOR = 2, + ENTITY_EDITOR = 1 +} + export const PrivilegeTypes = { ADMIN_PRIV: { bit: 4, - value: 16 + value: PrivilegeType.ADMIN }, REINDEX_SEARCH_SERVER_PRIV: { bit: 3, - value: 8 + value: PrivilegeType.REINDEX_SEARCH_SERVER }, RELATIONSHIP_TYPE_EDITOR_PRIV: { bit: 2, - value: 4 + value: PrivilegeType.RELATIONSHIP_TYPE_EDITOR }, IDENTIFIER_TYPE_EDITOR_PRIV: { bit: 1, - value: 2 + value: PrivilegeType.IDENTIFIER_TYPE_EDITOR }, - ENTITY_EDITING_PRIV: { + ENTITY_EDITOR_PRIV: { bit: 0, - value: 1 + value: PrivilegeType.ENTITY_EDITOR } }; diff --git a/src/server/helpers/auth.js b/src/server/helpers/auth.ts similarity index 96% rename from src/server/helpers/auth.js rename to src/server/helpers/auth.ts index 4b85d08e13..3113cf8030 100644 --- a/src/server/helpers/auth.js +++ b/src/server/helpers/auth.ts @@ -20,6 +20,7 @@ import * as MusicBrainzOAuth from 'passport-musicbrainz-oauth2'; import * as error from '../../common/helpers/error'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import StrategyMock from './mock-passport-strategy'; import _ from 'lodash'; import config from '../../common/helpers/config'; @@ -173,14 +174,14 @@ export function isAuthenticatedForCollectionView(req, res, next) { ); } -export function isAuthorized(flag) { +export function isAuthorized(priv: PrivilegeType) { return async (req, res, next) => { try { const {Editor} = req.app.locals.orm; const editor = await Editor.query({where: {id: req.user.id}}) .fetch({require: true}); /* eslint-disable no-bitwise */ - if (editor.get('privs') & flag) { + if (editor.get('privs') & priv) { return next(); } throw new error.NotAuthorizedError( diff --git a/src/server/routes/adminPanel.js b/src/server/routes/adminPanel.tsx similarity index 92% rename from src/server/routes/adminPanel.js rename to src/server/routes/adminPanel.tsx index fbf5671b55..4744e081f6 100644 --- a/src/server/routes/adminPanel.js +++ b/src/server/routes/adminPanel.tsx @@ -26,14 +26,20 @@ import {snakeCase as _snakeCase, isNil} from 'lodash'; import {escapeProps, generateProps} from '../helpers/props'; import AdminPanelSearchPage from '../../client/components/pages/admin-panel-search'; import Layout from '../../client/containers/layout'; -import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import express from 'express'; import target from '../templates/target'; -const ADMIN = PrivilegeTypes.ADMIN_PRIV.value; +type PassportRequest = express.Request & { + user: any, + session: any, + query: any +}; + +const {ADMIN} = PrivilegeType; const router = express.Router(); @@ -41,14 +47,14 @@ const router = express.Router(); * Generates React markup for the search page that is rendered by the user's * browser. */ -router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req, res, next) => { +router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req: PassportRequest, res, next) => { const {orm} = req.app.locals; const query = req.query.q ?? ''; const type = 'editor'; const size = req.query.size ? parseInt(req.query.size, 10) : 20; const from = req.query.from ? parseInt(req.query.from, 10) : 0; try { - let searchResults = { + let searchResults: any = { initialResults: [], query, total: 0 diff --git a/src/server/routes/editor.js b/src/server/routes/editor.tsx similarity index 95% rename from src/server/routes/editor.js rename to src/server/routes/editor.tsx index 2cadb34e95..fa33139bdc 100644 --- a/src/server/routes/editor.js +++ b/src/server/routes/editor.tsx @@ -31,7 +31,7 @@ import CollectionsPage from '../../client/components/pages/collections'; import EditorContainer from '../../client/containers/editor'; import EditorRevisionPage from '../../client/components/pages/editor-revision'; import Layout from '../../client/containers/layout'; -import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import ProfileForm from '../../client/components/forms/profile'; import ProfileTab from '../../client/components/pages/parts/editor-profile'; import React from 'react'; @@ -43,11 +43,17 @@ import {getOrderedRevisionForEditorPage} from '../helpers/revisions'; import target from '../templates/target'; -const ADMIN = PrivilegeTypes.ADMIN_PRIV.value; +type PassportRequest = express.Request & { + user: any, + session: any, + query: any +}; + +const {ADMIN} = PrivilegeType; const router = express.Router(); -router.get('/edit', auth.isAuthenticated, async (req, res, next) => { +router.get('/edit', auth.isAuthenticated, async (req: PassportRequest, res, next) => { const {Editor, Gender, TitleUnlock} = req.app.locals.orm; // Prepare three promises to be resolved in parallel to fetch the required @@ -73,7 +79,7 @@ router.get('/edit', auth.isAuthenticated, async (req, res, next) => { // Parallel fetch the three required models. Only "editorModelPromise" has // "require: true", so only that can throw a NotFoundError, which is why the // other two model fetch operations have no error handling. - const [editorModel, titleUnlockModel, genderModel] = await Promise.all([ + const [editorModel, titleUnlockModel, genderModel]: any = await Promise.all([ editorModelPromise, titleUnlockModelPromise, gendersModelPromise ]).catch(next); @@ -120,7 +126,7 @@ function isCurrentUser(reqUserID, sessionUser) { return reqUserID === sessionUser.id; } -router.post('/edit/handler', auth.isAuthenticatedForHandler, (req, res) => { +router.post('/edit/handler', auth.isAuthenticatedForHandler, (req: PassportRequest, res) => { async function runAsync() { const {Editor} = req.app.locals.orm; @@ -281,7 +287,7 @@ function achievementColToEditorGetJSON(achievementCol) { }; } -router.get('/:id', async (req, res, next) => { +router.get('/:id', async (req: PassportRequest, res, next) => { const {AchievementUnlock, Revision} = req.app.locals.orm; const userId = parseInt(req.params.id, 10); if (!userId) { @@ -337,7 +343,7 @@ router.get('/:id', async (req, res, next) => { }); // eslint-disable-next-line consistent-return -router.get('/:id/revisions', async (req, res, next) => { +router.get('/:id/revisions', async (req: PassportRequest, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; @@ -395,7 +401,7 @@ router.get('/:id/revisions', async (req, res, next) => { }); -router.get('/:id/revisions/revisions', async (req, res, next) => { +router.get('/:id/revisions/revisions', async (req: PassportRequest, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; @@ -501,7 +507,7 @@ async function setAchievementUnlockedField(achievements, unlocks, userId, orm) { return {model}; } -router.get('/:id/achievements', async (req, res, next) => { +router.get('/:id/achievements', async (req: PassportRequest, res, next) => { const {AchievementType, AchievementUnlock} = req.app.locals.orm; const userId = parseInt(req.params.id, 10); @@ -588,7 +594,7 @@ async function rankUpdate(orm, editorId, bodyRank, rank) { } -router.post('/:id/achievements/', auth.isAuthenticated, async (req, res) => { +router.post('/:id/achievements/', auth.isAuthenticated, async (req: PassportRequest, res) => { const {orm} = req.app.locals; const userId = parseInt(req.params.id, 10); if (!isCurrentUser(userId, req.user)) { @@ -602,7 +608,7 @@ router.post('/:id/achievements/', auth.isAuthenticated, async (req, res) => { // eslint-disable-next-line consistent-return -router.get('/:id/collections', async (req, res, next) => { +router.get('/:id/collections', async (req: PassportRequest, res, next) => { const {Editor, TitleUnlock} = req.app.locals.orm; const DEFAULT_MAX_COLLECTIONS = 20; @@ -665,7 +671,7 @@ router.get('/:id/collections', async (req, res, next) => { }); // eslint-disable-next-line consistent-return -router.get('/:id/collections/collections', async (req, res, next) => { +router.get('/:id/collections/collections', async (req: PassportRequest, res, next) => { try { const size = req.query.size ? parseInt(req.query.size, 10) : 20; const from = req.query.from ? parseInt(req.query.from, 10) : 0; diff --git a/src/server/routes/entity/author.js b/src/server/routes/entity/author.ts similarity index 94% rename from src/server/routes/entity/author.js rename to src/server/routes/entity/author.ts index 02025c87e7..59405019fb 100644 --- a/src/server/routes/entity/author.js +++ b/src/server/routes/entity/author.ts @@ -30,7 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; -import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -46,6 +46,10 @@ const additionalAuthorProps = [ 'endAreaId' ]; +type PassportRequest = express.Request & { + user: any, + session: any +}; export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( @@ -87,7 +91,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'author', transformNewForm, additionalAuthorProps, true ); -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; /** **************************** *********** Routes ************ @@ -100,7 +104,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, - async (req, res) => { + async (req: PassportRequest, res) => { const markupProps = generateEntityProps( 'author', req, res, { genderOptions: res.locals.genders @@ -140,7 +144,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, - async (req, res) => { + async (req: PassportRequest, res) => { const {orm} = req.app.locals; const entity = await utils.parseInitialState(req, 'author'); if (entity.authorSection) { @@ -201,12 +205,12 @@ function _setAuthorTitle(res) { ); } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res) => { _setAuthorTitle(res); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -216,7 +220,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req, res) => { + (req: PassportRequest, res) => { const {orm} = req.app.locals; const {AuthorHeader, AuthorRevision} = orm; return entityRoutes.handleDelete( @@ -225,13 +229,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req, res, next) => { +router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { const {AuthorRevision} = req.app.locals.orm; _setAuthorTitle(res); entityRoutes.displayRevisions(req, res, next, AuthorRevision); }); -router.get('/:bbid/revisions/revisions', (req, res, next) => { +router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { const {AuthorRevision} = req.app.locals.orm; _setAuthorTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, AuthorRevision); @@ -305,7 +309,7 @@ export function authorToFormState(author) { } )); - const optionalSections = {}; + const optionalSections: any = {}; if (author.annotation) { optionalSections.annotationSection = author.annotation; } @@ -327,7 +331,7 @@ router.get( middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req, res) => { + (req: PassportRequest, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'author', req, res, { genderOptions: res.locals.genders diff --git a/src/server/routes/entity/edition-group.js b/src/server/routes/entity/edition-group.ts similarity index 93% rename from src/server/routes/entity/edition-group.js rename to src/server/routes/entity/edition-group.ts index 2462444e2d..9c886092ca 100644 --- a/src/server/routes/entity/edition-group.js +++ b/src/server/routes/entity/edition-group.ts @@ -30,7 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; -import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -41,6 +41,11 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ +type PassportRequest = express.Request & { + user: any, + session: any +}; + export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection @@ -83,7 +88,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'editionGroup', transformNewForm, 'typeId', true ); -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; /** **************************** *********** Routes ************ @@ -96,7 +101,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadEditionGroupTypes, middleware.loadRelationshipTypes, - async (req, res) => { + async (req: PassportRequest, res) => { const markupProps = generateEntityProps( 'editionGroup', req, res, {} ); @@ -134,7 +139,7 @@ router.get( router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadEditionGroupTypes, - middleware.loadRelationshipTypes, async (req, res) => { + middleware.loadRelationshipTypes, async (req: PassportRequest, res) => { const entity = await utils.parseInitialState(req, 'editionGroup'); const {orm} = req.app.locals; const {EditionGroupType} = orm; @@ -190,13 +195,13 @@ function _setEditionGroupTitle(res) { ); } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res) => { _setEditionGroupTitle(res); res.locals.entity.editions.sort(entityRoutes.compareEntitiesByDate); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -206,7 +211,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req, res) => { + (req: PassportRequest, res) => { const {orm} = req.app.locals; const {EditionGroupHeader, EditionGroupRevision} = orm; return entityRoutes.handleDelete( @@ -215,13 +220,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req, res, next) => { +router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { const {EditionGroupRevision} = req.app.locals.orm; _setEditionGroupTitle(res); entityRoutes.displayRevisions(req, res, next, EditionGroupRevision); }); -router.get('/:bbid/revisions/revisions', (req, res, next) => { +router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { const {EditionGroupRevision} = req.app.locals.orm; _setEditionGroupTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, EditionGroupRevision); @@ -289,7 +294,7 @@ export function editionGroupToFormState(editionGroup) { } )); - const optionalSections = {}; + const optionalSections: any = {}; if (editionGroup.annotation) { optionalSections.annotationSection = editionGroup.annotation; } @@ -301,7 +306,7 @@ export function editionGroupToFormState(editionGroup) { }) ) : []; - const authorCreditEditor = {}; + const authorCreditEditor: any = {}; for (const credit of credits) { authorCreditEditor[credit.position] = credit; } @@ -329,7 +334,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionGroupTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req, res) => { + (req: PassportRequest, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'editionGroup', req, res, {}, editionGroupToFormState )); diff --git a/src/server/routes/entity/edition.ts b/src/server/routes/entity/edition.ts index 516c69bd5d..2442e75c9f 100644 --- a/src/server/routes/entity/edition.ts +++ b/src/server/routes/entity/edition.ts @@ -31,7 +31,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; -import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; import {RelationshipTypes} from '../../../client/entity-editor/relationship-editor/types'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; @@ -133,7 +133,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'edition', transformNewForm, additionalEditionProps, true ); -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; /** **************************** *********** Routes ************* diff --git a/src/server/routes/entity/publisher.js b/src/server/routes/entity/publisher.ts similarity index 93% rename from src/server/routes/entity/publisher.js rename to src/server/routes/entity/publisher.ts index 59ddf45639..0ed4fb0387 100644 --- a/src/server/routes/entity/publisher.js +++ b/src/server/routes/entity/publisher.ts @@ -30,7 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; -import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -42,6 +42,10 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ +type PassportRequest = express.Request & { + user: any, + session: any +}; const additionalPublisherProps = [ 'typeId', 'areaId', 'beginDate', 'endDate', 'ended' @@ -83,7 +87,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'publisher', transformNewForm, additionalPublisherProps, true ); -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; /** **************************** *********** Routes ************* @@ -97,7 +101,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadPublisherTypes, middleware.loadRelationshipTypes, - async (req, res) => { + async (req: PassportRequest, res) => { const markupProps = generateEntityProps( 'publisher', req, res, {} ); @@ -136,7 +140,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadPublisherTypes, middleware.loadRelationshipTypes, - async (req, res) => { + async (req: PassportRequest, res) => { const {orm} = req.app.locals; const {PublisherType} = orm; const entity = await utils.parseInitialState(req, 'publisher'); @@ -193,7 +197,7 @@ function _setPublisherTitle(res) { } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res, next) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res, next) => { // Fetch editions const {Publisher} = req.app.locals.orm; const editionRelationsToFetch = [ @@ -218,7 +222,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipedi .catch(next); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -228,7 +232,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req, res) => { + (req: PassportRequest, res) => { const {orm} = req.app.locals; const {PublisherHeader, PublisherRevision} = orm; return entityRoutes.handleDelete( @@ -237,13 +241,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req, res, next) => { +router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { const {PublisherRevision} = req.app.locals.orm; _setPublisherTitle(res); entityRoutes.displayRevisions(req, res, next, PublisherRevision); }); -router.get('/:bbid/revisions/revisions', (req, res, next) => { +router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { const {PublisherRevision} = req.app.locals.orm; _setPublisherTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, PublisherRevision); @@ -314,7 +318,7 @@ export function publisherToFormState(publisher) { } )); - const optionalSections = {}; + const optionalSections: any = {}; if (publisher.annotation) { optionalSections.annotationSection = publisher.annotation; } @@ -334,7 +338,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadPublisherTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req, res) => { + (req: PassportRequest, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'publisher', req, res, {}, publisherToFormState )); diff --git a/src/server/routes/entity/series.js b/src/server/routes/entity/series.ts similarity index 93% rename from src/server/routes/entity/series.js rename to src/server/routes/entity/series.ts index 7579a911e3..dd049fa166 100644 --- a/src/server/routes/entity/series.js +++ b/src/server/routes/entity/series.ts @@ -30,7 +30,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; -import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; import express from 'express'; @@ -40,6 +40,12 @@ import target from '../../templates/target'; /** **************************** *********** Helpers ************ *******************************/ + +type PassportRequest = express.Request & { + user: any, + session: any +}; + const additionalSeriesProps = [ 'entityType', 'orderingTypeId' ]; @@ -80,7 +86,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'series', transformNewForm, additionalSeriesProps, true ); -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; /** **************************** *********** Routes ************ @@ -93,7 +99,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, - async (req, res) => { + async (req: PassportRequest, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'series', req, res, {} )); @@ -130,7 +136,7 @@ router.get( router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, - middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, async (req, res) => { + middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, async (req: PassportRequest, res) => { const entity = await utils.parseInitialState(req, 'series'); if (entity.seriesSection) { const orderingTypes = ['Automatic', 'Manual']; @@ -200,12 +206,12 @@ function _setSeriesTitle(res) { } router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadSeriesItems, middleware.loadGenders, - middleware.loadWikipediaExtract, (req, res) => { + middleware.loadWikipediaExtract, (req: PassportRequest, res) => { _setSeriesTitle(res); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -215,7 +221,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req, res) => { + (req: PassportRequest, res) => { const {orm} = req.app.locals; const {SeriesHeader, SeriesRevision} = orm; return entityRoutes.handleDelete( @@ -224,13 +230,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req, res, next) => { +router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { const {SeriesRevision} = req.app.locals.orm; _setSeriesTitle(res); entityRoutes.displayRevisions(req, res, next, SeriesRevision); }); -router.get('/:bbid/revisions/revisions', (req, res, next) => { +router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { const {SeriesRevision} = req.app.locals.orm; _setSeriesTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, SeriesRevision); @@ -304,7 +310,7 @@ export function seriesToFormState(series) { } }); - const optionalSections = {}; + const optionalSections: any = {}; if (series.annotation) { optionalSections.annotationSection = series.annotation; } @@ -324,7 +330,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadSeriesOrderingTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req, res) => { + (req: PassportRequest, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'series', req, res, {}, seriesToFormState )); diff --git a/src/server/routes/entity/work.js b/src/server/routes/entity/work.ts similarity index 94% rename from src/server/routes/entity/work.js rename to src/server/routes/entity/work.ts index 82638b1f76..8edfd937ee 100644 --- a/src/server/routes/entity/work.js +++ b/src/server/routes/entity/work.ts @@ -31,7 +31,7 @@ import { } from '../../helpers/entityRouteUtils'; import {ConflictError} from '../../../common/helpers/error'; -import {PrivilegeTypes} from '../../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; import {RelationshipTypes} from '../../../client/entity-editor/relationship-editor/types'; import _ from 'lodash'; import {escapeProps} from '../../helpers/props'; @@ -44,6 +44,11 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ +type PassportRequest = express.Request & { + user: any, + session: any +}; + export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection @@ -81,7 +86,7 @@ const mergeHandler = makeEntityCreateOrEditHandler( 'work', transformNewForm, 'typeId', true ); -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; /** **************************** *********** Routes ************* @@ -95,7 +100,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadRelationshipTypes, - (req, res, next) => { + (req: PassportRequest, res, next) => { const {Author, Edition} = req.app.locals.orm; let relationshipTypeId; let initialRelationshipIndex = 0; @@ -168,7 +173,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadRelationshipTypes, - async (req, res, next) => { + async (req: PassportRequest, res, next) => { const {WorkType} = req.app.locals.orm; const entity = await utils.parseInitialState(req, 'work'); if (entity.workSection?.type) { @@ -225,12 +230,12 @@ function _setWorkTitle(res) { ); } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res) => { _setWorkTitle(res); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -240,7 +245,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req, res) => { + (req: PassportRequest, res) => { const {orm} = req.app.locals; const {WorkHeader, WorkRevision} = orm; return entityRoutes.handleDelete( @@ -249,13 +254,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req, res, next) => { +router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { const {WorkRevision} = req.app.locals.orm; _setWorkTitle(res); entityRoutes.displayRevisions(req, res, next, WorkRevision); }); -router.get('/:bbid/revisions/revisions', (req, res, next) => { +router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { const {WorkRevision} = req.app.locals.orm; _setWorkTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, WorkRevision); @@ -325,7 +330,7 @@ export function workToFormState(work) { } )); - const optionalSections = {}; + const optionalSections: any = {}; if (work.annotation) { optionalSections.annotationSection = work.annotation; } @@ -345,7 +350,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadWorkTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req, res) => { + (req: PassportRequest, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'work', req, res, {}, workToFormState )); diff --git a/src/server/routes/merge.ts b/src/server/routes/merge.ts index beec4cf9bb..bdd53e21c6 100644 --- a/src/server/routes/merge.ts +++ b/src/server/routes/merge.ts @@ -30,7 +30,7 @@ import { entityMergeMarkup, generateEntityMergeProps } from '../helpers/entityRouteUtils'; -import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import _ from 'lodash'; import {escapeProps} from '../helpers/props'; import express from 'express'; @@ -43,7 +43,7 @@ type PassportRequest = express.Request & { session: any }; -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; const router = express.Router(); diff --git a/src/server/routes/reviews.js b/src/server/routes/reviews.ts similarity index 87% rename from src/server/routes/reviews.js rename to src/server/routes/reviews.ts index cf5d539029..52ec45e1b5 100644 --- a/src/server/routes/reviews.js +++ b/src/server/routes/reviews.ts @@ -19,11 +19,15 @@ import * as auth from '../helpers/auth'; import * as cbHelper from '../helpers/critiquebrainz'; -import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import express from 'express'; -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +type PassportRequest = express.Request & { + user: any, + session: any +}; +const {ENTITY_EDITOR} = PrivilegeType; const router = express.Router(); @@ -33,7 +37,7 @@ router.get('/:entityType/:bbid/reviews', async (req, res) => { res.json(reviews); }); -router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), async (req, res) => { +router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), async (req: PassportRequest, res) => { const editorId = req.user.id; const {orm} = req.app.locals; @@ -45,7 +49,7 @@ router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorize review ); - let newAccessToken = ''; + let newAccessToken: any = ''; // If the token has expired, we try to refresh it and then submit the review again. if (response?.error === 'invalid_token') { try { diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index 7148a4694c..4560528ef4 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -1,13 +1,13 @@ import * as middleware from '../helpers/middleware'; import {createEntitiesHandler, generateUnifiedProps, unifiedFormMarkup} from '../helpers/entityRouteUtils'; import {isAuthenticated, isAuthenticatedForHandler, isAuthorized} from '../helpers/auth'; -import {PrivilegeTypes} from '../../common/helpers/privileges-utils'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import {escapeProps} from '../helpers/props'; import express from 'express'; import target from '../templates/target'; -const ENTITY_EDITOR = PrivilegeTypes.ENTITY_EDITING_PRIV.value; +const {ENTITY_EDITOR} = PrivilegeType; type PassportRequest = express.Request & {user: any, session: any}; diff --git a/test/src/server/routes/editor.js b/test/src/server/routes/editor.js index 0111290a1e..350e07b4d1 100644 --- a/test/src/server/routes/editor.js +++ b/test/src/server/routes/editor.js @@ -4,7 +4,7 @@ import {createEditor, truncateEntities} from '../../../test-helpers/create-entit import app from '../../../../src/server/app'; import chai from 'chai'; import chaiHttp from 'chai-http'; -import {getEditorActivity} from '../../../../src/server/routes/editor.js'; +import {getEditorActivity} from '../../../../src/server/routes/editor'; import orm from '../../../bookbrainz-data'; From 29cdf7c007bee5b3f6ae1cf99b4ffb3ca26844fd Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sat, 8 Jul 2023 13:33:10 +0530 Subject: [PATCH 19/88] extend Request type via declaration merging and use parseQuery --- src/common/helpers/{session.js => session.ts} | 17 ++++++- src/server/helpers/auth.ts | 6 +++ src/server/helpers/utils.ts | 4 ++ src/server/routes/adminPanel.tsx | 23 +++++----- src/server/routes/editor.tsx | 45 +++++++++---------- src/server/routes/entity/author.ts | 28 ++++++------ src/server/routes/entity/edition-group.ts | 30 +++++++------ src/server/routes/entity/edition.ts | 32 +++++++------ src/server/routes/entity/entity.tsx | 3 +- src/server/routes/entity/publisher.ts | 23 +++++----- src/server/routes/entity/series.ts | 23 +++++----- src/server/routes/entity/work.ts | 23 +++++----- src/server/routes/merge.ts | 13 ++---- src/server/routes/reviews.ts | 6 +-- src/server/routes/unifiedform.ts | 4 +- src/server/routes/wikimedia.ts | 7 +-- 16 files changed, 148 insertions(+), 139 deletions(-) rename src/common/helpers/{session.js => session.ts} (80%) diff --git a/src/common/helpers/session.js b/src/common/helpers/session.ts similarity index 80% rename from src/common/helpers/session.js rename to src/common/helpers/session.ts index 96353c3173..4339c2f9bc 100644 --- a/src/common/helpers/session.js +++ b/src/common/helpers/session.ts @@ -6,6 +6,21 @@ import redisClient from './cache'; import session from 'express-session'; +type Session = { + cookie: any, + mergeQueue?: any, + resave: boolean, + saveUninitialized: boolean, + secret: any, + store?: any +}; + +declare module 'express-serve-static-core' { + interface Request { + session: Session; + } +} + const debug = Debug('bbsite'); /** @@ -14,7 +29,7 @@ const debug = Debug('bbsite'); * @returns {function} Express session request handler. */ function setupSessions(environment) { - const sessionOptions = { + const sessionOptions: Session = { cookie: { maxAge: _get(config, 'session.maxAge', 2592000000), secure: _get(config, 'session.secure', false) diff --git a/src/server/helpers/auth.ts b/src/server/helpers/auth.ts index 3113cf8030..6a00c68195 100644 --- a/src/server/helpers/auth.ts +++ b/src/server/helpers/auth.ts @@ -29,6 +29,12 @@ import passport from 'passport'; import status from 'http-status'; +declare module 'express-serve-static-core' { + interface Request { + user: any; + } +} + async function _linkMBAccount(orm, bbUserJSON, mbUserJSON) { const {Editor} = orm; const fetchedEditor = await new Editor({id: bbUserJSON.id}) diff --git a/src/server/helpers/utils.ts b/src/server/helpers/utils.ts index c9781363af..b80c61181f 100644 --- a/src/server/helpers/utils.ts +++ b/src/server/helpers/utils.ts @@ -357,3 +357,7 @@ export async function parseInitialState(req, type):Promise> } return initialState; } + +export function parseQuery(url: string) { + return new URLSearchParams(url.replace(/^.+?\?/, '')); +} diff --git a/src/server/routes/adminPanel.tsx b/src/server/routes/adminPanel.tsx index 4744e081f6..151d59802f 100644 --- a/src/server/routes/adminPanel.tsx +++ b/src/server/routes/adminPanel.tsx @@ -30,31 +30,32 @@ import {PrivilegeType} from '../../common/helpers/privileges-utils'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import express from 'express'; +import {parseQuery} from '../helpers/utils'; import target from '../templates/target'; -type PassportRequest = express.Request & { - user: any, - session: any, - query: any -}; - const {ADMIN} = PrivilegeType; +type SearchResultsT = { + initialResults: any, + query: string, + total?: number +}; + const router = express.Router(); /** * Generates React markup for the search page that is rendered by the user's * browser. */ -router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req: PassportRequest, res, next) => { +router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req, res, next) => { const {orm} = req.app.locals; - const query = req.query.q ?? ''; + const query = parseQuery(req.url).get('q') ?? ''; const type = 'editor'; - const size = req.query.size ? parseInt(req.query.size, 10) : 20; - const from = req.query.from ? parseInt(req.query.from, 10) : 0; + const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; + const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; try { - let searchResults: any = { + let searchResults: SearchResultsT = { initialResults: [], query, total: 0 diff --git a/src/server/routes/editor.tsx b/src/server/routes/editor.tsx index fa33139bdc..37f5d1a195 100644 --- a/src/server/routes/editor.tsx +++ b/src/server/routes/editor.tsx @@ -40,20 +40,15 @@ import _ from 'lodash'; import express from 'express'; import {getOrderedCollectionsForEditorPage} from '../helpers/collections'; import {getOrderedRevisionForEditorPage} from '../helpers/revisions'; +import {parseQuery} from '../helpers/utils'; import target from '../templates/target'; -type PassportRequest = express.Request & { - user: any, - session: any, - query: any -}; - const {ADMIN} = PrivilegeType; const router = express.Router(); -router.get('/edit', auth.isAuthenticated, async (req: PassportRequest, res, next) => { +router.get('/edit', auth.isAuthenticated, async (req, res, next) => { const {Editor, Gender, TitleUnlock} = req.app.locals.orm; // Prepare three promises to be resolved in parallel to fetch the required @@ -126,7 +121,7 @@ function isCurrentUser(reqUserID, sessionUser) { return reqUserID === sessionUser.id; } -router.post('/edit/handler', auth.isAuthenticatedForHandler, (req: PassportRequest, res) => { +router.post('/edit/handler', auth.isAuthenticatedForHandler, (req, res) => { async function runAsync() { const {Editor} = req.app.locals.orm; @@ -287,7 +282,7 @@ function achievementColToEditorGetJSON(achievementCol) { }; } -router.get('/:id', async (req: PassportRequest, res, next) => { +router.get('/:id', async (req, res, next) => { const {AchievementUnlock, Revision} = req.app.locals.orm; const userId = parseInt(req.params.id, 10); if (!userId) { @@ -343,14 +338,14 @@ router.get('/:id', async (req: PassportRequest, res, next) => { }); // eslint-disable-next-line consistent-return -router.get('/:id/revisions', async (req: PassportRequest, res, next) => { +router.get('/:id/revisions', async (req, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; const size = - req.query.size ? parseInt(req.query.size, 10) : DEFAULT_MAX_REVISIONS; + req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : DEFAULT_MAX_REVISIONS; const from = - req.query.from ? parseInt(req.query.from, 10) : DEFAULT_REVISION_OFFSET; + req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : DEFAULT_REVISION_OFFSET; try { // get 1 more result to check nextEnabled @@ -401,14 +396,14 @@ router.get('/:id/revisions', async (req: PassportRequest, res, next) => { }); -router.get('/:id/revisions/revisions', async (req: PassportRequest, res, next) => { +router.get('/:id/revisions/revisions', async (req, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; const size = - req.query.size ? parseInt(req.query.size, 10) : DEFAULT_MAX_REVISIONS; + req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : DEFAULT_MAX_REVISIONS; const from = - req.query.from ? parseInt(req.query.from, 10) : DEFAULT_REVISION_OFFSET; + req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : DEFAULT_REVISION_OFFSET; const orderedRevisions = await getOrderedRevisionForEditorPage(from, size, req).catch(next); @@ -507,7 +502,7 @@ async function setAchievementUnlockedField(achievements, unlocks, userId, orm) { return {model}; } -router.get('/:id/achievements', async (req: PassportRequest, res, next) => { +router.get('/:id/achievements', async (req, res, next) => { const {AchievementType, AchievementUnlock} = req.app.locals.orm; const userId = parseInt(req.params.id, 10); @@ -594,7 +589,7 @@ async function rankUpdate(orm, editorId, bodyRank, rank) { } -router.post('/:id/achievements/', auth.isAuthenticated, async (req: PassportRequest, res) => { +router.post('/:id/achievements/', auth.isAuthenticated, async (req, res) => { const {orm} = req.app.locals; const userId = parseInt(req.params.id, 10); if (!isCurrentUser(userId, req.user)) { @@ -608,18 +603,18 @@ router.post('/:id/achievements/', auth.isAuthenticated, async (req: PassportRequ // eslint-disable-next-line consistent-return -router.get('/:id/collections', async (req: PassportRequest, res, next) => { +router.get('/:id/collections', async (req, res, next) => { const {Editor, TitleUnlock} = req.app.locals.orm; const DEFAULT_MAX_COLLECTIONS = 20; const DEFAULT_COLLECTION_OFFSET = 0; const size = - req.query.size ? parseInt(req.query.size, 10) : DEFAULT_MAX_COLLECTIONS; + req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : DEFAULT_MAX_COLLECTIONS; const from = - req.query.from ? parseInt(req.query.from, 10) : DEFAULT_COLLECTION_OFFSET; + req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : DEFAULT_COLLECTION_OFFSET; - const type = req.query.type ? req.query.type : null; + const type = req.query.type ? parseQuery(req.url).get('type') : null; try { const entityTypes = _.keys(commonUtils.getEntityModels(req.app.locals.orm)); @@ -671,11 +666,11 @@ router.get('/:id/collections', async (req: PassportRequest, res, next) => { }); // eslint-disable-next-line consistent-return -router.get('/:id/collections/collections', async (req: PassportRequest, res, next) => { +router.get('/:id/collections/collections', async (req, res, next) => { try { - const size = req.query.size ? parseInt(req.query.size, 10) : 20; - const from = req.query.from ? parseInt(req.query.from, 10) : 0; - const type = req.query.type ? req.query.type : null; + const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; + const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; + const type = req.query.type ? parseQuery(req.url).get('type') : null; const entityTypes = _.keys(commonUtils.getEntityModels(req.app.locals.orm)); if (!entityTypes.includes(type) && type !== null) { throw new error.BadRequestError(`Type ${type} do not exist`); diff --git a/src/server/routes/entity/author.ts b/src/server/routes/entity/author.ts index 59405019fb..aa71e7f977 100644 --- a/src/server/routes/entity/author.ts +++ b/src/server/routes/entity/author.ts @@ -37,20 +37,20 @@ import express from 'express'; import log from 'log'; import target from '../../templates/target'; + /** **************************** *********** Helpers ************ *******************************/ +type OptionalSectionsT = { + annotationSection?: any +}; + const additionalAuthorProps = [ 'typeId', 'genderId', 'beginAreaId', 'beginDate', 'endDate', 'ended', 'endAreaId' ]; -type PassportRequest = express.Request & { - user: any, - session: any -}; - export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection @@ -104,7 +104,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, - async (req: PassportRequest, res) => { + async (req, res) => { const markupProps = generateEntityProps( 'author', req, res, { genderOptions: res.locals.genders @@ -144,7 +144,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadRelationshipTypes, - async (req: PassportRequest, res) => { + async (req, res) => { const {orm} = req.app.locals; const entity = await utils.parseInitialState(req, 'author'); if (entity.authorSection) { @@ -205,12 +205,12 @@ function _setAuthorTitle(res) { ); } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res) => { _setAuthorTitle(res); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -220,7 +220,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { const {orm} = req.app.locals; const {AuthorHeader, AuthorRevision} = orm; return entityRoutes.handleDelete( @@ -229,13 +229,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions', (req, res, next) => { const {AuthorRevision} = req.app.locals.orm; _setAuthorTitle(res); entityRoutes.displayRevisions(req, res, next, AuthorRevision); }); -router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions/revisions', (req, res, next) => { const {AuthorRevision} = req.app.locals.orm; _setAuthorTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, AuthorRevision); @@ -309,7 +309,7 @@ export function authorToFormState(author) { } )); - const optionalSections: any = {}; + const optionalSections: OptionalSectionsT = {}; if (author.annotation) { optionalSections.annotationSection = author.annotation; } @@ -331,7 +331,7 @@ router.get( middleware.loadGenders, middleware.loadLanguages, middleware.loadAuthorTypes, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req: PassportRequest, res) => { + (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'author', req, res, { genderOptions: res.locals.genders diff --git a/src/server/routes/entity/edition-group.ts b/src/server/routes/entity/edition-group.ts index 9c886092ca..b07063634a 100644 --- a/src/server/routes/entity/edition-group.ts +++ b/src/server/routes/entity/edition-group.ts @@ -37,13 +37,17 @@ import express from 'express'; import log from 'log'; import target from '../../templates/target'; + /** **************************** *********** Helpers ************ *******************************/ -type PassportRequest = express.Request & { - user: any, - session: any +type OptionalSectionsT = { + annotationSection?: any +}; + +type AuthorCreditEditorT = { + n0?: entityRoutes.AuthorCreditEditorT }; export function transformNewForm(data) { @@ -101,7 +105,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadEditionGroupTypes, middleware.loadRelationshipTypes, - async (req: PassportRequest, res) => { + async (req, res) => { const markupProps = generateEntityProps( 'editionGroup', req, res, {} ); @@ -139,7 +143,7 @@ router.get( router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadEditionGroupTypes, - middleware.loadRelationshipTypes, async (req: PassportRequest, res) => { + middleware.loadRelationshipTypes, async (req, res) => { const entity = await utils.parseInitialState(req, 'editionGroup'); const {orm} = req.app.locals; const {EditionGroupType} = orm; @@ -195,13 +199,13 @@ function _setEditionGroupTitle(res) { ); } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res) => { _setEditionGroupTitle(res); res.locals.entity.editions.sort(entityRoutes.compareEntitiesByDate); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -211,7 +215,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { const {orm} = req.app.locals; const {EditionGroupHeader, EditionGroupRevision} = orm; return entityRoutes.handleDelete( @@ -220,13 +224,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions', (req, res, next) => { const {EditionGroupRevision} = req.app.locals.orm; _setEditionGroupTitle(res); entityRoutes.displayRevisions(req, res, next, EditionGroupRevision); }); -router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions/revisions', (req, res, next) => { const {EditionGroupRevision} = req.app.locals.orm; _setEditionGroupTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, EditionGroupRevision); @@ -294,7 +298,7 @@ export function editionGroupToFormState(editionGroup) { } )); - const optionalSections: any = {}; + const optionalSections: OptionalSectionsT = {}; if (editionGroup.annotation) { optionalSections.annotationSection = editionGroup.annotation; } @@ -306,7 +310,7 @@ export function editionGroupToFormState(editionGroup) { }) ) : []; - const authorCreditEditor: any = {}; + const authorCreditEditor: AuthorCreditEditorT = {}; for (const credit of credits) { authorCreditEditor[credit.position] = credit; } @@ -334,7 +338,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionGroupTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req: PassportRequest, res) => { + (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'editionGroup', req, res, {}, editionGroupToFormState )); diff --git a/src/server/routes/entity/edition.ts b/src/server/routes/entity/edition.ts index 2442e75c9f..7c97305462 100644 --- a/src/server/routes/entity/edition.ts +++ b/src/server/routes/entity/edition.ts @@ -44,15 +44,19 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ +type OptionalSectionsT = { + annotationSection?: any +}; + +type AuthorCreditEditorT ={ + n0?: entityRoutes.AuthorCreditEditorT +}; + const additionalEditionProps = [ 'editionGroupBbid', 'width', 'height', 'depth', 'weight', 'pages', 'formatId', 'statusId' ]; -type PassportRequest = express.Request & { - user: any, - session: any -}; export function transformNewForm(data) { const aliases = entityRoutes.constructAliases( data.aliasEditor, data.nameSection @@ -146,7 +150,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadRelationshipTypes, - (req:PassportRequest, res, next) => { + (req, res, next) => { const {EditionGroup, Publisher, Work} = req.app.locals.orm; const propsPromise = generateEntityProps( 'edition', req, res, {} @@ -253,7 +257,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadRelationshipTypes, - async (req:PassportRequest, res, next) => { + async (req, res, next) => { // parsing submitted data to correct format const entity = await utils.parseInitialState(req, 'edition'); if (entity.editionSection) { @@ -333,26 +337,26 @@ function _setEditionTitle(res) { } router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWorkTableAuthors, - middleware.loadWikipediaExtract, (req:PassportRequest, res) => { + middleware.loadWikipediaExtract, (req, res) => { _setEditionTitle(res); entityRoutes.displayEntity(req, res); } ); -router.get('/:bbid/revisions', (req:PassportRequest, res, next) => { +router.get('/:bbid/revisions', (req, res, next) => { const {EditionRevision} = req.app.locals.orm; _setEditionTitle(res); entityRoutes.displayRevisions(req, res, next, EditionRevision); }); -router.get('/:bbid/revisions/revisions', (req:PassportRequest, res, next) => { +router.get('/:bbid/revisions/revisions', (req, res, next) => { const {EditionRevision} = req.app.locals.orm; _setEditionTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, EditionRevision); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req:PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -362,7 +366,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req:PassportRequest, res) => { + (req, res) => { const {orm} = req.app.locals; const {EditionHeader, EditionRevision} = orm; return entityRoutes.handleDelete( @@ -406,7 +410,7 @@ export function editionToFormState(edition) { }) ) : []; - const authorCreditEditor:any = {}; + const authorCreditEditor: AuthorCreditEditorT = {}; for (const credit of credits) { authorCreditEditor[credit.position] = credit; } @@ -483,7 +487,7 @@ export function editionToFormState(edition) { } )); - const optionalSections:any = {}; + const optionalSections: OptionalSectionsT = {}; if (edition.annotation) { optionalSections.annotationSection = edition.annotation; } @@ -505,7 +509,7 @@ router.get( middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req:PassportRequest, res) => { + (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'edition', req, res, {}, editionToFormState )); diff --git a/src/server/routes/entity/entity.tsx b/src/server/routes/entity/entity.tsx index db6865bc85..445418dab9 100644 --- a/src/server/routes/entity/entity.tsx +++ b/src/server/routes/entity/entity.tsx @@ -1360,13 +1360,12 @@ type AuthorT = { id: number }; -type AuthorCreditEditorT = { +export type AuthorCreditEditorT = { author: AuthorT, joinPhrase: string, name: string }; - export function constructAuthorCredit( authorCreditEditor: Record ) { diff --git a/src/server/routes/entity/publisher.ts b/src/server/routes/entity/publisher.ts index 0ed4fb0387..e874ce06bf 100644 --- a/src/server/routes/entity/publisher.ts +++ b/src/server/routes/entity/publisher.ts @@ -42,9 +42,8 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ -type PassportRequest = express.Request & { - user: any, - session: any +type OptionalSectionsT = { + annotationSection?: any }; const additionalPublisherProps = [ @@ -101,7 +100,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadPublisherTypes, middleware.loadRelationshipTypes, - async (req: PassportRequest, res) => { + async (req, res) => { const markupProps = generateEntityProps( 'publisher', req, res, {} ); @@ -140,7 +139,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadPublisherTypes, middleware.loadRelationshipTypes, - async (req: PassportRequest, res) => { + async (req, res) => { const {orm} = req.app.locals; const {PublisherType} = orm; const entity = await utils.parseInitialState(req, 'publisher'); @@ -197,7 +196,7 @@ function _setPublisherTitle(res) { } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res, next) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res, next) => { // Fetch editions const {Publisher} = req.app.locals.orm; const editionRelationsToFetch = [ @@ -222,7 +221,7 @@ router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipedi .catch(next); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -232,7 +231,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { const {orm} = req.app.locals; const {PublisherHeader, PublisherRevision} = orm; return entityRoutes.handleDelete( @@ -241,13 +240,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions', (req, res, next) => { const {PublisherRevision} = req.app.locals.orm; _setPublisherTitle(res); entityRoutes.displayRevisions(req, res, next, PublisherRevision); }); -router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions/revisions', (req, res, next) => { const {PublisherRevision} = req.app.locals.orm; _setPublisherTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, PublisherRevision); @@ -318,7 +317,7 @@ export function publisherToFormState(publisher) { } )); - const optionalSections: any = {}; + const optionalSections: OptionalSectionsT = {}; if (publisher.annotation) { optionalSections.annotationSection = publisher.annotation; } @@ -338,7 +337,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadPublisherTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req: PassportRequest, res) => { + (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'publisher', req, res, {}, publisherToFormState )); diff --git a/src/server/routes/entity/series.ts b/src/server/routes/entity/series.ts index dd049fa166..836450b4b0 100644 --- a/src/server/routes/entity/series.ts +++ b/src/server/routes/entity/series.ts @@ -41,9 +41,8 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ -type PassportRequest = express.Request & { - user: any, - session: any +type OptionalSectionsT = { + annotationSection?: any }; const additionalSeriesProps = [ @@ -99,7 +98,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, - async (req: PassportRequest, res) => { + async (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'series', req, res, {} )); @@ -136,7 +135,7 @@ router.get( router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, - middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, async (req: PassportRequest, res) => { + middleware.loadRelationshipTypes, middleware.loadSeriesOrderingTypes, async (req, res) => { const entity = await utils.parseInitialState(req, 'series'); if (entity.seriesSection) { const orderingTypes = ['Automatic', 'Manual']; @@ -206,12 +205,12 @@ function _setSeriesTitle(res) { } router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadSeriesItems, middleware.loadGenders, - middleware.loadWikipediaExtract, (req: PassportRequest, res) => { + middleware.loadWikipediaExtract, (req, res) => { _setSeriesTitle(res); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -221,7 +220,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { const {orm} = req.app.locals; const {SeriesHeader, SeriesRevision} = orm; return entityRoutes.handleDelete( @@ -230,13 +229,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions', (req, res, next) => { const {SeriesRevision} = req.app.locals.orm; _setSeriesTitle(res); entityRoutes.displayRevisions(req, res, next, SeriesRevision); }); -router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions/revisions', (req, res, next) => { const {SeriesRevision} = req.app.locals.orm; _setSeriesTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, SeriesRevision); @@ -310,7 +309,7 @@ export function seriesToFormState(series) { } }); - const optionalSections: any = {}; + const optionalSections: OptionalSectionsT = {}; if (series.annotation) { optionalSections.annotationSection = series.annotation; } @@ -330,7 +329,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadSeriesOrderingTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req: PassportRequest, res) => { + (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'series', req, res, {}, seriesToFormState )); diff --git a/src/server/routes/entity/work.ts b/src/server/routes/entity/work.ts index 8edfd937ee..9a0890a49f 100644 --- a/src/server/routes/entity/work.ts +++ b/src/server/routes/entity/work.ts @@ -44,9 +44,8 @@ import target from '../../templates/target'; *********** Helpers ************ *******************************/ -type PassportRequest = express.Request & { - user: any, - session: any +type OptionalSectionsT = { + annotationSection?: any }; export function transformNewForm(data) { @@ -100,7 +99,7 @@ router.get( '/create', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadRelationshipTypes, - (req: PassportRequest, res, next) => { + (req, res, next) => { const {Author, Edition} = req.app.locals.orm; let relationshipTypeId; let initialRelationshipIndex = 0; @@ -173,7 +172,7 @@ router.post( '/create', entityRoutes.displayPreview, auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadRelationshipTypes, - async (req: PassportRequest, res, next) => { + async (req, res, next) => { const {WorkType} = req.app.locals.orm; const entity = await utils.parseInitialState(req, 'work'); if (entity.workSection?.type) { @@ -230,12 +229,12 @@ function _setWorkTitle(res) { ); } -router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req: PassportRequest, res) => { +router.get('/:bbid', middleware.loadEntityRelationships, middleware.loadWikipediaExtract, (req, res) => { _setWorkTitle(res); entityRoutes.displayEntity(req, res); }); -router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req: PassportRequest, res, next) => { +router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), (req, res, next) => { if (!res.locals.entity.dataId) { return next(new ConflictError('This entity has already been deleted')); } @@ -245,7 +244,7 @@ router.get('/:bbid/delete', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO router.post( '/:bbid/delete/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { const {orm} = req.app.locals; const {WorkHeader, WorkRevision} = orm; return entityRoutes.handleDelete( @@ -254,13 +253,13 @@ router.post( } ); -router.get('/:bbid/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions', (req, res, next) => { const {WorkRevision} = req.app.locals.orm; _setWorkTitle(res); entityRoutes.displayRevisions(req, res, next, WorkRevision); }); -router.get('/:bbid/revisions/revisions', (req: PassportRequest, res, next) => { +router.get('/:bbid/revisions/revisions', (req, res, next) => { const {WorkRevision} = req.app.locals.orm; _setWorkTitle(res); entityRoutes.updateDisplayedRevisions(req, res, next, WorkRevision); @@ -330,7 +329,7 @@ export function workToFormState(work) { } )); - const optionalSections: any = {}; + const optionalSections: OptionalSectionsT = {}; if (work.annotation) { optionalSections.annotationSection = work.annotation; } @@ -350,7 +349,7 @@ router.get( '/:bbid/edit', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadWorkTypes, middleware.loadLanguages, middleware.loadEntityRelationships, middleware.loadRelationshipTypes, - (req: PassportRequest, res) => { + (req, res) => { const {markup, props} = entityEditorMarkup(generateEntityProps( 'work', req, res, {}, workToFormState )); diff --git a/src/server/routes/merge.ts b/src/server/routes/merge.ts index bdd53e21c6..e975aaa3ba 100644 --- a/src/server/routes/merge.ts +++ b/src/server/routes/merge.ts @@ -38,11 +38,6 @@ import renderRelationship from '../helpers/render'; import targetTemplate from '../templates/target'; -type PassportRequest = express.Request & { - user: any, - session: any -}; - const {ENTITY_EDITOR} = PrivilegeType; const router = express.Router(); @@ -259,7 +254,7 @@ async function getEntityByBBID(orm, transacting, bbid) { router.get('/add/:bbid', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), - async (req: PassportRequest, res, next) => { + async (req, res, next) => { const {orm}: {orm?: any} = req.app.locals; let {mergeQueue} = req.session; if (_.isNil(req.params.bbid) || @@ -315,7 +310,7 @@ router.get('/add/:bbid', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), }); router.get('/remove/:bbid', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { const {mergeQueue} = req.session; if (!mergeQueue || _.isNil(req.params.bbid)) { res.redirect(req.headers.referer); @@ -334,7 +329,7 @@ router.get('/remove/:bbid', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITO }); router.get('/cancel', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), - (req: PassportRequest, res) => { + (req, res) => { req.session.mergeQueue = null; res.redirect(req.headers.referer); }); @@ -342,7 +337,7 @@ router.get('/cancel', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), router.get('/submit/:targetBBID?', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadLanguages, middleware.loadRelationshipTypes, - async (req: PassportRequest, res, next) => { + async (req, res, next) => { const {orm}: {orm?: any} = req.app.locals; const {bookshelf} = orm; const {mergeQueue} = req.session; diff --git a/src/server/routes/reviews.ts b/src/server/routes/reviews.ts index 52ec45e1b5..eff2de76ee 100644 --- a/src/server/routes/reviews.ts +++ b/src/server/routes/reviews.ts @@ -23,10 +23,6 @@ import {PrivilegeType} from '../../common/helpers/privileges-utils'; import express from 'express'; -type PassportRequest = express.Request & { - user: any, - session: any -}; const {ENTITY_EDITOR} = PrivilegeType; const router = express.Router(); @@ -37,7 +33,7 @@ router.get('/:entityType/:bbid/reviews', async (req, res) => { res.json(reviews); }); -router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), async (req: PassportRequest, res) => { +router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorized(ENTITY_EDITOR), async (req, res) => { const editorId = req.user.id; const {orm} = req.app.locals; diff --git a/src/server/routes/unifiedform.ts b/src/server/routes/unifiedform.ts index 4560528ef4..fd7ca83abc 100644 --- a/src/server/routes/unifiedform.ts +++ b/src/server/routes/unifiedform.ts @@ -9,13 +9,11 @@ import target from '../templates/target'; const {ENTITY_EDITOR} = PrivilegeType; -type PassportRequest = express.Request & {user: any, session: any}; - const router = express.Router(); router.get('/create', isAuthenticated, isAuthorized(ENTITY_EDITOR), middleware.loadIdentifierTypes, middleware.loadEditionStatuses, middleware.loadEditionFormats, middleware.loadEditionGroupTypes, middleware.loadSeriesOrderingTypes, middleware.loadLanguages, middleware.loadWorkTypes, middleware.loadGenders, middleware.loadPublisherTypes, middleware.loadAuthorTypes, - middleware.loadRelationshipTypes, (req:PassportRequest, res:express.Response) => { + middleware.loadRelationshipTypes, (req, res:express.Response) => { const props = generateUnifiedProps(req, res, { genderOptions: res.locals.genders }); diff --git a/src/server/routes/wikimedia.ts b/src/server/routes/wikimedia.ts index 00af5c8341..02d2c59cdb 100644 --- a/src/server/routes/wikimedia.ts +++ b/src/server/routes/wikimedia.ts @@ -20,16 +20,11 @@ import {getWikipediaExtract, selectWikipediaPage} from '../helpers/wikimedia'; import express from 'express'; import {getAcceptedLanguageCodes} from '../helpers/i18n'; import log from 'log'; - - -function parseQuery(url: string) { - return new URLSearchParams(url.replace(/^.+?\?/, '')); -} +import {parseQuery} from '../helpers/utils'; const router = express.Router(); - router.get('/wikidata/:id/wikipedia-extract', async (req, res) => { const browserLanguages = getAcceptedLanguageCodes(req); // using `req.query.language` for a parameter that might be `string`, `string[]` (or something else) is a pain From bdfd8e5e6fd5fabff63f306db36c583e4e6306a7 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 11 Jul 2023 12:58:08 +0530 Subject: [PATCH 20/88] use type ExternalServiceTokenT --- src/server/routes/reviews.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/routes/reviews.ts b/src/server/routes/reviews.ts index eff2de76ee..582962adf9 100644 --- a/src/server/routes/reviews.ts +++ b/src/server/routes/reviews.ts @@ -19,6 +19,7 @@ import * as auth from '../helpers/auth'; import * as cbHelper from '../helpers/critiquebrainz'; +import {ExternalServiceTokenT} from 'bookbrainz-data/lib/func/types'; import {PrivilegeType} from '../../common/helpers/privileges-utils'; import express from 'express'; @@ -45,7 +46,7 @@ router.post('/:entityType/:bbid/reviews', auth.isAuthenticated, auth.isAuthorize review ); - let newAccessToken: any = ''; + let newAccessToken: ExternalServiceTokenT = {}; // If the token has expired, we try to refresh it and then submit the review again. if (response?.error === 'invalid_token') { try { From b629a0ae88d7b306c76f38c802dbf5cc0ee4dd1f Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 14 Jul 2023 20:57:21 +0530 Subject: [PATCH 21/88] Add a note field in the privs-edit-modal --- .../pages/parts/privs-edit-modal.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index f60aea3749..6cd3ee211e 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -31,16 +31,18 @@ class PrivsEditModal extends React.Component { constructor(props) { super(props); this.state = { + note: '', privs: props.targetUser.privs, submittable: false }; this.handleBitChange = this.handleBitChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.handleNoteChange = this.handleNoteChange.bind(this); } async handleSubmit() { - const {privs} = this.state; + const {privs, note} = this.state; const oldPrivs = this.props.targetUser.privs; if (privs === oldPrivs) { return; @@ -49,6 +51,7 @@ class PrivsEditModal extends React.Component { const data = { adminId: this.props.adminId, newPrivs: privs, + note, oldPrivs, targetUserId: this.props.targetUser.id }; @@ -93,6 +96,12 @@ class PrivsEditModal extends React.Component { } } + handleNoteChange(event) { + this.setState({ + note: event.target.value + }); + } + /* eslint-disable react/jsx-no-bind */ render() { const switches = Object.values(PrivilegeTypes).map(priv => ( @@ -106,6 +115,19 @@ class PrivsEditModal extends React.Component { /> )); + const noteField = this.state.submittable && + + + Note/Reason: + + + ; + return (
{switches} +
+ {noteField}
From c1cd25e9d1363e2dbba57d4819cda905cae0b7db Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 14 Jul 2023 21:00:00 +0530 Subject: [PATCH 22/88] create an admin log when changing privs --- src/common/helpers/privileges-utils.ts | 4 ++ src/server/helpers/adminLogs.ts | 56 ++++++++++++++++++++++++++ src/server/routes/editor.tsx | 14 ++++--- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/server/helpers/adminLogs.ts diff --git a/src/common/helpers/privileges-utils.ts b/src/common/helpers/privileges-utils.ts index fa1137f281..922adc5361 100644 --- a/src/common/helpers/privileges-utils.ts +++ b/src/common/helpers/privileges-utils.ts @@ -30,6 +30,10 @@ export enum PrivilegeType { ENTITY_EDITOR = 1 } +export enum AdminActionType { + CHANGE_PRIV = 'Change Privileges' +} + export const PrivilegeTypes = { ADMIN_PRIV: { bit: 4, diff --git a/src/server/helpers/adminLogs.ts b/src/server/helpers/adminLogs.ts new file mode 100644 index 0000000000..dc20903e92 --- /dev/null +++ b/src/server/helpers/adminLogs.ts @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {AdminActionType} from '../../common/helpers/privileges-utils'; + + +export async function createAdminLog(actionType: AdminActionType, adminId: number, + newPrivs: number, note: string, oldPrivs: number, targetUserId: number, AdminLog) { + await new AdminLog({ + actionType, + adminId, + newPrivs, + note, + oldPrivs, + targetUserId + }).save(null, {method: 'insert'}); +} + +/** + * Fetches Admin logs for Show All Admin Logs page + * Fetches the last 'size' number of admin logs with offset 'from' + * + * @param {number} from - the offset value + * @param {number} size - no. of last logs required + * @param {object} orm - the BookBrainz ORM, initialized during app setup + * @returns {array} - orderedLogs + */ +export async function getOrderedAdminLogs(from, size, orm) { + const {AdminLog} = orm; + const logs = await new AdminLog().orderBy('time', 'DESC') + .fetchPage({ + limit: size, + offset: from, + withRelated: [ + 'admin', + 'targetUser' + ] + }); + const logsJSON = logs.toJSON(); + return logsJSON; +} diff --git a/src/server/routes/editor.tsx b/src/server/routes/editor.tsx index 37f5d1a195..b95ffa27c9 100644 --- a/src/server/routes/editor.tsx +++ b/src/server/routes/editor.tsx @@ -23,6 +23,7 @@ import * as error from '../../common/helpers/error'; import * as handler from '../helpers/handler'; import * as propHelpers from '../../client/helpers/props'; import * as search from '../../common/helpers/search'; +import {AdminActionType, PrivilegeType} from '../../common/helpers/privileges-utils'; import {eachMonthOfInterval, format, isAfter, isValid} from 'date-fns'; import {escapeProps, generateProps} from '../helpers/props'; import {getConsecutiveDaysWithEdits, getEntityVisits, getTypeCreation} from '../helpers/achievement'; @@ -31,12 +32,12 @@ import CollectionsPage from '../../client/components/pages/collections'; import EditorContainer from '../../client/containers/editor'; import EditorRevisionPage from '../../client/components/pages/editor-revision'; import Layout from '../../client/containers/layout'; -import {PrivilegeType} from '../../common/helpers/privileges-utils'; import ProfileForm from '../../client/components/forms/profile'; import ProfileTab from '../../client/components/pages/parts/editor-profile'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; +import {createAdminLog} from '../helpers/adminLogs'; import express from 'express'; import {getOrderedCollectionsForEditorPage} from '../helpers/collections'; import {getOrderedRevisionForEditorPage} from '../helpers/revisions'; @@ -179,18 +180,21 @@ router.post('/edit/handler', auth.isAuthenticatedForHandler, (req, res) => { }); router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ADMIN), async (req, res, next) => { - const {Editor} = req.app.locals.orm; + const {Editor, AdminLog} = req.app.locals.orm; + const {adminId, newPrivs, note, oldPrivs, targetUserId} = req.body; + const actionType = AdminActionType.CHANGE_PRIV; try { const editor = await Editor - .forge({id: parseInt(req.body.targetUserId, 10)}) + .forge({id: parseInt(targetUserId, 10)}) .fetch({require: true}) .catch(Editor.NotFoundError, () => next(new error.NotFoundError('Editor not found', req))); - if (editor.get('privs') === req.body.newPrivs) { + if (editor.get('privs') === newPrivs) { return next(new error.FormSubmissionError( 'No change to Privileges', req )); } - await editor.save({privs: req.body.newPrivs}); + await editor.save({privs: newPrivs}); + await createAdminLog(actionType, adminId, newPrivs, note, oldPrivs, targetUserId, AdminLog); return res.status(200).send(); } catch (err) { From cb91281cc8ece134655ebbc2bc8141c6533c422f Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 14 Jul 2023 21:03:50 +0530 Subject: [PATCH 23/88] feat(AdminSystem): Add an Admin Logs Page --- src/client/components/footer.js | 6 + src/client/components/pages/adminLogs.js | 74 ++++++++++ .../pages/parts/admin-log-statement.js | 45 ++++++ .../pages/parts/admin-logs-table.js | 87 ++++++++++++ src/client/controllers/adminLogs.js | 47 +++++++ src/client/helpers/adminLogs.tsx | 130 ++++++++++++++++++ src/server/routes.js | 2 + src/server/routes/adminLogs.tsx | 88 ++++++++++++ webpack.client.js | 1 + 9 files changed, 480 insertions(+) create mode 100644 src/client/components/pages/adminLogs.js create mode 100644 src/client/components/pages/parts/admin-log-statement.js create mode 100644 src/client/components/pages/parts/admin-logs-table.js create mode 100644 src/client/controllers/adminLogs.js create mode 100644 src/client/helpers/adminLogs.tsx create mode 100644 src/server/routes/adminLogs.tsx diff --git a/src/client/components/footer.js b/src/client/components/footer.js index b7369da614..5829990bad 100644 --- a/src/client/components/footer.js +++ b/src/client/components/footer.js @@ -59,6 +59,12 @@ function Footer(props) { + + + Admin logs + + +  —  Privacy & Terms diff --git a/src/client/components/pages/adminLogs.js b/src/client/components/pages/adminLogs.js new file mode 100644 index 0000000000..bbc6a95fe7 --- /dev/null +++ b/src/client/components/pages/adminLogs.js @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import AdminLogsTable from './parts/admin-logs-table'; +import PagerElement from './parts/pager'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +class AdminLogsPage extends React.Component { + constructor(props) { + super(props); + this.state = { + results: this.props.results + }; + + // React does not autobind non-React class methods + this.searchResultsCallback = this.searchResultsCallback.bind(this); + this.paginationUrl = './admin-logs/admin-logs'; + } + + searchResultsCallback(newResults) { + this.setState({results: newResults}); + } + + render() { + return ( +
+ + +
+ ); + } +} + + +AdminLogsPage.displayName = 'AdminLogsPage'; +AdminLogsPage.propTypes = { + from: PropTypes.number, + nextEnabled: PropTypes.bool.isRequired, + results: PropTypes.array, + size: PropTypes.number +}; +AdminLogsPage.defaultProps = { + from: 0, + results: [], + size: 20 +}; + +export default AdminLogsPage; diff --git a/src/client/components/pages/parts/admin-log-statement.js b/src/client/components/pages/parts/admin-log-statement.js new file mode 100644 index 0000000000..1d01fa138c --- /dev/null +++ b/src/client/components/pages/parts/admin-log-statement.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import {constructAdminLogStatement} from '../../../helpers/adminLogs'; + + +function AdminLogStatement({logData}) { + const {note} = logData; + return ( +
+ {constructAdminLogStatement(logData)} + { + note.length ? +
+ Note/Reason: {note} +
: null + } +
+ ); +} + +AdminLogStatement.propTypes = { + logData: PropTypes.object.isRequired +}; + +AdminLogStatement.displayName = 'AdminLogStatement'; + +export default AdminLogStatement; diff --git a/src/client/components/pages/parts/admin-logs-table.js b/src/client/components/pages/parts/admin-logs-table.js new file mode 100644 index 0000000000..d8dc0ffe7a --- /dev/null +++ b/src/client/components/pages/parts/admin-logs-table.js @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as bootstrap from 'react-bootstrap'; +import * as utilsHelper from '../../../helpers/utils'; +import AdminLogStatement from './admin-log-statement'; +import PropTypes from 'prop-types'; +import React from 'react'; + + +const {Table} = bootstrap; +const {formatDate} = utilsHelper; + +function AdminLogsTable(props) { + const {results, tableHeading} = props; + return ( +
+
+

{tableHeading}

+
+
+ { + results.length > 0 ? + + + + + + + + + + { + results.map((logData) => ( + + + + + )) + } + +
ActionDate
+ + + {formatDate(new Date(logData.time), true)} +
: + +
+

No logs to show

+
+
+ } +
+ + ); +} + +AdminLogsTable.propTypes = { + results: PropTypes.array.isRequired, + tableHeading: PropTypes.node +}; +AdminLogsTable.defaultProps = { + tableHeading: 'Admin Logs' +}; + +AdminLogsTable.displayName = 'AdminLogsTable'; + +export default AdminLogsTable; diff --git a/src/client/controllers/adminLogs.js b/src/client/controllers/adminLogs.js new file mode 100644 index 0000000000..8bda832719 --- /dev/null +++ b/src/client/controllers/adminLogs.js @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {extractChildProps, extractLayoutProps} from '../helpers/props'; +import AdminLogsPage from '../components/pages/adminLogs'; +import {AppContainer} from 'react-hot-loader'; +import Layout from '../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; +const markup = ( + + + + + +); + +ReactDOM.hydrate(markup, document.getElementById('target')); + +/* + * As we are not exporting a component, + * we cannot use the react-hot-loader module wrapper, + * but instead directly use webpack Hot Module Replacement API + */ + +if (module.hot) { + module.hot.accept(); +} diff --git a/src/client/helpers/adminLogs.tsx b/src/client/helpers/adminLogs.tsx new file mode 100644 index 0000000000..d60f69a253 --- /dev/null +++ b/src/client/helpers/adminLogs.tsx @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as React from 'react'; +import {AdminActionType, PrivilegeTypes, getPrivilegeTitleFromBit} from '../../common/helpers/privileges-utils'; +import {sanitize} from 'isomorphic-dompurify'; + + +/* eslint-disable no-bitwise */ +function getPrivsAdded(newPrivs, oldPrivs) { + const privsAdded = []; + const totalBits = Object.keys(PrivilegeTypes).length; + + for (let i = 0; i < totalBits; i++) { + if (!(oldPrivs & (1 << i)) && (newPrivs & (1 << i))) { + privsAdded.push(getPrivilegeTitleFromBit(i)); + } + } + + return privsAdded; +} + +function getPrivsRemoved(newPrivs, oldPrivs) { + const privsRemoved = []; + const totalBits = Object.keys(PrivilegeTypes).length; + + for (let i = 0; i < totalBits; i++) { + if (!(newPrivs & (1 << i)) && (oldPrivs & (1 << i))) { + privsRemoved.push(getPrivilegeTitleFromBit(i)); + } + } + + return privsRemoved; +} + +function constructPrivsChangeStatement(logData) { + const {newPrivs, oldPrivs, targetUserId, targetUser, adminId, admin} = logData; + const privsAdded = getPrivsAdded(newPrivs, oldPrivs); + const privsRemoved = getPrivsRemoved(newPrivs, oldPrivs); + + let grantStatement = ''; + if (privsAdded.length) { + grantStatement = ' granted '; + for (let i = 0; i < privsAdded.length; i++) { + if (i !== 0 && i === privsAdded.length - 1) { + grantStatement += ' and '; + grantStatement += `${privsAdded[i]}`; + } + else if (i !== 0) { + grantStatement += ', '; + grantStatement += `${privsAdded[i]}`; + } + else { + grantStatement += `${privsAdded[i]}`; + } + } + grantStatement += ' privilege'; + if (privsAdded.length > 1) { + grantStatement += 's'; + } + } + const andStatement = privsAdded.length && privsRemoved.length ? ' and' : ''; + + let removedStatement = ''; + if (privsRemoved.length) { + removedStatement = ' removed '; + for (let i = 0; i < privsRemoved.length; i++) { + if (i !== 0 && i === privsRemoved.length - 1) { + removedStatement += ' and '; + removedStatement += `${privsRemoved[i]}`; + } + else if (i !== 0) { + removedStatement += ', '; + removedStatement += `${privsRemoved[i]}`; + } + else { + removedStatement += `${privsRemoved[i]}`; + } + } + removedStatement += ' privilege'; + if (privsRemoved.length > 1) { + removedStatement += 's'; + } + } + + const preposition = privsRemoved.length ? ' from ' : ' to '; + + const finalStatement = grantStatement + andStatement + removedStatement + preposition; + /* eslint-disable react/no-danger */ + // We are disabling this rule because we are already sanitizing the html here + return ( +
+ ); +} + +/** + * Constructs a log statement for each administrative action for the Admin Logs Page + * @function constructAdminLogStatement + * @param {object} logData - the data for the admin log action + * @returns {string} A statement of the log depending upon the AdminActionType + */ +export function constructAdminLogStatement(logData) { + if (logData.actionType === AdminActionType.CHANGE_PRIV) { + return constructPrivsChangeStatement(logData); + } + return ''; +} diff --git a/src/server/routes.js b/src/server/routes.js index d13ffa50e7..999b1e1fa1 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +import adminLogsRouter from './routes/adminLogs'; import adminPanelRouter from './routes/adminPanel'; import authRouter from './routes/auth'; import authorRouter from './routes/entity/author'; @@ -54,6 +55,7 @@ function initRootRoutes(app) { app.use('/statistics', statisticsRouter); app.use('/external-service', externalServiceRouter); app.use('/admin-panel', adminPanelRouter); + app.use('/admin-logs', adminLogsRouter); } function initEditionGroupRoutes(app) { diff --git a/src/server/routes/adminLogs.tsx b/src/server/routes/adminLogs.tsx new file mode 100644 index 0000000000..a845f9b816 --- /dev/null +++ b/src/server/routes/adminLogs.tsx @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as propHelpers from '../../client/helpers/props'; +import {escapeProps, generateProps} from '../helpers/props'; +import AdminLogsPage from '../../client/components/pages/adminLogs'; +import Layout from '../../client/containers/layout'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import express from 'express'; +import {getNextEnabledAndResultsArray} from '../../common/helpers/utils'; +import {getOrderedAdminLogs} from '../helpers/adminLogs'; +import {parseQuery} from '../helpers/utils'; +import target from '../templates/target'; + + +const router = express.Router(); + +router.get('/', async (req, res, next) => { + const {orm} = req.app.locals; + const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; + const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; + + function render(results, nextEnabled) { + const props = generateProps(req, res, { + from, + nextEnabled, + results, + size + }); + + const markup = ReactDOMServer.renderToString( + + + + ); + + res.send(target({ + markup, + props: escapeProps(props), + script: '/js/adminLogs.js', + title: 'Admin Logs' + })); + } + + try { + // fetch 1 more revision than required to check nextEnabled + const orderedLogs = await getOrderedAdminLogs(from, size + 1, orm); + const {newResultsArray, nextEnabled} = getNextEnabledAndResultsArray(orderedLogs, size); + return render(newResultsArray, nextEnabled); + } + catch (err) { + return next(err); + } +}); + + +// eslint-disable-next-line consistent-return +router.get('/admin-logs', async (req, res, next) => { + const {orm} = req.app.locals; + const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; + const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; + + try { + const orderedRevisions = await getOrderedAdminLogs(from, size, orm); + res.send(orderedRevisions); + } + catch (err) { + return next(err); + } +}); + +export default router; diff --git a/webpack.client.js b/webpack.client.js index 372f3fd91d..9bcfc4e5ac 100644 --- a/webpack.client.js +++ b/webpack.client.js @@ -19,6 +19,7 @@ const cleanWebpackPluginOpts = { const clientConfig = { context: path.resolve(__dirname, 'src', 'client'), entry: { + adminLogs: ['./controllers/adminLogs'], adminPanel: ['./controllers/admin-panel'], collection: ['./controllers/collection/collection'], 'collection/create': ['./controllers/collection/userCollectionForm'], From 9a2e6b9f5c8502c9e145c75f05a7fd44de1fb41b Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 18 Jul 2023 12:10:41 +0530 Subject: [PATCH 24/88] bump bookbrainz-data version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 60c4f79c8c..ced38c1bbc 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.11", "array-move": "^3.0.1", - "bookbrainz-data": "4.0.0", + "bookbrainz-data": "4.1.1", "chart.js": "^2.9.4", "chartjs-adapter-date-fns": "^1.0.0", "classnames": "^2.3.2", From 8c261c61995f1ce7c2876aadfbb3ac880a94eca4 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 18 Jul 2023 20:50:59 +0530 Subject: [PATCH 25/88] add yarn.lock --- yarn.lock | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index f0c0d18bf0..258ef6920a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1514,6 +1514,16 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@metabrainz/bookshelf@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@metabrainz/bookshelf/-/bookshelf-1.3.1.tgz#e8a1e607b1dc074ecfc43781aced87e9ea42e496" + integrity sha512-zlcMCXPrhddIfAyiD+XawtwRRn8cs3/f3ELYz3vTrRLm5dZKtWSpXPRn3uBgunejgV2Zc3/ljRkXQEW1EbpYXg== + dependencies: + bluebird "^3.7.2" + create-error "~0.3.1" + inflection "^1.12.0" + lodash "^4.17.15" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" @@ -2627,12 +2637,12 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" -bookbrainz-data@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/bookbrainz-data/-/bookbrainz-data-4.0.0.tgz#7451db52ea16d54d4cf297fec5b90c0c35a207d0" - integrity sha512-Uh8YLasXWGxqOtBuM0UWxQ1oAEe8jaV0LEASuouYL3dO+Gelm4UrvSmdofK4JbVoyjZta9h4aD7R1rf+CBYYhQ== +bookbrainz-data@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/bookbrainz-data/-/bookbrainz-data-4.1.1.tgz#911b55f9772f1dc0d813ba66477b544c47b08bcf" + integrity sha512-nCf6v9e62lqRqCZjjMWkdQO4VW8HbCgP1CzY8R6J3TSB0fgQb+fAMhu00I84H7qIsqc5Q1obspHrBxQ5Y78fXg== dependencies: - bookshelf bookbrainz/bookshelf#1.3.0 + "@metabrainz/bookshelf" "^1.3.1" bookshelf-virtuals-plugin "^1.0.0" deep-diff "^1.0.2" immutable "^3.8.2" @@ -2648,15 +2658,6 @@ bookshelf-virtuals-plugin@^1.0.0: dependencies: lodash "^4.17.15" -bookshelf@bookbrainz/bookshelf#1.3.0: - version "1.3.0" - resolved "https://codeload.github.com/bookbrainz/bookshelf/tar.gz/d4bf2ae1e8891d5fa6777b7814a3eaba86d71f34" - dependencies: - bluebird "^3.7.2" - create-error "~0.3.1" - inflection "^1.12.0" - lodash "^4.17.15" - boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" From c3ab4d755a9881c75d45125201498455288bc79b Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 18 Jul 2023 21:51:07 +0530 Subject: [PATCH 26/88] fix editor test --- test/src/server/routes/editor.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/src/server/routes/editor.js b/test/src/server/routes/editor.js index 350e07b4d1..ccbc6c4a20 100644 --- a/test/src/server/routes/editor.js +++ b/test/src/server/routes/editor.js @@ -12,6 +12,7 @@ chai.use(chaiHttp); const {expect} = chai; const {Editor} = orm; +const adminId = 123456; const targetUserId = 1; const oldPrivs = 1; const newPrivs = 3; @@ -142,7 +143,7 @@ describe('getEditorActivity', () => { describe('Editor with Administrator priv', () => { let agent; beforeEach(async () => { - await createEditor(123456, 16); + await createEditor(adminId, 16); agent = await chai.request.agent(app); await agent.get('/cb'); }); @@ -150,9 +151,13 @@ describe('Editor with Administrator priv', () => { it('should be able to edit privs of an editor', async () => { await createEditor(targetUserId, oldPrivs); + const note = ''; const data = { - targetUserId, - newPrivs + adminId, + newPrivs, + note, + oldPrivs, + targetUserId }; const res = await agent.post('/editor/privs/edit/handler').send(data); @@ -168,7 +173,7 @@ describe('Editor with Administrator priv', () => { describe('Editor without Administrator priv', () => { let agent; beforeEach(async () => { - await createEditor(123456, 15); + await createEditor(adminId, 15); agent = await chai.request.agent(app); await agent.get('/cb'); }); @@ -176,9 +181,13 @@ describe('Editor without Administrator priv', () => { it('should not be able to edit privs of an editor', async () => { await createEditor(targetUserId, oldPrivs); + const note = ''; const data = { - targetUserId, - newPrivs + adminId, + newPrivs, + note, + oldPrivs, + targetUserId }; const res = await agent.post('/editor/privs/edit/handler').send(data); From fba4f80ab245d30086d850ecb76ef3140dedb783 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Wed, 19 Jul 2023 20:57:31 +0530 Subject: [PATCH 27/88] change to functional component and other improvements to adminlogs --- .../pages/{adminLogs.js => adminLogs.tsx} | 67 ++++++++----------- src/client/helpers/adminLogs.tsx | 47 +++++-------- src/server/helpers/adminLogs.ts | 19 +++++- src/server/routes/editor.tsx | 18 +++-- 4 files changed, 75 insertions(+), 76 deletions(-) rename src/client/components/pages/{adminLogs.js => adminLogs.tsx} (50%) diff --git a/src/client/components/pages/adminLogs.js b/src/client/components/pages/adminLogs.tsx similarity index 50% rename from src/client/components/pages/adminLogs.js rename to src/client/components/pages/adminLogs.tsx index bbc6a95fe7..4b8d1d2a42 100644 --- a/src/client/components/pages/adminLogs.js +++ b/src/client/components/pages/adminLogs.tsx @@ -16,55 +16,42 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +import React, {useCallback, useState} from 'react'; +import {AdminLogDataT} from '../../../server/helpers/adminLogs'; import AdminLogsTable from './parts/admin-logs-table'; import PagerElement from './parts/pager'; -import PropTypes from 'prop-types'; -import React from 'react'; -class AdminLogsPage extends React.Component { - constructor(props) { - super(props); - this.state = { - results: this.props.results - }; - - // React does not autobind non-React class methods - this.searchResultsCallback = this.searchResultsCallback.bind(this); - this.paginationUrl = './admin-logs/admin-logs'; - } - - searchResultsCallback(newResults) { - this.setState({results: newResults}); - } +type Props = { + from?: number, + nextEnabled: number, + results?: AdminLogDataT[], + size?: number +}; - render() { - return ( -
- - -
- ); - } +function AdminLogsPage({from, nextEnabled, results, size}: Props) { + const [logs, setLogs] = useState(results); + const searchResultsCallback = useCallback((newResults) => setLogs(newResults), []); + const paginationUrl = './admin-logs/admin-logs'; + return ( +
+ + +
+ ); } AdminLogsPage.displayName = 'AdminLogsPage'; -AdminLogsPage.propTypes = { - from: PropTypes.number, - nextEnabled: PropTypes.bool.isRequired, - results: PropTypes.array, - size: PropTypes.number -}; AdminLogsPage.defaultProps = { from: 0, results: [], diff --git a/src/client/helpers/adminLogs.tsx b/src/client/helpers/adminLogs.tsx index d60f69a253..fe06ac6199 100644 --- a/src/client/helpers/adminLogs.tsx +++ b/src/client/helpers/adminLogs.tsx @@ -18,11 +18,12 @@ import * as React from 'react'; import {AdminActionType, PrivilegeTypes, getPrivilegeTitleFromBit} from '../../common/helpers/privileges-utils'; +import {AdminLogDataT} from '../../server/helpers/adminLogs'; import {sanitize} from 'isomorphic-dompurify'; /* eslint-disable no-bitwise */ -function getPrivsAdded(newPrivs, oldPrivs) { +function getPrivsAdded(newPrivs: number, oldPrivs: number) { const privsAdded = []; const totalBits = Object.keys(PrivilegeTypes).length; @@ -35,7 +36,7 @@ function getPrivsAdded(newPrivs, oldPrivs) { return privsAdded; } -function getPrivsRemoved(newPrivs, oldPrivs) { +function getPrivsRemoved(newPrivs: number, oldPrivs: number) { const privsRemoved = []; const totalBits = Object.keys(PrivilegeTypes).length; @@ -48,7 +49,7 @@ function getPrivsRemoved(newPrivs, oldPrivs) { return privsRemoved; } -function constructPrivsChangeStatement(logData) { +function constructPrivsChangeStatement(logData: AdminLogDataT) { const {newPrivs, oldPrivs, targetUserId, targetUser, adminId, admin} = logData; const privsAdded = getPrivsAdded(newPrivs, oldPrivs); const privsRemoved = getPrivsRemoved(newPrivs, oldPrivs); @@ -56,19 +57,13 @@ function constructPrivsChangeStatement(logData) { let grantStatement = ''; if (privsAdded.length) { grantStatement = ' granted '; - for (let i = 0; i < privsAdded.length; i++) { - if (i !== 0 && i === privsAdded.length - 1) { - grantStatement += ' and '; - grantStatement += `${privsAdded[i]}`; - } - else if (i !== 0) { - grantStatement += ', '; - grantStatement += `${privsAdded[i]}`; - } - else { - grantStatement += `${privsAdded[i]}`; - } + const lastItem = privsAdded.at(-1); + const otherPrivs = privsAdded.slice(0, -1); + if (otherPrivs.length) { + grantStatement += otherPrivs.map(priv => `${priv}`).join(', '); + grantStatement += ' and '; } + grantStatement += `${lastItem}`; grantStatement += ' privilege'; if (privsAdded.length > 1) { grantStatement += 's'; @@ -79,19 +74,13 @@ function constructPrivsChangeStatement(logData) { let removedStatement = ''; if (privsRemoved.length) { removedStatement = ' removed '; - for (let i = 0; i < privsRemoved.length; i++) { - if (i !== 0 && i === privsRemoved.length - 1) { - removedStatement += ' and '; - removedStatement += `${privsRemoved[i]}`; - } - else if (i !== 0) { - removedStatement += ', '; - removedStatement += `${privsRemoved[i]}`; - } - else { - removedStatement += `${privsRemoved[i]}`; - } + const lastItem = privsRemoved.at(-1); + const otherPrivs = privsRemoved.slice(0, -1); + if (otherPrivs.length) { + removedStatement += otherPrivs.map(priv => `${priv}`).join(', '); + removedStatement += ' and '; } + removedStatement += `${lastItem}`; removedStatement += ' privilege'; if (privsRemoved.length > 1) { removedStatement += 's'; @@ -119,10 +108,10 @@ function constructPrivsChangeStatement(logData) { /** * Constructs a log statement for each administrative action for the Admin Logs Page * @function constructAdminLogStatement - * @param {object} logData - the data for the admin log action + * @param {AdminLogDataT} logData - the data for the admin log action * @returns {string} A statement of the log depending upon the AdminActionType */ -export function constructAdminLogStatement(logData) { +export function constructAdminLogStatement(logData: AdminLogDataT) { if (logData.actionType === AdminActionType.CHANGE_PRIV) { return constructPrivsChangeStatement(logData); } diff --git a/src/server/helpers/adminLogs.ts b/src/server/helpers/adminLogs.ts index dc20903e92..49fdff640b 100644 --- a/src/server/helpers/adminLogs.ts +++ b/src/server/helpers/adminLogs.ts @@ -19,8 +19,21 @@ import {AdminActionType} from '../../common/helpers/privileges-utils'; -export async function createAdminLog(actionType: AdminActionType, adminId: number, - newPrivs: number, note: string, oldPrivs: number, targetUserId: number, AdminLog) { +export type AdminLogDataT = { + actionType: AdminActionType, + admin?: any, + adminId: number, + id?: number, + newPrivs: number, + note: string, + oldPrivs: number, + targetUser?: any, + targetUserId: number, + time?: any +}; + +export async function createAdminLog(logData: AdminLogDataT, AdminLog, transacting: any) { + const {actionType, adminId, newPrivs, note, oldPrivs, targetUserId} = logData; await new AdminLog({ actionType, adminId, @@ -28,7 +41,7 @@ export async function createAdminLog(actionType: AdminActionType, adminId: numbe note, oldPrivs, targetUserId - }).save(null, {method: 'insert'}); + }).save(null, {method: 'insert', transacting}); } /** diff --git a/src/server/routes/editor.tsx b/src/server/routes/editor.tsx index b95ffa27c9..b3bae97d35 100644 --- a/src/server/routes/editor.tsx +++ b/src/server/routes/editor.tsx @@ -24,6 +24,7 @@ import * as handler from '../helpers/handler'; import * as propHelpers from '../../client/helpers/props'; import * as search from '../../common/helpers/search'; import {AdminActionType, PrivilegeType} from '../../common/helpers/privileges-utils'; +import {AdminLogDataT, createAdminLog} from '../helpers/adminLogs'; import {eachMonthOfInterval, format, isAfter, isValid} from 'date-fns'; import {escapeProps, generateProps} from '../helpers/props'; import {getConsecutiveDaysWithEdits, getEntityVisits, getTypeCreation} from '../helpers/achievement'; @@ -37,7 +38,6 @@ import ProfileTab from '../../client/components/pages/parts/editor-profile'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; -import {createAdminLog} from '../helpers/adminLogs'; import express from 'express'; import {getOrderedCollectionsForEditorPage} from '../helpers/collections'; import {getOrderedRevisionForEditorPage} from '../helpers/revisions'; @@ -180,13 +180,14 @@ router.post('/edit/handler', auth.isAuthenticatedForHandler, (req, res) => { }); router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthorized(ADMIN), async (req, res, next) => { - const {Editor, AdminLog} = req.app.locals.orm; + const {Editor, AdminLog, bookshelf} = req.app.locals.orm; const {adminId, newPrivs, note, oldPrivs, targetUserId} = req.body; const actionType = AdminActionType.CHANGE_PRIV; try { + const trx = await bookshelf.transaction(); const editor = await Editor .forge({id: parseInt(targetUserId, 10)}) - .fetch({require: true}) + .fetch({require: true, transacting: trx}) .catch(Editor.NotFoundError, () => next(new error.NotFoundError('Editor not found', req))); if (editor.get('privs') === newPrivs) { return next(new error.FormSubmissionError( @@ -194,7 +195,16 @@ router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthor )); } await editor.save({privs: newPrivs}); - await createAdminLog(actionType, adminId, newPrivs, note, oldPrivs, targetUserId, AdminLog); + const logData: AdminLogDataT = { + actionType, + adminId, + newPrivs, + note, + oldPrivs, + targetUserId + }; + await createAdminLog(logData, AdminLog, trx); + await trx.commit(); return res.status(200).send(); } catch (err) { From 873bc7a7dd4406d5b2ef7646e789e878a7e07e43 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Wed, 19 Jul 2023 21:36:54 +0530 Subject: [PATCH 28/88] make adding notes compulsory --- .../pages/parts/privs-edit-modal.js | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 6cd3ee211e..0a7b9e3390 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -83,10 +83,19 @@ class PrivsEditModal extends React.Component { handleBitChange(bit) { const newPrivs = this.state.privs ^ (1 << bit); if (this.props.targetUser.privs !== newPrivs) { - this.setState({ - privs: newPrivs, - submittable: true - }); + // If we have also added a note, then set submittable also true + if (this.state.note.length) { + this.setState({ + privs: newPrivs, + submittable: true + }); + } + else { + this.setState({ + privs: newPrivs, + submittable: false + }); + } } else { this.setState({ @@ -97,9 +106,27 @@ class PrivsEditModal extends React.Component { } handleNoteChange(event) { - this.setState({ - note: event.target.value - }); + const newPrivs = this.state.privs; + if (event.target.value.length) { + // If the privs have also been changed, then set submittable to true + if (this.props.targetUser.privs !== newPrivs) { + this.setState({ + note: event.target.value, + submittable: true + }); + } + else { + this.setState({ + note: event.target.value + }); + } + } + else { + this.setState({ + note: event.target.value, + submittable: false + }); + } } /* eslint-disable react/jsx-no-bind */ @@ -115,7 +142,7 @@ class PrivsEditModal extends React.Component { /> )); - const noteField = this.state.submittable && + const noteField = ( Note/Reason: @@ -126,7 +153,8 @@ class PrivsEditModal extends React.Component { rows="2" onChange={this.handleNoteChange} /> - ; + + ); return ( Date: Wed, 19 Jul 2023 21:54:18 +0530 Subject: [PATCH 29/88] improve footer links --- src/client/components/footer.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/client/components/footer.js b/src/client/components/footer.js index 5829990bad..dbebc79fac 100644 --- a/src/client/components/footer.js +++ b/src/client/components/footer.js @@ -64,7 +64,22 @@ function Footer(props) { Admin logs
-  —  + + + + + + + Alpha Software —{' '} + + {siteRevision || 'unknown revision'} + —  + + Report a Bug + + + + Privacy & Terms @@ -72,17 +87,6 @@ function Footer(props) { - ); From 35163de4e98e71d26ce6c97942e73bfab5bdc929 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 20 Jul 2023 09:30:09 +0530 Subject: [PATCH 30/88] improve query parser code --- .../pages/parts/privs-edit-modal.js | 31 +++++-------------- src/server/routes/adminLogs.tsx | 10 +++--- src/server/routes/adminPanel.tsx | 5 +-- src/server/routes/editor.tsx | 29 ++++++++--------- 4 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 0a7b9e3390..568d2df57e 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -84,18 +84,10 @@ class PrivsEditModal extends React.Component { const newPrivs = this.state.privs ^ (1 << bit); if (this.props.targetUser.privs !== newPrivs) { // If we have also added a note, then set submittable also true - if (this.state.note.length) { - this.setState({ - privs: newPrivs, - submittable: true - }); - } - else { - this.setState({ - privs: newPrivs, - submittable: false - }); - } + this.setState(prevState => ({ + privs: newPrivs, + submittable: Boolean(prevState.note.length) + })); } else { this.setState({ @@ -109,17 +101,10 @@ class PrivsEditModal extends React.Component { const newPrivs = this.state.privs; if (event.target.value.length) { // If the privs have also been changed, then set submittable to true - if (this.props.targetUser.privs !== newPrivs) { - this.setState({ - note: event.target.value, - submittable: true - }); - } - else { - this.setState({ - note: event.target.value - }); - } + this.setState({ + note: event.target.value, + submittable: Boolean(this.props.targetUser.privs !== newPrivs) + }); } else { this.setState({ diff --git a/src/server/routes/adminLogs.tsx b/src/server/routes/adminLogs.tsx index a845f9b816..c46c8183d5 100644 --- a/src/server/routes/adminLogs.tsx +++ b/src/server/routes/adminLogs.tsx @@ -33,8 +33,9 @@ const router = express.Router(); router.get('/', async (req, res, next) => { const {orm} = req.app.locals; - const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; - const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; + const query = parseQuery(req.url); + const size = parseInt(query.get('size'), 10) || 20; + const from = parseInt(query.get('from'), 10) || 0; function render(results, nextEnabled) { const props = generateProps(req, res, { @@ -73,8 +74,9 @@ router.get('/', async (req, res, next) => { // eslint-disable-next-line consistent-return router.get('/admin-logs', async (req, res, next) => { const {orm} = req.app.locals; - const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; - const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; + const query = parseQuery(req.url); + const size = parseInt(query.get('size'), 10) || 20; + const from = parseInt(query.get('from'), 10) || 0; try { const orderedRevisions = await getOrderedAdminLogs(from, size, orm); diff --git a/src/server/routes/adminPanel.tsx b/src/server/routes/adminPanel.tsx index 151d59802f..9193a9a2d3 100644 --- a/src/server/routes/adminPanel.tsx +++ b/src/server/routes/adminPanel.tsx @@ -52,8 +52,9 @@ router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req, res, const {orm} = req.app.locals; const query = parseQuery(req.url).get('q') ?? ''; const type = 'editor'; - const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; - const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; + const urlQry = parseQuery(req.url); + const size = parseInt(urlQry.get('size'), 10) || 20; + const from = parseInt(urlQry.get('from'), 10) || 0; try { let searchResults: SearchResultsT = { initialResults: [], diff --git a/src/server/routes/editor.tsx b/src/server/routes/editor.tsx index b3bae97d35..60440401b5 100644 --- a/src/server/routes/editor.tsx +++ b/src/server/routes/editor.tsx @@ -355,11 +355,9 @@ router.get('/:id', async (req, res, next) => { router.get('/:id/revisions', async (req, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; - - const size = - req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : DEFAULT_MAX_REVISIONS; - const from = - req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : DEFAULT_REVISION_OFFSET; + const query = parseQuery(req.url); + const size = parseInt(query.get('size'), 10) || DEFAULT_MAX_REVISIONS; + const from = parseInt(query.get('from'), 10) || DEFAULT_REVISION_OFFSET; try { // get 1 more result to check nextEnabled @@ -414,10 +412,9 @@ router.get('/:id/revisions/revisions', async (req, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; - const size = - req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : DEFAULT_MAX_REVISIONS; - const from = - req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : DEFAULT_REVISION_OFFSET; + const query = parseQuery(req.url); + const size = parseInt(query.get('size'), 10) || DEFAULT_MAX_REVISIONS; + const from = parseInt(query.get('from'), 10) || DEFAULT_REVISION_OFFSET; const orderedRevisions = await getOrderedRevisionForEditorPage(from, size, req).catch(next); @@ -623,10 +620,9 @@ router.get('/:id/collections', async (req, res, next) => { const DEFAULT_MAX_COLLECTIONS = 20; const DEFAULT_COLLECTION_OFFSET = 0; - const size = - req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : DEFAULT_MAX_COLLECTIONS; - const from = - req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : DEFAULT_COLLECTION_OFFSET; + const query = parseQuery(req.url); + const size = parseInt(query.get('size'), 10) || DEFAULT_MAX_COLLECTIONS; + const from = parseInt(query.get('from'), 10) || DEFAULT_COLLECTION_OFFSET; const type = req.query.type ? parseQuery(req.url).get('type') : null; @@ -682,9 +678,10 @@ router.get('/:id/collections', async (req, res, next) => { // eslint-disable-next-line consistent-return router.get('/:id/collections/collections', async (req, res, next) => { try { - const size = req.query.size ? parseInt(parseQuery(req.url).get('size'), 10) : 20; - const from = req.query.from ? parseInt(parseQuery(req.url).get('from'), 10) : 0; - const type = req.query.type ? parseQuery(req.url).get('type') : null; + const query = parseQuery(req.url); + const size = parseInt(query.get('size'), 10) || 20; + const from = parseInt(query.get('from'), 10) || 0; + const type = query.get('type'); const entityTypes = _.keys(commonUtils.getEntityModels(req.app.locals.orm)); if (!entityTypes.includes(type) && type !== null) { throw new error.BadRequestError(`Type ${type} do not exist`); From 66da8e3b885836c69d967ccaa8ec5e99d9f79707 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 20 Jul 2023 09:53:42 +0530 Subject: [PATCH 31/88] create helper function --- src/server/helpers/utils.ts | 4 ++++ src/server/routes/adminLogs.tsx | 10 +++++----- src/server/routes/adminPanel.tsx | 6 +++--- src/server/routes/editor.tsx | 20 ++++++++++---------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/server/helpers/utils.ts b/src/server/helpers/utils.ts index b80c61181f..22a556c3d2 100644 --- a/src/server/helpers/utils.ts +++ b/src/server/helpers/utils.ts @@ -361,3 +361,7 @@ export async function parseInitialState(req, type):Promise> export function parseQuery(url: string) { return new URLSearchParams(url.replace(/^.+?\?/, '')); } + +export function getIntFromQueryParams(query: URLSearchParams, type: string, fallback = 0) { + return parseInt(query.get(type), 10) || fallback; +} diff --git a/src/server/routes/adminLogs.tsx b/src/server/routes/adminLogs.tsx index c46c8183d5..c12f3c9e9a 100644 --- a/src/server/routes/adminLogs.tsx +++ b/src/server/routes/adminLogs.tsx @@ -18,6 +18,7 @@ import * as propHelpers from '../../client/helpers/props'; import {escapeProps, generateProps} from '../helpers/props'; +import {getIntFromQueryParams, parseQuery} from '../helpers/utils'; import AdminLogsPage from '../../client/components/pages/adminLogs'; import Layout from '../../client/containers/layout'; import React from 'react'; @@ -25,7 +26,6 @@ import ReactDOMServer from 'react-dom/server'; import express from 'express'; import {getNextEnabledAndResultsArray} from '../../common/helpers/utils'; import {getOrderedAdminLogs} from '../helpers/adminLogs'; -import {parseQuery} from '../helpers/utils'; import target from '../templates/target'; @@ -34,8 +34,8 @@ const router = express.Router(); router.get('/', async (req, res, next) => { const {orm} = req.app.locals; const query = parseQuery(req.url); - const size = parseInt(query.get('size'), 10) || 20; - const from = parseInt(query.get('from'), 10) || 0; + const size = getIntFromQueryParams(query, 'size', 20); + const from = getIntFromQueryParams(query, 'from'); function render(results, nextEnabled) { const props = generateProps(req, res, { @@ -75,8 +75,8 @@ router.get('/', async (req, res, next) => { router.get('/admin-logs', async (req, res, next) => { const {orm} = req.app.locals; const query = parseQuery(req.url); - const size = parseInt(query.get('size'), 10) || 20; - const from = parseInt(query.get('from'), 10) || 0; + const size = getIntFromQueryParams(query, 'size', 20); + const from = getIntFromQueryParams(query, 'from'); try { const orderedRevisions = await getOrderedAdminLogs(from, size, orm); diff --git a/src/server/routes/adminPanel.tsx b/src/server/routes/adminPanel.tsx index 9193a9a2d3..7d451c5378 100644 --- a/src/server/routes/adminPanel.tsx +++ b/src/server/routes/adminPanel.tsx @@ -24,13 +24,13 @@ import * as search from '../../common/helpers/search'; import {snakeCase as _snakeCase, isNil} from 'lodash'; import {escapeProps, generateProps} from '../helpers/props'; +import {getIntFromQueryParams, parseQuery} from '../helpers/utils'; import AdminPanelSearchPage from '../../client/components/pages/admin-panel-search'; import Layout from '../../client/containers/layout'; import {PrivilegeType} from '../../common/helpers/privileges-utils'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import express from 'express'; -import {parseQuery} from '../helpers/utils'; import target from '../templates/target'; @@ -53,8 +53,8 @@ router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req, res, const query = parseQuery(req.url).get('q') ?? ''; const type = 'editor'; const urlQry = parseQuery(req.url); - const size = parseInt(urlQry.get('size'), 10) || 20; - const from = parseInt(urlQry.get('from'), 10) || 0; + const size = getIntFromQueryParams(urlQry, 'size', 20); + const from = getIntFromQueryParams(urlQry, 'from'); try { let searchResults: SearchResultsT = { initialResults: [], diff --git a/src/server/routes/editor.tsx b/src/server/routes/editor.tsx index 60440401b5..0a1e79996b 100644 --- a/src/server/routes/editor.tsx +++ b/src/server/routes/editor.tsx @@ -28,6 +28,7 @@ import {AdminLogDataT, createAdminLog} from '../helpers/adminLogs'; import {eachMonthOfInterval, format, isAfter, isValid} from 'date-fns'; import {escapeProps, generateProps} from '../helpers/props'; import {getConsecutiveDaysWithEdits, getEntityVisits, getTypeCreation} from '../helpers/achievement'; +import {getIntFromQueryParams, parseQuery} from '../helpers/utils'; import AchievementsTab from '../../client/components/pages/parts/editor-achievements'; import CollectionsPage from '../../client/components/pages/collections'; import EditorContainer from '../../client/containers/editor'; @@ -41,7 +42,6 @@ import _ from 'lodash'; import express from 'express'; import {getOrderedCollectionsForEditorPage} from '../helpers/collections'; import {getOrderedRevisionForEditorPage} from '../helpers/revisions'; -import {parseQuery} from '../helpers/utils'; import target from '../templates/target'; @@ -356,8 +356,8 @@ router.get('/:id/revisions', async (req, res, next) => { const DEFAULT_MAX_REVISIONS = 20; const DEFAULT_REVISION_OFFSET = 0; const query = parseQuery(req.url); - const size = parseInt(query.get('size'), 10) || DEFAULT_MAX_REVISIONS; - const from = parseInt(query.get('from'), 10) || DEFAULT_REVISION_OFFSET; + const size = getIntFromQueryParams(query, 'size', DEFAULT_MAX_REVISIONS); + const from = getIntFromQueryParams(query, 'from', DEFAULT_REVISION_OFFSET); try { // get 1 more result to check nextEnabled @@ -413,8 +413,8 @@ router.get('/:id/revisions/revisions', async (req, res, next) => { const DEFAULT_REVISION_OFFSET = 0; const query = parseQuery(req.url); - const size = parseInt(query.get('size'), 10) || DEFAULT_MAX_REVISIONS; - const from = parseInt(query.get('from'), 10) || DEFAULT_REVISION_OFFSET; + const size = getIntFromQueryParams(query, 'size', DEFAULT_MAX_REVISIONS); + const from = getIntFromQueryParams(query, 'from', DEFAULT_REVISION_OFFSET); const orderedRevisions = await getOrderedRevisionForEditorPage(from, size, req).catch(next); @@ -621,10 +621,10 @@ router.get('/:id/collections', async (req, res, next) => { const DEFAULT_COLLECTION_OFFSET = 0; const query = parseQuery(req.url); - const size = parseInt(query.get('size'), 10) || DEFAULT_MAX_COLLECTIONS; - const from = parseInt(query.get('from'), 10) || DEFAULT_COLLECTION_OFFSET; + const size = getIntFromQueryParams(query, 'size', DEFAULT_MAX_COLLECTIONS); + const from = getIntFromQueryParams(query, 'from', DEFAULT_COLLECTION_OFFSET); - const type = req.query.type ? parseQuery(req.url).get('type') : null; + const type = query.get('type'); try { const entityTypes = _.keys(commonUtils.getEntityModels(req.app.locals.orm)); @@ -679,8 +679,8 @@ router.get('/:id/collections', async (req, res, next) => { router.get('/:id/collections/collections', async (req, res, next) => { try { const query = parseQuery(req.url); - const size = parseInt(query.get('size'), 10) || 20; - const from = parseInt(query.get('from'), 10) || 0; + const size = getIntFromQueryParams(query, 'size', 20); + const from = getIntFromQueryParams(query, 'from'); const type = query.get('type'); const entityTypes = _.keys(commonUtils.getEntityModels(req.app.locals.orm)); if (!entityTypes.includes(type) && type !== null) { From ada13ce9ec6e625b97b2e7e6db468715758db6af Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 20 Jul 2023 14:55:40 +0530 Subject: [PATCH 32/88] fix variable and parameter name --- src/server/helpers/utils.ts | 4 ++-- src/server/routes/adminPanel.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/helpers/utils.ts b/src/server/helpers/utils.ts index 22a556c3d2..a9659a9d37 100644 --- a/src/server/helpers/utils.ts +++ b/src/server/helpers/utils.ts @@ -362,6 +362,6 @@ export function parseQuery(url: string) { return new URLSearchParams(url.replace(/^.+?\?/, '')); } -export function getIntFromQueryParams(query: URLSearchParams, type: string, fallback = 0) { - return parseInt(query.get(type), 10) || fallback; +export function getIntFromQueryParams(query: URLSearchParams, name: string, fallback = 0) { + return parseInt(query.get(name), 10) || fallback; } diff --git a/src/server/routes/adminPanel.tsx b/src/server/routes/adminPanel.tsx index 7d451c5378..764552abe5 100644 --- a/src/server/routes/adminPanel.tsx +++ b/src/server/routes/adminPanel.tsx @@ -50,11 +50,11 @@ const router = express.Router(); */ router.get('/', auth.isAuthenticated, auth.isAuthorized(ADMIN), async (req, res, next) => { const {orm} = req.app.locals; - const query = parseQuery(req.url).get('q') ?? ''; const type = 'editor'; - const urlQry = parseQuery(req.url); - const size = getIntFromQueryParams(urlQry, 'size', 20); - const from = getIntFromQueryParams(urlQry, 'from'); + const urlQuery = parseQuery(req.url); + const query = urlQuery.get('q') ?? ''; + const size = getIntFromQueryParams(urlQuery, 'size', 20); + const from = getIntFromQueryParams(urlQuery, 'from'); try { let searchResults: SearchResultsT = { initialResults: [], From 3550b5b5fd78cea485aa6efdf7f5c6256e6bbabc Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 21 Jul 2023 00:22:18 +0530 Subject: [PATCH 33/88] refactor code and make note field required --- src/client/components/pages/adminLogs.tsx | 5 ++-- .../pages/parts/privs-edit-modal.js | 1 + src/client/helpers/adminLogs.tsx | 24 ++++++------------- src/server/routes/adminLogs.tsx | 2 +- src/server/routes/editor.tsx | 7 +++++- test/src/server/routes/editor.js | 4 ++-- 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/client/components/pages/adminLogs.tsx b/src/client/components/pages/adminLogs.tsx index 4b8d1d2a42..db365e8e71 100644 --- a/src/client/components/pages/adminLogs.tsx +++ b/src/client/components/pages/adminLogs.tsx @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import React, {useCallback, useState} from 'react'; +import React, {useState} from 'react'; import {AdminLogDataT} from '../../../server/helpers/adminLogs'; import AdminLogsTable from './parts/admin-logs-table'; import PagerElement from './parts/pager'; @@ -31,7 +31,6 @@ type Props = { function AdminLogsPage({from, nextEnabled, results, size}: Props) { const [logs, setLogs] = useState(results); - const searchResultsCallback = useCallback((newResults) => setLogs(newResults), []); const paginationUrl = './admin-logs/admin-logs'; return (
@@ -43,7 +42,7 @@ function AdminLogsPage({from, nextEnabled, results, size}: Props) { nextEnabled={nextEnabled} paginationUrl={paginationUrl} results={logs} - searchResultsCallback={searchResultsCallback} + searchResultsCallback={setLogs} size={size} />
diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 568d2df57e..1421a53324 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -133,6 +133,7 @@ class PrivsEditModal extends React.Component { Note/Reason: { } try { - // fetch 1 more revision than required to check nextEnabled + // fetch 1 more log item than required to check nextEnabled const orderedLogs = await getOrderedAdminLogs(from, size + 1, orm); const {newResultsArray, nextEnabled} = getNextEnabledAndResultsArray(orderedLogs, size); return render(newResultsArray, nextEnabled); diff --git a/src/server/routes/editor.tsx b/src/server/routes/editor.tsx index 0a1e79996b..5f9e0f0a3c 100644 --- a/src/server/routes/editor.tsx +++ b/src/server/routes/editor.tsx @@ -194,7 +194,12 @@ router.post('/privs/edit/handler', auth.isAuthenticatedForHandler, auth.isAuthor 'No change to Privileges', req )); } - await editor.save({privs: newPrivs}); + if (!note.length) { + return next(new error.FormSubmissionError( + 'You must add a note to this action!', req + )); + } + await editor.save({privs: newPrivs}, {transacting: trx}); const logData: AdminLogDataT = { actionType, adminId, diff --git a/test/src/server/routes/editor.js b/test/src/server/routes/editor.js index ccbc6c4a20..75ea3e2f4c 100644 --- a/test/src/server/routes/editor.js +++ b/test/src/server/routes/editor.js @@ -151,7 +151,7 @@ describe('Editor with Administrator priv', () => { it('should be able to edit privs of an editor', async () => { await createEditor(targetUserId, oldPrivs); - const note = ''; + const note = 'testing'; const data = { adminId, newPrivs, @@ -181,7 +181,7 @@ describe('Editor without Administrator priv', () => { it('should not be able to edit privs of an editor', async () => { await createEditor(targetUserId, oldPrivs); - const note = ''; + const note = 'testing'; const data = { adminId, newPrivs, From 9c52acae3084371f62968e91a226aacbb8a9b73b Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 21 Jul 2023 19:27:41 +0530 Subject: [PATCH 34/88] use selective imports --- src/client/components/pages/parts/admin-log-statement.js | 4 ++-- src/client/components/pages/parts/admin-logs-table.js | 7 ++----- src/client/components/pages/parts/privs-edit-modal.js | 6 ++---- src/server/routes/adminLogs.tsx | 4 ++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/client/components/pages/parts/admin-log-statement.js b/src/client/components/pages/parts/admin-log-statement.js index 1d01fa138c..8e1d7ecda7 100644 --- a/src/client/components/pages/parts/admin-log-statement.js +++ b/src/client/components/pages/parts/admin-log-statement.js @@ -24,7 +24,7 @@ import {constructAdminLogStatement} from '../../../helpers/adminLogs'; function AdminLogStatement({logData}) { const {note} = logData; return ( -
+ <> {constructAdminLogStatement(logData)} { note.length ? @@ -32,7 +32,7 @@ function AdminLogStatement({logData}) { Note/Reason: {note}
: null } -
+ ); } diff --git a/src/client/components/pages/parts/admin-logs-table.js b/src/client/components/pages/parts/admin-logs-table.js index d8dc0ffe7a..a61c4ee021 100644 --- a/src/client/components/pages/parts/admin-logs-table.js +++ b/src/client/components/pages/parts/admin-logs-table.js @@ -16,16 +16,13 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import * as bootstrap from 'react-bootstrap'; -import * as utilsHelper from '../../../helpers/utils'; import AdminLogStatement from './admin-log-statement'; import PropTypes from 'prop-types'; import React from 'react'; +import {Table} from 'react-bootstrap'; +import {formatDate} from '../../../helpers/utils'; -const {Table} = bootstrap; -const {formatDate} = utilsHelper; - function AdminLogsTable(props) { const {results, tableHeading} = props; return ( diff --git a/src/client/components/pages/parts/privs-edit-modal.js b/src/client/components/pages/parts/privs-edit-modal.js index 1421a53324..7ad3c36d88 100644 --- a/src/client/components/pages/parts/privs-edit-modal.js +++ b/src/client/components/pages/parts/privs-edit-modal.js @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import * as bootstrap from 'react-bootstrap'; +import {Button, Form, Modal} from 'react-bootstrap'; import {PrivilegeTypes, getPrivilegeShieldIcon, getPrivilegeTitleFromBit} from '../../../../common/helpers/privileges-utils'; import {faPencilAlt, faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; @@ -25,8 +25,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -const {Button, Form, Modal} = bootstrap; - class PrivsEditModal extends React.Component { constructor(props) { super(props); @@ -135,8 +133,8 @@ class PrivsEditModal extends React.Component { diff --git a/src/server/routes/adminLogs.tsx b/src/server/routes/adminLogs.tsx index 38805cd82e..7dae17ba54 100644 --- a/src/server/routes/adminLogs.tsx +++ b/src/server/routes/adminLogs.tsx @@ -79,8 +79,8 @@ router.get('/admin-logs', async (req, res, next) => { const from = getIntFromQueryParams(query, 'from'); try { - const orderedRevisions = await getOrderedAdminLogs(from, size, orm); - res.send(orderedRevisions); + const orderedLogs = await getOrderedAdminLogs(from, size, orm); + res.json(orderedLogs); } catch (err) { return next(err); From 715ec778e6f1d651a1cbb22499b909a40c951ea2 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 28 Jul 2023 14:31:52 +0530 Subject: [PATCH 35/88] apply break-word in footer --- src/client/stylesheets/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/stylesheets/style.scss b/src/client/stylesheets/style.scss index dd184adbe7..2c9fd1b85f 100644 --- a/src/client/stylesheets/style.scss +++ b/src/client/stylesheets/style.scss @@ -237,6 +237,7 @@ html { .footer { position: absolute; width: 100%; + word-break: break-word; background-color: #f5f5f5; bottom: 0; } From b7b2230f0a857680be77ee59f80072e3e3077054 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Fri, 28 Jul 2023 19:19:18 +0530 Subject: [PATCH 36/88] feat(RelationshipType): Relationship Type Editor --- .../forms/type-editor/relationship-type.tsx | 362 ++++++++++++++++++ .../forms/type-editor/typeUtils.tsx | 74 ++++ .../type-editor/relationship-type.tsx | 51 +++ src/server/helpers/middleware.ts | 2 + src/server/routes.js | 2 + .../routes/type-editor/relationship-type.tsx | 51 +++ .../routes/type-editor/relationship-types.tsx | 90 +++++ webpack.client.js | 3 +- 8 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 src/client/components/forms/type-editor/relationship-type.tsx create mode 100644 src/client/components/forms/type-editor/typeUtils.tsx create mode 100644 src/client/controllers/type-editor/relationship-type.tsx create mode 100644 src/server/routes/type-editor/relationship-type.tsx create mode 100644 src/server/routes/type-editor/relationship-types.tsx diff --git a/src/client/components/forms/type-editor/relationship-type.tsx b/src/client/components/forms/type-editor/relationship-type.tsx new file mode 100644 index 0000000000..5897168688 --- /dev/null +++ b/src/client/components/forms/type-editor/relationship-type.tsx @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import {Button, Card, Col, Form, Modal, Row} from 'react-bootstrap'; +import React, {ChangeEvent, FormEvent, useCallback, useState} from 'react'; +import {RelationshipTypeDataT, RelationshipTypeEditorPropsT, defaultRelationshipTypeData, entityTypeOptions, renderSelectedParent} from './typeUtils'; +import {faPencilAlt, faPlus, faTimes} from '@fortawesome/free-solid-svg-icons'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import ReactSelect from 'react-select'; + + +function RelationshipTypeEditor({relationshipTypeData, parentTypes}: RelationshipTypeEditorPropsT) { + const [formData, setFormData] = useState(relationshipTypeData); + + // State for the ParentType modal + const [showModal, setShowModal] = useState(false); + const [selectedParentType, setSelectedParentType] = useState(formData.parentId); + const [childOrder, setChildOrder] = useState(formData.childOrder); + + // Callback function for opening the modal + const handleAddParent = useCallback(() => { + setShowModal(true); + }, []); + + // Callback function for closing the modal, the state of the modal should alse be reset + const handleModalClose = useCallback(() => { + setSelectedParentType(null); + setChildOrder(0); + setShowModal(false); + }, []); + + // Function to handle parent type selection in ParentType modal + const handleParentTypeChange = useCallback((selectedOption) => { + if (selectedOption) { + setSelectedParentType(selectedOption.id); + } + else { + setSelectedParentType(null); + } + }, [selectedParentType]); + + // Function to handle child order input in ParentType modal + const handleChildOrderChange = useCallback((event: ChangeEvent) => { + const value = parseInt(event.target.value, 10); + setChildOrder(isNaN(value) ? 0 : value); + }, [formData, childOrder]); + + // Function to handle parent removal using useCallback + const handleRemoveParent = useCallback(() => { + setFormData((prevFormData) => ({ + ...prevFormData, + childOrder: 0, parentId: null + })); + setChildOrder(0); + setSelectedParentType(null); + }, [formData]); + + const handleEditParent = useCallback(() => { + setShowModal(true); + }, []); + + // Function to handle parent type and child order edit submission + const handleModalSubmit = useCallback(() => { + if (selectedParentType !== null) { + setFormData({ + ...formData, + childOrder, parentId: selectedParentType + }); + setShowModal(false); + } + }, [formData, childOrder, selectedParentType]); + + const handleInputChange = useCallback((event: ChangeEvent) => { + const {name, value} = event.target; + setFormData((prevFormData) => ({ + ...prevFormData, + [name]: value + })); + }, [formData]); + + const handleDeprecatedChange = useCallback((event: ChangeEvent) => { + const {value} = event.target; + setFormData((prevFormData) => ({ + ...prevFormData, + deprecated: value === 'true' + })); + }, [formData]); + + const getEntityTypeLabel = useCallback(option => option.name, []); + + const getEntityTypeValue = useCallback(option => option.name, []); + + const getParentTypeValue = useCallback(option => option.id, []); + + // Callback function to format the option label to include both forwardStatement and reverseStatement + const formatParentTypeOptionLabel = useCallback(option => ( +
+
{option.sourceEntityType} {option.linkPhrase} {option.targetEntityType}
+
{option.targetEntityType} {option.reverseLinkPhrase} {option.sourceEntityType}
+
+ ), []); + + const handleSourceEntityTypeChange = useCallback((selectedOption) => { + if (selectedOption) { + setFormData({...formData, sourceEntityType: selectedOption.name}); + } + else { + setFormData({...formData, sourceEntityType: null}); + } + }, [formData]); + + const handleTargetEntityTypeChange = useCallback((selectedOption) => { + if (selectedOption) { + setFormData({...formData, targetEntityType: selectedOption.name}); + } + else { + setFormData({...formData, targetEntityType: null}); + } + }, [formData]); + + const handleSubmit = useCallback((event: FormEvent) => { + event.preventDefault(); + // console.log(formData); + }, [formData]); + + const lgCol = {offset: 3, span: 6}; + + return ( +
+ + + Add Relationship Type + + + + + + Label + + + + + + + + Description + + + + + + + + Link Phrase + + + + + + + + Reverse Link Phrase + + + + + + + + Source Entity Type + option.name === relationshipTypeData.sourceEntityType)} + getOptionLabel={getEntityTypeLabel} + getOptionValue={getEntityTypeValue} + instanceId="sourceEntityType" + options={entityTypeOptions} + placeholder="Select Source Entity Type" + onChange={handleSourceEntityTypeChange} + /> + + + + + + + Target Entity Type + option.name === relationshipTypeData.targetEntityType)} + getOptionLabel={getEntityTypeLabel} + getOptionValue={getEntityTypeValue} + instanceId="targetEntityType" + options={entityTypeOptions} + placeholder="Select Target Entity Type" + onChange={handleTargetEntityTypeChange} + /> + + + + + + + Parent Relationship + {!formData.parentId ? ( + + + + + + ) : ( + + + {formData.parentId && renderSelectedParent(formData.parentId, formData.childOrder, parentTypes)} +
+ + +
+ +
+ )} +
+ +
+ + + + + Deprecated: + + + + + + + + + + + + + {/* Modal for selecting parent type */} + + + {formData.parentId ? 'Edit Parent' : 'Add a Parent'} + + + + Parent Type: + option.id === selectedParentType)} + onChange={handleParentTypeChange} + /> + + + Child Order: + + + + + + + + +
+
+
+ ); +} + +RelationshipTypeEditor.defaultProps = { + relationshipTypeData: defaultRelationshipTypeData +}; + +export default RelationshipTypeEditor; diff --git a/src/client/components/forms/type-editor/typeUtils.tsx b/src/client/components/forms/type-editor/typeUtils.tsx new file mode 100644 index 0000000000..812eae3507 --- /dev/null +++ b/src/client/components/forms/type-editor/typeUtils.tsx @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import type {EntityTypeString} from 'bookbrainz-data/lib/types/entity'; +import React from 'react'; + + +export interface RelationshipTypeDataT { + id?: number; + label: string; + description: string; + linkPhrase: string; + reverseLinkPhrase: string; + deprecated: boolean; + parentId: number | null; + childOrder: number; + sourceEntityType: EntityTypeString; + targetEntityType: EntityTypeString; +} + +export interface RelationshipTypeEditorPropsT { + relationshipTypeData: RelationshipTypeDataT; + parentTypes: RelationshipTypeDataT[]; + attributeTypes: any; + user: any; +} + +export const defaultRelationshipTypeData: RelationshipTypeDataT = { + childOrder: 0, + deprecated: false, + description: '', + label: '', + linkPhrase: '', + parentId: null, + reverseLinkPhrase: '', + sourceEntityType: null, + targetEntityType: null +}; + +export const entityTypeOptions = ['Author', 'Work', 'Series', 'Edition', 'Edition-Group', 'Publisher'].map((entity) => ({ + name: entity +})); + +export function renderSelectedParent(selectedParentID: number, childOrder: number, parentTypes: RelationshipTypeDataT[]) { + const parent = parentTypes.find(relationship => relationship.id === selectedParentID); + if (parent) { + return ( +
+
+ Forward Phrase: {parent.sourceEntityType} {parent.linkPhrase} {parent.targetEntityType} +
+
+ Reverse Phrase: {parent.targetEntityType} {parent.reverseLinkPhrase} {parent.sourceEntityType} +
+
Child Order: {childOrder}
+
+ ); + } + return null; +} diff --git a/src/client/controllers/type-editor/relationship-type.tsx b/src/client/controllers/type-editor/relationship-type.tsx new file mode 100644 index 0000000000..e04265a076 --- /dev/null +++ b/src/client/controllers/type-editor/relationship-type.tsx @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import {extractChildProps, extractLayoutProps} from '../../helpers/props'; +import {AppContainer} from 'react-hot-loader'; +import Layout from '../../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import RelationshipTypeEditor from '../../components/forms/type-editor/relationship-type'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; + +ReactDOM.hydrate( + + + + + , + document.getElementById('target') +); + + +/* + * As we are not exporting a component, + * we cannot use the react-hot-loader module wrapper, + * but instead directly use webpack Hot Module Replacement API + */ + +if (module.hot) { + module.hot.accept(); +} + diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index 0349ec2bfe..3cc31ea731 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -71,6 +71,8 @@ export const loadSeriesOrderingTypes = makeLoader('SeriesOrderingType', 'seriesOrderingTypes'); export const loadRelationshipTypes = makeLoader('RelationshipType', 'relationshipTypes', null, ['attributeTypes']); +export const loadParentRelationshipTypes = + makeLoader('RelationshipType', 'parentTypes'); export const loadGenders = makeLoader('Gender', 'genders', (a, b) => a.id > b.id); diff --git a/src/server/routes.js b/src/server/routes.js index 999b1e1fa1..3974439375 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -31,6 +31,7 @@ import indexRouter from './routes/index'; import mergeRouter from './routes/merge'; import publisherRouter from './routes/entity/publisher'; import registerRouter from './routes/register'; +import relationshipTypeRouter from './routes/type-editor/relationship-type'; import reviewsRouter from './routes/reviews'; import revisionRouter from './routes/revision'; import revisionsRouter from './routes/revisions'; @@ -56,6 +57,7 @@ function initRootRoutes(app) { app.use('/external-service', externalServiceRouter); app.use('/admin-panel', adminPanelRouter); app.use('/admin-logs', adminLogsRouter); + app.use('/relationship-type', relationshipTypeRouter); } function initEditionGroupRoutes(app) { diff --git a/src/server/routes/type-editor/relationship-type.tsx b/src/server/routes/type-editor/relationship-type.tsx new file mode 100644 index 0000000000..18154f8a2e --- /dev/null +++ b/src/server/routes/type-editor/relationship-type.tsx @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as middleware from '../../helpers/middleware'; +import * as propHelpers from '../../../client/helpers/props'; +import {escapeProps, generateProps} from '../../helpers/props'; +import Layout from '../../../client/containers/layout'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import RelationshipTypeEditor from '../../../client/components/forms/type-editor/relationship-type'; +import express from 'express'; +import target from '../../templates/target'; + + +const router = express.Router(); + +router.get('/create', auth.isAuthenticated, middleware.loadParentRelationshipTypes, (req, res) => { + const {parentTypes} = res.locals; + const props = generateProps(req, res, { + parentTypes + }); + const script = '/js/relationship-type/create.js'; + const markup = ReactDOMServer.renderToString( + + + + ); + res.send(target({ + markup, + props: escapeProps(props), + script + })); +}); + +export default router; diff --git a/src/server/routes/type-editor/relationship-types.tsx b/src/server/routes/type-editor/relationship-types.tsx new file mode 100644 index 0000000000..aa6e5a2303 --- /dev/null +++ b/src/server/routes/type-editor/relationship-types.tsx @@ -0,0 +1,90 @@ +// /* +// * Copyright (C) 2023 Shivam Awasthi +// * +// * This program is free software; you can redistribute it and/or modify +// * it under the terms of the GNU General Public License as published by +// * the Free Software Foundation; either version 2 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 General Public License for more details. +// * +// * You should have received a copy of the GNU General Public License along +// * with this program; if not, write to the Free Software Foundation, Inc., +// * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// */ + +// import * as propHelpers from '../../../client/helpers/props'; +// import {escapeProps, generateProps} from '../../helpers/props'; +// import {getIntFromQueryParams, parseQuery} from '../../helpers/utils'; +// import Layout from '../../../client/containers/layout'; +// import React from 'react'; +// import ReactDOMServer from 'react-dom/server'; +// import express from 'express'; +// import {getNextEnabledAndResultsArray} from '../../../common/helpers/utils'; +// import {getOrderedAdminLogs} from '../../helpers/adminLogs'; +// import target from '../../templates/target'; +// import RelationshipTypesPage from '../../../client/components/pages/relationshipTypes'; + + +// const router = express.Router(); + +// router.get('/', async (req, res, next) => { +// const {orm} = req.app.locals; +// const query = parseQuery(req.url); +// const size = getIntFromQueryParams(query, 'size', 20); +// const from = getIntFromQueryParams(query, 'from'); + +// function render(results, nextEnabled) { +// const props = generateProps(req, res, { +// from, +// nextEnabled, +// results, +// size +// }); + +// const markup = ReactDOMServer.renderToString( +// +// +// +// ); + +// res.send(target({ +// markup, +// props: escapeProps(props), +// script: '/js/relationshipTypes.js', +// title: 'Relationship Types' +// })); +// } + +// try { +// // fetch 1 more relationship type than required to check nextEnabled +// const orderedRelationshipTypes = await getOrderedRelationshipTypes(from, size + 1, orm); +// const {newResultsArray, nextEnabled} = getNextEnabledAndResultsArray(orderedRelationshipTypes, size); +// return render(newResultsArray, nextEnabled); +// } +// catch (err) { +// return next(err); +// } +// }); + + +// // eslint-disable-next-line consistent-return +// router.get('/admin-logs', async (req, res, next) => { +// const {orm} = req.app.locals; +// const query = parseQuery(req.url); +// const size = getIntFromQueryParams(query, 'size', 20); +// const from = getIntFromQueryParams(query, 'from'); + +// try { +// const orderedLogs = await getOrderedAdminLogs(from, size, orm); +// res.json(orderedLogs); +// } +// catch (err) { +// return next(err); +// } +// }); + +// export default router; diff --git a/webpack.client.js b/webpack.client.js index 9bcfc4e5ac..3a19173e05 100644 --- a/webpack.client.js +++ b/webpack.client.js @@ -41,7 +41,8 @@ const clientConfig = { 'entity-editor': ['./entity-editor/controller.js'], 'unified-form':['./unified-form/controller.js'], 'entity-merge': ['./entity-editor/entity-merge.tsx'], - style: './stylesheets/style.scss' + style: './stylesheets/style.scss', + 'relationship-type/create': ['./controllers/type-editor/relationship-type.tsx'] }, externals: { moment: 'moment' From a675445f37f07bb3c79f93ad7bc5610c43ec0028 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Mon, 31 Jul 2023 20:35:39 +0530 Subject: [PATCH 37/88] feat(RelationshipTypes): Add a Relationship Types page --- .../pages/parts/relationship-types-tree.tsx | 104 ++++++++++++++++++ .../components/pages/relationshipTypes.tsx | 43 ++++++++ src/client/controllers/relationshipTypes.tsx | 49 +++++++++ src/server/helpers/typeRouteUtils.ts | 104 ++++++++++++++++++ src/server/routes.js | 2 + src/server/routes/relationship-types.tsx | 49 +++++++++ .../routes/type-editor/relationship-types.tsx | 90 --------------- webpack.client.js | 1 + 8 files changed, 352 insertions(+), 90 deletions(-) create mode 100644 src/client/components/pages/parts/relationship-types-tree.tsx create mode 100644 src/client/components/pages/relationshipTypes.tsx create mode 100644 src/client/controllers/relationshipTypes.tsx create mode 100644 src/server/helpers/typeRouteUtils.ts create mode 100644 src/server/routes/relationship-types.tsx delete mode 100644 src/server/routes/type-editor/relationship-types.tsx diff --git a/src/client/components/pages/parts/relationship-types-tree.tsx b/src/client/components/pages/parts/relationship-types-tree.tsx new file mode 100644 index 0000000000..db0a646422 --- /dev/null +++ b/src/client/components/pages/parts/relationship-types-tree.tsx @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import React, {useCallback, useState} from 'react'; +import {Button} from 'react-bootstrap'; +import {RelationshipTypeDataT} from '../../forms/type-editor/typeUtils'; + + +type RelationshipTypeTreePropsT = { + relationshipTypes: RelationshipTypeDataT[], + parentId?: number | null, + indentLevel?: number +}; + +function RelationshipTypeTree({relationshipTypes, parentId, indentLevel}: RelationshipTypeTreePropsT) { + const [expandedRelationshipTypeIds, setExpandedRelationshipTypeIds] = useState([]); + + function toggleExpand(relTypeId) { + setExpandedRelationshipTypeIds((prevExpandedIds) => { + if (prevExpandedIds.includes(relTypeId)) { + return prevExpandedIds.filter((id) => id !== relTypeId); + } + return [...prevExpandedIds, relTypeId]; + }); + } + + const handleClick = useCallback((event) => { + const relationshipTypeId = parseInt(event.target.value, 10); + toggleExpand(relationshipTypeId); + }, [expandedRelationshipTypeIds]); + + const filteredRelationshipTypes = relationshipTypes.filter((relType) => relType.parentId === parentId); + + return ( +
    + {filteredRelationshipTypes.map(relType => { + let relOuterClass = `margin-left-d${indentLevel * 20}`; + if (relType.deprecated) { + relOuterClass = `margin-left-d${indentLevel * 20} text-muted`; + } + return ( +
  • + + {relType.label} + + + {expandedRelationshipTypeIds.includes(relType.id) && ( +
    +
    Forward link phrase: {relType.linkPhrase}
    +
    Reverse link phrase: {relType.reverseLinkPhrase}
    +
    Source Entity Type: {relType.sourceEntityType}
    +
    Target Entity Type: {relType.targetEntityType}
    +
    Description: {relType.description}
    +
    Child Order: {relType.childOrder}
    +
    Deprecated: {relType.deprecated ? 'Yes' : 'No'}
    +
    + +
    +
    + )} + +
  • + ); + })} +
+ ); +} + +RelationshipTypeTree.defaultProps = { + indentLevel: 0, + parentId: null +}; + +export default RelationshipTypeTree; diff --git a/src/client/components/pages/relationshipTypes.tsx b/src/client/components/pages/relationshipTypes.tsx new file mode 100644 index 0000000000..d589261587 --- /dev/null +++ b/src/client/components/pages/relationshipTypes.tsx @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import {Card} from 'react-bootstrap'; +import React from 'react'; +import {RelationshipTypeDataT} from '../forms/type-editor/typeUtils'; +import RelationshipTypeTree from './parts/relationship-types-tree'; + + +type Props = { + relationshipTypes: RelationshipTypeDataT[] +}; + +function RelationshipTypesPage({relationshipTypes}: Props) { + return ( + + + Relationship Types + + + + + + ); +} + +export default RelationshipTypesPage; diff --git a/src/client/controllers/relationshipTypes.tsx b/src/client/controllers/relationshipTypes.tsx new file mode 100644 index 0000000000..fd01373c9d --- /dev/null +++ b/src/client/controllers/relationshipTypes.tsx @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {AppContainer} from 'react-hot-loader'; +import Layout from '../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import RelationshipTypesPage from '../components/pages/relationshipTypes'; +import {extractLayoutProps} from '../helpers/props'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; +const markup = ( + + + + + +); + +ReactDOM.hydrate(markup, document.getElementById('target')); + +/* + * As we are not exporting a component, + * we cannot use the react-hot-loader module wrapper, + * but instead directly use webpack Hot Module Replacement API + */ + +if (module.hot) { + module.hot.accept(); +} diff --git a/src/server/helpers/typeRouteUtils.ts b/src/server/helpers/typeRouteUtils.ts new file mode 100644 index 0000000000..42d77f9c8e --- /dev/null +++ b/src/server/helpers/typeRouteUtils.ts @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +function getChangedAttributeTypes(oldAttributes, newAttributes) { + const attributesCommon = oldAttributes.filter(value => newAttributes.includes(value)); + const attributesToBeRemoved = oldAttributes.filter(value => !attributesCommon.includes(value)); + const attributesToBeAdded = newAttributes.filter(value => !attributesCommon.includes(value)); + + return {attributesToBeAdded, attributesToBeRemoved}; +} + +/** + * A handler for create or edit actions on relationship types. + * @param {object} req - request object + * @param {object} res - response object + * @param {object} next - next object + * @returns {promise} res.send promise + * @description + * Creates a new reationship type or updates an existing relationship type + */ +export async function relationshipTypeCreateOrEditHandler(req, res, next) { + try { + const {RelationshipType, RelationshipTypeAttributeType, bookshelf} = req.app.locals.orm; + const trx = await bookshelf.transaction(); + let newRelationshipType; + let method; + if (!req.params.id) { + newRelationshipType = await RelationshipType.forge(); + method = 'insert'; + } + else { + newRelationshipType = await RelationshipType.forge({id: parseInt(req.params.id, 10)}).fetch({ + require: true + }); + method = 'update'; + } + const { + attributeTypes, + childOrder, + deprecated, + description, + label, + linkPhrase, + oldAttributeTypes, + parentId, + reverseLinkPhrase, + sourceEntityType, + targetEntityType + } = req.body; + + + newRelationshipType.set('description', description); + newRelationshipType.set('label', label); + newRelationshipType.set('deprecated', deprecated); + newRelationshipType.set('linkPhrase', linkPhrase); + newRelationshipType.set('reverseLinkPhrase', reverseLinkPhrase); + newRelationshipType.set('childOrder', childOrder); + newRelationshipType.set('parentId', parentId); + newRelationshipType.set('sourceEntityType', sourceEntityType); + newRelationshipType.set('targetEntityType', targetEntityType); + + const relationshipType = await newRelationshipType.save(null, {method}, {transacting: trx}); + // Attributes + const {attributesToBeAdded, attributesToBeRemoved} = getChangedAttributeTypes(oldAttributeTypes, attributeTypes); + + attributesToBeRemoved.map(async attributeID => { + await new RelationshipTypeAttributeType() + .query((qb) => { + qb.where('relationship_type', newRelationshipType.id); + qb.where('attribute_type', attributeID); + }).destroy(); + }); + + attributesToBeAdded.map(async attributeID => { + const newRelTypeAttrType = await new RelationshipTypeAttributeType(); + newRelTypeAttrType.set('relationshipType', relationshipType.id); + newRelTypeAttrType.set('attributeType', attributeID); + + await newRelTypeAttrType.save(null, {method: 'insert'}, {transacting: trx}); + }); + + await trx.commit(); + + return res.send(relationshipType.toJSON()); + } + catch (err) { + return next(err); + } +} diff --git a/src/server/routes.js b/src/server/routes.js index 3974439375..a4c3d66981 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -32,6 +32,7 @@ import mergeRouter from './routes/merge'; import publisherRouter from './routes/entity/publisher'; import registerRouter from './routes/register'; import relationshipTypeRouter from './routes/type-editor/relationship-type'; +import relationshipTypesRouter from './routes/relationship-types'; import reviewsRouter from './routes/reviews'; import revisionRouter from './routes/revision'; import revisionsRouter from './routes/revisions'; @@ -58,6 +59,7 @@ function initRootRoutes(app) { app.use('/admin-panel', adminPanelRouter); app.use('/admin-logs', adminLogsRouter); app.use('/relationship-type', relationshipTypeRouter); + app.use('/relationship-types', relationshipTypesRouter); } function initEditionGroupRoutes(app) { diff --git a/src/server/routes/relationship-types.tsx b/src/server/routes/relationship-types.tsx new file mode 100644 index 0000000000..01c4aaff3d --- /dev/null +++ b/src/server/routes/relationship-types.tsx @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import * as middleware from '../helpers/middleware'; +import * as propHelpers from '../../client/helpers/props'; +import {escapeProps, generateProps} from '../helpers/props'; +import Layout from '../../client/containers/layout'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import RelationshipTypesPage from '../../client/components/pages/relationshipTypes'; +import express from 'express'; +import target from '../templates/target'; + + +const router = express.Router(); + +router.get('/', middleware.loadRelationshipTypes, (req, res) => { + const {relationshipTypes} = res.locals; + const props = generateProps(req, res, { + relationshipTypes + }); + const markup = ReactDOMServer.renderToString( + + + + ); + res.send(target({ + markup, + props: escapeProps(props), + script: '/js/relationshipTypes.js', + title: 'Relationship Types' + })); +}); + +export default router; diff --git a/src/server/routes/type-editor/relationship-types.tsx b/src/server/routes/type-editor/relationship-types.tsx deleted file mode 100644 index aa6e5a2303..0000000000 --- a/src/server/routes/type-editor/relationship-types.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// /* -// * Copyright (C) 2023 Shivam Awasthi -// * -// * This program is free software; you can redistribute it and/or modify -// * it under the terms of the GNU General Public License as published by -// * the Free Software Foundation; either version 2 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 General Public License for more details. -// * -// * You should have received a copy of the GNU General Public License along -// * with this program; if not, write to the Free Software Foundation, Inc., -// * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// */ - -// import * as propHelpers from '../../../client/helpers/props'; -// import {escapeProps, generateProps} from '../../helpers/props'; -// import {getIntFromQueryParams, parseQuery} from '../../helpers/utils'; -// import Layout from '../../../client/containers/layout'; -// import React from 'react'; -// import ReactDOMServer from 'react-dom/server'; -// import express from 'express'; -// import {getNextEnabledAndResultsArray} from '../../../common/helpers/utils'; -// import {getOrderedAdminLogs} from '../../helpers/adminLogs'; -// import target from '../../templates/target'; -// import RelationshipTypesPage from '../../../client/components/pages/relationshipTypes'; - - -// const router = express.Router(); - -// router.get('/', async (req, res, next) => { -// const {orm} = req.app.locals; -// const query = parseQuery(req.url); -// const size = getIntFromQueryParams(query, 'size', 20); -// const from = getIntFromQueryParams(query, 'from'); - -// function render(results, nextEnabled) { -// const props = generateProps(req, res, { -// from, -// nextEnabled, -// results, -// size -// }); - -// const markup = ReactDOMServer.renderToString( -// -// -// -// ); - -// res.send(target({ -// markup, -// props: escapeProps(props), -// script: '/js/relationshipTypes.js', -// title: 'Relationship Types' -// })); -// } - -// try { -// // fetch 1 more relationship type than required to check nextEnabled -// const orderedRelationshipTypes = await getOrderedRelationshipTypes(from, size + 1, orm); -// const {newResultsArray, nextEnabled} = getNextEnabledAndResultsArray(orderedRelationshipTypes, size); -// return render(newResultsArray, nextEnabled); -// } -// catch (err) { -// return next(err); -// } -// }); - - -// // eslint-disable-next-line consistent-return -// router.get('/admin-logs', async (req, res, next) => { -// const {orm} = req.app.locals; -// const query = parseQuery(req.url); -// const size = getIntFromQueryParams(query, 'size', 20); -// const from = getIntFromQueryParams(query, 'from'); - -// try { -// const orderedLogs = await getOrderedAdminLogs(from, size, orm); -// res.json(orderedLogs); -// } -// catch (err) { -// return next(err); -// } -// }); - -// export default router; diff --git a/webpack.client.js b/webpack.client.js index 3a19173e05..8cef258b61 100644 --- a/webpack.client.js +++ b/webpack.client.js @@ -30,6 +30,7 @@ const clientConfig = { externalService: ['./controllers/externalService.js'], index: ['./controllers/index.js'], registrationDetails: ['./controllers/registrationDetails.js'], + relationshipTypes: ['./controllers/relationshipTypes.tsx'], revision: ['./controllers/revision.js'], revisions: ['./controllers/revisions.js'], search: ['./controllers/search.js'], From 1c49e91b565683c12067679facd34cdccbe00244 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Mon, 31 Jul 2023 20:36:07 +0530 Subject: [PATCH 38/88] Privilege Dropdown improvements --- src/client/containers/layout.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 58386ca9d0..24da9048f7 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -23,7 +23,7 @@ import * as bootstrap from 'react-bootstrap'; import { - faChartLine, faGripVertical, faLink, faListUl, faPlus, faQuestionCircle, + faChartLine, faGripVertical, faLink, faListUl, faNewspaper, faPlus, faQuestionCircle, faSearch, faShieldHalved, faSignInAlt, faSignOutAlt, faTrophy, faUserCircle, faUserGear } from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; @@ -149,6 +149,17 @@ class Layout extends React.Component { Admin Panel + + + Admin Logs + + + + Relationship Type Editor + + + Relationship Types + Date: Mon, 31 Jul 2023 20:37:48 +0530 Subject: [PATCH 39/88] feat(RelationshipTypeEditor): Add Attribute Types and other improvements --- .../forms/type-editor/relationship-type.tsx | 100 ++++++++++++++++-- .../forms/type-editor/typeUtils.tsx | 16 ++- src/server/helpers/middleware.ts | 10 ++ .../routes/type-editor/relationship-type.tsx | 49 ++++++++- 4 files changed, 159 insertions(+), 16 deletions(-) diff --git a/src/client/components/forms/type-editor/relationship-type.tsx b/src/client/components/forms/type-editor/relationship-type.tsx index 5897168688..ec131ca8ca 100644 --- a/src/client/components/forms/type-editor/relationship-type.tsx +++ b/src/client/components/forms/type-editor/relationship-type.tsx @@ -15,15 +15,17 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {Button, Card, Col, Form, Modal, Row} from 'react-bootstrap'; +import {Alert, Button, Card, Col, Form, Modal, Row} from 'react-bootstrap'; import React, {ChangeEvent, FormEvent, useCallback, useState} from 'react'; import {RelationshipTypeDataT, RelationshipTypeEditorPropsT, defaultRelationshipTypeData, entityTypeOptions, renderSelectedParent} from './typeUtils'; import {faPencilAlt, faPlus, faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import ReactSelect from 'react-select'; +import classNames from 'classnames'; +import request from 'superagent'; -function RelationshipTypeEditor({relationshipTypeData, parentTypes}: RelationshipTypeEditorPropsT) { +function RelationshipTypeEditor({relationshipTypeData, parentTypes, attributeTypes}: RelationshipTypeEditorPropsT) { const [formData, setFormData] = useState(relationshipTypeData); // State for the ParentType modal @@ -31,6 +33,19 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi const [selectedParentType, setSelectedParentType] = useState(formData.parentId); const [childOrder, setChildOrder] = useState(formData.childOrder); + const [showError, setShowError] = useState(false); + + const handleAttributeTypesChange = useCallback( + (selectedOptions) => { + const selectedHouseTypeIds = selectedOptions.map((option) => option.id); + setFormData((prevFormData) => ({ + ...prevFormData, + attributeTypes: selectedHouseTypeIds + })); + }, + [formData.attributeTypes] + ); + // Callback function for opening the modal const handleAddParent = useCallback(() => { setShowModal(true); @@ -59,7 +74,7 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi setChildOrder(isNaN(value) ? 0 : value); }, [formData, childOrder]); - // Function to handle parent removal using useCallback + // Function to handle parent removal const handleRemoveParent = useCallback(() => { setFormData((prevFormData) => ({ ...prevFormData, @@ -76,10 +91,10 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi // Function to handle parent type and child order edit submission const handleModalSubmit = useCallback(() => { if (selectedParentType !== null) { - setFormData({ - ...formData, + setFormData((prevFormData) => ({ + ...prevFormData, childOrder, parentId: selectedParentType - }); + })); setShowModal(false); } }, [formData, childOrder, selectedParentType]); @@ -106,6 +121,9 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi const getParentTypeValue = useCallback(option => option.id, []); + const getAttributeTypeOptionValue = useCallback(option => option.id, []); + const getAttributeTypeOptionLabel = useCallback(option => option.name, []); + // Callback function to format the option label to include both forwardStatement and reverseStatement const formatParentTypeOptionLabel = useCallback(option => (
@@ -114,9 +132,14 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi
), []); + const formatAttributeTypeOptionLabel = useCallback(option =>
{option.name}
, []); + const handleSourceEntityTypeChange = useCallback((selectedOption) => { if (selectedOption) { setFormData({...formData, sourceEntityType: selectedOption.name}); + if (formData.targetEntityType) { + setShowError(false); + } } else { setFormData({...formData, sourceEntityType: null}); @@ -126,16 +149,45 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi const handleTargetEntityTypeChange = useCallback((selectedOption) => { if (selectedOption) { setFormData({...formData, targetEntityType: selectedOption.name}); + if (formData.sourceEntityType) { + setShowError(false); + } } else { setFormData({...formData, targetEntityType: null}); } }, [formData]); - const handleSubmit = useCallback((event: FormEvent) => { - event.preventDefault(); - // console.log(formData); - }, [formData]); + const errorAlertClass = classNames('text-center', 'margin-top-1', {'d-none': !showError}); + + function isValid() { + return Boolean(formData.sourceEntityType && formData.targetEntityType); + } + + const handleSubmit = useCallback(async (event: FormEvent) => { + event.preventDefault(); + if (!isValid()) { + setShowError(true); + return; + } + + let submissionURL; + if (relationshipTypeData.id) { + submissionURL = `/relationship-type/${relationshipTypeData.id}/edit/handler`; + } + else { + submissionURL = '/relationship-type/create/handler'; + } + + try { + await request.post(submissionURL) + .send({oldAttributeTypes: relationshipTypeData.attributeTypes, ...formData}); + window.location.href = '/relationship-types'; + } + catch (err) { + throw new Error(err); + } + }, [formData, showError]); const lgCol = {offset: 3, span: 6}; @@ -304,6 +356,33 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi + + + + Attribute Types: + formData.attributeTypes.includes(attributeType.id))} + onChange={handleAttributeTypesChange} + /> + + + + + + { + showError && +
+ Error: Incomplete form! Select Source and Target Parent Types. +
+ } + +
@@ -318,7 +397,6 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes}: Relationshi Parent Type: ({ +export const entityTypeOptions = ['Author', 'Work', 'Series', 'Edition', 'EditionGroup', 'Publisher'].map((entity) => ({ name: entity })); diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index 3cc31ea731..b918e3d8f3 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -73,6 +73,8 @@ export const loadRelationshipTypes = makeLoader('RelationshipType', 'relationshipTypes', null, ['attributeTypes']); export const loadParentRelationshipTypes = makeLoader('RelationshipType', 'parentTypes'); +export const loadRelationshipAttributeTypes = + makeLoader('RelationshipAttributeType', 'attributeTypes'); export const loadGenders = makeLoader('Gender', 'genders', (a, b) => a.id > b.id); @@ -242,6 +244,14 @@ export function checkValidRevisionId(req: $Request, res: $Response, next: NextFu return next(); } +export function checkValidRelationshipTypeId(req: $Request, res: $Response, next: NextFunction, id: string) { + const idToNumber = _.toNumber(id); + if (!_.isInteger(idToNumber) || (_.isInteger(idToNumber) && idToNumber <= 0)) { + return next(new error.BadRequestError(`Invalid Relationship Type id: ${req.params.id}`, req)); + } + return next(); +} + export async function redirectedBbid(req: $Request, res: $Response, next: NextFunction, bbid: string) { if (!commonUtils.isValidBBID(bbid)) { return next(new error.BadRequestError(`Invalid bbid: ${req.params.bbid}`, req)); diff --git a/src/server/routes/type-editor/relationship-type.tsx b/src/server/routes/type-editor/relationship-type.tsx index 18154f8a2e..30aa983081 100644 --- a/src/server/routes/type-editor/relationship-type.tsx +++ b/src/server/routes/type-editor/relationship-type.tsx @@ -17,6 +17,7 @@ */ import * as auth from '../../helpers/auth'; +import * as error from '../../../common/helpers/error'; import * as middleware from '../../helpers/middleware'; import * as propHelpers from '../../../client/helpers/props'; import {escapeProps, generateProps} from '../../helpers/props'; @@ -25,15 +26,16 @@ import React from 'react'; import ReactDOMServer from 'react-dom/server'; import RelationshipTypeEditor from '../../../client/components/forms/type-editor/relationship-type'; import express from 'express'; +import {relationshipTypeCreateOrEditHandler} from '../../helpers/typeRouteUtils'; import target from '../../templates/target'; const router = express.Router(); -router.get('/create', auth.isAuthenticated, middleware.loadParentRelationshipTypes, (req, res) => { - const {parentTypes} = res.locals; +router.get('/create', auth.isAuthenticated, middleware.loadParentRelationshipTypes, middleware.loadRelationshipAttributeTypes, (req, res) => { + const {parentTypes, attributeTypes} = res.locals; const props = generateProps(req, res, { - parentTypes + attributeTypes, parentTypes }); const script = '/js/relationship-type/create.js'; const markup = ReactDOMServer.renderToString( @@ -48,4 +50,45 @@ router.get('/create', auth.isAuthenticated, middleware.loadParentRelationshipTyp })); }); +router.post('/create/handler', auth.isAuthenticated, relationshipTypeCreateOrEditHandler); + +router.param( + 'id', + middleware.checkValidRelationshipTypeId +); + +router.get('/:id/edit', auth.isAuthenticated, middleware.loadParentRelationshipTypes, middleware.loadRelationshipAttributeTypes, async (req, res) => { + const {RelationshipType} = req.app.locals.orm; + const {parentTypes, attributeTypes} = res.locals; + const relationshipType = await new RelationshipType({id: req.params.id}) + .fetch({withRelated: ['attributeTypes']}) + .catch(RelationshipType.NotFoundError, () => { + throw new error.NotFoundError(`RelationshipType with id ${req.params.id} not found`, req); + }); + const relationshipTypeData = relationshipType.toJSON(); + const attributeTypesIds = relationshipTypeData.attributeTypes.map(attributeTypeData => attributeTypeData.id); + + relationshipTypeData.attributeTypes = attributeTypesIds; + + const props = generateProps(req, res, { + attributeTypes, parentTypes, relationshipTypeData + }); + const script = '/js/relationship-type/create.js'; + const markup = ReactDOMServer.renderToString( + + + + ); + res.send(target({ + markup, + props: escapeProps(props), + script + })); +}); + +router.post('/:id/edit/handler', auth.isAuthenticated, relationshipTypeCreateOrEditHandler); + export default router; From cb5dec372acd7cb9ac69ca74e1c249526ccebf8c Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Mon, 31 Jul 2023 20:40:02 +0530 Subject: [PATCH 40/88] fix(RelationshipTypeEditor): Uniformize Label names of fields --- .../components/forms/type-editor/relationship-type.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/components/forms/type-editor/relationship-type.tsx b/src/client/components/forms/type-editor/relationship-type.tsx index ec131ca8ca..e91ee0ece9 100644 --- a/src/client/components/forms/type-editor/relationship-type.tsx +++ b/src/client/components/forms/type-editor/relationship-type.tsx @@ -342,7 +342,7 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes, attributeTyp - Deprecated: + Deprecated - Attribute Types: + Attribute Types - Child Order: + Child Order Date: Mon, 31 Jul 2023 21:16:35 +0530 Subject: [PATCH 41/88] fix(AdminSystem): Add authorization to all the new routes --- src/client/components/footer.js | 11 +-- src/client/containers/layout.js | 68 ++++++++----- src/common/helpers/privileges-utils.ts | 11 +++ src/server/routes/adminLogs.tsx | 9 +- src/server/routes/relationship-types.tsx | 6 +- .../routes/type-editor/relationship-type.tsx | 97 ++++++++++--------- 6 files changed, 118 insertions(+), 84 deletions(-) diff --git a/src/client/components/footer.js b/src/client/components/footer.js index dbebc79fac..a5bbefc83a 100644 --- a/src/client/components/footer.js +++ b/src/client/components/footer.js @@ -60,8 +60,8 @@ function Footer(props) { - - Admin logs + + Privacy & Terms @@ -79,13 +79,6 @@ function Footer(props) { - - - - Privacy & Terms - - - diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 24da9048f7..45fee3a683 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -5,6 +5,7 @@ * 2016 Sean Burke * 2016 Ohm Patel * 2015 Leo Verto + * 2023 Shivam Awasthi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,6 +23,7 @@ */ import * as bootstrap from 'react-bootstrap'; +import {PrivilegeType, checkPrivilege} from '../../common/helpers/privileges-utils'; import { faChartLine, faGripVertical, faLink, faListUl, faNewspaper, faPlus, faQuestionCircle, faSearch, faShieldHalved, faSignInAlt, faSignOutAlt, faTrophy, faUserCircle, faUserGear @@ -130,37 +132,53 @@ class Layout extends React.Component {
); + const showPrivilegeDropdown = user.privs > 1; + const adminOptions = ( + <> + + + Admin Panel + + + + Admin Logs + + + ); + + const relationshipTypeEditorOptions = ( + <> + + Relationship Type Editor + + + Relationship Types + + + ); + + const privilegeDropDown = ( + + {checkPrivilege(user.privs, PrivilegeType.ADMIN) && adminOptions} + {checkPrivilege(user.privs, PrivilegeType.RELATIONSHIP_TYPE_EDITOR) && relationshipTypeEditorOptions} + + ); + const disableSignUp = this.props.disableSignUp ? {disabled: true} : {}; return ( - + {this.renderDocsDropdown()} { user && user.id ? this.renderLoggedInDropdown() : this.renderGuestDropdown() diff --git a/src/client/controllers/index.js b/src/client/controllers/index.js index 0df230aa37..84cf739be2 100644 --- a/src/client/controllers/index.js +++ b/src/client/controllers/index.js @@ -25,6 +25,7 @@ import AboutPage from '../../client/components/pages/about'; import {AppContainer} from 'react-hot-loader'; import ContributePage from '../../client/components/pages/contribute'; import DevelopPage from '../../client/components/pages/develop'; +import FAQPage from '../components/pages/faq'; import HelpPage from '../../client/components/pages/help'; import Index from '../components/pages/index'; import Layout from '../containers/layout'; @@ -44,6 +45,7 @@ const pageMap = { About: AboutPage, Contribute: ContributePage, Develop: DevelopPage, + FAQs: FAQPage, Help: HelpPage, Index, Licensing: LicensingPage, diff --git a/src/server/routes/index.js b/src/server/routes/index.js index ba47b739e4..d62f767e69 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -23,6 +23,7 @@ import {escapeProps, generateProps} from '../helpers/props'; import AboutPage from '../../client/components/pages/about'; import ContributePage from '../../client/components/pages/contribute'; import DevelopPage from '../../client/components/pages/develop'; +import FAQPage from '../../client/components/pages/faq'; import HelpPage from '../../client/components/pages/help'; import Index from '../../client/components/pages/index'; import Layout from '../../client/containers/layout'; @@ -107,5 +108,6 @@ _createStaticRoute('/develop', 'Develop', DevelopPage); _createStaticRoute('/help', 'Help', HelpPage); _createStaticRoute('/licensing', 'Licensing', LicensingPage); _createStaticRoute('/privacy', 'Privacy', PrivacyPage); +_createStaticRoute('/faq', 'FAQs', FAQPage); export default router; From 6b38d032217b48f9c3f880724fec8c1ad3bff782 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sat, 12 Aug 2023 01:09:45 +0530 Subject: [PATCH 48/88] fix relationship types routes and other misc improvements --- .../pages/parts/relationship-types-tree.tsx | 40 ++++++++++++------- .../pages/relationship-type-matrix.tsx | 6 ++- src/client/controllers/relationshipTypes.tsx | 19 +++++++-- src/server/helpers/typeRouteUtils.ts | 6 +-- src/server/routes/relationship-types.tsx | 5 ++- 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/client/components/pages/parts/relationship-types-tree.tsx b/src/client/components/pages/parts/relationship-types-tree.tsx index db0a646422..412789804a 100644 --- a/src/client/components/pages/parts/relationship-types-tree.tsx +++ b/src/client/components/pages/parts/relationship-types-tree.tsx @@ -18,6 +18,7 @@ import React, {useCallback, useState} from 'react'; import {Button} from 'react-bootstrap'; import {RelationshipTypeDataT} from '../../forms/type-editor/typeUtils'; +import {genEntityIconHTMLElement} from '../../../helpers/entity'; type RelationshipTypeTreePropsT = { @@ -41,39 +42,48 @@ function RelationshipTypeTree({relationshipTypes, parentId, indentLevel}: Relati const handleClick = useCallback((event) => { const relationshipTypeId = parseInt(event.target.value, 10); toggleExpand(relationshipTypeId); - }, [expandedRelationshipTypeIds]); + }, []); const filteredRelationshipTypes = relationshipTypes.filter((relType) => relType.parentId === parentId); return (
    {filteredRelationshipTypes.map(relType => { - let relOuterClass = `margin-left-d${indentLevel * 20}`; + let relOuterClass = `margin-left-d${indentLevel * 10}`; if (relType.deprecated) { - relOuterClass = `margin-left-d${indentLevel * 20} text-muted`; + relOuterClass = `margin-left-d${indentLevel * 10} text-muted`; } + const sourceIconElement = genEntityIconHTMLElement(relType.sourceEntityType); + const targetIconElement = genEntityIconHTMLElement(relType.targetEntityType); + const relInnerElementsClass = `margin-left-d${(indentLevel + 1) * 10} small`; return (
  • - - {relType.label} +

    + {relType.label}:  + {sourceIconElement}{relType.sourceEntityType}  + {relType.linkPhrase} {targetIconElement} {relType.targetEntityType} - +

    + {relType.description} +

    +

    {expandedRelationshipTypeIds.includes(relType.id) && ( -
    -
    Forward link phrase: {relType.linkPhrase}
    -
    Reverse link phrase: {relType.reverseLinkPhrase}
    -
    Source Entity Type: {relType.sourceEntityType}
    -
    Target Entity Type: {relType.targetEntityType}
    -
    Description: {relType.description}
    -
    Child Order: {relType.childOrder}
    -
    Deprecated: {relType.deprecated ? 'Yes' : 'No'}
    -
    +
    +
    Forward link phrase: {relType.linkPhrase}
    +
    Reverse link phrase: {relType.reverseLinkPhrase}
    +
    Source Entity Type: {relType.sourceEntityType}
    +
    Target Entity Type: {relType.targetEntityType}
    +
    Description: {relType.description}
    +
    Child Order: {relType.childOrder}
    +
    Deprecated: {relType.deprecated ? 'Yes' : 'No'}
    +
    +

    + {idenType.description} +

    +

    + {expandedIdentifierTypeIds.includes(idenType.id) && ( +
    +
    Detection RegEx: {idenType.detectionRegex}
    +
    Validation RegEx: {idenType.validationRegex}
    +
    Display Template: {idenType.displayTemplate}
    +
    Description: {idenType.description}
    +
    Child Order: {idenType.childOrder}
    +
    Deprecated: {idenType.deprecated ? 'Yes' : 'No'}
    +
    + +
    +
    + )} + +
  • + ); + })} +
+ ); +} + +IdentifierTypeTree.defaultProps = { + indentLevel: 0, + parentId: null +}; + +export default IdentifierTypeTree; diff --git a/src/client/controllers/identifierTypes.tsx b/src/client/controllers/identifierTypes.tsx new file mode 100644 index 0000000000..4ee03a6771 --- /dev/null +++ b/src/client/controllers/identifierTypes.tsx @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import {extractChildProps, extractLayoutProps} from '../helpers/props'; +import {AppContainer} from 'react-hot-loader'; +import IdentifierTypesPage from '../components/pages/identifierTypes'; +import Layout from '../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; + +const markup = ( + + + + + +); + +ReactDOM.hydrate(markup, document.getElementById('target')); + +/* + * As we are not exporting a component, + * we cannot use the react-hot-loader module wrapper, + * but instead directly use webpack Hot Module Replacement API + */ + +if (module.hot) { + module.hot.accept(); +} diff --git a/src/server/routes.js b/src/server/routes.js index a4c3d66981..9df46abdcd 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -27,6 +27,7 @@ import editionGroupRouter from './routes/entity/edition-group'; import editionRouter from './routes/entity/edition'; import editorRouter from './routes/editor'; import externalServiceRouter from './routes/externalService'; +import identifierTypesRouter from './routes/identifier-types'; import indexRouter from './routes/index'; import mergeRouter from './routes/merge'; import publisherRouter from './routes/entity/publisher'; @@ -60,6 +61,7 @@ function initRootRoutes(app) { app.use('/admin-logs', adminLogsRouter); app.use('/relationship-type', relationshipTypeRouter); app.use('/relationship-types', relationshipTypesRouter); + app.use('/identifier-types', identifierTypesRouter); } function initEditionGroupRoutes(app) { diff --git a/src/server/routes/identifier-types.tsx b/src/server/routes/identifier-types.tsx new file mode 100644 index 0000000000..b3f2c689b5 --- /dev/null +++ b/src/server/routes/identifier-types.tsx @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import * as middleware from '../helpers/middleware'; +import * as propHelpers from '../../client/helpers/props'; +import {escapeProps, generateProps} from '../helpers/props'; +import IdentifierTypesPage from '../../client/components/pages/identifierTypes'; +import Layout from '../../client/containers/layout'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import express from 'express'; +import target from '../templates/target'; + + +const router = express.Router(); + +router.get('/', middleware.loadIdentifierTypes, (req, res) => { + const props = generateProps(req, res); + + const markup = ReactDOMServer.renderToString( + + + + ); + + res.send(target({ + markup, + props: escapeProps(props), + script: '/js/identifierTypes.js', + title: 'Identifier Types' + })); +}); + +export default router; diff --git a/webpack.client.js b/webpack.client.js index 990b2ce533..35978c6fb4 100644 --- a/webpack.client.js +++ b/webpack.client.js @@ -28,6 +28,7 @@ const clientConfig = { preview: ['./controllers/preview.js'], error: ['./controllers/error.js'], externalService: ['./controllers/externalService.js'], + identifierTypes: ['./controllers/identifierTypes.tsx'], index: ['./controllers/index.js'], registrationDetails: ['./controllers/registrationDetails.js'], relationshipTypes: ['./controllers/relationshipTypes.tsx'], From d8cfdef8bf11aaefaac73d4a987c791a6cffffb5 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sun, 20 Aug 2023 00:21:21 +0530 Subject: [PATCH 65/88] feat(identifierTypes): Identifier Type Editor --- .../forms/type-editor/identifier-type.tsx | 446 ++++++++++++++++++ .../forms/type-editor/typeUtils.tsx | 50 ++ .../type-editor/identifier-type.tsx | 51 ++ src/server/helpers/middleware.ts | 2 + src/server/helpers/typeRouteUtils.ts | 55 +++ src/server/routes.js | 2 + .../routes/type-editor/identifier-type.tsx | 93 ++++ webpack.client.js | 3 +- 8 files changed, 701 insertions(+), 1 deletion(-) create mode 100644 src/client/components/forms/type-editor/identifier-type.tsx create mode 100644 src/client/controllers/type-editor/identifier-type.tsx create mode 100644 src/server/routes/type-editor/identifier-type.tsx diff --git a/src/client/components/forms/type-editor/identifier-type.tsx b/src/client/components/forms/type-editor/identifier-type.tsx new file mode 100644 index 0000000000..efd4c39a60 --- /dev/null +++ b/src/client/components/forms/type-editor/identifier-type.tsx @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import {Alert, Button, Card, Col, Form, Modal, Row} from 'react-bootstrap'; +import {IdentifierTypeDataT, IdentifierTypeEditorPropsT, + defaultIdentifierTypeData, entityTypeOptions, renderSelectedParentIdentifierType} from './typeUtils'; +import React, {ChangeEvent, FormEvent, useCallback, useEffect, useState} from 'react'; +import {faPencilAlt, faPlus, faTimes} from '@fortawesome/free-solid-svg-icons'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import ReactSelect from 'react-select'; +import classNames from 'classnames'; +import request from 'superagent'; + + +function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeEditorPropsT) { + const [formData, setFormData] = useState(identifierTypeData); + + // State for the ParentType modal + const [showModal, setShowModal] = useState(false); + const [selectedParentType, setSelectedParentType] = useState(formData.parentId); + const [childOrder, setChildOrder] = useState(formData.childOrder); + + const [isFormEdited, setIsFormEdited] = useState(false); + + const [filteredParentTypes, setFilteredParentTypes] = useState(parentTypes); + useEffect(() => { + if (formData.entityType) { + const filteredTypes = parentTypes.filter(type => type.entityType === formData.entityType); + setFilteredParentTypes(filteredTypes); + } + else { + setFilteredParentTypes(parentTypes); + } + }, [formData.entityType]); + + // Check if the form data is different from the initial data + useEffect(() => { + const isEdited = JSON.stringify(formData) !== JSON.stringify(identifierTypeData); + setIsFormEdited(isEdited); + }, [formData, identifierTypeData]); + + // This flag is used to check if any field has been updated while editing a type + const [noChangesError, setNoChangesError] = useState(false); + + // This flag is used to check whether we have selected source and target entity types + const [showIncompleteFormError, setShowIncompleteFormError] = useState(false); + + // Callback function for opening the modal + const handleAddParent = useCallback(() => { + setShowModal(true); + }, []); + + // Callback function for closing the modal, the state of the modal should alse be reset + const handleModalClose = useCallback(() => { + setSelectedParentType(null); + setChildOrder(0); + setShowModal(false); + }, []); + + // Function to handle parent type selection in ParentType modal + const handleParentTypeChange = useCallback((selectedOption) => { + if (selectedOption) { + setSelectedParentType(selectedOption.id); + } + else { + setSelectedParentType(null); + } + }, [selectedParentType]); + + // Function to handle child order input in ParentType modal + const handleChildOrderChange = useCallback((event: ChangeEvent) => { + const value = parseInt(event.target.value, 10); + setChildOrder(isNaN(value) ? 0 : value); + }, [formData, childOrder]); + + // Function to handle parent removal + const handleRemoveParent = useCallback(() => { + setFormData((prevFormData) => ({ + ...prevFormData, + childOrder: 0, parentId: null + })); + setChildOrder(0); + setSelectedParentType(null); + }, [formData]); + + const handleEditParent = useCallback(() => { + setShowModal(true); + }, []); + + // Function to handle parent type and child order edit submission + const handleModalSubmit = useCallback(() => { + if (selectedParentType !== null) { + setFormData((prevFormData) => ({ + ...prevFormData, + childOrder, parentId: selectedParentType + })); + setShowModal(false); + } + }, [formData, childOrder, selectedParentType]); + + const handleInputChange = useCallback((event: ChangeEvent) => { + const {name, value} = event.target; + setFormData((prevFormData) => ({ + ...prevFormData, + [name]: value + })); + }, [formData]); + + const handleDeprecatedChange = useCallback((event: ChangeEvent) => { + const {value} = event.target; + setFormData((prevFormData) => ({ + ...prevFormData, + deprecated: value === 'true' + })); + }, [formData]); + + const errorAlertClass = classNames('text-center', 'margin-top-1', {'d-none': !showIncompleteFormError}); + + const getEntityTypeLabel = useCallback(option => option.name, []); + + const getEntityTypeValue = useCallback(option => option.name, []); + + const getParentTypeValue = useCallback(option => option.id, []); + + // Callback function to format the option label + const formatParentTypeOptionLabel = useCallback(option => ( +
+
+ Label: {option.label} +
+
+ Description: {option.description} +
+
+ ), []); + + const handleEntityTypeChange = useCallback((selectedOption) => { + if (selectedOption) { + setFormData({...formData, entityType: selectedOption.name}); + } + else { + setFormData({...formData, entityType: null}); + } + }, [formData]); + + function isValid() { + return Boolean(formData.entityType); + } + + const handleSubmit = useCallback(async (event: FormEvent) => { + event.preventDefault(); + if (!isValid()) { + setShowIncompleteFormError(true); + return; + } + if (!isFormEdited) { + // Display an error message if no changes were made + setNoChangesError(true); + // Hide the error message after 3 seconds + setTimeout(() => { + setNoChangesError(false); + }, 3000); + return; + } + + let submissionURL; + if (identifierTypeData.id) { + submissionURL = `/identifier-type/${identifierTypeData.id}/edit/handler`; + } + else { + submissionURL = '/identifier-type/create/handler'; + } + + try { + await request.post(submissionURL) + .send(formData); + window.location.href = '/identifier-types'; + } + catch (err) { + throw new Error(err); + } + }, [formData, isFormEdited, showIncompleteFormError]); + + // When we change EntityType, then we must check the validity of the parentType + // in case it is already selected + useEffect(() => { + if (formData.parentId) { + const parentType = parentTypes.find(type => type.id === formData.parentId); + if (formData.entityType !== parentType.entityType) { + handleRemoveParent(); + } + } + }, [formData.entityType]); + + const lgCol = {offset: 3, span: 6}; + + return ( +
+ + + Add Identifier Type + + + + + + Label + + + + + + + + Description + + + + + + + + Detection RegEx + + + + + + + + Validation RegEx + + + + + + + + Display Template + + + + + + + + Entity Type + option.name === identifierTypeData.entityType)} + getOptionLabel={getEntityTypeLabel} + getOptionValue={getEntityTypeValue} + instanceId="entityType" + options={entityTypeOptions} + placeholder="Select Entity Type" + onChange={handleEntityTypeChange} + /> + + + + + + + Parent Identifier Type + {!formData.parentId ? ( + + + + + + ) : ( + + + {formData.parentId && + renderSelectedParentIdentifierType(formData.parentId, formData.childOrder, parentTypes)} +
+ + +
+ +
+ )} +
+ +
+ + + + + Deprecated + + + + + + + + + + { + showIncompleteFormError && +
+ Error: Incomplete form! Select Entity Type. +
+ } + +
+ + + { + noChangesError && + Error: No updated field. + } + + + + + + + + {/* Modal for selecting parent type */} + + + {formData.parentId ? 'Edit Parent' : 'Add a Parent'} + + + + Parent Type: + option.id === selectedParentType)} + onChange={handleParentTypeChange} + /> + + + Child Order + + + + + + + + +
+
+
+ ); +} + +IdentifierTypeEditor.defaultProps = { + identifierTypeData: defaultIdentifierTypeData +}; + +export default IdentifierTypeEditor; diff --git a/src/client/components/forms/type-editor/typeUtils.tsx b/src/client/components/forms/type-editor/typeUtils.tsx index 1f738b848a..63fe89e316 100644 --- a/src/client/components/forms/type-editor/typeUtils.tsx +++ b/src/client/components/forms/type-editor/typeUtils.tsx @@ -34,6 +34,19 @@ export interface RelationshipTypeDataT { targetEntityType: EntityTypeString; } +export interface IdentifierTypeDataT { + childOrder: number; + deprecated: boolean; + description: string; + detectionRegex: string | null; + displayTemplate: string; + entityType: EntityTypeString; + id?: number; + label: string; + parentId: number | null; + validationRegex: string; +} + export interface AttributeTypeDataT { childOrder: number, description: string, @@ -51,6 +64,12 @@ export interface RelationshipTypeEditorPropsT { user: any; } +export interface IdentifierTypeEditorPropsT { + identifierTypeData: IdentifierTypeDataT; + parentTypes: IdentifierTypeDataT[]; + user: any; +} + export const defaultRelationshipTypeData: RelationshipTypeDataT = { attributeTypes: [], childOrder: 0, @@ -64,6 +83,18 @@ export const defaultRelationshipTypeData: RelationshipTypeDataT = { targetEntityType: null }; +export const defaultIdentifierTypeData: IdentifierTypeDataT = { + childOrder: 0, + deprecated: false, + description: '', + detectionRegex: null, + displayTemplate: '', + entityType: null, + label: '', + parentId: null, + validationRegex: '' +}; + export const entityTypeOptions = ENTITY_TYPES.map((entity) => ({ name: entity })); @@ -85,3 +116,22 @@ export function renderSelectedParent(selectedParentID: number, childOrder: numbe } return null; } + +export function renderSelectedParentIdentifierType(selectedParentID: number, childOrder: number, parentTypes: IdentifierTypeDataT[]) { + const parent = parentTypes.find(identifierType => identifierType.id === selectedParentID); + if (parent) { + return ( +
+
+ Label: {parent.label} +
+
+ Description: {parent.description} +
+
Child Order: {childOrder}
+
+ ); + } + return null; +} + diff --git a/src/client/controllers/type-editor/identifier-type.tsx b/src/client/controllers/type-editor/identifier-type.tsx new file mode 100644 index 0000000000..d9bb415975 --- /dev/null +++ b/src/client/controllers/type-editor/identifier-type.tsx @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import {extractChildProps, extractLayoutProps} from '../../helpers/props'; +import {AppContainer} from 'react-hot-loader'; +import IdentifierTypeEditor from '../../components/forms/type-editor/identifier-type'; +import Layout from '../../containers/layout'; +import React from 'react'; +import ReactDOM from 'react-dom'; + + +const propsTarget = document.getElementById('props'); +const props = propsTarget ? JSON.parse(propsTarget.innerHTML) : {}; + +ReactDOM.hydrate( + + + + + , + document.getElementById('target') +); + + +/* + * As we are not exporting a component, + * we cannot use the react-hot-loader module wrapper, + * but instead directly use webpack Hot Module Replacement API + */ + +if (module.hot) { + module.hot.accept(); +} + diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index ccd3923a15..2d6b39a0e8 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -73,6 +73,8 @@ export const loadRelationshipTypes = makeLoader('RelationshipType', 'relationshipTypes', null, ['attributeTypes']); export const loadParentRelationshipTypes = makeLoader('RelationshipType', 'parentTypes'); +export const loadParentIdentifierTypes = + makeLoader('IdentifierType', 'parentTypes'); export const loadRelationshipAttributeTypes = makeLoader('RelationshipAttributeType', 'attributeTypes'); diff --git a/src/server/helpers/typeRouteUtils.ts b/src/server/helpers/typeRouteUtils.ts index bee0881605..e423f29ec2 100644 --- a/src/server/helpers/typeRouteUtils.ts +++ b/src/server/helpers/typeRouteUtils.ts @@ -104,3 +104,58 @@ export async function relationshipTypeCreateOrEditHandler(req, res, next) { return next(err); } } + +/** + * A handler for create or edit actions on identifier types. + * @param {object} req - request object + * @param {object} res - response object + * @param {object} next - next object + * @returns {promise} res.send promise + * @description + * Creates a new identifier type or updates an existing identifier type + */ +export async function identifierTypeCreateOrEditHandler(req, res, next) { + try { + const {IdentifierType} = req.app.locals.orm; + let newIdentifierType; + let method; + if (!req.params.id) { + newIdentifierType = await IdentifierType.forge(); + method = 'insert'; + } + else { + newIdentifierType = await IdentifierType.forge({id: parseInt(req.params.id, 10)}).fetch({ + require: true + }); + method = 'update'; + } + const { + childOrder, + deprecated, + description, + detectionRegex, + displayTemplate, + entityType, + label, + parentId, + validationRegex + } = req.body; + + newIdentifierType.set('description', description); + newIdentifierType.set('label', label); + newIdentifierType.set('deprecated', deprecated); + newIdentifierType.set('detectionRegex', detectionRegex); + newIdentifierType.set('displayTemplate', displayTemplate); + newIdentifierType.set('childOrder', childOrder); + newIdentifierType.set('parentId', parentId); + newIdentifierType.set('entityType', entityType); + newIdentifierType.set('validationRegex', validationRegex); + + const identifierType = await newIdentifierType.save(null, {method}); + + return res.send(identifierType.toJSON()); + } + catch (err) { + return next(err); + } +} diff --git a/src/server/routes.js b/src/server/routes.js index 9df46abdcd..6763c83978 100644 --- a/src/server/routes.js +++ b/src/server/routes.js @@ -27,6 +27,7 @@ import editionGroupRouter from './routes/entity/edition-group'; import editionRouter from './routes/entity/edition'; import editorRouter from './routes/editor'; import externalServiceRouter from './routes/externalService'; +import identifierTypeRouter from './routes/type-editor/identifier-type'; import identifierTypesRouter from './routes/identifier-types'; import indexRouter from './routes/index'; import mergeRouter from './routes/merge'; @@ -61,6 +62,7 @@ function initRootRoutes(app) { app.use('/admin-logs', adminLogsRouter); app.use('/relationship-type', relationshipTypeRouter); app.use('/relationship-types', relationshipTypesRouter); + app.use('/identifier-type', identifierTypeRouter); app.use('/identifier-types', identifierTypesRouter); } diff --git a/src/server/routes/type-editor/identifier-type.tsx b/src/server/routes/type-editor/identifier-type.tsx new file mode 100644 index 0000000000..1df46a2316 --- /dev/null +++ b/src/server/routes/type-editor/identifier-type.tsx @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 Shivam Awasthi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +import * as auth from '../../helpers/auth'; +import * as error from '../../../common/helpers/error'; +import * as middleware from '../../helpers/middleware'; +import * as propHelpers from '../../../client/helpers/props'; +import {escapeProps, generateProps} from '../../helpers/props'; +import IdentifierTypeEditor from '../../../client/components/forms/type-editor/identifier-type'; +import Layout from '../../../client/containers/layout'; +import {PrivilegeType} from '../../../common/helpers/privileges-utils'; +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import express from 'express'; +import {identifierTypeCreateOrEditHandler} from '../../helpers/typeRouteUtils'; +import target from '../../templates/target'; + + +const router = express.Router(); + +const {IDENTIFIER_TYPE_EDITOR} = PrivilegeType; + +router.get('/create', auth.isAuthenticated, auth.isAuthorized(IDENTIFIER_TYPE_EDITOR), + middleware.loadParentIdentifierTypes, (req, res) => { + const {parentTypes} = res.locals; + const props = generateProps(req, res, { + parentTypes + }); + const script = '/js/identifier-type-editor.js'; + const markup = ReactDOMServer.renderToString( + + + + ); + res.send(target({ + markup, + props: escapeProps(props), + script + })); + }); + +router.post('/create/handler', auth.isAuthenticated, auth.isAuthorized(IDENTIFIER_TYPE_EDITOR), identifierTypeCreateOrEditHandler); + +router.param( + 'id', + middleware.checkValidRelationshipTypeId +); + +router.get('/:id/edit', auth.isAuthenticated, auth.isAuthorized(IDENTIFIER_TYPE_EDITOR), + middleware.loadParentIdentifierTypes, async (req, res) => { + const {IdentifierType} = req.app.locals.orm; + const {parentTypes} = res.locals; + const identifierType = await new IdentifierType({id: req.params.id}) + .fetch({require: true}) + .catch(IdentifierType.NotFoundError, () => { + throw new error.NotFoundError(`IdentifierType with id ${req.params.id} not found`, req); + }); + const identifierTypeData = identifierType.toJSON(); + const props = generateProps(req, res, {identifierTypeData, parentTypes}); + const script = '/js/identifier-type-editor.js'; + const markup = ReactDOMServer.renderToString( + + + + ); + res.send(target({ + markup, + props: escapeProps(props), + script + })); + }); + +router.post('/:id/edit/handler', auth.isAuthenticated, auth.isAuthorized(IDENTIFIER_TYPE_EDITOR), identifierTypeCreateOrEditHandler); + +export default router; diff --git a/webpack.client.js b/webpack.client.js index 35978c6fb4..97e2b63616 100644 --- a/webpack.client.js +++ b/webpack.client.js @@ -44,7 +44,8 @@ const clientConfig = { 'unified-form':['./unified-form/controller.js'], 'entity-merge': ['./entity-editor/entity-merge.tsx'], style: './stylesheets/style.scss', - 'relationship-type-editor': ['./controllers/type-editor/relationship-type.tsx'] + 'relationship-type-editor': ['./controllers/type-editor/relationship-type.tsx'], + 'identifier-type-editor': ['./controllers/type-editor/identifier-type.tsx'] }, externals: { moment: 'moment' From 533156a846d645c01503f3b1e245555b8e7cf485 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sun, 20 Aug 2023 00:30:33 +0530 Subject: [PATCH 66/88] fix(TypeEditors): fix names of common utilities --- .../components/forms/type-editor/relationship-type.tsx | 6 ++++-- src/client/components/forms/type-editor/typeUtils.tsx | 2 +- src/client/components/pages/parts/identifier-types-tree.tsx | 2 +- .../components/pages/parts/relationship-types-tree.tsx | 2 +- src/client/stylesheets/style.scss | 2 +- src/server/helpers/middleware.ts | 4 ++-- src/server/routes/type-editor/identifier-type.tsx | 2 +- src/server/routes/type-editor/relationship-type.tsx | 2 +- 8 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/client/components/forms/type-editor/relationship-type.tsx b/src/client/components/forms/type-editor/relationship-type.tsx index d0431ce668..6b4599c07c 100644 --- a/src/client/components/forms/type-editor/relationship-type.tsx +++ b/src/client/components/forms/type-editor/relationship-type.tsx @@ -17,7 +17,8 @@ */ import {Alert, Button, Card, Col, Form, Modal, Row} from 'react-bootstrap'; import React, {ChangeEvent, FormEvent, useCallback, useEffect, useState} from 'react'; -import {RelationshipTypeDataT, RelationshipTypeEditorPropsT, defaultRelationshipTypeData, entityTypeOptions, renderSelectedParent} from './typeUtils'; +import {RelationshipTypeDataT, RelationshipTypeEditorPropsT, + defaultRelationshipTypeData, entityTypeOptions, renderSelectedParentRelationshipType} from './typeUtils'; import {faPencilAlt, faPlus, faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import ReactSelect from 'react-select'; @@ -373,7 +374,8 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes, attributeTyp ) : ( - {formData.parentId && renderSelectedParent(formData.parentId, formData.childOrder, parentTypes)} + {formData.parentId && + renderSelectedParentRelationshipType(formData.parentId, formData.childOrder, parentTypes)}
diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 575869ac78..4502e3f36f 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -23,6 +23,7 @@ */ import * as bootstrap from 'react-bootstrap'; +import {IdentifierTypeEditorIcon, RelationshipTypeEditorIcon} from '../helpers/utils'; import {PrivilegeType, checkPrivilege} from '../../common/helpers/privileges-utils'; import { faBarcode, @@ -34,7 +35,6 @@ import Footer from './../components/footer'; import MergeQueue from '../components/pages/parts/merge-queue'; import PropTypes from 'prop-types'; import React from 'react'; -import {RelationshipTypeEditorIcon} from '../helpers/utils'; import {genEntityIconHTMLElement} from '../helpers/entity'; @@ -110,19 +110,19 @@ class Layout extends React.Component { onMouseDown={this.handleMouseDown} > - + {' Help '} - + {' FAQs '} - + {' Relationship Types '} - + {' Identifier Types '} @@ -175,11 +175,11 @@ class Layout extends React.Component { const adminOptions = ( <> - + Admin Panel - + Admin Logs @@ -197,6 +197,7 @@ class Layout extends React.Component { const identifierTypeEditorOptions = ( <> + {IdentifierTypeEditorIcon} Add Identifier Type diff --git a/src/client/helpers/utils.tsx b/src/client/helpers/utils.tsx index 9c62d6bd5e..e043af41cd 100644 --- a/src/client/helpers/utils.tsx +++ b/src/client/helpers/utils.tsx @@ -16,7 +16,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import {faLink, faPlus} from '@fortawesome/free-solid-svg-icons'; +import {faBarcode, faLink, faPlus} from '@fortawesome/free-solid-svg-icons'; import AuthorTable from '../components/pages/entities/author-table'; import DOMPurify from 'isomorphic-dompurify'; import EditionGroupTable from '../components/pages/entities/editionGroup-table'; @@ -235,3 +235,10 @@ export const RelationshipTypeEditorIcon = ( ); + +export const IdentifierTypeEditorIcon = ( + + + + +); diff --git a/src/server/helpers/typeRouteUtils.ts b/src/server/helpers/typeRouteUtils.ts index e423f29ec2..b5aa66281e 100644 --- a/src/server/helpers/typeRouteUtils.ts +++ b/src/server/helpers/typeRouteUtils.ts @@ -15,7 +15,6 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ - function getChangedAttributeTypes(oldAttributes, newAttributes) { const commonAttributes = oldAttributes.filter(value => newAttributes.includes(value)); const attributesToBeRemoved = oldAttributes.filter(value => !commonAttributes.includes(value)); @@ -109,12 +108,11 @@ export async function relationshipTypeCreateOrEditHandler(req, res, next) { * A handler for create or edit actions on identifier types. * @param {object} req - request object * @param {object} res - response object - * @param {object} next - next object * @returns {promise} res.send promise * @description * Creates a new identifier type or updates an existing identifier type */ -export async function identifierTypeCreateOrEditHandler(req, res, next) { +export async function identifierTypeCreateOrEditHandler(req, res) { try { const {IdentifierType} = req.app.locals.orm; let newIdentifierType; @@ -153,9 +151,9 @@ export async function identifierTypeCreateOrEditHandler(req, res, next) { const identifierType = await newIdentifierType.save(null, {method}); - return res.send(identifierType.toJSON()); + return res.status(200).send(identifierType.toJSON()); } catch (err) { - return next(err); + return res.status(500).json({error: 'A problem occured when processing the data'}); } } From a6f97b11dbac8e1743af82d4c5c29a9fdae74d5b Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 22 Aug 2023 23:10:05 +0530 Subject: [PATCH 70/88] misc: identifierType details improvement --- .../components/forms/type-editor/identifier-type.tsx | 2 +- .../components/pages/parts/identifier-types-tree.tsx | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/components/forms/type-editor/identifier-type.tsx b/src/client/components/forms/type-editor/identifier-type.tsx index 0661e0de4a..7e7fa1b9a1 100644 --- a/src/client/components/forms/type-editor/identifier-type.tsx +++ b/src/client/components/forms/type-editor/identifier-type.tsx @@ -209,7 +209,7 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE handleRemoveParent(); } } - }, [formData.entityType, formData.parentId]); + }, [formData.entityType]); const lgCol = {offset: 3, span: 6}; diff --git a/src/client/components/pages/parts/identifier-types-tree.tsx b/src/client/components/pages/parts/identifier-types-tree.tsx index 17df2a0bdc..7c3a244497 100644 --- a/src/client/components/pages/parts/identifier-types-tree.tsx +++ b/src/client/components/pages/parts/identifier-types-tree.tsx @@ -56,7 +56,7 @@ function IdentifierTypeTree({identifierTypes, parentId, indentLevel}: Identifier } return (
  • -

    +

    {idenType.label}  -

    - {idenType.description} -

    -

    +
    +
    {idenType.displayTemplate}
    +
    {idenType.description}
    +
    +
    {expandedIdentifierTypeIds.includes(idenType.id) && (
    Detection RegEx: {idenType.detectionRegex}
    From 261ac7b171666d4cd8641b4cbc201830a2f8fc74 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Tue, 22 Aug 2023 23:25:21 +0530 Subject: [PATCH 71/88] fix: remove unnecessary transaction --- test/test-helpers/create-entities.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/test/test-helpers/create-entities.js b/test/test-helpers/create-entities.js index 2ffac90b60..b7dcd7c1c6 100644 --- a/test/test-helpers/create-entities.js +++ b/test/test-helpers/create-entities.js @@ -165,22 +165,18 @@ export function createEditor(editorId, privs = 1) { }); } -export function createRelationshipType() { - return orm.bookshelf.knex.transaction(async (transacting) => { - const relationshipType = await new RelationshipType(relationshipTypeData) - .save(null, {method: 'insert', transacting}); +export async function createRelationshipType() { + const relationshipType = await new RelationshipType(relationshipTypeData) + .save(null, {method: 'insert'}); - return relationshipType.id; - }); + return relationshipType.id; } -export function createIdentifierType() { - return orm.bookshelf.knex.transaction(async (transacting) => { - const identifierType = await new IdentifierType(identifierTypeData) - .save(null, {method: 'insert', transacting}); +export async function createIdentifierType() { + const identifierType = await new IdentifierType(identifierTypeData) + .save(null, {method: 'insert'}); - return identifierType.id; - }); + return identifierType.id; } async function createAliasAndAliasSet() { From 4abe7f0444de9ce95912f0e50f4bc3adc9357cad Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Wed, 23 Aug 2023 21:48:02 +0530 Subject: [PATCH 72/88] sql: fix identifier-types sequence --- sql/migrations/2023-08-23-identifier-types-sequence/up.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 sql/migrations/2023-08-23-identifier-types-sequence/up.sql diff --git a/sql/migrations/2023-08-23-identifier-types-sequence/up.sql b/sql/migrations/2023-08-23-identifier-types-sequence/up.sql new file mode 100644 index 0000000000..fcb0f04c20 --- /dev/null +++ b/sql/migrations/2023-08-23-identifier-types-sequence/up.sql @@ -0,0 +1,3 @@ +BEGIN TRANSACTION; +ALTER SEQUENCE identifier_type_id_seq RESTART WITH 40; +COMMIT; \ No newline at end of file From 240d2fc9ea076268318c80d73dbaee1e787e4533 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 24 Aug 2023 22:23:06 +0530 Subject: [PATCH 73/88] misc: use error helpers, improve variable names, and fix sequence --- .../2023-08-23-identifier-types-sequence/up.sql | 2 +- .../components/forms/type-editor/identifier-type.tsx | 2 +- .../components/pages/parts/identifier-types-tree.tsx | 12 ++++++------ src/server/helpers/typeRouteUtils.ts | 7 +++++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/sql/migrations/2023-08-23-identifier-types-sequence/up.sql b/sql/migrations/2023-08-23-identifier-types-sequence/up.sql index fcb0f04c20..52aa5ef700 100644 --- a/sql/migrations/2023-08-23-identifier-types-sequence/up.sql +++ b/sql/migrations/2023-08-23-identifier-types-sequence/up.sql @@ -1,3 +1,3 @@ BEGIN TRANSACTION; -ALTER SEQUENCE identifier_type_id_seq RESTART WITH 40; +SELECT SETVAL('identifier_type_id_seq', (SELECT MAX(id) FROM identifier_type)); COMMIT; \ No newline at end of file diff --git a/src/client/components/forms/type-editor/identifier-type.tsx b/src/client/components/forms/type-editor/identifier-type.tsx index 7e7fa1b9a1..8e0186eeda 100644 --- a/src/client/components/forms/type-editor/identifier-type.tsx +++ b/src/client/components/forms/type-editor/identifier-type.tsx @@ -31,7 +31,7 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE // State for the ParentType modal const [showModal, setShowModal] = useState(false); - const [selectedParentType, setSelectedParentType] = useState(formData.parentId); + const [selectedParentType, setSelectedParentType] = useState(identifierTypeData.parentId); const [childOrder, setChildOrder] = useState(formData.childOrder); const [filteredParentTypes, setFilteredParentTypes] = useState(parentTypes); diff --git a/src/client/components/pages/parts/identifier-types-tree.tsx b/src/client/components/pages/parts/identifier-types-tree.tsx index 7c3a244497..f3f91e60de 100644 --- a/src/client/components/pages/parts/identifier-types-tree.tsx +++ b/src/client/components/pages/parts/identifier-types-tree.tsx @@ -30,18 +30,18 @@ type IdentifierTypeTreePropsT = { function IdentifierTypeTree({identifierTypes, parentId, indentLevel}: IdentifierTypeTreePropsT) { const [expandedIdentifierTypeIds, setExpandedIdentifierTypeIds] = useState([]); - function toggleExpand(idenTypeId) { + function toggleExpand(identifierTypeId) { setExpandedIdentifierTypeIds((prevExpandedIds) => { - if (prevExpandedIds.includes(idenTypeId)) { - return prevExpandedIds.filter((id) => id !== idenTypeId); + if (prevExpandedIds.includes(identifierTypeId)) { + return prevExpandedIds.filter((id) => id !== identifierTypeId); } - return [...prevExpandedIds, idenTypeId]; + return [...prevExpandedIds, identifierTypeId]; }); } const handleClick = useCallback((event) => { - const IdentifierTypeId = parseInt(event.target.value, 10); - toggleExpand(IdentifierTypeId); + const identifierTypeId = parseInt(event.target.value, 10); + toggleExpand(identifierTypeId); }, []); let filteredIdentifierTypes = identifierTypes.filter((idenType) => idenType.parentId === parentId); diff --git a/src/server/helpers/typeRouteUtils.ts b/src/server/helpers/typeRouteUtils.ts index b5aa66281e..9954f65ea7 100644 --- a/src/server/helpers/typeRouteUtils.ts +++ b/src/server/helpers/typeRouteUtils.ts @@ -15,6 +15,9 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +import * as error from '../../common/helpers/error'; + + function getChangedAttributeTypes(oldAttributes, newAttributes) { const commonAttributes = oldAttributes.filter(value => newAttributes.includes(value)); const attributesToBeRemoved = oldAttributes.filter(value => !commonAttributes.includes(value)); @@ -125,7 +128,7 @@ export async function identifierTypeCreateOrEditHandler(req, res) { newIdentifierType = await IdentifierType.forge({id: parseInt(req.params.id, 10)}).fetch({ require: true }); - method = 'update'; + method = 'insert'; } const { childOrder, @@ -154,6 +157,6 @@ export async function identifierTypeCreateOrEditHandler(req, res) { return res.status(200).send(identifierType.toJSON()); } catch (err) { - return res.status(500).json({error: 'A problem occured when processing the data'}); + return error.sendErrorAsJSON(res, new error.SiteError('A problem occurred while saving the identifier type')); } } From e56d8a83243805c1f0288075ea8221b14561dd9b Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 24 Aug 2023 22:54:44 +0530 Subject: [PATCH 74/88] feat: add reindex search engine as a privilege --- src/client/containers/layout.js | 11 +++++++++++ src/server/routes/search.js | 20 +++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/client/containers/layout.js b/src/client/containers/layout.js index 4502e3f36f..7caab06ac3 100644 --- a/src/client/containers/layout.js +++ b/src/client/containers/layout.js @@ -35,6 +35,7 @@ import Footer from './../components/footer'; import MergeQueue from '../components/pages/parts/merge-queue'; import PropTypes from 'prop-types'; import React from 'react'; +import {faSearchengin} from '@fortawesome/free-brands-svg-icons'; import {genEntityIconHTMLElement} from '../helpers/entity'; @@ -194,6 +195,15 @@ class Layout extends React.Component { ); + const reindexSearchEngineOption = ( + <> + + + Reindex Search Server + + + ); + const identifierTypeEditorOptions = ( <> @@ -213,6 +223,7 @@ class Layout extends React.Component { {checkPrivilege(user.privs, PrivilegeType.ADMIN) && adminOptions} {checkPrivilege(user.privs, PrivilegeType.RELATIONSHIP_TYPE_EDITOR) && relationshipTypeEditorOptions} {checkPrivilege(user.privs, PrivilegeType.IDENTIFIER_TYPE_EDITOR) && identifierTypeEditorOptions} + {checkPrivilege(user.privs, PrivilegeType.REINDEX_SEARCH_SERVER) && reindexSearchEngineOption} ); diff --git a/src/server/routes/search.js b/src/server/routes/search.js index 7644f98758..f7d270a558 100644 --- a/src/server/routes/search.js +++ b/src/server/routes/search.js @@ -28,6 +28,7 @@ import {keys as _keys, snakeCase as _snakeCase, isNil} from 'lodash'; import {escapeProps, generateProps} from '../helpers/props'; import Layout from '../../client/containers/layout'; +import {PrivilegeType} from '../../common/helpers/privileges-utils'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import SearchPage from '../../client/components/pages/search'; @@ -36,6 +37,7 @@ import target from '../templates/target'; const router = express.Router(); +const {REINDEX_SEARCH_SERVER} = PrivilegeType; /** * Generates React markup for the search page that is rendered by the user's @@ -142,23 +144,11 @@ router.get('/exists', (req, res) => { * * @throws {error.PermissionDeniedError} - Thrown if user is not admin. */ -router.get('/reindex', auth.isAuthenticated, (req, res) => { +router.get('/reindex', auth.isAuthenticated, auth.isAuthorized(REINDEX_SEARCH_SERVER), async (req, res) => { const {orm} = req.app.locals; - const indexPromise = new Promise((resolve) => { - // TODO: This is hacky, and we should replace it once we switch to SOLR. - const trustedUsers = ['Leftmost Cat', 'LordSputnik', 'Monkey', 'iliekcomputers']; + await search.generateIndex(orm); - const NO_MATCH = -1; - if (trustedUsers.indexOf(req.user.name) === NO_MATCH) { - throw new error.PermissionDeniedError(null, req); - } - - resolve(); - }) - .then(() => search.generateIndex(orm)) - .then(() => ({success: true})); - - handler.sendPromiseResult(res, indexPromise); + handler.sendPromiseResult(res, {success: true}); }); router.get('/entity/:bbid', async (req, res) => { From 3917a8445e3e2aef803dba9934ab40d2dfe2d36c Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Thu, 24 Aug 2023 23:05:10 +0530 Subject: [PATCH 75/88] misc: typeRouteUtils fix --- src/server/helpers/typeRouteUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/helpers/typeRouteUtils.ts b/src/server/helpers/typeRouteUtils.ts index 9954f65ea7..fc56a651fc 100644 --- a/src/server/helpers/typeRouteUtils.ts +++ b/src/server/helpers/typeRouteUtils.ts @@ -128,7 +128,7 @@ export async function identifierTypeCreateOrEditHandler(req, res) { newIdentifierType = await IdentifierType.forge({id: parseInt(req.params.id, 10)}).fetch({ require: true }); - method = 'insert'; + method = 'update'; } const { childOrder, From f28ab6ceb0d69a405979937669201a2c514ac7de Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Mon, 28 Aug 2023 13:00:42 +0000 Subject: [PATCH 76/88] fix: Redirect after login --- src/server/routes/auth.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/routes/auth.js b/src/server/routes/auth.js index d25b1401dc..32ce4d598b 100644 --- a/src/server/routes/auth.js +++ b/src/server/routes/auth.js @@ -44,6 +44,10 @@ router.get('/cb', (req, res, next) => { return res.redirect('/register/details'); } + const redirectTo = + req.session.redirectTo ? req.session.redirectTo : '/'; + req.session.redirectTo = null; + return req.logIn(user, async (loginErr) => { if (loginErr) { return next(loginErr); @@ -60,9 +64,6 @@ router.get('/cb', (req, res, next) => { return next(error); } - const redirectTo = - req.session.redirectTo ? req.session.redirectTo : '/'; - req.session.redirectTo = null; return res.redirect(redirectTo); }); })(req, res, next); From d979cd5047ae84f36f48c56de76190279fd421dc Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Tue, 12 Sep 2023 12:45:42 +0200 Subject: [PATCH 77/88] Remove travi config files We moved away from Travis as our CI environment years ago, these are just leftover from then --- .dockerignore | 1 - .editorconfig | 2 +- .travis.yml | 61 --------------------------------------------------- 3 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 .travis.yml diff --git a/.dockerignore b/.dockerignore index 74b390232d..08971d21c8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,6 +15,5 @@ test/ .gitignore .gitmodules .tern-project -.travis-yml LICENSE README.md diff --git a/.editorconfig b/.editorconfig index 8dce551d59..0ff53c21f6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,6 +11,6 @@ insert_final_newline = true trim_trailing_whitespace = true # The YAML spec explicitly forbids tabs; we use YAML for ESLint -[{package.json,.eslintrc,.travis.yml}] +[{package.json,.eslintrc}] indent_size = 2 indent_style = space diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 537d90805a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -language: node_js -node_js: - - "node" - - "lts/*" - -notifications: - email: false - irc: - channels: - - "libera.chat#bookbrainz" - on_success: always - on_failure: always - template: - - "Project %{repository_name} build #%{build_number}: %{result} in %{elapsed_time}: %{build_url}" - -cache: - directories: - - node_modules - -services: - - postgresql - - elasticsearch - -addons: - postgresql: "12" - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - postgresql-12 - - postgresql-client-12 - - g++-4.8 - -env: - global: - - CXX=g++-4.8 - -before_install: - - sudo service postgresql stop - - printf "local all all trust\nhost all all 127.0.0.1 trust" > pg_hba.conf - - sudo chown postgres pg_hba.conf - - sudo mv pg_hba.conf /etc/postgresql/12/main - - sed -e 's/^port.*/port = 5432/' /etc/postgresql/12/main/postgresql.conf > postgresql.conf - - sudo chown postgres postgresql.conf - - sudo mv postgresql.conf /etc/postgresql/12/main - - sudo service postgresql start 12 - -before_script: - - psql -c 'CREATE DATABASE bookbrainz_test;' -U postgres - - psql -c 'CREATE EXTENSION "uuid-ossp"; CREATE SCHEMA musicbrainz; CREATE SCHEMA bookbrainz;' -d bookbrainz_test -U postgres - - psql -f sql/schemas/musicbrainz.sql -d bookbrainz_test -U postgres - - psql -f sql/schemas/bookbrainz.sql -d bookbrainz_test -U postgres - - psql -f sql/scripts/create_triggers.sql -d bookbrainz_test -U postgres - - ./node_modules/.bin/eslint -v - - sleep 5 - -script: - - npm run-script test-ci - -after_script: - - npm install -g coveralls && coveralls < coverage/lcov.info From d1213087e81f1f27c40fcf8be90e7297324e4cb7 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Tue, 12 Sep 2023 12:47:28 +0200 Subject: [PATCH 78/88] chore: Update to Node 18 Node 16 is EOL, time to upgrade! --- .github/workflows/ci.yml | 2 +- Dockerfile | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df94098d2e..8f1828b5e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: uses: actions/setup-node@v1 with: # We could also test on multiple Node versions if needed: https://github.com/actions/setup-node#matrix-testing - node-version: '16' + node-version: '18' # Enables caching NPM dependencies (uses https://github.com/actions/cache under the hood) cache: 'yarn' diff --git a/Dockerfile b/Dockerfile index 9b6d410ea8..0b12c66378 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM metabrainz/node:16 as bookbrainz-base +FROM metabrainz/node:18 as bookbrainz-base ARG DEPLOY_ENV ARG GIT_COMMIT_SHA diff --git a/package.json b/package.json index 008fe8bd52..44c545d3f3 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dupreport": "npx jsinspect src/ || true" }, "engines": { - "node": ">= 16.16.0" + "node": ">= 18" }, "browserslist": "> 0.25%, not dead", "dependencies": { From fddcf26326d66f97ef02ef8d48c459cf08c6e46d Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sat, 16 Sep 2023 16:32:29 +0530 Subject: [PATCH 79/88] misc: remove .then from errorhandling --- .../forms/type-editor/identifier-type.tsx | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/client/components/forms/type-editor/identifier-type.tsx b/src/client/components/forms/type-editor/identifier-type.tsx index 8e0186eeda..b07dac5cd3 100644 --- a/src/client/components/forms/type-editor/identifier-type.tsx +++ b/src/client/components/forms/type-editor/identifier-type.tsx @@ -71,8 +71,7 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE // Function to handle child order input in ParentType modal const handleChildOrderChange = useCallback((event: ChangeEvent) => { - const value = parseInt(event.target.value, 10); - setChildOrder(isNaN(value) ? 0 : value); + setChildOrder(event.target.valueAsNumber || 0); }, [formData, childOrder]); // Function to handle parent removal @@ -150,7 +149,7 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE } function isFormEdited() { - return Boolean(JSON.stringify(formData) !== JSON.stringify(identifierTypeData)); + return JSON.stringify(formData) !== JSON.stringify(identifierTypeData); } const handleSubmit = useCallback(async (event: FormEvent) => { @@ -179,24 +178,15 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE } try { - await request.post(submissionURL).send(formData) - .end((error, response) => { - if (error) { - if (response && response.body && response.body.error) { - const errorMessage = response.body.error; - setErrorMsg(errorMessage); - setTimeout(() => { - setErrorMsg(''); - }, 3000); - } - } - else { - window.location.href = '/identifier-types'; - } - }); + await request.post(submissionURL).send(formData); + window.location.href = '/identifier-types'; } catch (err) { - throw new Error(err); + const errorMessage = err.response.body.error; + setErrorMsg(errorMessage); + setTimeout(() => { + setErrorMsg(''); + }, 3000); } }, [formData, isFormEdited, errorMsg]); @@ -205,7 +195,7 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE useEffect(() => { if (formData.parentId) { const parentType = parentTypes.find(type => type.id === formData.parentId); - if (formData.entityType && formData.entityType !== parentType.entityType) { + if (formData.entityType && formData.entityType !== parentType?.entityType) { handleRemoveParent(); } } @@ -424,7 +414,7 @@ function IdentifierTypeEditor({identifierTypeData, parentTypes}: IdentifierTypeE Close From 45c3016412c4a1e57bfb1fd2bc5ea2b0e12ff464 Mon Sep 17 00:00:00 2001 From: the-good-boy Date: Sat, 16 Sep 2023 16:32:57 +0530 Subject: [PATCH 80/88] fix: should not be able to add itself as the parentType --- src/server/routes/type-editor/identifier-type.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/routes/type-editor/identifier-type.tsx b/src/server/routes/type-editor/identifier-type.tsx index fca54b47ef..6719d3d631 100644 --- a/src/server/routes/type-editor/identifier-type.tsx +++ b/src/server/routes/type-editor/identifier-type.tsx @@ -65,6 +65,7 @@ router.get('/:id/edit', auth.isAuthenticated, auth.isAuthorized(IDENTIFIER_TYPE_ middleware.loadParentIdentifierTypes, async (req, res, next) => { const {IdentifierType} = req.app.locals.orm; const {parentTypes} = res.locals; + res.locals.parentTypes = parentTypes.filter(type => type.id !== parseInt(req.params.id, 10)); try { const identifierType = await new IdentifierType({id: req.params.id}) .fetch({require: true}) @@ -72,7 +73,7 @@ router.get('/:id/edit', auth.isAuthenticated, auth.isAuthorized(IDENTIFIER_TYPE_ throw new error.NotFoundError(`IdentifierType with id ${req.params.id} not found`, req); }); const identifierTypeData = identifierType.toJSON(); - const props = generateProps(req, res, {identifierTypeData, parentTypes}); + const props = generateProps(req, res, {identifierTypeData}); const script = '/js/identifier-type-editor.js'; const markup = ReactDOMServer.renderToString( From fd820932372f7923748b0a5f5e7d9ac735f081fc Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 18 Sep 2023 18:16:42 +0200 Subject: [PATCH 81/88] fix: Change modal confirm button text As discussed in https://github.com/metabrainz/bookbrainz-site/pull/1012#discussion_r1327948101 --- src/client/components/forms/type-editor/relationship-type.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/components/forms/type-editor/relationship-type.tsx b/src/client/components/forms/type-editor/relationship-type.tsx index 6b4599c07c..98db122ac8 100644 --- a/src/client/components/forms/type-editor/relationship-type.tsx +++ b/src/client/components/forms/type-editor/relationship-type.tsx @@ -497,7 +497,7 @@ function RelationshipTypeEditor({relationshipTypeData, parentTypes, attributeTyp Close From f55076c95223584bb8c18b3701be65951c0424e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 11:21:41 +0000 Subject: [PATCH 82/88] chore(deps-dev): bump jsdom from 20.0.0 to 22.1.0 Bumps [jsdom](https://github.com/jsdom/jsdom) from 20.0.0 to 22.1.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/20.0.0...22.1.0) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 157 +++++++++++++++++++++++++-------------------------- 2 files changed, 77 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index 426d8b340b..33f83fd093 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "eslint-webpack-plugin": "^2.4.1", "faker": "^4.1.0", "file-loader": "^6.2.0", - "jsdom": "20.0.0", + "jsdom": "22.1.0", "jsdom-global": "3.0.2", "mini-css-extract-plugin": "^2.7.4", "mocha": "^9.1.3", diff --git a/yarn.lock b/yarn.lock index 044e85840a..c504e90081 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2229,12 +2229,7 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abab@^2.0.3, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== - -abab@^2.0.6: +abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== @@ -2285,12 +2280,7 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1: - version "8.7.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" - integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== - -acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== @@ -3348,11 +3338,6 @@ cssom@^0.4.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== -cssom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" - integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== - cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -3365,6 +3350,13 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +cssstyle@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-3.0.0.tgz#17ca9c87d26eac764bb8cfd00583cff21ce0277a" + integrity sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg== + dependencies: + rrweb-cssom "^0.6.0" + csstype@^3.0.2: version "3.0.10" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" @@ -3387,14 +3379,14 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -data-urls@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" - integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== +data-urls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" + integrity sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g== dependencies: abab "^2.0.6" whatwg-mimetype "^3.0.0" - whatwg-url "^11.0.0" + whatwg-url "^12.0.0" date-fns@^2.15.0, date-fns@^2.24.0: version "2.28.0" @@ -3434,10 +3426,10 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decimal.js@^10.2.1, decimal.js@^10.3.1: - version "10.3.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" - integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decimal.js@^10.2.1, decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== decompress-response@^3.3.0: version "3.3.0" @@ -3723,6 +3715,11 @@ entities@^4.2.0, entities@^4.3.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + envinfo@^7.7.3: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -4992,15 +4989,7 @@ http-status@^1.6.2: resolved "https://registry.yarnpkg.com/http-status/-/http-status-1.6.2.tgz#6dc05188a9856d67d96e48e8b4fd645c719ce82a" integrity sha512-oUExvfNckrpTpDazph7kNG8sQi5au3BeTo0idaZFXEhTaJKu7GNJCLHI0rYY2wljm548MSTM+Ljj/c6anqu2zQ== -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -5584,37 +5573,33 @@ jsdom-global@3.0.2: resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-3.0.2.tgz#6bd299c13b0c4626b2da2c0393cd4385d606acb9" integrity sha512-t1KMcBkz/pT5JrvcJbpUR2u/w1kO9jXctaaGJ0vZDzwFnIvGWw9IDSRciT83kIs8Bnw4qpOl8bQK08V01YgMPg== -jsdom@20.0.0: - version "20.0.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.0.tgz#882825ac9cc5e5bbee704ba16143e1fa78361ebf" - integrity sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA== +jsdom@22.1.0: + version "22.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8" + integrity sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw== dependencies: abab "^2.0.6" - acorn "^8.7.1" - acorn-globals "^6.0.0" - cssom "^0.5.0" - cssstyle "^2.3.0" - data-urls "^3.0.2" - decimal.js "^10.3.1" + cssstyle "^3.0.0" + data-urls "^4.0.0" + decimal.js "^10.4.3" domexception "^4.0.0" - escodegen "^2.0.0" form-data "^4.0.0" html-encoding-sniffer "^3.0.0" http-proxy-agent "^5.0.0" https-proxy-agent "^5.0.1" is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "^7.0.0" + nwsapi "^2.2.4" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" saxes "^6.0.0" symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^3.0.0" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" webidl-conversions "^7.0.0" whatwg-encoding "^2.0.0" whatwg-mimetype "^3.0.0" - whatwg-url "^11.0.0" - ws "^8.8.0" + whatwg-url "^12.0.1" + ws "^8.13.0" xml-name-validator "^4.0.0" jsdom@^16.5.2: @@ -6361,10 +6346,10 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -nwsapi@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +nwsapi@^2.2.0, nwsapi@^2.2.4: + version "2.2.7" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" + integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== oauth@0.9.x: version "0.9.15" @@ -6625,12 +6610,12 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -parse5@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.0.0.tgz#51f74a5257f5fcc536389e8c2d0b3802e1bfa91a" - integrity sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g== +parse5@^7.0.0, parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== dependencies: - entities "^4.3.0" + entities "^4.4.0" parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" @@ -7008,6 +6993,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + pupa@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" @@ -7596,6 +7586,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rrweb-cssom@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" + integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" @@ -8242,7 +8237,7 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^4.0.0: +tough-cookie@^4.0.0, tough-cookie@^4.1.2: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== @@ -8259,12 +8254,12 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" -tr46@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" - integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== +tr46@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-4.1.1.tgz#281a758dcc82aeb4fe38c7dfe4d11a395aac8469" + integrity sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw== dependencies: - punycode "^2.1.1" + punycode "^2.3.0" tsconfig-paths@^3.14.1: version "3.14.1" @@ -8542,10 +8537,10 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -w3c-xmlserializer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" - integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== dependencies: xml-name-validator "^4.0.0" @@ -8707,12 +8702,12 @@ whatwg-mimetype@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== -whatwg-url@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" - integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== +whatwg-url@^12.0.0, whatwg-url@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-12.0.1.tgz#fd7bcc71192e7c3a2a97b9a8d6b094853ed8773c" + integrity sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ== dependencies: - tr46 "^3.0.0" + tr46 "^4.1.1" webidl-conversions "^7.0.0" whatwg-url@^8.0.0, whatwg-url@^8.5.0: @@ -8801,10 +8796,10 @@ ws@^7.3.1, ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== -ws@^8.8.0: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" - integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== +ws@^8.13.0: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== xdg-basedir@^4.0.0: version "4.0.0" From eaacc8a3701f4e7226a470471a0064c62dba1803 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 21 Sep 2023 16:26:57 +0000 Subject: [PATCH 83/88] Add authors to search index terms --- src/common/helpers/search.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/common/helpers/search.js b/src/common/helpers/search.js index b32bd8321c..5f2fe25d33 100644 --- a/src/common/helpers/search.js +++ b/src/common/helpers/search.js @@ -549,6 +549,9 @@ export function searchByName(orm, name, type, size, from) { index: _index, type: sanitizeEntityType(type) }; + if (type === 'work') { + dslQuery.body.query.multi_match.fields.push('authors'); + } return _searchForEntities(orm, dslQuery); } From 2111d5e0863f176bbdec2a6480ab42b08495214d Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 21 Sep 2023 17:11:26 +0000 Subject: [PATCH 84/88] Style author names in search results --- src/client/components/pages/parts/search-results.js | 6 +++++- src/client/entity-editor/common/entity.tsx | 7 +++++-- src/client/entity-editor/common/linked-entity.tsx | 7 +++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/client/components/pages/parts/search-results.js b/src/client/components/pages/parts/search-results.js index 4fd69ba61a..7f3d592afa 100644 --- a/src/client/components/pages/parts/search-results.js +++ b/src/client/components/pages/parts/search-results.js @@ -19,7 +19,7 @@ import * as bootstrap from 'react-bootstrap'; -import {differenceBy as _differenceBy, kebabCase as _kebabCase, startCase as _startCase} from 'lodash'; +import {differenceBy as _differenceBy, kebabCase as _kebabCase, startCase as _startCase, toLower} from 'lodash'; import AddToCollectionModal from './add-to-collection-modal'; import PropTypes from 'prop-types'; @@ -185,6 +185,10 @@ class SearchResults extends React.Component { {name} {disambiguation} + { + toLower(result.type) === 'work' && Boolean(result.authors?.length) && + {result.authors.join(', ')} + } { !this.props.condensed && diff --git a/src/client/entity-editor/common/entity.tsx b/src/client/entity-editor/common/entity.tsx index 8e3bc7e25c..0f6c29d83d 100644 --- a/src/client/entity-editor/common/entity.tsx +++ b/src/client/entity-editor/common/entity.tsx @@ -42,10 +42,13 @@ function Entity( } {nameComponent} { - authors ? ({authors}) : - disambiguation && + disambiguation && ({disambiguation}) } + { + authors && + {authors} + } { language && {language} diff --git a/src/client/entity-editor/common/linked-entity.tsx b/src/client/entity-editor/common/linked-entity.tsx index 8508c577cf..2798f1cbf1 100644 --- a/src/client/entity-editor/common/linked-entity.tsx +++ b/src/client/entity-editor/common/linked-entity.tsx @@ -102,10 +102,9 @@ class LinkedEntity extends React.Component {  ({disambiguation}) } - {authors && - -  — {authors} - + { + authors && + {authors} } {' '} {externalLinkComponent} From 84b5703004f130ff474f5c66a34aa55284e421cc Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 21 Sep 2023 19:18:55 +0200 Subject: [PATCH 85/88] Lint --- src/client/components/pages/parts/search-results.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/components/pages/parts/search-results.js b/src/client/components/pages/parts/search-results.js index 7f3d592afa..c38291be08 100644 --- a/src/client/components/pages/parts/search-results.js +++ b/src/client/components/pages/parts/search-results.js @@ -187,7 +187,7 @@ class SearchResults extends React.Component { { toLower(result.type) === 'work' && Boolean(result.authors?.length) && - {result.authors.join(', ')} + {result.authors.join(', ')} } { From 583ab54c4b7e73e135fafc438819170efe08de50 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 21 Sep 2023 19:25:10 +0200 Subject: [PATCH 86/88] chore: Fix linting errors and warnings There are two warnings left regarding todos that would need to be fixed in a separate PR (the "do" in "todo") --- .../components/input/drag-and-drop-image.tsx | 1 + src/client/components/pages/revision.js | 21 +++++++++++++++---- .../work-section/work-section.tsx | 7 ++++--- src/client/helpers/utils.tsx | 6 +++--- .../unified-form/submit-tab/summary.tsx | 1 - .../helpers/diffFormatters/authorCredit.ts | 1 - src/server/routes/search.js | 2 +- 7 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/client/components/input/drag-and-drop-image.tsx b/src/client/components/input/drag-and-drop-image.tsx index d4a4a57fe4..82caa7b788 100644 --- a/src/client/components/input/drag-and-drop-image.tsx +++ b/src/client/components/input/drag-and-drop-image.tsx @@ -19,6 +19,7 @@ import PropTypes from 'prop-types'; import React from 'react'; + const {useCallback} = React; /** diff --git a/src/client/components/pages/revision.js b/src/client/components/pages/revision.js index 505db6eec4..93ed0f532f 100644 --- a/src/client/components/pages/revision.js +++ b/src/client/components/pages/revision.js @@ -105,18 +105,31 @@ class RevisionPage extends React.Component { let deleteBadge = null; if (diff.isDeletion) { if (diff.entityRevision.isMerge) { - mergeBadge = Merged; + mergeBadge = ( + Merged + ); } else { - deleteBadge = - - Deleted; + deleteBadge = ( + - Deleted + ); } } return (

    {diff.isNew && - + New} + + New + } {mergeBadge} {deleteBadge} ) { +function workTypeSelectMenuOption(props: Select.OptionProps) { const {data, label} = props; const {depth, id, description} = data; let indentationClass; diff --git a/src/client/helpers/utils.tsx b/src/client/helpers/utils.tsx index 67a4c76286..6545b33980 100644 --- a/src/client/helpers/utils.tsx +++ b/src/client/helpers/utils.tsx @@ -181,11 +181,11 @@ export function dateObjectToISOString(value: DateObject) { export function stringToHTMLWithLinks(content: string) { // eslint-disable-next-line max-len, no-useless-escape const urlRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%~*@\.\w_]*)#?(?:[\.\!\/\\:\w]*))?)/g; - content = content.replace( + const replacedContent = content.replace( urlRegex, - (url) => `${url}` + (url) => `${url}` ); - const sanitizedHtml = DOMPurify.sanitize(content, {ADD_ATTR: ['target']}); + const sanitizedHtml = DOMPurify.sanitize(replacedContent, {ADD_ATTR: ['target']}); // eslint-disable-next-line react/no-danger return ; } diff --git a/src/client/unified-form/submit-tab/summary.tsx b/src/client/unified-form/submit-tab/summary.tsx index 5a87824f89..5dc8a026f5 100644 --- a/src/client/unified-form/submit-tab/summary.tsx +++ b/src/client/unified-form/submit-tab/summary.tsx @@ -1,4 +1,3 @@ -import {Badge, ListGroup} from 'react-bootstrap'; import {Entity, State, SummarySectionProps, SummarySectionStateProps} from '../interface/type'; import Immutable from 'immutable'; import React from 'react'; diff --git a/src/server/helpers/diffFormatters/authorCredit.ts b/src/server/helpers/diffFormatters/authorCredit.ts index f8bbea16b4..b7bf4bd10c 100644 --- a/src/server/helpers/diffFormatters/authorCredit.ts +++ b/src/server/helpers/diffFormatters/authorCredit.ts @@ -16,7 +16,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -import * as base from './base'; import * as set from './set'; diff --git a/src/server/routes/search.js b/src/server/routes/search.js index f7d270a558..4aaa036941 100644 --- a/src/server/routes/search.js +++ b/src/server/routes/search.js @@ -58,7 +58,7 @@ router.get('/', async (req, res, next) => { if (query) { // get 1 more results to check nextEnabled const searchResponse = await search.searchByName(orm, query, _snakeCase(type), size + 1, from); - const {results: entities, total} = searchResponse; + const {results: entities} = searchResponse; searchResults = { initialResults: entities.filter(entity => !isNil(entity)), query From 5f6742033ea61615651a5d425ffe442535cb08e7 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 22 Sep 2023 14:18:06 +0200 Subject: [PATCH 87/88] fix(JSDocs): Remove empty @returns statement Currently making the JSDocs generation workflow fail with an error. --- src/server/helpers/middleware.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/helpers/middleware.ts b/src/server/helpers/middleware.ts index a5544b81c7..6cc0a59071 100644 --- a/src/server/helpers/middleware.ts +++ b/src/server/helpers/middleware.ts @@ -140,7 +140,6 @@ export async function loadWorkTableAuthors(req: $Request, res: $Response, next: * @param {Object} entity - The entity to load the relationships for. * @param {Object} relationshipSet - The RelationshipSet model. * @param {Object} orm - The ORM instance. - * @returns */ export async function addRelationships(entity, relationshipSet, orm) { From a9c5a0a40ed2b33d641f6b6fecd3677cde92cd29 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 22 Sep 2023 14:33:48 +0000 Subject: [PATCH 88/88] search: search by author name when "all entities" is selected --- src/common/helpers/search.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/common/helpers/search.js b/src/common/helpers/search.js index 5f2fe25d33..813c189034 100644 --- a/src/common/helpers/search.js +++ b/src/common/helpers/search.js @@ -528,6 +528,7 @@ export async function checkIfExists(orm, name, type) { } export function searchByName(orm, name, type, size, from) { + const sanitizedEntityType = sanitizeEntityType(type); const dslQuery = { body: { from, @@ -547,9 +548,9 @@ export function searchByName(orm, name, type, size, from) { size }, index: _index, - type: sanitizeEntityType(type) + type: sanitizedEntityType }; - if (type === 'work') { + if (sanitizedEntityType === 'work' || (Array.isArray(sanitizedEntityType) && sanitizedEntityType.includes('work'))) { dslQuery.body.query.multi_match.fields.push('authors'); }