strapi-plugin-comments 2.0.2 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.husky/pre-commit +4 -0
  2. package/README.md +31 -8
  3. package/__mocks__/initSetup.js +31 -2
  4. package/admin/src/components/FormSwitch/index.js +18 -0
  5. package/admin/src/components/FormSwitch/styles.js +8 -0
  6. package/admin/src/hooks/useConfig.js +49 -0
  7. package/admin/src/index.js +31 -2
  8. package/admin/src/pages/Details/index.js +5 -2
  9. package/admin/src/pages/Settings/components/RestartAlert/styles.js +8 -0
  10. package/admin/src/pages/Settings/index.js +392 -0
  11. package/admin/src/pages/Settings/utils/api.js +60 -0
  12. package/admin/src/permissions.js +3 -0
  13. package/admin/src/translations/en.json +46 -1
  14. package/admin/src/utils/handleAPIError.js +3 -2
  15. package/admin/src/utils/index.js +2 -1
  16. package/admin/src/utils/parseRegExp.js +10 -0
  17. package/package.json +10 -7
  18. package/server/bootstrap.js +24 -0
  19. package/server/config/index.js +1 -0
  20. package/server/controllers/admin.js +42 -8
  21. package/server/controllers/client.js +11 -2
  22. package/server/controllers/utils/__tests__/parsers.test.js +94 -0
  23. package/server/controllers/utils/parsers.js +15 -13
  24. package/server/graphql/index.js +7 -8
  25. package/server/graphql/queries/findAllFlat.js +1 -1
  26. package/server/graphql/resolvers-config.js +2 -4
  27. package/server/graphql/types/index.js +3 -0
  28. package/server/graphql/types/report-reason.js +10 -10
  29. package/server/graphql/types/response-find-all.js +7 -0
  30. package/server/graphql/types/response-meta.js +6 -0
  31. package/server/graphql/types/response-pagination.js +11 -0
  32. package/server/register.js +1 -3
  33. package/server/routes/admin.js +32 -0
  34. package/server/services/__tests__/common.test.js +95 -14
  35. package/server/services/admin.js +57 -6
  36. package/server/services/client.js +14 -8
  37. package/server/services/common.js +92 -15
  38. package/server/services/utils/__tests__/functions.test.js +12 -1
  39. package/server/services/utils/functions.js +9 -1
  40. package/server/utils/constants.js +9 -0
  41. package/strapi-server.js +12 -5
@@ -10,7 +10,7 @@ const {
10
10
  getRelatedGroups,
11
11
  resolveUserContextError,
12
12
  } = require('./utils/functions');
13
- const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
13
+ const { APPROVAL_STATUS, REGEX, CONFIG_PARAMS } = require('./../utils/constants')
14
14
 
