From a07010f50ce537d2ef5a30b2f9ce13c2b0a30ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 10 Oct 2019 15:38:20 +0200 Subject: [PATCH 01/12] First step of implementing direct report relation --- backend/src/schema/index.js | 2 +- backend/src/schema/resolvers/reports.js | 21 ++++++------ backend/src/schema/resolvers/reports.spec.js | 2 +- backend/src/schema/types/schema.gql | 35 ++++++++++++-------- backend/src/schema/types/type/REPORTED.gql | 30 +++++++++++++++++ backend/src/seed/seed-db.js | 2 +- cypress/integration/common/report.js | 2 +- webapp/graphql/Moderation.js | 7 ++-- 8 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 backend/src/schema/types/type/REPORTED.gql diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 4d1b9574e..e3c835447 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -35,7 +35,7 @@ export default applyScalars( 'Notfication', 'Post', 'Comment', - 'Report', + 'REPORTED', 'Statistics', 'LoggedInUser', 'Location', diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 4b86e57f4..2cf1187e0 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -8,10 +8,10 @@ export default { { driver, _req, user }, _resolveInfo, ) => { - const reportId = uuid() + // Wolle const reportId = uuid() const session = driver.session() const reportProperties = { - id: reportId, + // Wolle id: reportId, createdAt: new Date().toISOString(), reasonCategory, reasonDescription, @@ -19,7 +19,7 @@ export default { const reportQueryRes = await session.run( ` - MATCH (u:User {id:$submitterId})-[:REPORTED]->(report)-[:REPORTED]->(resource {id: $resourceId}) + MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId}) RETURN labels(resource)[0] as label `, { @@ -39,22 +39,22 @@ export default { const res = await session.run( ` - MATCH (submitter:User {id: $userId}) + MATCH (submitter:User {id: $submitterId}) MATCH (resource {id: $resourceId}) WHERE resource:User OR resource:Comment OR resource:Post - CREATE (report:Report {reportProperties}) - MERGE (resource)<-[:REPORTED]-(report) - MERGE (report)<-[:REPORTED]-(submitter) + CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter) RETURN report, submitter, resource, labels(resource)[0] as type `, { resourceId, - userId: user.id, - reportProperties, + submitterId: user.id, + createdAt: reportProperties.createdAt, + reasonCategory: reportProperties.reasonCategory, + reasonDescription: reportProperties.reasonDescription, }, ) - session.close() + session.close() const [dbResponse] = res.records.map(r => { return { @@ -65,6 +65,7 @@ export default { } }) if (!dbResponse) return null + const { report, submitter, resource, type } = dbResponse const response = { diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 4b0aa7ba4..cf89dab7a 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -16,7 +16,7 @@ describe('report', () => { const categoryIds = ['cat9'] beforeEach(async () => { - returnedObject = '{ id }' + returnedObject = '{ createdAt }' variables = { resourceId: 'whatever', reasonCategory: 'reason-category-dummy', diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index cdf0d7f87..e4986ad73 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -24,7 +24,6 @@ type Mutation { changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean! - report(resourceId: ID!, reasonCategory: String!, reasonDescription: String!): Report disable(id: ID!): ID enable(id: ID!): ID # Shout the given Type and ID @@ -35,18 +34,28 @@ type Mutation { unfollowUser(id: ID!): User } -type Report { - id: ID! - createdAt: String! - reasonCategory: String! - reasonDescription: String! - submitter: User @relation(name: "REPORTED", direction: "IN") - type: String! - @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") - comment: Comment @relation(name: "REPORTED", direction: "OUT") - post: Post @relation(name: "REPORTED", direction: "OUT") - user: User @relation(name: "REPORTED", direction: "OUT") -} +# Wolle +# type Report { + # not necessary + # id: ID! + # done + # createdAt: String! + # done + # reasonCategory: String! + # done + # reasonDescription: String! + # done + # submitter: User @relation(name: "REPORTED", direction: "IN") + # done + # type: String! + # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") + # seems unused + # comment: Comment @relation(name: "REPORTED", direction: "OUT") + # seems unused + # post: Post @relation(name: "REPORTED", direction: "OUT") + # seems unused + # user: User @relation(name: "REPORTED", direction: "OUT") +# } enum Deletable { Post diff --git a/backend/src/schema/types/type/REPORTED.gql b/backend/src/schema/types/type/REPORTED.gql new file mode 100644 index 000000000..881184bb5 --- /dev/null +++ b/backend/src/schema/types/type/REPORTED.gql @@ -0,0 +1,30 @@ +type REPORTED { + createdAt: String + reasonCategory: String + reasonDescription: String + submitter: User + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user") + resource: ReportReource + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") + resourceId: ID + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource {.id}") + type: String + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]") + user: User + post: Post + comment: Comment +} + +union ReportReource = User | Post | Comment + +enum ReportOrdering { + createdAt_desc +} + +type Query { + Report(orderBy: ReportOrdering): [REPORTED] +} + +type Mutation { + report(resourceId: ID!, reasonCategory: String!, reasonDescription: String!): REPORTED +} diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 89180ac2e..aae955752 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -657,7 +657,7 @@ import { gql } from '../jest/helpers' reasonCategory: $reasonCategory reasonDescription: $reasonDescription ) { - id + type } } ` diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 907c3d5eb..ab9a90e36 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -129,7 +129,7 @@ Given('somebody reported the following posts:', table => { .authenticateAs(submitter) .mutate(`mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { - id + type } }`, { resourceId, diff --git a/webapp/graphql/Moderation.js b/webapp/graphql/Moderation.js index d86205042..981b8146e 100644 --- a/webapp/graphql/Moderation.js +++ b/webapp/graphql/Moderation.js @@ -1,14 +1,15 @@ import gql from 'graphql-tag' export const reportListQuery = () => { + // no limit vor the moment like before: "Report(first: 20, orderBy: createdAt_desc)" return gql` query { - Report(first: 20, orderBy: createdAt_desc) { - id + Report(orderBy: createdAt_desc) { createdAt reasonCategory reasonDescription type + resourceId submitter { id slug @@ -89,7 +90,7 @@ export const reportMutation = () => { reasonCategory: $reasonCategory reasonDescription: $reasonDescription ) { - id + type } } ` From 82228c6c99c4b33ab20ddfbc13cce6ac6f95792c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 11 Oct 2019 16:35:15 +0200 Subject: [PATCH 02/12] Refactored backend database to a single `REPORTED` relation --- .../src/middleware/permissionsMiddleware.js | 2 +- backend/src/schema/index.js | 2 +- backend/src/schema/resolvers/reports.js | 69 +++- backend/src/schema/resolvers/reports.spec.js | 296 ++++++++++++++++-- backend/src/schema/types/schema.gql | 23 -- backend/src/schema/types/type/REPORTED.gql | 10 +- webapp/graphql/Moderation.js | 4 +- 7 files changed, 330 insertions(+), 76 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index df87f743e..31efb9316 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -122,7 +122,7 @@ const permissions = shield( embed: allow, Category: allow, Tag: allow, - Report: isModerator, + reports: isModerator, statistics: allow, currentUser: allow, Post: or(onlyEnabledContent, isModerator), diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index e3c835447..871810d47 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -35,7 +35,6 @@ export default applyScalars( 'Notfication', 'Post', 'Comment', - 'REPORTED', 'Statistics', 'LoggedInUser', 'Location', @@ -43,6 +42,7 @@ export default applyScalars( 'User', 'EMOTED', 'NOTIFIED', + 'REPORTED', ], // add 'User' here as soon as possible }, diff --git a/backend/src/schema/resolvers/reports.js b/backend/src/schema/resolvers/reports.js index 2cf1187e0..dd9375995 100644 --- a/backend/src/schema/resolvers/reports.js +++ b/backend/src/schema/resolvers/reports.js @@ -1,17 +1,10 @@ -import uuid from 'uuid/v4' - export default { Mutation: { - report: async ( - _parent, - { resourceId, reasonCategory, reasonDescription }, - { driver, _req, user }, - _resolveInfo, - ) => { - // Wolle const reportId = uuid() + report: async (_parent, params, { driver, _req, user }, _resolveInfo) => { + const { resourceId, reasonCategory, reasonDescription } = params + const session = driver.session() const reportProperties = { - // Wolle id: reportId, createdAt: new Date().toISOString(), reasonCategory, reasonDescription, @@ -54,7 +47,7 @@ export default { }, ) - session.close() + session.close() const [dbResponse] = res.records.map(r => { return { @@ -88,6 +81,60 @@ export default { break } + return response + }, + }, + Query: { + reports: async (_parent, _params, { driver, _req, _user }, _resolveInfo) => { + const session = driver.session() + const res = await session.run( + ` + MATCH (submitter:User)-[report:REPORTED]->(resource) + WHERE resource:User OR resource:Comment OR resource:Post + RETURN report, submitter, resource, labels(resource)[0] as type + `, + {}, + ) + session.close() + + const dbResponse = res.records.map(r => { + return { + report: r.get('report'), + submitter: r.get('submitter'), + resource: r.get('resource'), + type: r.get('type'), + } + }) + if (!dbResponse) return null + + const response = [] + dbResponse.forEach(ele => { + const { report, submitter, resource, type } = ele + + const responseEle = { + ...report.properties, + resource: resource.properties, + resourceId: resource.properties.id, + post: null, + comment: null, + user: null, + submitter: submitter.properties, + type, + } + switch (type) { + case 'Post': + responseEle.post = resource.properties + break + case 'Comment': + responseEle.comment = resource.properties + break + case 'User': + responseEle.user = resource.properties + break + } + response.push(responseEle) + }) + return response }, }, diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index cf89dab7a..919153611 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,51 +1,24 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' -import { neode } from '../../bootstrap/neo4j' +import { host, login, gql } from '../../jest/helpers' +import { getDriver, neode } from '../../bootstrap/neo4j' +import { createTestClient } from 'apollo-server-testing' +import createServer from '../.././server' const factory = Factory() const instance = neode() +const driver = getDriver() -describe('report', () => { +describe('report mutation', () => { let reportMutation let headers + let client let returnedObject let variables let createPostVariables let user const categoryIds = ['cat9'] - beforeEach(async () => { - returnedObject = '{ createdAt }' - variables = { - resourceId: 'whatever', - reasonCategory: 'reason-category-dummy', - reasonDescription: 'Violates code of conduct !!!', - } - headers = {} - user = await factory.create('User', { - email: 'test@example.org', - password: '1234', - id: 'u1', - }) - await factory.create('User', { - id: 'u2', - name: 'abusive-user', - role: 'user', - email: 'abusive-user@example.org', - }) - await instance.create('Category', { - id: 'cat9', - name: 'Democracy & Politics', - icon: 'university', - }) - }) - - afterEach(async () => { - await factory.cleanDatabase() - }) - - let client const action = () => { // because of the template `${returnedObject}` the 'gql' tag from 'jest/helpers' is not working here reportMutation = ` @@ -59,6 +32,37 @@ describe('report', () => { return client.request(reportMutation, variables) } + beforeEach(async () => { + returnedObject = '{ createdAt }' + variables = { + resourceId: 'whatever', + reasonCategory: 'reason-category-dummy', + reasonDescription: 'Violates code of conduct !!!', + } + headers = {} + user = await factory.create('User', { + id: 'u1', + role: 'user', + email: 'test@example.org', + password: '1234', + }) + await factory.create('User', { + id: 'u2', + role: 'user', + name: 'abusive-user', + email: 'abusive-user@example.org', + }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + describe('unauthenticated', () => { it('throws authorization error', async () => { await expect(action()).rejects.toThrow('Not Authorised') @@ -287,3 +291,227 @@ describe('report', () => { }) }) }) + +describe('reports query', () => { + let query + let mutate + let authenticatedUser = null + let moderator + let user + let author + const categoryIds = ['cat9'] + + const reportMutation = gql` + mutation($resourceId: ID!, $reasonCategory: String!, $reasonDescription: String!) { + report( + resourceId: $resourceId + reasonCategory: $reasonCategory + reasonDescription: $reasonDescription + ) { + type + } + } + ` + const reportsQuery = gql` + query { + reports(orderBy: createdAt_desc) { + createdAt + reasonCategory + reasonDescription + submitter { + id + } + resourceId + type + user { + id + } + post { + id + } + comment { + id + } + } + } + ` + + beforeAll(() => { + const { server } = createServer({ + context: () => { + return { + driver, + user: authenticatedUser, + } + }, + }) + query = createTestClient(server).query + mutate = createTestClient(server).mutate + }) + + beforeEach(async () => { + authenticatedUser = null + + moderator = await factory.create('User', { + id: 'mod1', + role: 'moderator', + email: 'moderator@example.org', + password: '1234', + }) + user = await factory.create('User', { + id: 'user1', + role: 'user', + email: 'test@example.org', + password: '1234', + }) + author = await factory.create('User', { + id: 'auth1', + role: 'user', + name: 'abusive-user', + email: 'abusive-user@example.org', + }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) + + await Promise.all([ + factory.create('Post', { + author, + id: 'p1', + categoryIds, + content: 'Not for you', + }), + factory.create('Post', { + author: moderator, + id: 'p2', + categoryIds, + content: 'Already seen post mention', + }), + factory.create('Post', { + author: user, + id: 'p3', + categoryIds, + content: 'You have been mentioned in a post', + }), + ]) + await Promise.all([ + factory.create('Comment', { + author: user, + id: 'c1', + postId: 'p1', + }), + ]) + + authenticatedUser = await user.toJson() + await Promise.all([ + mutate({ + mutation: reportMutation, + variables: { + resourceId: 'p1', + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + }, + }), + mutate({ + mutation: reportMutation, + variables: { + resourceId: 'c1', + reasonCategory: 'discrimination-etc', + reasonDescription: 'This post is bigoted', + }, + }), + mutate({ + mutation: reportMutation, + variables: { + resourceId: 'auth1', + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', + }, + }), + ]) + authenticatedUser = null + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + const { data, errors } = await query({ query: reportsQuery }) + expect(data).toEqual({ + reports: null, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + }) + + it('roll "user" gets no reports', async () => { + authenticatedUser = await user.toJson() + const { data, errors } = await query({ query: reportsQuery }) + expect(data).toEqual({ + reports: null, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + }) + + it('roll "moderator" gets reports', async () => { + const expected = { + // to check 'orderBy: createdAt_desc' is not possible here, because 'createdAt' does not differ + reports: expect.arrayContaining([ + expect.objectContaining({ + createdAt: expect.any(String), + reasonCategory: 'doxing', + reasonDescription: 'This user is harassing me with bigoted remarks', + submitter: expect.objectContaining({ + id: 'user1', + }), + resourceId: 'auth1', + type: 'User', + user: expect.objectContaining({ + id: 'auth1', + }), + post: null, + comment: null, + }), + expect.objectContaining({ + createdAt: expect.any(String), + reasonCategory: 'other', + reasonDescription: 'This comment is bigoted', + submitter: expect.objectContaining({ + id: 'user1', + }), + resourceId: 'p1', + type: 'Post', + user: null, + post: expect.objectContaining({ + id: 'p1', + }), + comment: null, + }), + expect.objectContaining({ + createdAt: expect.any(String), + reasonCategory: 'discrimination-etc', + reasonDescription: 'This post is bigoted', + submitter: expect.objectContaining({ + id: 'user1', + }), + resourceId: 'c1', + type: 'Comment', + user: null, + post: null, + comment: expect.objectContaining({ + id: 'c1', + }), + }), + ]), + } + + authenticatedUser = await moderator.toJson() + const { data } = await query({ query: reportsQuery }) + expect(data).toEqual(expected) + }) + }) +}) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index e4986ad73..27fd2206c 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -34,29 +34,6 @@ type Mutation { unfollowUser(id: ID!): User } -# Wolle -# type Report { - # not necessary - # id: ID! - # done - # createdAt: String! - # done - # reasonCategory: String! - # done - # reasonDescription: String! - # done - # submitter: User @relation(name: "REPORTED", direction: "IN") - # done - # type: String! - # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") - # seems unused - # comment: Comment @relation(name: "REPORTED", direction: "OUT") - # seems unused - # post: Post @relation(name: "REPORTED", direction: "OUT") - # seems unused - # user: User @relation(name: "REPORTED", direction: "OUT") -# } - enum Deletable { Post Comment diff --git a/backend/src/schema/types/type/REPORTED.gql b/backend/src/schema/types/type/REPORTED.gql index 881184bb5..276c5913b 100644 --- a/backend/src/schema/types/type/REPORTED.gql +++ b/backend/src/schema/types/type/REPORTED.gql @@ -4,8 +4,9 @@ type REPORTED { reasonDescription: String submitter: User @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user") - resource: ReportReource - @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") + # not yet supported + # resource: ReportReource + # @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource") resourceId: ID @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource {.id}") type: String @@ -15,14 +16,15 @@ type REPORTED { comment: Comment } -union ReportReource = User | Post | Comment +# not yet supported +# union ReportReource = User | Post | Comment enum ReportOrdering { createdAt_desc } type Query { - Report(orderBy: ReportOrdering): [REPORTED] + reports(orderBy: ReportOrdering): [REPORTED] } type Mutation { diff --git a/webapp/graphql/Moderation.js b/webapp/graphql/Moderation.js index 981b8146e..8ead28b61 100644 --- a/webapp/graphql/Moderation.js +++ b/webapp/graphql/Moderation.js @@ -1,10 +1,10 @@ import gql from 'graphql-tag' export const reportListQuery = () => { - // no limit vor the moment like before: "Report(first: 20, orderBy: createdAt_desc)" + // no limit vor the moment like before: "reports(first: 20, orderBy: createdAt_desc)" return gql` query { - Report(orderBy: createdAt_desc) { + reports(orderBy: createdAt_desc) { createdAt reasonCategory reasonDescription From 1c8385120cb03b58b2e8a001d6aa92e0b15a66c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 11 Oct 2019 16:36:37 +0200 Subject: [PATCH 03/12] Add date-time to the moderators report list --- webapp/locales/de.json | 1 + webapp/locales/en.json | 1 + webapp/pages/moderation/index.vue | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index e54fcfcb0..1277b5737 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -431,6 +431,7 @@ "name": "Meldungen", "reasonCategory": "Kategorie", "reasonDescription": "Beschreibung", + "createdAt": "Datum", "submitter": "Gemeldet von", "disabledBy": "Deaktiviert von" } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index aa59a3732..7d08aa638 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -432,6 +432,7 @@ "name": "Reports", "reasonCategory": "Category", "reasonDescription": "Description", + "createdAt": "Date", "submitter": "Reported by", "disabledBy": "Disabled by" } diff --git a/webapp/pages/moderation/index.vue b/webapp/pages/moderation/index.vue index 1f05c93ab..535b2613c 100644 --- a/webapp/pages/moderation/index.vue +++ b/webapp/pages/moderation/index.vue @@ -1,7 +1,7 @@