15
15
  /**
16
16
  * Comments Plugin - Client services
@@ -31,16 +31,15 @@ module.exports = ({ strapi }) => ({
31
31
  throw new PluginError(400, `Field "related" got incorrect format, use format like "api::<collection name>.<content type name>:<entity id>"`);
32
32
  }
33
33
 
34
- const [ uid, relatedStringId ] = getRelatedGroups(relation);
35
- const parsedRelatedId = parseInt(relatedStringId);
36
- const relatedId = isNumber(parsedRelatedId) ? parsedRelatedId : relatedStringId;
34
+ const [ uid, relatedId ] = await this.getCommonService().parseRelationString(relation);
37
35
 
38
36
  const relatedEntity = await strapi.db.query(uid).findOne(relatedId);
39
37
  if (!relatedEntity) {
40
38
  throw new PluginError(400, `Relation for field "related" does not exist. Check your payload please.`);
41
39
  }
42
40
 
43
- const isApprovalFlowEnabled = this.getCommonService().getConfig('approvalFlow', []).includes(uid) ||
41
+ const approvalFlow = await this.getCommonService().getConfig(CONFIG_PARAMS.APPROVAL_FLOW, []);
42
+ const isApprovalFlowEnabled = approvalFlow.includes(uid) ||
44
43
  relatedEntity.requireCommentsApproval;
45
44
 
46
45
  const linkToThread = !isNil(threadOf) ? await this.getCommonService().findOne({
@@ -57,7 +56,7 @@ module.exports = ({ strapi }) => ({
57
56
  throw resolveUserContextError(user);
58
57
  }
59
58
 
60
- if (this.getCommonService().checkBadWords(content) && singleRelationFulfilled) {
59
+ if (await this.getCommonService().checkBadWords(content) && singleRelationFulfilled) {
61
60
  const { author = {}, ...rest } = data;
62
61
  let authorData = {};
63
62
  if (validContext && user) {
@@ -115,6 +114,8 @@ module.exports = ({ strapi }) => ({
115
114
  throw new PluginError(400, `Request property "relation" got incorrect format, use format like "api::<collection name>.<content type name>:<entity id>"`);
116
115
  }
117
116
 
117
+ await this.getCommonService().parseRelationString(relation);
118
+
118
119
  const existingEntity = await this.getCommonService().findOne({
119
120
  id,
120
121
  related: relation,
@@ -127,7 +128,7 @@ module.exports = ({ strapi }) => ({
127
128
  }
128
129
 
129
130
  if (isEqualEntity(existingEntity, data, user) && content) {
130
- if (this.getCommonService().checkBadWords(content)) {
131
+ if (await this.getCommonService().checkBadWords(content)) {
131
132
  const entity = await strapi.db.query(getModelUid('comment')).update({
132
133
  where: { id },
133
134
  data: { content },
@@ -144,6 +145,9 @@ module.exports = ({ strapi }) => ({
144
145
  if (!this.getCommonService().isValidUserContext(user)) {
145
146
  throw resolveUserContextError(user);
146
147
  }
148
+
149
+ await this.getCommonService().parseRelationString(relation);
150
+
147
151
  const reportAgainstEntity = await this.getCommonService().findOne({
148
152
  id,
149
153
  related: relation,
@@ -185,6 +189,8 @@ module.exports = ({ strapi }) => ({
185
189
  throw new PluginError(403, `You're not allowed to take an action on that entity. Make sure that you've provided proper "authorId" or authenticated your request properly.`);
186
190
  }
187
191
 
192
+ await this.getCommonService().parseRelationString(relation);
193
+
188
194
  let entity;
189
195
  try {
190
196
  const byAuthor = user?.id ? {
@@ -216,7 +222,7 @@ module.exports = ({ strapi }) => ({
216
222
  },
217
223
 
218
224
  async sendAbuseReportEmail(reason, content) {
219
- const rolesToBeNotified = this.getCommonService().getConfig('moderatorRoles') || ['strapi-super-admin'];
225
+ const rolesToBeNotified = await this.getCommonService().getConfig(CONFIG_PARAMS.MODERATOR_ROLES) || ['strapi-super-admin'];
220
226
 
221
227
  const adminUserModel = strapi.db.query('admin::user');
222
228
  const emails = await adminUserModel.findMany({
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const BadWordsFilter = require('bad-words');
4
- const { isArray, isNumber, isObject, isNil, isString, first, parseInt, set } = require('lodash');
5
- const { REGEX } = require('../utils/constants');
4
+ const { isArray, isNumber, isObject, isNil, isString, first, parseInt, set, get } = require('lodash');
5
+ const { REGEX, CONFIG_PARAMS } = require('../utils/constants');
6
6
  const PluginError = require('./../utils/error');
7
7
  const {
8
8
  getModelUid,
@@ -10,6 +10,7 @@ const {
10
10
  buildNestedStructure,
11
11
  filterOurResolvedReports,
12
12
  buildAuthorModel,
13
+ buildConfigQueryProp,
13
14
  } = require('./utils/functions');
14
15
 
15
16
  /**
@@ -18,11 +19,26 @@ const {
18
19
 
19
20
  module.exports = ({ strapi }) => ({
20
21
 
21
- getConfig(prop, defaultValue) {
22
- let queryProp = prop;
23
- if (prop && isArray(prop)) {
24
- queryProp = prop.join('.');
22
+ async getConfig(prop, defaultValue, useLocal = false) {
23
+ const queryProp = buildConfigQueryProp(prop);
24
+ const pluginStore = await this.getPluginStore();
25
+ const config = await pluginStore.get({ key: 'config' });
26
+
27
+ let result;
28
+ if (config && !useLocal) {
29
+ result = queryProp ? get(config, queryProp, defaultValue) : config;
30
+ } else {
31
+ result = this.getLocalConfig(queryProp, defaultValue);
25
32
  }
33
+ return isNil(result) ? defaultValue : result;
34
+ },
35
+
36
+ async getPluginStore() {
37
+ return strapi.store({ type: 'plugin', name: 'comments' });
38
+ },
39
+
40
+ getLocalConfig(prop, defaultValue) {
41
+ const queryProp = buildConfigQueryProp(prop);
26
42
  const result = strapi.config.get(`plugin.comments${ queryProp ? '.' + queryProp : ''}`);
27
43
  return isNil(result) ? defaultValue : result;
28
44
  },
@@ -52,12 +68,35 @@ module.exports = ({ strapi }) => ({
52
68
  };
53
69
  }
54
70
 
71
+ let meta = {};
55
72
  if (pagination && isObject(pagination)) {
56
- const { page = 1, pageSize = 10 } = pagination;
73
+ const parsedPagination = Object.keys(pagination).reduce((prev, curr) => ({
74
+ ...prev,
75
+ [curr]: parseInt(pagination[curr]),
76
+ }), {});
77
+ const { page, pageSize = 10, limit, start = 0 } = parsedPagination
78
+ const paginationByPage = !isNil(page);
79
+
57
80
  queryExtension = {
58
81
  ...queryExtension,
59
- offset: (page - 1) * pageSize,
60
- limit: pageSize,
82
+ offset: paginationByPage ? (page - 1) * pageSize : start,
83
+ limit: paginationByPage ? pageSize : limit,
84
+ };
85
+
86
+ const metaPagination = paginationByPage ? {
87
+ pagination: {
88
+ page,
89
+ pageSize,
90
+ },
91
+ } : {
92
+ pagination: {
93
+ start,
94
+ limit,
95
+ },
96
+ };
97
+
98
+ meta = {
99
+ ...metaPagination,
61
100
  };
62
101
  }
63
102
 
@@ -73,6 +112,26 @@ module.exports = ({ strapi }) => ({
73
112
  ...queryExtension,
74
113
  });
75
114
 
115
+ if (pagination?.withCount && (pagination.withCount === 'true' || pagination.withCount === true)) {
116
+ const total = await strapi.db.query(getModelUid('comment'))
117
+ .count({
118
+ where: {
119
+ ...query,
120
+ }
121
+ });
122
+ const pageCount = Math.floor(total / meta.pagination.pageSize);
123
+ meta = {
124
+ ...meta,
125
+ pagination: {
126
+ ...meta.pagination,
127
+ pageCount: !isNil(meta.pagination.page) ?
128
+ total % meta.pagination.pageSize === 0 ? pageCount : pageCount + 1 :
129
+ undefined,
130
+ total,
131
+ },
132
+ };
133
+ }
134
+
76
135
  const entriesWithThreads = await Promise.all(entries.map(async _ => {
77
136
  const [nestedEntries, count] = await strapi.db.query(getModelUid('comment'))
78
137
  .findWithCount({
@@ -96,9 +155,15 @@ module.exports = ({ strapi }) => ({
96
155
  });
97
156
 
98
157
  if (relatedEntities.filter(_ => _).length > 0) {
99
- return result.map(_ => this.mergeRelatedEntityTo(_, relatedEntities));
158
+ return {
159
+ data: result.map(_ => this.mergeRelatedEntityTo(_, relatedEntities)),
160
+ meta,
161
+ };
100
162
  }
101
- return result;
163
+ return {
164
+ data: result,
165
+ meta,
166
+ };
102
167
  },
103
168
 
104
169
  // Find comments and create relations tree structure
@@ -109,7 +174,7 @@ module.exports = ({ strapi }) => ({
109
174
  startingFromId = null,
110
175
  dropBlockedThreads = false,
111
176
  }, relatedEntity) {
112
- const entities = await this.findAllFlat({ query, populate, sort }, relatedEntity);
177
+ const entities = await this.findAllFlat({ query, populate, sort }, relatedEntity)?.data;
113
178
  return buildNestedStructure(entities, startingFromId, 'threadOf', dropBlockedThreads, false);
114
179
  },
115
180
 
@@ -124,7 +189,7 @@ module.exports = ({ strapi }) => ({
124
189
  },
125
190
  });
126
191
  if (!entity){
127
- throw new PluginError(404, 'Not found');
192
+ throw new PluginError(404, 'Comment does not exist. Check your payload please.');
128
193
  }
129
194
  return filterOurResolvedReports(this.sanitizeCommentEntity(entity));
130
195
  },
@@ -202,8 +267,20 @@ module.exports = ({ strapi }) => ({
202
267
  return user ? !isNil(user?.id) : true;
203
268
  },
204
269
 
205
- checkBadWords(content) {
206
- const config = this.getConfig('badWords', true);
270
+ async parseRelationString(relation) {
271
+ const [ uid, relatedStringId ] = getRelatedGroups(relation);
272
+ const parsedRelatedId = parseInt(relatedStringId);
273
+ const relatedId = isNumber(parsedRelatedId) ? parsedRelatedId : relatedStringId;
274
+
275
+ const enabledCollections = await this.getConfig(CONFIG_PARAMS.ENABLED_COLLECTIONS, []);
276
+ if (!enabledCollections.includes(uid)) {
277
+ throw new PluginError(403, `Action not allowed for collection '${uid}'. Use one of: ${ enabledCollections.join(', ') }`);
278
+ }
279
+ return [ uid, relatedId];
280
+ },
281
+
282
+ async checkBadWords(content) {
283
+ const config = await this.getConfig(CONFIG_PARAMS.BAD_WORDS, true);
207
284
  if (config) {
208
285
  const filter = new BadWordsFilter(isObject(config) ? config : undefined);
209
286
  if (content && filter.isProfane(content)) {
@@ -1,5 +1,5 @@
1
1
  const PluginError = require('../../../utils/error');
2
- const { resolveUserContextError, getModelUid } = require('../functions');
2
+ const { resolveUserContextError, getModelUid, buildConfigQueryProp } = require('../functions');
3
3
 
4
4
  beforeEach(() => {
5
5
  Object.defineProperty(global, 'strapi', {
@@ -39,4 +39,15 @@ describe('Test service functions utils', () => {
39
39
  }
40
40
  });
41
41
  });
42
+
43
+ describe('Building config query props', () => {
44
+ test('Should return prop as is', () => {
45
+ expect(buildConfigQueryProp('my.sample.prop')).toBe('my.sample.prop');
46
+ });
47
+
48
+ test('Should build query prop with \'.\'', () => {
49
+ expect(buildConfigQueryProp(['my', 'sample', 'prop'])).toBe('my.sample.prop');
50
+ });
51
+ });
52
+
42
53
  });
@@ -1,6 +1,6 @@
1
1
  const PluginError = require('./../../utils/error');
2
2
  const { REGEX } = require('./../../utils/constants')
3
- const { first, isObject, isEmpty } = require('lodash');
3
+ const { first, isObject, isArray, isEmpty } = require('lodash');
4
4
 
5
5
  const buildNestedStructure = (
6
6
  entities,
@@ -93,6 +93,14 @@ module.exports = {
93
93
  };
94
94
  },
95
95
 
96
+ buildConfigQueryProp(prop) {
97
+ let queryProp = prop;
98
+ if (prop && isArray(prop)) {
99
+ queryProp = prop.join('.');
100
+ }
101
+ return queryProp;
102
+ },
103
+
96
104
  resolveUserContextError: user => {
97
105
  if (user) {
98
106
  throw new PluginError(401, 'Not authenticated');
@@ -1,3 +1,11 @@
1
+ const CONFIG_PARAMS = {
2
+ ENABLED_COLLECTIONS: 'enabledCollections',
3
+ APPROVAL_FLOW: 'approvalFlow',
4
+ ENTRY_LABEL: 'entryLabel',
5
+ MODERATOR_ROLES: 'moderatorRoles',
6
+ BAD_WORDS: 'badWords',
7
+ };
8
+
1
9
  const APPROVAL_STATUS = {
2
10
  PENDING: "PENDING",
3
11
  APPROVED: "APPROVED",
@@ -12,6 +20,7 @@ const REGEX = {
12
20
  };
13
21
 
14
22
  module.exports = {
23
+ CONFIG_PARAMS,
15
24
  APPROVAL_STATUS,
16
25
  REGEX,
17
26
  };
package/strapi-server.js CHANGED
@@ -4,7 +4,7 @@ const { isArray, isEmpty, isString } = require('lodash');
4
4
 
5
5
  const server = require('./server');
6
6
  const contentTypes = require('./content-types');
7
- const { REGEX } = require('./server/utils/constants');
7
+ const { REGEX, CONFIG_PARAMS } = require('./server/utils/constants');
8
8
 
9
9
  module.exports = () => ({
10
10
  ...server,
@@ -15,24 +15,31 @@ module.exports = () => ({
15
15
  ...server.config.default,
16
16
  }),
17
17
  validator: (config) => {
18
+ // Check enabledCollections values
19
+ const enabledCollectionsValid = config[CONFIG_PARAMS.ENABLED_COLLECTIONS]
20
+ .filter(_ => !REGEX.uid.test(_)).length === 0;
21
+ if (!enabledCollectionsValid) {
22
+ throw new Error(`'enabledCollections' must contain Content Types identifiers in the format like 'api::<collection name>.<content type name>'`);
23
+ }
24
+
18
25
  // Check moderatorRoles values
19
- const moderatorRolesValid = config.moderatorRoles
26
+ const moderatorRolesValid = config[CONFIG_PARAMS.MODERATOR_ROLES]
20
27
  .filter(_ => !isString(_)).length === 0;
21
28
  if (!moderatorRolesValid) {
22
29
  throw new Error(`'moderatorRoles' must roles keys`);
23
30
  }
24
31
 
25
32
  // Check approvalFlow values
26
- const approvalFlowValid = config.approvalFlow
33
+ const approvalFlowValid = config[CONFIG_PARAMS.APPROVAL_FLOW]
27
34
  .filter(_ => !REGEX.uid.test(_)).length === 0;
28
35
  if (!approvalFlowValid) {
29
36
  throw new Error(`'approvalFlow' must contain Content Types identifiers in the format like 'api::<collection name>.<content type name>'`);
30
37
  }
31
38
 
32
39
  // Check entryLabel keys and values
33
- const entryLabelKeysValid = Object.keys(config.entryLabel)
40
+ const entryLabelKeysValid = Object.keys(config[CONFIG_PARAMS.ENTRY_LABEL])
34
41
  .filter(_ => _ !== '*')
35
- .filter(_ => !(REGEX.uid.test(_) && isArray(config.entryLabel[_]) && !isEmpty(config.entryLabel[_]))).length === 0;
42
+ .filter(_ => !(REGEX.uid.test(_) && isArray(config[CONFIG_PARAMS.ENTRY_LABEL][_]) && !isEmpty(config[CONFIG_PARAMS.ENTRY_LABEL][_]))).length === 0;
36
43
 
37
44
  if (!entryLabelKeysValid) {
38
45
  throw new Error(`'entryLabel' must contain records in the format like 'api::<collection name>.<content type name>': ['Field1', 'field_2']`);